## Summary A cleanup pass across the blog: route and feed bugs fixed, SEO basics added, smaller and lazier images, accessibility fixes, and a real README. No content or design changes beyond small fixes. ## Bug fixes - **Tag/category pages**: static params now use slugs, so pre-rendered pages match the links (before, `/tags/standard-model` was only rendered on demand and the pre-built pages said "Tag not found"). Unknown slugs now return a real 404, and there's a new not-found page. - **Recently updated**: sorts by last modification *before* taking the top 5, so older posts that were updated recently now show up. - **Atom feed**: `rel="self"` now points to `/feed.xml` (was `/feed`); `<updated>` uses the `updated` frontmatter field. - **No-JS flash**: the `no-js` → `js` class swap runs in an inline `<head>` script before first paint, not after hydration. - **Markdown links**: only internal paths go through `next/link`; `#anchors`, `mailto:` and links without an `href` no longer break. - **Invalid HTML**: images in a paragraph together with text (e.g. "**Screenshots:**" followed by images on the Projects page) produced `<p><figure>` and a hydration error. Those paragraphs are now split. - **Dates**: frontmatter dates are normalized to ISO, so `<time datetime>` values are valid. Formatting is pinned to Europe/Prague so a UTC build server can't shift a date by a day. ## SEO - `sitemap.xml` and `robots.txt` - Canonical URLs, Twitter cards and a feed link on every page, built by a shared `pageMetadata()` helper - Generated Open Graph images: a site card, and for each post its cover image or a branded title card (pre-rendered at build time) - `BlogPosting` JSON-LD on post pages ## Performance - Only the post cover and the latest-post image are preloaded (was: every image) - In-post image quality 100 → 90, plus responsive `sizes` - Screenshots downscaled to 1800px wide; `linkboard-old.png` converted to WebP (3.1 MB → 60 KB) - `CodeWrapper` is now a server component; only the copy button ships to the client - Mermaid CDN script pinned to `11.17.2` for reproducible builds ## Accessibility - Accessible names for icon-only links (share, social, mobile nav) and `aria-current` on active nav items - Copy button: uses React state instead of mutating `innerHTML`, handles clipboard errors, announces the result to screen readers, and is no longer inside an `aria-hidden` header - Code-block line numbers hidden from screen readers - Image captions: the Markdown `"title"` becomes the caption; captions that only repeat the alt text are hidden from screen readers ## Content & maintenance - **Frontmatter validation**: a missing or malformed field fails the build and names the file, instead of rendering a broken post - **`draft: true` support**: drafts only render in development - About/Projects/Setup pages share one `MarkdownPage` component; tag/category lists share `PostList` - Markdown rendering is cached, so metadata and page share one render - New README covering features, env vars, frontmatter schema, Markdown extensions and deployment - Removed unused ESLint setup and the unrelated `hast` package; `oxlint`, `oxfmt` and `puppeteer` moved to devDependencies (locked versions unchanged) - Typo fix in the Tessera cover alt text; removed unused `math`/`mermaid` flags from two posts ## Deployment notes - **`SITE_URL` is now required**: the build fails with a clear error if it's missing. - **Puppeteer is a devDependency now**: Nixpacks installs dev dependencies for the build by default, but check that Mermaid diagrams render on the first deploy (the Standard Model post has one). ## Testing - `tsc --noEmit` and `bun run lint` pass - `bun run build` passes; all 81 pages are generated statically - Smoke-tested `next start`: posts, tags, categories, feed, sitemap and OG images return 200; unknown tags and routes return 404 - No block elements nested inside `<p>` in any built page Reviewed-on: #1
3.8 KiB
Ephemeris
Source of blog.hangerthem.com — my personal blog and portfolio. Posts are plain Markdown files rendered to static HTML at build time by a custom unified pipeline.
Features
- Fully static — every post, tag and category page is pre-rendered with Next.js (App Router, React Server Components)
- Custom Markdown extensions via small rehype plugins in
lib/:- Callouts:
> [!TIP],> [!WARNING],> [!INFO],> [!QUOTE] - Highlights:
==marked text== - Definition lists (a term line followed by
: definitionlines) - Code blocks with language label, line numbers and a copy button
- Callouts:
- Math with KaTeX and diagrams with Mermaid, rendered to SVG at build time — no client-side JS for either
- Table of contents generated from
h2/h3headings with scroll-spy - SEO — canonical URLs, Open Graph/Twitter cards, generated OG images, JSON-LD, sitemap, robots.txt and an Atom feed
- Works without JavaScript; JS-only UI (like copy buttons) is hidden when it's unavailable
Stack
Next.js 16 · React 19 · TypeScript · Tailwind CSS 4 · unified (remark/rehype) · highlight.js · KaTeX · Mermaid (via Puppeteer) · Bun · oxlint/oxfmt
Getting started
bun install
SITE_URL=http://localhost:3000 bun dev
| Script | Purpose |
|---|---|
bun dev |
Development server |
bun run build |
Production build (static generation) |
bun start |
Serve the production build |
bun run lint |
oxlint + formatting check |
bun run lint:fix |
Auto-fix lint issues and format |
Environment variables
| Variable | Required | Description |
|---|---|---|
SITE_URL |
yes | Public origin, e.g. https://blog.hangerthem.com |
SOURCE_COMMIT |
no | Commit hash shown in the sidebar (falls back to git rev-parse) |
GIT_ORIGIN_URL |
no | Repository URL; enables the "Source Code" link and commit link |
Writing posts
Posts live in content/posts/; the file name is the URL slug. Standalone pages (About, Projects, Setup) live in content/pages/.
---
title: 'Tessera: A Privacy-First Card Wallet'
description: 120–160 characters work best for search results.
date: 2026-09-18 12:00:00 +0200
updated: 2026-09-18 12:30:00 +0200 # optional
category: Projects
tags: [projects, pwa, privacy]
coverImage: tessera-cover.png # optional, from public/posts/, 1200×630 recommended
coverImageAlt: Tessera logo next to two loyalty cards
math: true # optional, enables KaTeX
mermaid: true # optional, enables Mermaid diagrams
draft: true # optional, only rendered in development
---
Frontmatter is validated at build time, so a missing or malformed field fails the build with the file name instead of producing a broken page.
Images go in public/posts/ and are referenced by file name: .
Project structure
app/ Routes, metadata, feed, sitemap and OG image generation
components/ UI split into content/, layout/, post/ and ui/
content/ Markdown posts and pages
lib/ Markdown pipeline, rehype plugins, tag/category helpers
public/posts/ Post images
Deployment
Deployed with Nixpacks (see nixpacks.toml). Mermaid diagrams are rendered with headless Chromium during the build, so the build environment needs Chromium's system libraries and network access to fetch the Mermaid script.