# Personal Blog Template — Research Brief

> **Assumption:** Long-form personal blog for a technically fluent indie writer — an engineer, designer, or researcher who publishes substantive essays on their own domain. Not a corporate devrel blog. Not a portfolio homepage that also has a blog tab. The blog IS the site.

> **Persona:** A senior software engineer who writes long-form reflections on building systems, technical trade-offs, and the craft of programming — think "what I learned shipping this in production" meets "here's why I changed my mind about X." Placeholder posts are anchored in this voice.

---

## Audience & primary CTA
- **Who visits:** Engineers, designers, and curious generalists following RSS feeds, Hacker News "Show HN" posts, or a personal referral. They are reading, not buying. Dwell time is the metric.
- **Primary CTA:** Subscribe via RSS (or email newsletter widget, Buttondown-powered) — capture the repeat reader.
- **Secondary CTAs:** Share post link; browse by tag; view About page; follow on GitHub/X.

---

## Reference sites (real businesses)

- https://maggieappleton.com — Essay/Note distinction is the design system: essays are polished long-form, notes are seedlings. Visual thumbnails per post. Digital garden framing. Generous whitespace. Illustrative graphics per essay rather than stock photos. The clearest model for typed content collections.
- https://jvns.ca — Categorical sidebar navigation, starred posts signal canonical reads, thematic grouping over pure chronology. Extremely low visual complexity. Proves that a no-decoration, text-first grid earns trust and return readers.
- https://www.dbreunig.com — Two-tier post listing: featured (curated) + recent. Minimal bio in hero, clean chronological list, no pagination. Shows that a bio + writing index is all a homepage needs.
- https://patrickcollison.com — Flat-nav minimalism. The entire site is text links. No imagery. High-trust through restraint. Demonstrates that negative space and correct hierarchy read as "serious thinker."
- https://joelcalifa.com — First-person voice breaks the fourth wall. Short-form bio with CTAs embedded inline. Sparse blog list with infrequent but substantive posts. Shows that low post volume is a feature if signaled correctly ("I don't write often, but when I do it counts").
- https://www.robinsloan.com — Treats the site as a living index of work across eras. Serif-led, newsletter-anchored, "starred entries" editorial curation. Proves the /now page and colophon are legitimate reader anchors, not vanity pages.

---

## Existing templates surveyed

- https://astro.build/themes/details/blog/ — "Ultra-minimal" Astro starter. Ships with no dark mode, no serif typography, no RSS, no tag pages, no reading time. A blank canvas, not a finished blog. Strength: official, well-documented. Weakness: ships nothing the niche actually needs.
- https://github.com/Charca/astro-blog-template — Community Astro blog. Sans-serif, light-only, basic Tailwind prose styles. No code-block language labels, no footnote support. Passable but generic.
- Hugo PaperMod — Most-cloned Hugo blog theme. Good performance, built-in search. Dark mode present but toggle is an afterthought. Typography is Inter throughout — no serif option. Code blocks are functional not beautiful. Feels like a developer theme cosplaying as a writer theme. Design is dated (2021 era).
- Eleventy eleventy-base-blog — Bare-bones. No Tailwind, no dark mode, no reading time, no RSS full-content. Proof of concept, not a template.
- Ghost default Casper — Beautiful serif-led reading experience but requires Ghost CMS (paid hosting, Node.js). Static export is partial. Not GH Pages compatible without workarounds.
- **Saturated pattern:** Dark sans-serif "hacker blog" with monospace everywhere, teal or orange accent, GitHub-green code blocks. Fills the astro-themes directory. Explicitly what we are not building.
- **Missing:** A serif-first, content-focused Astro blog with typed MDX frontmatter, per-post code-block theming, a /now page, a /tags index, full-content RSS, and a dark mode tuned for serif reading — GH Pages deployable, MIT licensed, zero CMS dependency.

---

## Must-have sections (in order)

### Pages
1. **Home (`/`)** — Brief bio (2–3 lines, first-person), then post list. Two-tier: pinned/featured posts (manual `featured: true` frontmatter flag) above a chronological "all posts" list with title, date, tags, reading time. No sidebar. Single column ~680px max-width.
2. **Post page (`/blog/[slug]`)** — Title, date, reading time badge, tag chips, author byline, then body. Pull-quote blockquote styling. Code blocks with language label + copy button. Footnotes rendered at bottom with back-link anchors. Estimated reading time inline under the title.
3. **Tag index (`/tags/`)** — Alphabetical list of all tags with post counts. Each tag links to its feed.
4. **Tag feed (`/tags/[tag]`)** — Same post-list layout as home, filtered to tag. Shows tag name as H1.
5. **About (`/about`)** — Longer bio (2–4 paragraphs), current focus, social links. Schema `Person` here.
6. **Now (`/now`)** — Derek Sivers-style: what the author is working on, reading, thinking about. Updated periodically. Static MDX page, no special structure needed. A single column essay with a "Last updated" date.
7. **RSS feed (`/rss.xml`)** — Full-content RSS 2.0. Auto-linked in `<head>` via `<link rel="alternate">`.

### Not building
- Archive page — the tag index + home chronological list is sufficient; a separate /archive adds navigation debt.
- Newsletter subscription page — embed a single Buttondown iframe or `<form>` block in the home sidebar if the author wants it; it is not a page.
- Comments — no server; no Disqus (privacy tax); no third-party script injection. External link to GitHub Discussions is the static substitute if needed.

---

## Design conventions

- **Typography:** **Newsreader** (Google Fonts, variable, optical sizes) for all body copy and post headings — designed explicitly for on-screen long-form reading, with italics that actually look good (critical for pull quotes and emphasis). UI elements (nav, meta lines, tags, reading time, buttons) use **Inter** variable. Heading weight: Newsreader 700. Body: Newsreader 400, `font-size: 1.125rem`, `line-height: 1.75`, `max-width: 68ch`. This stack is entirely distinct from Days 1–4 (Geist, Inter-only, DM Sans).
- **Color palette tendency:** Light-default. Base: warm white `#FAFAF7` (slightly warm, not pure white — reduces eye strain on long reads). Body text: deep ink `#1A1A18`. Accent: **deep forest green** `#1C4A2E` for links, tag chips, and the RSS icon. Secondary text: `#6B6B63` (warm gray). No electric, no violet, no amber, no coral — all taken. Forest green on warm white reads as "considered, analog, literary." Dark mode: base `#141410` (warm near-black), body text `#E8E6DF` (warm off-white — slightly reduced contrast to ease serif weight in dark), accent lightens to `#4A9B65`.
- **Imagery style:** No hero images. Posts carry an optional `hero` frontmatter field (path to a committed image); if absent, the post page has no image — and that is fine. The /about page has one photo. No stock photography anywhere. Code blocks and pull quotes are the visual anchors in post pages.
- **Density:** Reader-first, sparse. Home page: 680px column, large post titles, generous vertical rhythm. A 2,000-word essay should have no horizontal chrome competing for attention. Navigation is one line: logo/name left, "About · Now · RSS" right.

---

## Trust signals to include

- Author photo on /about (committed as `src/assets/avatar.jpg`, used in schema `Person` and About page — not on the post page itself)
- Post count + date range on home ("32 posts since 2021") — built at compile time
- RSS feed linked in `<head>` AND visible in nav — signals serious, long-term publishing
- `/now` page with a visible "Last updated" date — proof the site is alive
- `rel="me"` links on social hrefs for web-of-trust (Mastodon verification compatibility)

---

## SEO / schema.org

- **Type per page:**
  - Home / About: `Person` — `name`, `url`, `sameAs[]` (GitHub, X/Twitter, LinkedIn, Mastodon), `description`, `jobTitle`
  - Post page: `BlogPosting` — `headline`, `description`, `datePublished`, `dateModified`, `author` (ref to `Person`), `url`, `keywords` (from tags), `wordCount`, `timeRequired` (ISO 8601 duration, e.g. `PT9M`)
  - Tag feed: `CollectionPage` — `name`, `description`, `url`
  - Site-wide: `WebSite` with `potentialAction: SearchAction` stub (even if search isn't implemented — it's a forward-compatible signal)
- **Required properties on `BlogPosting`:** `headline`, `url`, `datePublished`, `author.name`, `description`. `dateModified` uses `updatedDate` frontmatter if present, falls back to `pubDate`.
- **Suggested OG image:** The post title in Newsreader italic on warm white background, the author name below in Inter, a forest green top-border stripe. Generated at build time via `satori` using a committed OG template component; falls back to a static `og-default.png` if `satori` is not configured.

---

## Content collection frontmatter (canonical)

```ts
// src/content/config.ts
const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title:       z.string(),
    description: z.string(),
    pubDate:     z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    tags:        z.array(z.string()).default([]),
    draft:       z.boolean().default(false),
    featured:    z.boolean().default(false),
    hero:        z.string().optional(),   // relative path to committed image
    ogImage:     z.string().optional(),   // override auto-generated OG
    // readingTime is NOT a frontmatter field — computed at build via reading-time npm pkg
  }),
});
```

Reading time is computed in the Astro page component using `reading-time` on `post.body`. It is injected into page data, not stored in frontmatter, so it always reflects the actual prose.

---

## RSS feed conventions

- Format: **RSS 2.0** via `@astrojs/rss`. Not Atom. Rationale: broader reader compatibility; Atom support in `@astrojs/rss` requires extra namespace wiring with marginal benefit for the target audience.
- Content: **Full post HTML** in `<content:encoded>` using `sanitize-html` to strip unsupported tags. Reason: the target reader uses an RSS reader as their primary reading surface (Reeder, NetNewsWire, Readwise Reader). Excerpt-only RSS is a reader-hostile pattern; we reject it.
- `customData` includes `<language>en-us</language>` and `<atom:link>` self-reference for feed validators.

---

## Tag system

- Generated dynamically at build from all non-draft posts' `tags[]` via `getStaticPaths`.
- Tags are slugified at display time (`toLowerCase().replace(/\s+/g, '-')`); stored as human-readable in frontmatter (`"Build Systems"` → URL `/tags/build-systems`).
- Tag index sorts by post count descending. Tag feed pages sort by `pubDate` descending.
- Tags render as small pill chips on post cards and post pages. Max 4 chips shown on post cards; overflow hidden with `+N` label.

---

## Code block treatment

- **Renderer:** Astro's built-in Shiki with dual theme: `github-light` for light mode, `vesper` (dark, warm-toned) for dark mode — coordinated via CSS `light-dark()`. Vesper is warm (amber/ochre syntax colors on near-black) and avoids the cold-blue aesthetic of `github-dark`.
- **Language label:** Yes — top-right corner of the code block, small Inter caps, `opacity: 0.6`. It disappears on hover to show the copy button.
- **Copy button:** Yes — top-right on hover, `aria-label="Copy code"`, SVG icon, no JS framework required.
- **Line numbers:** Off by default. Author can enable per-block with `{showLineNumbers}` meta string. Rationale: line numbers add visual noise to short snippets (majority of use), but are needed for long referenced files.
- **No Twoslash.** Adds significant bundle complexity; the persona writes explanatory prose, not API reference docs. Revisit in v2 if there is demand.

---

## Footnotes

`remark-gfm` is sufficient. GFM footnotes (`[^1]`) render as numbered superscripts with a `<section>` block at the bottom. The default HTML output is complete and accessible. `remark-numbered-footnote-labels` only re-labels user-string refs as numbers — since we are already using numeric refs (`[^1]`, `[^2]`), the extra plugin adds no value and one more dependency. CSS handles the visual treatment: footnote back-links get `font-size: 0.75rem`, `color: accent-green`, and a horizontal rule separator above the footnote list.

---

## Reading time rendering

- On **post cards** (home, tag feed): appears in the meta line alongside the date — `Jan 14, 2025 · 9 min read` — in `#6B6B63` Inter small.
- On **the post page itself**: appears directly under the post title, before the tag chips — same style. It is NOT repeated in the post body or after the article. One placement in the page header is enough.
- Implementation: `import readingTime from 'reading-time'` called on `post.body` in the layout component, result passed as a prop.

---

## Dark/light mode

- **Default:** System preference (`prefers-color-scheme`). A toggle button in the nav (sun/moon icon) overrides and persists preference to `localStorage`. Light-default for SSR/no-JS contexts (warm white base is the safe fallback).
- **Serif dark mode treatment:** Body text in dark mode uses `#E8E6DF` (not pure `#FFFFFF`) — the reduced contrast (~15:1 instead of 21:1) prevents the "vibrating text" effect that serif strokes produce on pure white over black. Font-weight stays 400; do NOT lighten to 300 in dark mode (thin serifs break on low-DPI screens).
- **Code block:** The `vesper` Shiki theme is the dark-mode code palette — warm amber/ochre syntax on `#101010` — intentionally warmer than the blue-cold schemes in Days 1–3.

---

## Static-only fit

No concerns. All features work at build time or client-side:
- **Reading time:** computed at build from `post.body`, injected as a static string. Zero runtime cost.
- **Tag pages:** `getStaticPaths` at build. No dynamic routing needed.
- **RSS:** Astro endpoint (`src/pages/rss.xml.ts`) generates at build. Full-content HTML included.
- **Dark mode toggle:** `localStorage` + `class` toggle on `<html>`. No server.
- **Draft posts:** filtered at build via `draft: true` frontmatter; never emitted to output.
- **Now page:** static MDX with a manually maintained `updatedDate` at the top of the file. No CMS, no webhook needed. The operator edits the file and redeploys.
- **No newsletter form on the page by default.** If the author wants email capture, they drop a Buttondown `<iframe>` or form snippet into their About/Now MDX. We do not wire a form provider into the template core — this blog's primary subscription channel is RSS, not email.

---

## Differentiation — what we'll do better

1. **Serif-first reading experience:** No other free Astro blog template ships Newsreader with correct optical sizing, warm-white base, and 68ch measure as defaults. Every competitor defaults to Inter or Geist everywhere.
2. **Warm dual-theme code blocks:** `github-light` + `vesper` pairing — warm in both modes. Most templates use `github-dark` in dark mode (cold blue) which clashes with a warm editorial palette.
3. **Full-content RSS out of the box:** `@astrojs/rss` with `sanitize-html`, `<content:encoded>`, and a `<link rel="alternate">` in `<head>`. The Astro blog starter ships no RSS at all. PaperMod ships excerpt-only by default.
4. **Typed frontmatter with build-time reading time:** `zod` schema in `content/config.ts` catches frontmatter errors at build, not at runtime. Reading time is auto-computed — never stale, never missing.
5. **`/now` page as first-class route:** Static MDX page with "Last updated" date. None of the surveyed templates include it. For an indie writer, it is a trust signal and a retention hook (readers come back to check it).
6. **No dark-default:** Every "hacker blog" template defaults to dark. Light-default with a system-pref toggle is the correct choice for a reading-focused blog — it respects the reader's context (office, e-reader mode, print).
7. **MIT + GH Pages one-click:** Ghost requires paid hosting. PaperMod is Hugo (different toolchain). Astro starter needs base-path config. We ship the `pages.yml` workflow and correct `base` config pre-wired.

---

## Explicitly NOT building

- **Draft admin UI or CMS**: Authors edit MDX locally. `draft: true` suppresses the post from build output. No Netlify CMS, no Decap, no Keystatic wiring — adds complexity, breaks the "one repo, one deploy" contract.
- **Comments (Disqus, giscus, utterances)**: Each injects third-party scripts with tracking or GitHub authentication requirements. They conflict with the site's "no information collected" posture. Not in core; author can add giscus manually if they want it.
- **On-page like / clap buttons**: Medium-style engagement widgets. Require a third-party service or a server. Counter to the "slow, well-edited prose" register.
- **Cross-post automation (DEV.to, Medium, Hashnode)**: Out of scope for a static template. The author's canonical URL is their own domain; syndicating is a workflow choice, not a template concern.
- **Full-text search**: Pagefind or Fuse.js would add ~40KB and a build step. The tag index + browser `Cmd+F` covers 95% of the use case for a single-author blog with <200 posts. Search can be added in v2.
- **Social share buttons**: Share links (Twitter/X, LinkedIn) are explicitly excluded. They inject tracking pixels and add visual noise. The author's RSS feed and their own social presence handle distribution.
