Back to writing

How This Publication Is Built

A factual tour of the Astro content collection, writing routes, and small authoring workflow behind this technical publication.

4 min read

This site keeps its publishing system deliberately small. The source of truth for an article is one MDX file in src/content/blog/. Astro reads those files, validates their frontmatter, renders the content, and generates writing pages from the resulting collection. There is no database or editorial dashboard in this workflow.

That structure is a useful constraint for a technical publication. An article can be reviewed as a normal file, its metadata can be checked before rendering, and the final route is derived from its filename. The Astro content collections guide describes the broader model that this site follows.

A typed collection at the edge

The collection definition lives in src/content.config.ts. It uses Astro’s glob loader to discover Markdown and MDX files and a Zod schema to describe the frontmatter contract.

const blog = defineCollection({
  loader: glob({
    pattern: '**/*.{md,mdx}',
    base: './src/content/blog',
  }),
  schema: ({ image }) => z.object({
    title: z.string(),
    description: z.string(),
    publishedAt: z.coerce.date(),
    updatedAt: z.coerce.date().optional(),
    tags: z.array(z.string()),
    draft: z.boolean().default(false),
    cover: image().optional(),
    canonicalUrl: z.string().url().optional(),
  }),
});

The schema is intentionally narrow. A title and description provide the minimum page metadata. Dates remain real Date values after validation, tags are always an array, and draft has an explicit default. An optional cover can be checked by Astro’s image schema helper. A canonical URL is accepted only when it is a URL-shaped string.

The result is not just editor assistance. Invalid frontmatter is a content error rather than a partially rendered page. The same contract is available to the route and card components through CollectionEntry<'blog'>, so a metadata rename has a clear set of callers.

A route for the collection

The writing index loads the collection, filters drafts according to the build environment, and sorts by publication date. Dynamic pages use the filename-derived ID as the slug for /writing/[slug].

const posts = await getCollection('blog', ({ data }) =>
  import.meta.env.PROD ? !data.draft : true,
);

posts.sort(
  (left, right) =>
    right.data.publishedAt.valueOf() - left.data.publishedAt.valueOf(),
);

Keeping draft handling in the query makes the rule visible in both the index and the dynamic route. A draft is useful while writing and review is happening locally, but it is not part of a production page list. The route also passes publication data and an Article JSON-LD object to the shared layout, so social metadata and structured data do not need to be maintained in every article body.

The page itself remains semantic HTML: one article heading, a metadata block with machine-readable time values, a tag list, the rendered prose, and previous or next links. The Prose component supplies one stable wrapper for Markdown output. The MDX integration is available when an article needs a component, while plain Markdown remains a valid source format.

Why the authoring command is small

A new post starts with:

npm run new:post -- "A Useful Title"

The script creates a slugged .mdx file with a date, empty tags, a placeholder description, and draft: true. It does not publish anything and it refuses to overwrite an existing slug. The author then replaces the placeholder metadata, writes the article, and changes draft to false when the content is ready.

The command intentionally does not try to infer tags, summarize a post, or download an image. Those decisions need editorial context. A small script handles the repetitive filename and frontmatter work while leaving the content in the same reviewable format as every other article.

Images stay close to content

A post can omit a cover image. If it needs one, the image file belongs near the article source and is referenced by the cover field so the collection schema can validate it. The public directory remains appropriate for site-wide assets that are not part of a post’s content model. Astro’s assets documentation explains the distinction between processed images and public files.

This is a tradeoff rather than a universal rule. Content-local assets make an article easier to move as a unit. Public assets are simpler when a file is shared by several routes or must retain a stable URL. The metadata field allows either choice to stay explicit in review.

What this architecture does not promise

A static content collection is not a publishing platform with revisions, scheduled releases, or multi-user approvals. Those capabilities are intentionally outside this repository’s authoring path. The current contract gives the site typed metadata, deterministic routes, accessible article markup, and a predictable draft boundary.

That modest scope is the point. A reader sees a writing index and article pages. An author edits one MDX file. The build checks that the metadata is shaped correctly. When those responsibilities stay separate, the site can remain easy to understand without hiding the content workflow behind an unnecessary service.