Starting a Personal Technical Blog with Minimal Astro

Starting a personal technical blog often leads to adding a CMS, database, search, and comments before publishing the first post. If the goal is to write Markdown, generate list and detail pages, and publish an RSS feed, Astro’s static output is often enough.
This note follows the smallest path from Markdown to a validated collection entry, a dynamic route, and static HTML using Astro 7. The API changes over time, so I use the current Content Collections guide rather than older examples that use src/content/config.ts.
Start with a small directory layout
src/
content.config.ts
content/
signalcraft/
first-post.md
layouts/
SignalcraftLayout.astro
pages/
index.astro
signalcraft/
index.astro
[slug].astro
rss.xml.ts
public/
astro.config.mjs
Keep content input in content/, shared HTML in layouts/, and URL generation in pages/. Put unprocessed files such as favicons and static images in public/. The separation prevents article prose and presentation logic from growing together.
Validate input in src/content.config.ts
Current Astro projects can define a collection and loader in src/content.config.ts. A minimal schema accepts a title, optional description, publication date, and tags.
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";
const signalcraft = defineCollection({
loader: glob({
base: "./src/content/signalcraft",
pattern: "**/*.md",
}),
schema: z.object({
title: z.string().max(60),
description: z.string().optional(),
publishDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { signalcraft };
The schema is for finding input mistakes at build time, not for presentation. If you need drafts, add draft to the schema first and apply the same filter to both list and detail routes. Do not add frontmatter in a page without updating the collection contract.
Generate list and detail pages with getCollection()
Putting a Markdown file in a collection does not automatically make it a public page. A dynamic route uses getStaticPaths() to turn each entry into HTML. A minimal form, close to the Content Collections examples, is:
---
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const articles = await getCollection("signalcraft");
return articles.map((article) => ({
params: { slug: article.id },
props: { article },
}));
}
const { article } = Astro.props;
const { Content } = await render(article);
---
<article>
<h1>{article.data.title}</h1>
<Content />
</article>
Do not over-assume the URL from a filename. Check how the loader produces id, how locale prefixes are handled, and whether trailing slashes are used. Then keep those decisions in one URL helper.
Add verification on the first day
Before adding features, include:
- An
astro.config.mjswithsite - Title, description, and canonical URL
- Sitemap and RSS
- A 404 page
- Minimal readable CSS for headings, code, and tables
- Lint and build checks in pull requests
The site setting affects canonical URLs, sitemaps, and absolute RSS URLs. Confirm the public URL in the Astro Configuration Reference.
After adding one post, run the checks used by the project:
pnpm astro check
pnpm build
If astro check is not part of the project, follow its package.json scripts instead. A successful build does not prove that article links or RSS are correct, so inspect the generated list, detail page, and feed as well.
Compare GitHub Pages and Cloudflare
Both can serve a static Astro site. The important differences are URL configuration and future operations.
| Concern | GitHub Pages | Cloudflare Pages / Workers |
|---|---|---|
| Starting point | GitHub repository and Actions | Cloudflare Git or deployment settings |
| Static hosting | Sufficient for the goal | Sufficient for the goal |
| Preview | Configure with Actions | Push previews are straightforward to configure |
| Dynamic work | Often needs another service | Can extend to Workers |
| Main caveat | base for a project site |
Adapter and runtime dependencies |
For a GitHub Pages project site, align site, base, internal links, RSS, and sitemap with the repository name. Follow the Astro GitHub Pages guide and decide whether a custom domain is part of the initial setup.
For static Cloudflare output, start without an adapter. Add an adapter only when on-demand rendering or server work is needed, following the Cloudflare deployment guide. Depending on platform-specific APIs too early increases migration cost.
Add features in an order you can sustain
- Publish one Markdown post.
- Check the list, detail page, RSS, and metadata.
- Run lint and
astro buildin CI. - Add tags, search, or OG images when the number of posts justifies them.
- Consider a CMS or dynamic feature only when a real need appears.
The purpose of a minimal setup is not to avoid features forever. It is to keep the writing-to-publication path short and let actual operation justify the next addition.
