Skip to main content
aapp-hacks
← Back to articles
Author
app-hacks
Published
Updated
Reviewed

Astro / Content Collections / Publishing

Safely Manage Drafts and Publish Dates with Astro Content Collections

Use draft and pubDate validation to keep unpublished Astro posts out of lists, static routes, and sitemaps.

Safely Manage Drafts and Publish Dates with Astro Content Collections

Hiding a draft from an article index does not make it safe. If a static route or sitemap still includes it, the URL can be shared or discovered. Treat publication as one condition used consistently by frontmatter validation, indexes, static routes, and links.

Only entries that are not drafts and have a publication date are included in the index, routes, and sitemap.

Define the Publication Rule in the Schema

Use draft as the explicit state and pubDate as a required fact for published content.

const blogSchema = z
  .object({
    title: z.string().min(1),
    description: z.string().min(1),
    pubDate: z.coerce.date().nullable().default(null),
    draft: z.boolean().default(false),
    tags: z.array(z.string().min(1)).min(1),
  })
  .superRefine((post, context) => {
    if (!post.draft && !post.pubDate) {
      context.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Published posts require pubDate.',
        path: ['pubDate'],
      });
    }
  });

This stops a published entry without a date at build time. A draft should have draft: true and no pubDate. Avoid placing a future publication date on a draft when the application treats that value as a publication fact.

Use the Same Rule for Indexes and Routes

Excluding a draft from an index is insufficient if getStaticPaths() still creates its page. Define the rule once and use it for both.

const isPublished = (post: CollectionEntry<'blog'>) =>
  !post.data.draft && Boolean(post.data.pubDate);

const publishedPosts = posts.filter(isPublished);

If drafts must be visible during local development, add that as a separate condition rather than weakening the production rule.

const includeDrafts = import.meta.env.DEV;
const routablePosts = posts.filter(
  (post) => includeDrafts || isPublished(post),
);

The development server can then render drafts while the production build creates routes only for published posts.

Sitemap output depends on the routes that the build produces. Do not generate draft detail pages if they must stay out of the sitemap. Apply the same publication condition to related-post lists, previous/next navigation, and any RSS feed. A handwritten link from a published article to a draft is still a broken public link when the draft route is absent.

Verify the Built Output

Check the production output, not only the source code.

npm run build
test ! -e dist/blog/draft-article/index.html
! rg -q 'draft-article' dist/sitemap-0.xml

Check the index, detail-route directories, and sitemap for each language. Confirm both that the draft ID is absent and that each intended published article exists at its URL.

Keep the Publication Change Small

On publication day, change only the language-pair frontmatter fields: draft, pubDate, and, when needed, updatedDate and reviewedAt. Then build and inspect the output. Keeping publication state separate from content edits makes history and failures easier to understand.

Draft handling is not a single flag. A shared publication rule across schema validation, routes, links, and the sitemap prevents accidental public URLs.