# Ephemeris Source of [blog.hangerthem.com](https://blog.hangerthem.com) — my personal blog and portfolio. Posts are plain Markdown files rendered to static HTML at build time by a custom [unified](https://unifiedjs.com/) 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/`](lib/): - Callouts: `> [!TIP]`, `> [!WARNING]`, `> [!INFO]`, `> [!QUOTE]` - Highlights: `==marked text==` - Definition lists (a term line followed by `: definition` lines) - Code blocks with language label, line numbers and a copy button - **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`/`h3` headings 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 ```bash 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/`](content/posts/); the file name is the URL slug. Standalone pages (About, Projects, Setup) live in [`content/pages/`](content/pages/). ```yaml --- 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/`](public/posts/) and are referenced by file name: `![Alt text](image.png "Optional caption")`. ## 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](https://nixpacks.com/) (see [`nixpacks.toml`](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.