Files
andwati.com/TODO.md
T
andwati 12dd32ea4f Polish nav, transitions, layout, SEO, a11y, LaTeX, and Lighthouse CI
Nav bar & layout consistency
- Base.astro's `wide` layout now defaults on for every page (was opt-in),
  fixing the nav bar wrapping differently depending on page width
- Icon buttons for search and theme toggle (magnifying glass, sun/moon),
  replacing the plain "Search" text link and the "◐" glyph
- Search is now a Cmd/Ctrl+K modal over Pagefind (lazy-loaded on first
  open) instead of a separate /search/ page, matching the old site
- Fixed the article page's TOC sitting with unwanted left padding relative
  to the header (grid was center-justified instead of start-justified)

Smooth navigation
- Added Astro's ClientRouter for animated page transitions
- Every page-local script that only ran once at module load (three.js
  hero, minimap, comments, book-spine carousel, scroll-to-top, reading
  progress, theme toggle, Pagefind init) now re-initializes via
  astro:page-load, with AbortController-based cleanup so window/document
  listeners don't accumulate across client-side navigations

Restored from the legacy Zola site
- Reading progress bar (article pages) and scroll-to-top button
- Share buttons (X, Bluesky, LinkedIn, Reddit, HN, email) on writings
- Prev/next post navigation with the old label/title layout
- Utterances comments, wired to andwati/andwati.com with dark/light
  theme sync
- YouTube embeds: any plain <iframe src="...youtube-nocookie.com...">
  is now auto-wrapped in a responsive 16:9 container, gets
  loading="lazy", and emits VideoObject JSON-LD — no shortcode needed
- LaTeX: $inline$ and $$block$$ math renders to static HTML via KaTeX at
  build time (no client-side JS shipped for it)
- Tags page redesigned as a centered, alphabetical tag cloud

Removed the related-posts section per request.

Accessibility & contrast
- --accent (#d1502f) measured 4.0:1 on the light background, just under
  WCAG AA's 4.5:1 for normal text — darkened to #a84025 (5.7:1), plus
  matching light-mode-only fixes for the two hljs syntax colors and the
  tip/warning callout colors that had the same problem in light mode
- Fixed heading-order violations (h1 straight to h3 with nothing at h2)
  across the homepage, writings/portfolio/bookshelf/blogs lists, and tag
  pages — introduced a shared .entry-title class for the h2 list titles
- Fixed inconsistent title-link underline colors: a plain <a> carries its
  own default underline (in its own text color) instead of matching its
  parent heading's accent-colored one — h1/h2/h3 anchors now explicitly
  inherit the heading's decoration
- Bookshelf spine colors are generated per-title from a hash; the
  lightness-based text-color heuristic could still produce sub-4.5:1
  contrast for some hues (e.g. green) — now computes real WCAG contrast
  and adjusts

Performance
- The three.js homepage hero was the single largest main-thread cost
  site-wide (dragging that page's Lighthouse performance score to the low
  80s while every other page scored 95+): three.js is now dynamically
  imported only once the browser is idle (WebGL-support check split into
  its own tiny module so unsupported browsers never fetch it at all), the
  render loop is capped to ~30fps, and hover raycasting only runs on an
  actual pointer move instead of every frame

SEO & structured data
- Self-referential canonical links site-wide (with override support for
  posts first published elsewhere)
- Sitewide WebSite/Person JSON-LD, plus per-article BlogPosting,
  BreadcrumbList, and (when applicable) VideoObject JSON-LD
- robots.txt explicitly welcomes AI/LLM crawlers (GPTBot, ClaudeBot,
  PerplexityBot, Google-Extended, CCBot, etc.) and points at llms.txt
- Added an Atom feed (RSS and JSON Feed already existed)
- Sitemap now sets <lastmod> per entry from its own date/updated
  frontmatter instead of leaving every URL at build time

Analytics & comments infra
- Self-hosted Umami wiring via PUBLIC_UMAMI_SCRIPT_URL/WEBSITE_ID
  (apps/site/.env.example), no-ops until both are set
- sws.toml's CSP updated to allow utteranc.es and analytics.andwati.com —
  without this both were silently blocked in production

Lighthouse CI
- @lhci/cli wired up against the built site (mobile + desktop configs),
  `pnpm run lighthouse`, and a GitHub Actions workflow to run it on every
  push/PR — reporting only for now, no failing thresholds
2026-09-12 20:40:16 +03:00

12 KiB

Astro + Strapi Rebuild — TODO

Tracking doc for the astro-migration branch. See the locked spec in the approved plan for full rationale on each decision referenced below.

Done

  • Scaffold pnpm workspace (apps/site Astro, apps/cms Strapi + SQLite)
  • Switch tooling to pnpm-only, add Biome for lint/format
  • Define shared content/ schema (writings, portfolio, bookshelf, blogs) with TOML frontmatter matching the legacy Zola format
  • Bookshelf schema covers both books (ISBN) and papers (DOI/arXiv id)
  • Custom Astro content loader parsing +++ TOML frontmatter directly (no YAML conversion step)
  • Basic list/detail pages for all four content types, verified running end-to-end in-browser (pnpm --filter site dev)
  • CLAUDE.md updated: no agent self-attribution in commits/PRs, commit regularly after each working step
  • Strapi content-type definitions matching the content/ schema exactly (writings, portfolio, bookshelf, blogs) — verified boots and registers all four REST routes
  • apps/cms/Dockerfile.dev + root docker-compose.yml for running Strapi locally in a container (node:24-bookworm-slim, matches local Node version); apps/cms/Dockerfile production multi-stage build for later Dokploy deployment. Verified: builds, boots, admin panel and API reachable, shared content/ volume mounted at the right relative path

Content pipeline

  • Strapi custom provider/lifecycle hooks that read + write the same Markdown files in content/ (Strapi's DB stays a disposable cache) — apps/cms/src/utils/content-{file,sync}.ts, wired in src/index.ts
  • Verify round-trip: edit in Strapi admin → file on disk updates, and vice versa, without drift — verified create/update/delete via the content-manager API, plus idempotent re-import across a restart
  • Bookshelf ISBN lookup hook with local cover caching — apps/cms/src/utils/bookshelf-covers.ts fetches from Open Library by ISBN and caches into apps/site/public/covers/; verified end-to-end (real cover downloaded, cover_image synced to file, cleaned up on delete)
  • Bookshelf paper lookup hook — Semantic Scholar backfills title/authors by DOI/arXiv id when missing. No cover/thumbnail: neither Semantic Scholar nor arXiv offers a reliable image source for papers, so cover_image stays manually-set for papers (or gets one later from the OG-image pipeline below)
  • OG image auto-generation extended to all three content types (writings, portfolio, bookshelf) using the new design system — scripts/generate-og-images.astro-site.mjs, wired into apps/site's build script; Base.astro renders og:image/twitter:image from a per-page ogImage prop (writings/bookshelf detail pages wire it in; portfolio has no detail page yet to wire it into). Uses static-weight Fraunces/Newsreader font packages, not the variable fonts the site ships — satori's opentype.js parser can't read the variable fvar table
  • llms.txt generation extended to cover all content types — unblocked once site.config.ts existed (built above for feeds/meta anyway): scripts/generate-llms.astro-site.mjs reads all four content directories directly, wired into apps/site's build script

Design system

  • New typography scale + color system — dark-first (warm near-black, cream text, coral accent) with a warm-paper light mode; Fraunces (display/italic) + Newsreader (body) + JetBrains Mono (nav/meta/code), fluid clamp() type scale. src/styles/tokens.css. Referenced pixperk.tech and mcyoung.xyz — see the commit for specifics
  • Motion primitives: CSS + native Web Animations API spring utility — apps/site/src/lib/spring.ts (damped-oscillator keyframes, prefers-reduced-motion aware). Not wired into any component yet — the Three.js centerpiece below is the natural first user
  • Base layout/components replacing the placeholder Base.astro — real header/nav with active-link state, theme toggle, footer with social links. Verified light + dark in-browser across home, a writing detail page (TOC/headings), bookshelf, and 404
  • Homepage redesign around the Three.js centerpiece as hero — the constellation sits above a 3-column dashboard (latest writings, portfolio, bookshelf), verified rendering and hover/click working in-browser
  • New 404 page concept (terminal easter egg retired) — "Page Fault", now fully styled with the rest of the design system

Three.js centerpiece

  • Constellation data model: nodes are real content entries (writings, portfolio, bookshelf), edges are shared-tag connections — the same relationship "related posts" already uses, drawn instead of listed. No decorative/fake data. src/lib/constellation-data.ts
  • Built as an isolated component (Constellation.astro + a plain client <script> module, no framework), fibonacci-sphere layout, hover raycasting with a title tooltip, click-through to the entry
  • Fallback for prefers-reduced-motion (skips rotation/entrance animation) and no-WebGL (supportsWebGL() check hides the canvas entirely, falls back to a <noscript>-style plain count)
  • Perf budget check — scene is trivially light (24 nodes, 12x12-segment spheres, a handful of line-segment edges: well under 10k triangles), renders at 100+fps uncapped with no dropped frames. The real cost is the JS payload: three.js core pulls in ~130KB gzip / ~106KB brotli regardless of import * vs named imports (sideEffects: false already lets Rollup tree-shake fully; tried named imports, no size change). That script is a deferred module (doesn't block first paint/DCL) and the page was interactive in ~270ms in testing. No code changes made — budget is fine as shipped

Content features carried over from the Zola site

  • Series-nav (multi-part post series) — writings/[...slug].astro, uses the existing extra.series/extra.series_index fields
  • Table of contents — sticky sidebar on desktop (>=74rem), falls back to the original inline list below the tags on narrower viewports; same heading data rendered via a shared TocList.astro so the two aren't duplicated markup by hand. .container--wide bumped 64rem → 76rem so big monitors use more of the screen (prose column itself stays capped at 42rem for readability — the extra width is the TOC gutter); verified at 390/900/1180/1920px with no distortion at any width
  • Prev/next post navigation
  • Related posts (tag-based, tag-overlap ranked, limit 3)
  • Per-tag descriptions on tag archive pages — /tags/ + /tags/[tag]/, descriptions ported from zola.toml's [extra.tag_descriptions]
  • llms_description meta (site-wide default in site.config.ts + per-post override, rendered as <meta name="description"> in Base.astro)
  • RSS/JSON feeds — /rss.xml, /feed.json
  • Sitemap + robots.txt — @astrojs/sitemap, static public/robots.txt
  • Code block syntax highlighting — toml-content-loader.ts renders fenced code blocks through highlight.js's core build with only the 11 languages content actually uses registered (no bundled-theme/WASM grammar weight like shiki, which was tried first and swapped out for this lighter option); .hljs-* token classes in global.css are colored from the existing design tokens (--accent, --text-muted, ...), so they already switch with the site's theme with no extra [data-theme] rules needed

Search & comments

  • Pagefind integration — indexes the build output (pnpm build runs astro build && pagefind --site dist), /search/ page using Pagefind's default UI; verified with a real query in a browser
  • Comments: kept Utterances (not Webmentions) — Comments.astro, wired to andwati/andwati.com, with the same dark/light theme-sync-via- postMessage behavior the legacy site had, adapted to this site's data-theme attribute. The rel="webmention" link was removed since it's unused now. sws.toml's CSP updated to allow utteranc.es (script-src, frame-src) — without that the widget is silently blocked in production

Analytics

  • Umami — self-hosted at analytics.andwati.com. Analytics.astro renders the tracker script from PUBLIC_UMAMI_SCRIPT_URL + PUBLIC_UMAMI_WEBSITE_ID (set in apps/site/.env, gitignored — copy into the production environment's env vars too); sws.toml's CSP updated to allow that host in script-src/connect-src

Migration from the legacy Zola site

  • Script/process to convert content/posts/*.md into the new content/writings/ schema — scripts/migrate-posts-to-writings.mjs, additive only (content/posts/ untouched), re-runnable/idempotent. 2 of 20 posts (from-c-to-machine-code, self-hosting-gitea-and-mirroring-github) use note/tip/warning/danger callout shortcodes — flagged by the script, still left as-is in the body text
  • note/tip/warning/danger callout shortcode rendering — the legacy {% <note> %} ... {% </note> %} blocks are converted to styled .callout markup by toml-content-loader.ts's renderCallouts() (runs before the body's markdown parse), styled in global.css to match the new design system. Verified in-browser on from-c-to-machine-code
  • Preserve slugs/URLs via redirects — the script generates 301s from /posts/<slug>/ to /writings/<slug>/ into sws.toml, idempotently
  • Migrate content/archive/, content/about.md equivalents — /archive/ groups every published writing by year; /about/ reframes the legacy copy around the site's "digital legacy" identity while preserving its PGP and security contact details. Both are in the main navigation, and the linked .well-known files are copied into Astro's public output
  • Verify RSS subscribers and inbound links survive the cutover — can only really be checked once this is deployed live (needs your input: this is a production-cutover verification, not a build-time one)

Infra / deployment

  • Wire Strapi into the Dokploy deployment alongside the Astro build — docker-compose.prod.yml defines both production services, persistent CMS/Git volumes, reverse-proxy settings, and secret-only configuration
  • Build trigger decided and implemented: Strapi commits Markdown and generated covers to Git, pushes the production branch, then calls the Dokploy Compose deploy webhook so Astro rebuilds from the durable commit
  • Update Dockerfile/sws.toml for Astro's apps/site/dist output, current security headers, legacy redirects, and the custom 404 page
  • Launch the production Compose application and configure its domains, secrets, GitHub token, and deploy webhook in Dokploy — repository-side setup is complete in DEPLOYMENT.md; host access is still required

Cleanup

  • Remove Zola-specific files (zola.toml, templates/, sass/, themes/, static/, Makefile, bin/update-zola, content/posts/, content/archive/_index.md, content/about.md, content/_index.md, legacy OG/llms scripts) — verified safe: root Dockerfile already builds apps/site (not zola build), nothing in apps/site reads those content files, and content/writings/ has all 20 posts from content/posts/. Root package.json's legacy:* scripts already removed. Blocked: the actual git rm was refused by the permission system as irreversible destruction — run it yourself (fully reversible via git history/revert since these are tracked files): git rm -r templates/ sass/ themes/ static/ zola.toml Makefile bin/ content/posts/ content/archive/_index.md content/about.md content/_index.md scripts/generate-llms.mjs scripts/generate-og-images.mjs