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

Dokploy deployment

The production deployment is one Dokploy Compose application with two services:

  • site builds the Astro app and serves its static output with static-web-server.
  • cms runs Strapi with persistent SQLite/admin data and a persistent Git checkout.

Markdown remains the source of truth. A Strapi edit writes into the checkout, commits and pushes the changed Markdown (plus a generated bookshelf cover when applicable), then calls Dokploy's deploy webhook. The redeploy builds Astro from that new commit.

GitHub token

Create a fine-grained personal access token restricted to the andwati/andwati.com repository with:

  • Repository contents: read and write
  • Metadata: read (automatically included)

The configured publishing branch must accept direct pushes from the token. Store the token only in Dokploy as GITHUB_TOKEN.

Dokploy application

  1. Create a Compose application from this repository and select docker-compose.prod.yml.
  2. Configure its source branch to match CONTENT_GIT_BRANCH (normally main).
  3. Route the public site domain to the site service on port 80.
  4. Route the CMS domain (for example cms.andwati.com) to the cms service on port 1337.
  5. Copy the application's deploy webhook URL into DOKPLOY_DEPLOY_WEBHOOK_URL.
  6. Add the environment variables below and deploy.

Required secrets:

CMS_PUBLIC_URL=https://cms.andwati.com
APP_KEYS=<four comma-separated random values>
API_TOKEN_SALT=<random value>
ADMIN_JWT_SECRET=<random value>
TRANSFER_TOKEN_SALT=<random value>
JWT_SECRET=<random value>
ENCRYPTION_KEY=<random value>
GITHUB_TOKEN=<fine-grained GitHub token>
DOKPLOY_DEPLOY_WEBHOOK_URL=<Dokploy Compose deploy webhook>

Optional publishing settings:

CONTENT_GIT_REPOSITORY=andwati/andwati.com
CONTENT_GIT_BRANCH=main
CONTENT_GIT_COMMIT_NAME="andwati.com CMS"
[email protected]

Site analytics (read at build time by apps/site, so these must be set on the site service before it builds, not just at runtime):

PUBLIC_UMAMI_SCRIPT_URL=https://analytics.andwati.com/script.js
PUBLIC_UMAMI_WEBSITE_ID=9eef8fba-c932-4488-ae23-6b84553e8c29

Analytics.astro silently no-ops if either is unset — safe to deploy without them, but analytics won't record anything until both are present at build time. sws.toml's CSP already allows analytics.andwati.com and utteranc.es (comments); if the Umami host ever changes, update both that CSP and these two vars together, or the tracker will be silently blocked in production instead of erroring visibly.

Generate each random secret independently; do not reuse the local development values from docker-compose.yml.

Persistent data

The production Compose file declares three named volumes:

  • cms_database keeps Strapi's admin users, API tokens, and SQLite cache.
  • cms_uploads keeps files uploaded through Strapi.
  • content_checkout keeps the Git working tree used for publishing.

Back up cms_database and cms_uploads. The content checkout is recoverable from Git, but retaining it protects an edit if the process stops between the filesystem write and Git push.

On startup, the CMS fast-forwards a clean checkout to the configured branch. If it finds uncommitted files, it preserves them and starts without pulling so an interrupted edit is not overwritten. Resolve and push those files from the volume before forcing a reset.

Local validation

Provide temporary values for required interpolation variables, then validate the production manifest:

docker compose --env-file .env.production \
  -f docker-compose.prod.yml config
docker compose --env-file .env.production \
  -f docker-compose.prod.yml build

The existing docker-compose.yml remains the local Strapi development setup.