Skip to content

Writing blog posts

Blog posts are Markdown files in src/content/blog/. The theme uses Astro Content Collections, so each post gets a typed schema, a stable slug, and a generated route — for free.

Create src/content/blog/getting-started-with-aurora.md:

src/content/blog/getting-started-with-aurora.md
---
title: "Getting Started with Aurora"
description: "A 5-minute walk through Aurora's core concepts."
date: 2026-01-15
author: "Alex Rivera"
category: "Tutorial"
image: "/blog/getting-started.jpg"
draft: false
---
Welcome to Aurora. This post walks through everything you need to know to ship
your first project this afternoon.
## The first 30 seconds
Aurora ships with sensible defaults — you can start using it immediately…
## More headings
Markdown headings (`##`, `###`) become anchored section dividers. The theme
auto-generates a table of contents from them.
![Aurora dashboard](/blog/dashboard.png)
> Quotes get a tasteful tinted treatment.
`code` inline. Or a fenced block:
\`\`\`ts
const aurora = createAurora({ theme: "light" });
\`\`\`

That’s all. Save it and /blog/getting-started-with-aurora/ renders immediately — the slug is derived from the filename.

Frontmatter fieldWhere it shows
titlePost <h1>, blog index card title, <title> tag, OG title.
descriptionCard subtitle, meta description, OG description.
dateCard date, <time> element on the post. ISO format.
authorByline.
categoryCard tag, category filter on the blog index.
imageCard hero image, OG image for that post.
draftIf true, the post is excluded from the build entirely.

The schema lives in src/content.config.ts (or src/content/config.ts in older themes). Add a field there if you want the schema to accept it.

Set draft: true in the frontmatter to keep a post out of production:

---
title: "Half-written thoughts on observability"
draft: true
date: 2026-02-01
---

In dev (npm run dev), drafts are visible at their URL but hidden from the blog index. In npm run build, they’re excluded entirely.

category is a free-text string. The blog index (src/pages/blog/index.astro) reads every post’s category and renders a filter row at the top. To rename a category globally, edit the field in every post — categories aren’t centralized.

For tighter control, swap the schema to use a string union in src/content.config.ts:

src/content.config.ts
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
author: z.string(),
category: z.enum(["Tutorial", "Engineering", "Design", "Product"]),
image: z.string().optional(),
draft: z.boolean().default(false),
}),
});

Now Astro errors at build time if a post uses an unknown category — instant typo protection.

By default the slug is the filename minus .md. Override by adding to the frontmatter:

---
slug: "v2-launch"
title: "Aurora 2.0 — what's new"
---

Becomes /blog/v2-launch/ regardless of filename.

If you want to embed components inside posts (callouts, custom charts, an embedded video player), use .mdx instead of .md:

src/content/blog/launch.mdx
---
title: "Launch announcement"
date: 2026-03-01
author: "Sarah Kim"
category: "Product"
---
import VideoEmbed from "~/components/VideoEmbed.astro";
We just launched.
<VideoEmbed videoId="dQw4w9WgXcQ" />
The rest of the article continues in plain Markdown…

MDX requires @astrojs/mdxnpm install @astrojs/mdx and add it to astro.config.mjs:

import mdx from "@astrojs/mdx";
export default defineConfig({
integrations: [mdx(), /* … */],
});
  • src/pages/blog/index.astro — the listing page. Iterates getCollection("blog"), filters out drafts in production, sorts by date.
  • src/pages/blog/[slug].astro — the post template. Reads frontmatter, renders the Markdown body, wires up “next/previous post” navigation.

If you want to change the post layout (more whitespace, different sidebar, related-posts grid), edit [slug].astro directly.