A clean, minimalist blog built with Astro, using a monospaced type system and full light/dark mode. Posts are plain Markdown files. Deployment is fully automated via GitHub Actions.
- Prerequisites
- Project structure
- Local development
- Writing posts
- Customising the site
- Hosting on GitHub Pages — step by step
- Custom domain (optional)
- Updating the site
- Troubleshooting
- Node.js 18 or 20 — download from nodejs.org
- npm (ships with Node)
- Git — download from git-scm.com
- A GitHub account — free at github.com
Check your versions:
node -v # should print v18.x or v20.x
npm -v # should print 9.x or 10.x
git --version.
├── .github/
│ └── workflows/
│ └── deploy.yml ← GitHub Actions CI/CD pipeline
├── public/
│ └── favicon.svg ← Static assets (images, fonts, etc.)
├── src/
│ ├── components/
│ │ ├── Header.astro ← Site header + subscribe bar + nav
│ │ ├── Footer.astro ← Site footer
│ │ └── PostCard.astro ← Post list item
│ ├── content/
│ │ ├── config.ts ← Content collection schema
│ │ └── blog/
│ │ └── *.md ← Your blog posts go here
│ ├── layouts/
│ │ ├── Base.astro ← HTML shell + theme toggle
│ │ └── Post.astro ← Individual post layout
│ ├── pages/
│ │ ├── index.astro ← Home (post list)
│ │ ├── about.astro ← About page
│ │ ├── projects.astro ← Projects page
│ │ ├── 404.astro ← Not-found page
│ │ ├── feed.xml.ts ← RSS feed (auto-generated)
│ │ ├── blog/
│ │ │ └── [slug].astro ← Dynamic post route
│ │ └── tags/
│ │ ├── index.astro ← All tags
│ │ └── [tag].astro ← Posts by tag
│ └── styles/
│ └── global.css ← Full design system (tokens, components)
├── astro.config.mjs ← Astro configuration
├── package.json
└── tsconfig.json
# 1. Install dependencies
npm install
# 2. Start the dev server
npm run devOpen http://localhost:4321 in your browser. The site hot-reloads on every file save.
# Build for production (outputs to dist/)
npm run build
# Preview the production build locally
npm run previewEvery file in src/content/blog/ becomes a post. Create a new .md file:
src/content/blog/my-new-post.md
The filename becomes the URL slug: /blog/my-new-post
Every post needs a frontmatter block at the top:
---
title: "Your Post Title"
date: 2025-12-01
description: "A one or two sentence summary shown in the post list and RSS feed."
tags: ["tag-one", "tag-two"]
---
Your post content starts here...| Field | Type | Required | Description |
|---|---|---|---|
title |
string | ✅ yes | Post title |
date |
YYYY-MM-DD | ✅ yes | Publication date (used for sorting) |
description |
string | ✅ yes | Short summary (shown in list + RSS) |
tags |
string[] | no | Array of tags. Creates /tags/<tag> pages automatically |
draft |
boolean | no | Set true to hide a post from the site (default: false) |
featured |
boolean | no | Set true to pin as the hero post on the home page |
Standard Markdown plus:
- Code blocks with syntax highlighting (via Shiki):
```typescript const greet = (name: string) => `hello, ${name}`; ```
- Tables, blockquotes, footnotes
- MDX (
.mdxextension) for embedding Astro/HTML components in posts
Edit these files and replace placeholder values:
| File | What to change |
|---|---|
src/components/Header.astro |
Site name, tagline, GitHub/LinkedIn/Email/RSS URLs |
src/components/Footer.astro |
Name, GitHub URL, email |
src/pages/index.astro |
About blurb on home page |
src/pages/about.astro |
Full about page content |
src/pages/projects.astro |
Projects array |
astro.config.mjs |
site: URL (your GitHub Pages URL) |
All design tokens live at the top of src/styles/global.css:
:root {
--bg: #f9f7f4; /* page background */
--fg: #1a1916; /* primary text */
--fg-muted: #7a7672; /* secondary text */
--border: #d8d4ce; /* dividers */
--font: 'JetBrains Mono', 'Courier New', monospace;
}Change any value here and it propagates everywhere. Dark mode overrides are in the [data-theme="dark"] block immediately below.
Edit the links array in src/components/Header.astro:
const links = [
{ href: '/', label: 'writing' },
{ href: '/tags', label: 'tags' },
{ href: '/projects', label: 'projects' },
{ href: '/about', label: 'about' },
];Follow these steps exactly. The whole process takes about 10 minutes.
- Go to github.com/new
- Name the repository exactly one of:
yourusername.github.io— to publish athttps://yourusername.github.io✅ recommended- Any other name (e.g.
blog) — publishes athttps://yourusername.github.io/blog
- Set visibility to Public (required for free GitHub Pages)
- Do not initialise with a README (you already have one)
- Click Create repository
If you use a repo name other than
<username>.github.io, you must also setbase: '/repo-name/'inastro.config.mjs.
Open astro.config.mjs and set site to your real URL:
export default defineConfig({
site: 'https://yourusername.github.io',
// or: 'https://yourusername.github.io/repo-name'
...
});In your terminal, inside the project folder:
# Initialise git (skip if already done)
git init
# Stage all files
git add .
# Create the first commit
git commit -m "initial commit"
# Point to your GitHub repo (replace URL with yours)
git remote add origin https://github.com/yourusername/yourusername.github.io.git
# Push to GitHub
git push -u origin main- On GitHub, open your repository
- Click Settings (tab at the top)
- Scroll down to Pages in the left sidebar
- Under Build and deployment → Source, select GitHub Actions
- No further changes needed — the workflow file is already in
.github/workflows/deploy.yml
The workflow runs automatically on every push to main. Since you just pushed, it should already be running.
To check:
- Click the Actions tab in your repository
- You should see a workflow run called Deploy to GitHub Pages
- Click it to watch live logs
- When both the
buildanddeployjobs show a green ✅, your site is live
Go to https://yourusername.github.io (or your custom repo URL).
It may take 1–2 minutes after the workflow completes for DNS to propagate.
To use www.yourdomain.com or blog.yourdomain.com instead of yourusername.github.io:
Create public/CNAME with your domain on a single line (no https://):
yourdomain.com
site: 'https://yourdomain.com',At your domain registrar, add one of:
| Record type | Name | Value | Use when |
|---|---|---|---|
CNAME |
www |
yourusername.github.io |
using www.yourdomain.com |
A |
@ |
185.199.108.153 (×4 records) |
using apex yourdomain.com |
GitHub's four A record IPs: 185.199.108.153, 185.199.109.153, 185.199.110.153, 185.199.111.153
In Settings → Pages → Custom domain, enter your domain and click Save. GitHub will issue a free TLS certificate automatically (takes a few minutes).
# Create the file
touch src/content/blog/my-post.md
# Edit it, then commit and push
git add src/content/blog/my-post.md
git commit -m "add: my new post"
git pushGitHub Actions deploys automatically. Your post is live in ~1–2 minutes.
Make your changes, then:
git add -A
git commit -m "update: fixed typo in boredom post"
git pushGo to Actions → Deploy to GitHub Pages → Run workflow to trigger a deploy without a code change.
Build fails with "cannot find module" error
npm install # reinstall dependencies
git add package-lock.json
git commit -m "fix: update lockfile"
git pushSite shows 404 after deployment
- Check Settings → Pages → Source is set to GitHub Actions (not a branch)
- Verify the
deployjob completed successfully in the Actions tab - If using a non-root repo, confirm
baseis set inastro.config.mjs
Posts not appearing
- Confirm frontmatter has
title,date, anddescription - Check
draft: trueis not set - Date format must be
YYYY-MM-DD
Theme flashes on load
- This is prevented by the inline script in
Base.astro. If you see a flash, ensure you have not addedasyncordeferto that script tag.
RSS feed returning 404
- The feed is at
/feed.xml— confirmsite:is set correctly inastro.config.mjs
| Layer | Tool |
|---|---|
| Framework | Astro 4 |
| Content | Markdown / MDX via Astro Content Collections |
| Styling | Vanilla CSS with custom properties |
| Font | JetBrains Mono |
| Syntax hl. | Shiki (bundled with Astro) |
| RSS | @astrojs/rss |
| Sitemap | @astrojs/sitemap |
| CI/CD | GitHub Actions |
| Hosting | GitHub Pages |
MIT licence — fork freely.