Skip to content

Blog & Content

The blog uses Astro’s Loader API (introduced in Astro 4.14, stable in v5+). Blog posts and author profiles are Markdown files read from src/data/ at build time. There is no database — just files.


src/data/
├── blog/
│ ├── my-first-post.md
│ └── another-article.mdx
└── authors/
└── jane-doe.md

Create a new .md or .mdx file in src/data/blog/. The filename becomes the post slug.

---
title: "A Complete Guide to Brand Identity"
description: "Everything you need to know about building a strong, consistent brand."
pubDate: 2025-04-01
author: jane-doe # must match a filename in src/data/authors/
cover: ../../assets/blog/brand-identity.jpg
tags: ["branding", "design", "strategy"]
draft: false
---
Your post content here in Markdown...
FieldTypeRequiredDescription
titlestringPost title — shown in listings and <title>
descriptionstringShort summary — used as meta description
pubDateDatePublication date (YYYY-MM-DD)
updatedDateDateLast update date — shown if set
authorstringAuthor slug — must match src/data/authors/{slug}.md
coverimage()Cover image — use a relative path from the post file
tagsstring[]Array of tag strings — drives category pages
draftbooleanDefault false — drafts are excluded from production builds

Place cover images in src/assets/blog/ and reference them with a relative path:

cover: ../../assets/blog/my-image.jpg

Astro will optimise them (WebP conversion, responsive srcset) automatically.


Create a .md file in src/data/authors/. The filename is the author slug used in post frontmatter.

---
name: "Jane Doe"
role: "Creative Director"
bio: "Jane has led brand projects for Fortune 500 companies for over a decade."
avatar: ../../assets/authors/jane-doe.jpg
website: "https://janedoe.com"
twitter: "janedoe"
draft: false
---
FieldTypeRequiredDescription
namestringFull display name
rolestringJob title or specialty
biostringShort biography (1–2 sentences)
avatarimage()Profile photo — relative path from the file
websitestringFull URL including https://
twitterstringHandle without @
draftbooleanHides the author from listings if true

RouteFileDescription
/blogsrc/pages/blog/index.astroPaginated listing of all published posts
/blog/[slug]src/pages/blog/[...slug].astroIndividual post page
/blog/authorssrc/pages/blog/authors/index.astroAll authors listing
/blog/authors/[slug]src/pages/blog/authors/[slug].astroAuthor profile + their posts
/blog/categories/[tag]src/pages/blog/categories/[tag].astroPosts filtered by tag

The schema validates frontmatter at build time using Zod. If a post has an invalid field, the build fails with a clear error.

const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/data/blog" }),
schema: ({ image }) => z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
author: reference("authors").default({ collection: "authors", id: "kevin-hart" }),
cover: image().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});

The reference("authors") type links a post’s author field to the authors collection by slug, ensuring referential integrity at build time.


import { getCollection } from "astro:content";
// All published posts, sorted newest first
const posts = (await getCollection("blog", ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// A single post with its author
const post = await getEntry("blog", slug);
const author = await getEntry(post.data.author);

The Loader API makes it straightforward to swap the glob loader for a CMS loader without changing any page code. See the Strapi, Directus, or Payload CMS guides.