Files
andwati.com/content/README.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

3.8 KiB

content/

This directory is the canonical source of truth for all site content. It is shared between apps/site (Astro reads it directly at build time) and apps/cms (Strapi's custom provider reads/writes these same files — its SQLite database is only a disposable cache/index, never the source of record). The site must always be buildable straight from this directory with Strapi entirely absent.

Frontmatter is TOML (+++ ... +++), matching the legacy Zola posts under content/posts/ to ease migration. Each content type below is one file (or a directory with index.md for entries with co-located assets), one frontmatter schema.

writings/ (blog / essays)

+++
title = "..."
description = "..."
author = "andwati"
date = 2026-04-08
updated = 2026-04-10       # optional, only when the post has been revised
draft = false
canonical_url = ""         # optional, only if first published elsewhere
[taxonomies]
tags = ["..."]
[extra]
series = "..."             # optional
series_index = 1           # required if `series` is set
llms_description = "..."   # optional per-post override of the site-wide one
+++

portfolio/ (projects / professional work)

+++
title = "..."
description = "..."
role = "..."
date_start = 2025-01-01
date_end = 2025-06-01      # optional, omit if ongoing
outcome = "one-line result"
draft = false
[[links]]
label = "Live"
url = "https://..."
[[links]]
label = "Source"
url = "https://..."
[taxonomies]
tags = ["..."]             # stack / tech tags
+++

Body is a free-form Markdown case study.

bookshelf/ (books and papers)

kind selects which identifier fields apply — books use isbn, papers use doi/arxiv_id. Book covers are fetched from Open Library by ISBN and cached into apps/site/public/covers/ (served at /covers/...), never fetched at page-render time. Papers have no reliable cover source, so cover_image is set manually for them, if at all; Semantic Scholar is only used to backfill a missing title/authors by DOI/arXiv id.

Note: the field is named read_status, not status — Strapi's content-manager API treats a top-level status key as its own reserved draft/publish parameter even with draftAndPublish disabled, so an attribute literally named status silently breaks writes. Don't rename it back.

+++
title = "..."
kind = "book"               # "book" | "paper"
authors = ["..."]
isbn = "9780000000000"      # books only
doi = "10.1000/xyz123"      # papers only
arxiv_id = "2101.00001"     # papers only, optional
url = "https://..."         # papers only: landing page / source link
cover_image = "/covers/slug.jpg"
rating = 5                  # optional, 1-5
read_status = "read"        # "reading" | "read" | "abandoned"
date_started = 2026-01-01
date_finished = 2026-01-20  # optional, omit if still in progress
[taxonomies]
tags = ["..."]              # genre / topic
+++

Body is a free-form personal review/notes.

blogs/ (curated external blogs)

+++
title = "..."     # the blog/site's name
url = "https://..."
feed_url = "https://.../rss.xml"
+++

Body is a one-to-two sentence blurb on why you follow it. No live feed fetching — this list is hand-curated and static.

Markdown extras (all body content)

  • LaTeX: $inline$ and $$block$$ math renders to static HTML/MathML at build time (marked-katex-extension in toml-content-loader.ts) — no client-side JS shipped for it.
  • Callouts: {% <note> %} ... {% </note> %} (also tip/warning/ danger) renders as a styled callout box.
  • YouTube embeds: paste a plain <iframe src="https://www.youtube- nocookie.com/embed/VIDEO_ID" ...></iframe> — it's automatically wrapped in a responsive 16:9 container, gets loading="lazy", and emits VideoObject structured data. No shortcode needed.