This guide describes the structure of this website and the APIs used to build it. It was reviewed against Astro 7.4.4, the latest stable release on October 30, 2026. This repository currently uses Astro 7.4.2; the content collection and rendering APIs shown here apply to both versions.
Why Astro?
Astro is a framework for content-driven websites. It renders pages to HTML and only sends client-side JavaScript for features that need browser interaction. A Markdown blog can therefore use static pages while adding features such as theme switching, article search, and code copying with small browser scripts.
This website uses Astro’s default static output. Articles, paginated lists, RSS, and the sitemap are generated at build time. Publishing an article requires a new build and deployment.
Create an Astro project
Use Node.js 22.23.4 or later on a supported even-numbered release. Check the installation requirements when upgrading Astro; unsupported odd-numbered Node.js releases are not suitable for this setup.
Create a new project with the CLI wizard:
pnpm create astro@latest
Choose a project directory, then enter it and install the dependencies if the wizard did not already do so:
cd my-blog
pnpm install
pnpm dev
The development server normally starts at http://localhost:4321. For an existing checkout of this website, run pnpm install and pnpm dev in the repository root.
To create a production build and preview it locally:
pnpm build
pnpm preview
Static output is written to dist/. The preview server is intended for checking a build locally.
Project structure
The relevant files in this repository are:
/
├── public/
├── src/
│ ├── assets/
│ ├── components/
│ │ ├── Icon.astro
│ │ ├── IconPaths.ts
│ │ ├── PostSearch.astro
│ │ └── TocAside.astro
│ ├── content/
│ │ ├── _images/
│ │ ├── programming/
│ │ └── development-of-this-website.md
│ ├── content.config.ts
│ ├── layouts/
│ ├── pages/
│ │ ├── blog/
│ │ │ ├── [...page].astro
│ │ │ └── [...slug].astro
│ │ ├── index.astro
│ │ ├── robots.txt.ts
│ │ └── rss.xml.ts
│ ├── styles/
│ │ ├── global.css
│ │ └── theme.css
│ └── site.config.ts
├── astro.config.mjs
├── package.json
└── pnpm-lock.yaml
src/pages/ defines routes. Markdown loaded from src/content/ does not create routes by itself: the article route queries the collection and generates a page for each published entry.
Define a content collection
Astro’s Content Layer API uses loaders to read content. Configure collections in src/content.config.ts. The loader’s base determines where files are stored; a collection is not defined simply by creating a directory under src/content/.
This website has one collection named blog. Its glob() loader reads Markdown files recursively from src/content/, including nested directories such as programming/.
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content' }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
date: z.date(),
cover: image().optional(),
draft: z.boolean().optional(),
featured: z.boolean().optional(),
}),
});
export const collections = { blog };
The schema validates frontmatter and provides TypeScript types for collection entries. image() resolves local cover images relative to the Markdown file and exposes their metadata for Astro’s image components.
Use defineCollection from astro:content, glob from astro/loaders, and z from astro/zod. The loader-based configuration replaces the legacy type: "content" collection setup. See the content collections guide for other loaders and schema options.
Frontmatter fields
| Field | Purpose | Requirement |
|---|---|---|
title |
Article title, page title, and primary heading | Required string |
description |
Article summary, search text, and page metadata | Required string |
date |
Publication date used for sorting and RSS | Required date |
cover |
Local image path relative to the Markdown file | Optional |
draft |
Excludes an article from published lists, routes, search, and RSS when true |
Optional boolean |
featured |
Includes a published article in the featured selection when true |
Optional boolean |
The current schema does not define a tags field or a tag archive. Adding tags requires both a schema field and any desired display or routing behavior.
The optional booleans are undefined when omitted; they have no schema default. The website treats an omitted draft as published and an omitted featured as not featured.
Write a post
Create a Markdown file under src/content/. For example:
---
title: My first Astro post
description: How I created a static blog with Astro content collections.
date: 2026-10-30
cover: ./_images/brook.jpg
featured: true
draft: false
---
## Getting started
Write the article here.
Use the field name date. With this project’s z.date() schema, write an unquoted YAML date such as 2026-10-30, which the Markdown parser converts to a JavaScript Date. To accept quoted date strings as well, explicitly change the schema to z.coerce.date().
Omit cover if an article has no cover image. An empty string is not a valid optional image path.
Headings and the table of contents
The article template renders title as its <h1>. Start the article body at ## and use lower heading levels for subsections. This keeps the heading hierarchy consistent for readers and assistive technology.
render(post) returns both the rendered Content component and heading metadata. The website passes this metadata to TocAside.astro, which displays second- and third-level headings. Use the supplied heading slugs as anchor targets so the table of contents matches Astro’s generated HTML IDs.
Images in Markdown
Store images under src/ when Astro should process them. This website keeps article images beside the content in _images/ directories. Use a relative Markdown image path:

A nested post must use a path relative to its own location. For example, src/content/programming/sample-post.md can reference the shared cover with ../_images/brook.jpg.
Astro processes local images referenced through Markdown image syntax. Plain .md files do not execute Astro component imports; use an .astro template or install the MDX integration when you need to render <Image /> as a component within content. To load .mdx posts in this project, also extend the loader pattern to **/*.{md,mdx}.
Files under public/ are copied without optimization. A file at public/images/example.jpg is served as /images/example.jpg:

<img src="/images/example.jpg" alt="Example image" width="720" height="360">
A raw HTML <img> does not receive Astro’s image optimization. See the images guide for the differences between Markdown images, <Image />, and public assets.
Generate article pages
For static output, getStaticPaths() selects the entries that become pages. Use the entry’s id for routing and the standalone render() function to render its body.
The following is a minimal article route. The full route in this repository also includes the header, footer, table of contents, publication date, and code-copy controls.
---
import { getCollection, render } from 'astro:content';
import { Image } from 'astro:assets';
import BaseLayout from '@/layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
const { title, description, cover } = post.data;
---
<BaseLayout title={title} description={description}>
<main>
<h1>{title}</h1>
{cover && <Image src={cover} alt={title} width={720} />}
<article class="prose">
<Content />
</article>
</main>
</BaseLayout>
The glob() loader generates URL-friendly IDs from file paths by default. For example, programming/notes-cpp-basics.md has the ID programming/notes-cpp-basics, producing /blog/programming/notes-cpp-basics. The rest parameter [...slug] supports these nested paths. Its parameter name does not imply that entries have a post.slug property.
Pagination and search
Collection query results have no guaranteed sort order. Sort published entries explicitly before paginating:
const posts = (await getCollection('blog', ({ data }) => !data.draft)).sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
);
The list route uses paginate(posts, { pageSize: SITE.postPageSize }) inside getStaticPaths(). Its filename is [...page].astro, so the first page is /blog and subsequent pages are /blog/2, /blog/3, and so on.
PostSearch.astro searches the titles and descriptions of all published posts, including entries on other list pages. Search runs in the browser and does not include article bodies. Draft filtering is applied explicitly in the article route, lists, featured selection, search data, and RSS; Astro does not make a custom draft field private automatically.
Configure styles and site metadata
This website uses Tailwind CSS 4 through the @tailwindcss/vite plugin in astro.config.mjs. The stylesheet imports tailwindcss and registers @tailwindcss/typography for Markdown’s prose styles. A new project can use pnpm astro add tailwind to configure Tailwind; the integration is already configured in this repository.
src/styles/theme.css defines the Light and Dark color variables. The .dark class switches palettes, and ThemeToggle.astro stores the user’s choice. Both themes use solid backgrounds. UI icons are defined locally in IconPaths.ts and rendered through Icon.astro.
Set the production URL in src/site.config.ts through SITE.website. astro.config.mjs passes this value to Astro’s site option. Replace the domain, author, description, and social links with your own values before deploying.
BaseLayout.astro generates canonical URLs and social metadata. src/pages/rss.xml.ts uses @astrojs/rss to publish article summaries at /rss.xml, and @astrojs/sitemap generates /sitemap-index.xml during the build. The feed and sitemap must use the production domain; robots.txt points crawlers to the sitemap.
Deploy to Cloudflare
The current Astro deployment guide recommends Cloudflare Workers for new projects. This blog generates static assets, so it can use Workers Static Assets without a server-rendering adapter.
Deploy static assets with Workers
Install Wrangler in the project where you intend to configure deployment:
pnpm add -D wrangler
Create a Wrangler configuration at the project root. Choose a unique worker name and set compatibility_date to the date you configure deployment.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-astro-blog",
"compatibility_date": "2026-10-30",
"assets": {
"directory": "./dist",
"not_found_handling": "404-page",
},
}
The 404-page setting uses the generated 404.html for missing routes. Build the site before running the deployment command:
pnpm build
pnpm exec wrangler deploy
Wrangler authenticates with your Cloudflare account when needed. You can also connect the repository to Workers Builds for deployment on push. Configure the build environment with a supported Node.js version meeting Astro’s minimum requirement.
These commands and the Wrangler file are deployment instructions; Wrangler is not currently a dependency of this repository. If you later add on-demand rendering, follow the official guide to configure the @astrojs/cloudflare adapter and runtime settings.
Existing Cloudflare Pages projects
An existing Pages deployment can continue to publish this static build. Use the repository root as the build directory, pnpm build as the build command, and dist as the output directory. Configure a supported Node.js version, such as 22.23.4, with the build environment’s NODE_VERSION setting.
Commit pnpm-lock.yaml with the project so the deployment installs the locked dependency versions. After deployment, verify an article URL, a nested article URL, /rss.xml, /sitemap-index.xml, and the custom 404 page on the production domain.
