Blog & RSS
The blog is a live route: a listing at /blog/ and an article at /blog/<slug>/ for each post, plus an RSS feed at /rss.xml. A post’s frontmatter carries only what the layout slots — the byline, the category tag, the tags and the hero — while the article itself is free-form MDX. See Content Collections for the schema; this page is how the posts are rendered.
The listing
src/pages/blog/index.astro reads posts through @js/blogData’s getSortedPosts() — non-draft entries, newest first by pubDate — maps each to a card with toPostCard, and hands the cards to the shared Global/CardGrid under the “Archive Modules” heading. The page’s other section is Blog/BlogHero. The home page’s Latest Posts section uses the same toPostCard mapping, so a post looks identical wherever it appears.
The category badges
Each post carries a category string, and it drives a coloured retro badge on both the card and the article. The mapping lives in @js/postCards’s categoryMeta, which brackets the label (Quest becomes [Quest]) and picks a tone:
- Quest → success green
- Tech → primary blue
- Guide → warning amber
- Lore → a fixed maroon (this tone has no semantic token, so it is applied with an explicit class)
- Dev Log → info blue
The match is case-insensitive, and any category the map does not recognise falls back to blue — so a new category name renders sanely rather than breaking. If you want a fifth colour, add a row to CATEGORY_TONES.
The article
src/pages/blog/[slug].astro emits one page per post and composes Blog/BlogArticle (the article) and Blog/RelatedPosts, inside an 800px reading column. The route resolves several things the layout needs:
- The byline. The first entry in the post’s
authorsarray is resolved from the authors collection for itsnameandavatar, falling back tositeData.author.nameif the reference is somehow missing. - Reading time.
@js/readingTimecounts words in the raw MDX body at roughly 200 words per minute, floored at one minute, and renders it as the “N MIN READ” byline. It works on the raw body, so it needs no rendered HTML. - The date, formatted with the site locale and uppercased.
- Prev / next, the newer and older neighbours in the date-sorted list (
getAdjacentPosts), each side omitted at the ends of the list. - Related posts — up to two, same category first and then most recent, excluding the current post (
getRelatedPosts). The section is fully dynamic, so every “More Quests” link resolves to a real post rather than a placeholder.
The body itself is the rendered MDX, styled by the global .blog-prose class — headings pick up the pixel face, links flip from info blue to success green on hover, and fenced code sits in a fixed-dark panel. See Typography for the full prose treatment.
Structured data
The article passes two JSON-LD nodes: a BlogPosting (with the hero as its image, the author’s authorLink as author.url, and a publisher reference to the site’s Organization) and a BreadcrumbList matching the visible Home › Blog › <title> breadcrumb. The rule the theme follows is that a breadcrumb schema is only ever emitted alongside a visible breadcrumb, so markup and structured data agree. dateModified is emitted only when a post carries an updatedDate — never invented. See SEO.
The RSS feed
/rss.xml is a dependency-free endpoint. It renders RSS 2.0 through @js/rss (a pure, tested renderer — no @astrojs/rss), mapping the same getSortedPosts() list to feed items. Because it is a dynamic endpoint, its absolute URLs resolve against site, so setting your domain once keeps the feed correct. It is linked from the footer, from BaseHead, and from llms.txt. There is a single feed, since the site is single-language.
Adding a post
Create src/data/blog/<slug>/index.mdx with the required frontmatter and a body:
---
title: "The History of the Floppy Disk"
description: "1.44MB of pure nostalgia."
authors: ["admin"]
pubDate: 2026-07-01
heroImage: "./hero.jpg"
heroImageAlt: "A stack of 3.5-inch floppy disks"
category: "Lore"
tags: ["retro", "hardware"]
---
Your post, as MDX. Headings, lists, code — all styled by `.blog-prose`.
The authors reference must name a real author file (.min(1) requires at least one), the pubDate must parse as a date, and heroImage must resolve. Get any of those wrong and the build tells you which post and which field.