# personal-blog

A free, MIT-licensed **personal essay blog** template for indie writers — engineers, designers, researchers — who publish long-form on their own domain. Built with Astro 4 + Tailwind CSS + MDX.

**Light-default. Newsreader serif body. Forest-green accent. Full-content RSS.**

![Preview](./preview.png)

**Live demo:** [templates.allanninal.dev/personal-blog/](https://templates.allanninal.dev/personal-blog/)

---

## Features

- **Serif-first reading experience** — [Newsreader](https://fonts.google.com/specimen/Newsreader) (variable, with optical sizes) for body and post headings; Inter for the chrome (nav, meta lines, tags). 68ch measure, 1.75 line-height, generous vertical rhythm. Designed to be read.
- **Light-default, system-aware dark mode** — warm cream `#FAFAF7` base with deep ink text. Dark mode uses warm off-white `#E8E6DF` (not pure white) to prevent the "vibrating stroke" effect that serifs produce on high-contrast dark. Follows `prefers-color-scheme`, with a header toggle that persists to `localStorage`.
- **Forest-green accent (`#1C4A2E`)** — passes WCAG AA on warm white at 8.4:1. Lightens to `#62B47D` in dark mode. Used for links, eyebrows, and the RSS icon. A deliberate visual break from the lime/violet/amber/coral palettes of earlier templates.
- **Astro Content Collections with typed frontmatter** — `zod` schema in `src/content/config.ts` validates every post at build time. Fields: `title`, `description`, `pubDate`, `updatedDate`, `tags[]`, `draft`, `featured`, `hero`, `ogImage`. Reading time is auto-computed at build via [`reading-time`](https://github.com/ngryman/reading-time) — never stale, never missing.
- **Full-content RSS** — `@astrojs/rss` + `sanitize-html` emit each post's complete HTML in `<content:encoded>`. Subscribers in Reeder, NetNewsWire, or Readwise Reader get the entire essay in their feed reader instead of being forced back to your site for excerpts. Linked in `<head>` via `<link rel="alternate">`.
- **Tag pages** — generated at build from frontmatter. Tags are stored as human-readable in MDX (`"Build Systems"`) and slugified for URLs (`/tags/build-systems/`). Index page sorts by post count desc; tag feed sorts chronologically.
- **`/now` page** — Sivers-style "what I'm doing now" with a maintained "Last updated" date. A static MDX page, not a collection — you edit it like a document, not a blog post. Trust signal that the site is alive.
- **Dual-theme code blocks** — Astro's built-in Shiki with `github-light` (light) and `vesper` (dark, warm amber/ochre). Most blog templates use `github-dark` in dark mode (cold blue) which clashes with a warm editorial palette. Vesper coordinates.
- **Footnotes** — `remark-gfm` ships GFM footnotes (`[^1]`) with proper backref links, styled in serif with a quiet "Notes" eyebrow above the footnote section.
- **Schema.org per page type** — `WebSite` site-wide; `Person` on home and About; `BlogPosting` on post pages with `wordCount` + `timeRequired` (ISO 8601 duration); `CollectionPage` on tag indexes.
- **No comments, no third-party scripts, no tracking** — by design. The reader's RSS feed is the subscription channel.
- **GitHub Pages one-click** — base path resolved from `GITHUB_REPOSITORY` automatically.
- **Full test suite** — 84 Playwright tests: page rendering, JSON-LD shape, RSS full-content marker, OG type per page, all six routes through axe in both themes, theme toggle persistence, post body uses `#E8E6DF` in dark, no horizontal scroll at 4 viewports, prose-blog max-width on desktop, LinkedIn-first footer ordering, broken-link checking, Lighthouse ≥ 95 on Performance / Accessibility / Best Practices / SEO.

## Stack

- **Framework:** Astro 4 (static output)
- **Markdown:** MDX with `remark-gfm` (footnotes, tables, strikethrough)
- **Styles:** Tailwind CSS, CSS custom properties for the dual-theme tokens
- **Fonts:** Newsreader Variable + Inter Variable (self-hosted via @fontsource-variable)
- **Code highlighting:** Shiki dual theme (`github-light` + `vesper`), built into Astro
- **RSS:** `@astrojs/rss` + `sanitize-html` for full-content feeds
- **Reading time:** `reading-time` package, computed at build
- **Testing:** Playwright, @axe-core/playwright, Lighthouse CI, linkinator

## Quick start

```bash
unzip personal-blog-source.zip -d my-blog
cd my-blog
npm install
cp .env.example .env
npm run dev
```

## Customize

### 1. Site identity & nav

Edit `src/data/site.ts`:

```ts
export const site = {
  name: 'Slow Dispatch',
  tagline: 'Long-form notes on building software, slowly and with care.',
  description: '...',
  language: 'en-us',
  foundedYear: 2024,
  nav: [
    { label: 'Essays', href: '/' },
    { label: 'Tags',   href: '/tags/' },
    { label: 'Now',    href: '/now/' },
    { label: 'About',  href: '/about/' },
  ],
};
```

### 2. Author profile

Same file, `author` export:

```ts
export const author = {
  firstName: 'Allan',
  name: 'Allan Ninal',
  jobTitle: 'Senior Software Engineer',
  shortBio: '...',
  longBio: '...',
  location: 'Toronto, ON',
  email: 'you@yourdomain.com',
  socials: {
    linkedin: '...',
    github:   '...',
    x:        '...',
    mastodon: '',  // for rel=me web-of-trust
  },
  knowsAbout: ['software engineering', 'distributed systems', ...],
};
```

LinkedIn is listed first in the footer per this project's contact-priority convention.

### 3. Writing posts

Drop MDX files in `src/content/blog/`. Each post needs frontmatter:

```mdx
---
title: "How I changed my mind about caching"
description: "An essay on the most expensive engineering mistake I've made repeatedly."
pubDate: 2026-04-22
updatedDate: 2026-04-25       # optional
tags: ["Performance", "Pragmatism"]
featured: true                 # optional — bumps to the Featured section on home
draft: false                   # optional — true hides from build output
hero: "/images/hero.jpg"       # optional — currently rendered only on the post card
ogImage: "/og-custom.png"      # optional — overrides default OG image
---

Body of the essay in MDX. You can use:

- GFM footnotes[^1]
- Code blocks with triple backticks (Shiki dual-theme syntax highlighting)
- Tables, strikethrough, task lists (via remark-gfm)
- Any HTML / JSX components (it's MDX)

[^1]: Footnote text down here. Renders at the bottom with a backref.
```

The schema in `src/content/config.ts` validates frontmatter at build — typos fail the build.

### 4. Tags

Tags are human-readable strings in frontmatter (`"Build Systems"`). The build:
- Slugifies them to URLs (`/tags/build-systems/`)
- Generates a tag index at `/tags/`
- Generates per-tag feed pages
- Includes them as `categories` in the RSS feed

### 5. The `/now` page

Edit `src/pages/now.mdx`. The page is a single MDX file with a `lastUpdated` frontmatter date that renders under the title. Wrap in whatever sections you want — there's no schema.

### 6. RSS

The feed is generated at `src/pages/rss.xml.ts`. It emits **full-content** RSS 2.0 with `<content:encoded>`. To switch to excerpt-only (not recommended), change the `content: contentHtml || post.data.description` line.

The feed is auto-linked in every page's `<head>`:

```html
<link rel="alternate" type="application/rss+xml" title="Slow Dispatch — Essays" href="/rss.xml" />
```

### 7. Theme tokens

In `src/styles/global.css`, the `:root` block defines the light palette and `html.dark` defines the dark palette. The token surface is the canonical one across this project — `--color-bg`, `--color-surface`, `--color-surface-2`, `--color-text`, `--color-text-muted`, `--color-accent`, `--color-accent-hover`, `--color-on-accent`, `--color-border`. Swap `--color-accent` for a different niche register; the rest of the design adapts automatically.

If you change the accent, **verify it passes WCAG AA against `--color-bg`, `--color-surface`, and `--color-surface-2` in both modes**. The dark accent must also work on the reader's actual eye after a 2,000-word essay — what looks fine for 30 seconds in your editor may not work for 12 minutes of reading.

### 8. Code block theming

Edit `astro.config.mjs`:

```js
shikiConfig: {
  themes: { light: 'github-light', dark: 'vesper' },
  wrap: false,
},
```

Available themes: any [Shiki theme](https://shiki.style/themes). For a more colorful dark theme, try `dracula` or `night-owl`. For minimal monochrome, try `min-light` + `min-dark`.

---

## Deploy to GitHub Pages

> The source repo is private — there is no Fork button. Download the source ZIP from [templates.allanninal.dev/personal-blog/](https://templates.allanninal.dev/personal-blog/) and follow these steps.

1. **Unzip + init**:

   ```bash
   unzip personal-blog-source.zip -d my-blog
   cd my-blog
   git init && git add . && git commit -m "init from personal-blog template"
   ```

2. **Push**:

   ```bash
   git remote add origin https://github.com/<you>/<your-repo>.git
   git push -u origin main
   ```

3. **Settings → Pages → Source: GitHub Actions.** The included `pages.yml` builds and deploys on every push to `main`.

### Custom domain

1. Add a `CNAME` file in `public/` with your domain.
2. In repo Settings → Variables → Actions, set `PUBLIC_SITE_URL` and `PUBLIC_BASE_PATH=/`.

---

## Testing

```bash
npm run test:all
# = npm run build && npm run test && npm run test:links && npm run test:lighthouse
```

| Suite | What it covers |
|---|---|
| `tests/pages.spec.ts` | Every route renders, h1 + footer, no console errors; meta description; WebSite + Person JSON-LD on home; BlogPosting JSON-LD with `wordCount` + `timeRequired` on post pages; CollectionPage JSON-LD on tag pages; OG type per page; canonical absolute URLs; RSS link in head; RSS reachable + full-content marker; sitemap; robots.txt |
| `tests/interactions.spec.ts` | Theme toggle (flip + persist); ≥3 post cards on home; reading time on every card; post page chrome (title, date, reading-time, tag chips); prose-blog uses Newsreader; GFM footnotes render with backref; tag index lists tags + counts; tag feed filters correctly; header nav has all expected items; LinkedIn first in footer |
| `tests/responsive.spec.ts` | No horizontal scroll at 375/768/1280/1920px on every route; post body ≤820px wide on desktop; theme toggle hits 28px tap target |
| `tests/theme.spec.ts` | Every route renders correctly in both themes; default mode respects system pref + localStorage; dark-mode body color is `#E8E6DF` (warm off-white, NOT pure white) |
| `tests/a11y.spec.ts` | Zero axe violations on every route in both themes |
| `tests/links.spec.ts` | Externals open safely (`rel="noopener noreferrer"`); internal links resolve; mailto well-formed; post pages link back to home |

---

## Support and donations

This template is **free and MIT-licensed**. Use it for personal, commercial, or client work — no permission or attribution required.

If it saved you time: [**Ko-fi — ko-fi.com/allanninal**](https://ko-fi.com/allanninal).

### For cloning users

- The GitHub "Sponsor" button on your fork is driven by `.github/FUNDING.yml`. Edit `ko_fi: allanninal` to point at your own handle, or delete the file to hide the button.
- The footer "Support the maker" Ko-fi link is driven by `PUBLIC_AUTHOR_DONATE_URL`. Set it blank to hide it.

---

## License

MIT — see [LICENSE](./LICENSE).

Part of the [365-template series](https://templates.allanninal.dev/) — Day 5 of 365 free, MIT-licensed static website templates.
