Personal site for Adam Ferguson — home page, a data-driven résumé, and a Markdown blog. Built with Astro and deployed to GitHub Pages.
npm install
npm run dev # start the dev server (http://localhost:4321)
npm run build # production build → dist/
npm run preview # serve the production build locally| Path | Purpose |
|---|---|
src/data/resume.yaml |
Résumé source of truth (JSON Resume schema, YAML) |
src/data/site.mjs |
Site metadata, navigation, social links |
src/content/blog/ |
Blog posts (one Markdown file each) |
src/layouts/Base.astro |
Global layout (header, footer, theme, styles) |
src/pages/ |
Routes: /, /resume/, /blog/, /about/, etc. |
src/styles/ |
Design tokens (themes.css) + global + print CSS |
public/ |
Static assets: favicon, images, generated résumé PDF |
scripts/ |
Build/pipeline helpers (see below) |
.github/workflows/ |
GitHub Actions: build + deploy to Pages |
The résumé is a single YAML file (src/data/resume.yaml) validated against
the JSON Resume schema. It drives:
- the
/resume/page, - the home-page "Current focus" summary,
- the Person JSON-LD structured data,
- the generated PDF (
public/AdamFerguson-Resume.pdf), and - (optionally) a Word document.
- Edit
src/data/resume.yaml(keep it valid JSON Resume data — see schema). - Validate:
npm run validate:resume - Rebuild the PDF:
npm run pdf - Commit. CI validates the YAML and regenerates the PDF on every deploy.
The PDF is not committed to git — it's generated in CI from the
/resume/page so it always matches the web view (WYSIWYG).
npm run build # (or rely on an existing dist/)
npm run pdf # builds if needed, then renders via PlaywrightRequires Playwright's Chromium: npx playwright install chromium.
Posts are plain Markdown files in src/content/blog/. The filename is the
URL slug (e.g. my-post.md → /blog/my-post/).
Front matter:
---
title: "My post"
description: "A one-line summary shown on index + home."
date: 2026-08-19
tags: [astro, meta]
category: Engineering # optional
draft: false # omit to keep the post published
---- New post: add a file to
src/content/blog/, commit, push. - Tags / categories: derived from front matter; indexed at
/tags/and/categories/. - RSS:
/feed.xml(the old/atom.xmlredirects here). - Drafts: set
draft: trueto exclude from the site and feed.
Design tokens live in src/styles/:
themes.css— Tier-1 reference tokens. Each theme (ferra,slate,ink) is a set of--t-*values, selected via<html data-theme="...">.- Dark/light is an orthogonal axis (
data-mode), not a theme. global.css— maps reference → semantic → component tokens + layout.print.css—@media printrules used for the résumé PDF.
A small floating theme switcher appears in the dev server (npm run dev)
bottom-right. It writes your choice to localStorage
(af-theme, af-mode) and is disabled in production builds. To preview a
specific theme, set the data-theme / data-mode attributes in
src/layouts/Base.astro (the theme / mode consts), or use the dev toggle.
Deploys to GitHub Pages via GitHub Actions (.github/workflows/deploy.yml):
- Validates the résumé YAML.
- Builds the site.
- Renders the résumé PDF with Playwright.
- Uploads
dist/as the Pages artifact and deploys onmaster/main.
Required repo settings (one-time, in the GitHub UI):
- Settings → Pages → Build and deployment → Source: GitHub Actions.
- Settings → Pages → Custom domain:
adam-ferguson.com, with Enforce HTTPS on. (Point the DNS CNAME atadamferguson.github.com.)
The primary domain is adam-ferguson.com; adamferguson.github.com is an
automatic alias.
Old Jekyll URLs are preserved via client-side redirect pages generated in
public/ by scripts/make-redirects.mjs (e.g. /about.html → /about/,
/atom.xml → /feed.xml, legacy posts → /blog/). Re-run it after adding
redirects: node scripts/make-redirects.mjs.
- No analytics (by design, for now).
- Zero runtime JS on content pages; the only scripts are a tiny pre-paint theme hydrator and the dev-only theme toggle.
docs/plan.mdanddocs/planning-prompts.mdhold the approved plan and the original planning prompts/research.