Loading…
a327ex.com

Anchor Website 12

Opus 4.8

Summary

Anchor Website 12 (2026-06-30)

Continued the Anchor Website web-delivery work. Huge session: finished B1 (font subset), built the web <video> backend, stood the renderer up on a real staging domain (new.a327ex.com) with SEO layering, wired deep-link navigation, and did a long blog-fidelity pass (emphasis parser, old-theme font, font emboldening). Live a327ex.com (the Lua server) was never touched — everything is on staging.

What landed (in order)

1. B1 — Font subset (DONE)

  • New renderer/tools/subset_fonts.py (fonttools Subsetter) scans the SAME text the renderer reads (data/*.lua + media-cache/subs/*.vtt) for codepoints routing to the two FALLBACK-ONLY fonts (matching elements.lua is_cjk/is_symbol), subsets each from C:/Windows/Fonts to only those glyphs (keeps outlines + TT hinting → render-identical; keeps .notdef box). Yu Gothic R+M .ttc 27.7→2.43 MB .ttf (1851 of 2757 used CJK cps in the font), Segoe UI Symbol 2.5 MB→26 KB (53 cps). theme.lua CJK_REG/CJK_BOLD → assets/YuGoth{R,M}.ttf; .ttc deleted from assets; .gitignore updated. renderer.data 39 MB → ~11 MB.
  • ⚠ Boot-break caught in testing: I'd also deleted Oswald/IBMPlex/Arimo as "dead," but the ricochet/ framework theme (ricochet_theme_set('smooth')) loads Oswald-Medium + IBMPlexSans-Regular at boot → blank page. Lesson: grep ricochet/+anchor/ too. All those fonts are git-tracked (open-licensed); restored via git checkout.
  • ⚠ Pre-existing gap: Yu Gothic has no Hangul, so ko subtitle tracks were already boxing.

2. Web <video> backend (ENGINE — anchor.c, COMMITTED to Anchor2 7e87d1f)

  • Replaced the #else __EMSCRIPTEN__ video stubs with a real backend: each video is a hidden HTML <video> the browser decodes; video_pump_all uploads its current frame into the engine GL texture via texImage2D(video) each frame (EM_JS glue + a thin C WebVideo manager). Same video_* Lua API as desktop libav, so the renderer player/controls/CC/fullscreen are unchanged. GL texture holds native pixels; Texture.width/height = display size → layer_texture UV-0..1 quad downscales crisply. Audio via the <video> element itself (muted-retry autoplay fallback).

3. Subtitles lazy-load + THREADED dev server (the long debug)

  • On web the VTT is fetched lazily over HTTP (subs_parse's io.open failed synchronously); video_load_subs now kicks web_file_status + an update() poll parses it when it lands.
  • The real blocker (cost a deep bisect): tools/serve-web.py was single-threaded socketserver.TCPServer — a playing <video> holds a long-lived range request, so the dev server couldn't serve the subtitle fetch fired MID-PLAYBACK (stuck pending forever). Fixed → ThreadingTCPServer(daemon_threads). DEV-ONLY (nginx/R2 are concurrent in prod). Also added HTTP Range (206) to serve-web.py for <video> seeking.

4. Controls polish (renderer)

  • speed/CC menu latch (open from the button only, stay across the button→menu zone) so hovering the seekbar above them no longer pops the menu / eats seekbar clicks.
  • time readout click-toggles elapsed↔remaining (-mm:ss).
  • volume icon redrawn as a clean vector speaker (Tabler set has no volume glyph; no SVG rasterizer locally) + chevron sound-waves / X-when-muted.

5. B2/B3 STAGING — new.a327ex.com (drove the whole deploy over SSH + passwordless sudo)

  • I have SSH + passwordless sudo on the VPS (host alias a327ex, user adn, IP [REDACTED#1: infra-internal], deploy dir /srv/a327ex/work). Bundle + data/ (93 MB) rsynced (scp+tar) to /srv/a327ex/render/.
  • nginx block deploy/nginx-new-a327ex.conf (/etc/nginx/sites-available/new-a327ex): /data static, /media-cache/proxy media.a327ex.com (R2) incl. video 206 range, /media+/static from the live dirs, engine bundle static.
  • certbot LE cert for new.a327ex.com (webroot, auto-renew). DNS = grey-cloud A → VPS (owner adds CF records; I can't).

6. SEO layering (desktop)

  • nginx proxies the Lua server's HTML per page (server-rendered, per-page <title>/<meta> kept for crawlers/no-JS) + sub_filter injects <script src=/engine-boot.js> before </body> → the engine overlays the canvas for humans.
  • tools/engine-boot.js (injectable web-shell). ⚠ MUST mirror web-shell.html exactly (canvas id="canvas", html,body{height:100%}, same Module/fit) — a divergent setup broke web_native_resolution (→1920×1080 fallback) + canvas coverage.
  • Loading cover injected at the START of <body> (nginx sub_filter: opaque #1a1a1a div + CSS spinner) so the HTML never flashes + there's load feedback; engine-boot.js removes it once the first frame renders (or instantly on mobile).
  • MOBILE bails (UA check) → engine-boot.js doesn't boot; the normal responsive Lua site renders. (Proper mobile engine support = a separate deferred project.)
  • #4 embed waste: desktop engine-boot.js removes the Lua page's tweet/YT iframes + widget scripts after boot (covered anyway).
  • anchor.c (UNCOMMITTED): EM_JS bindings web_location_path() (window.location), web_push_path(p) (history.pushState), web_nav_pop() (popstate queue). Desktop no-ops. Registered next to open_url.
  • main.lua: section_by_slug + url_for_slug (/section/slug), nav_to_path, boot reads the URL → navigates to that page, load_page/show_home push the URL (with no_push for URL-driven nav), update() polls web_nav_pop for back/forward. URL scheme = /<section>/<slug>, / home.
  • Module.locateFile in engine-boot.js forces absolute URLs for renderer.data/anchor.wasm — without it a deep link like /posts/x fetched /posts/renderer.data → 404 → WebGL crash.

9. CF cache bug (diagnosed + fixed)

  • Owner orange-clouded new.a327ex.com (for #5 video edge-cache) → Cloudflare edge-cached renderer.js (stale) while renderer.data was fresh → manifest/payload mismatch → WebGL glUseProgram on undefined context crash. Fix: reverted to grey-cloud + set the engine bundle to Cache-Control: no-cache in nginx (so renderer.js + renderer.data always reload as a matched pair). Proper prod fix later = a CF Cache Rule bypassing /renderer.*,/anchor.* OR content-hashed filenames.

10. Emphasis parser fix (convert.lua)

  • Intraword */_ (Amrican, fck, snake_case) was parsed as emphasis, italicizing whole spans. Now: an opener with an alphanumeric char right before it is literal (matches Blot). Re-converted all 371 pages (run from renderer/: lua54 tools/convert.lua --all — resolves paths from cwd; running from the repo root breaks it).

11. Old-theme font fix (Source Sans)

  • The posts_old theme WOFFs live in static/fonts/ (NOT bundled) → on web file_exists failed → Arial fallback → "significantly changed." Converted the 4 used WOFFs → TTF into assets/source-sans/{regular,bold,italic}.ttf + assets/source-code/regular.ttf; repointed theme.lua (web FreeType may lack zlib/WOFF, so TTF is safer).

12. Font emboldening (ENGINE + renderer) — the font-thinness fix

  • Confirmed the body is Arial weight 400 in BOTH renderer + live site — same font/weight. The thinness is grayscale AA (engine) vs Windows ClearType (browser), which reads heavier. Gamma (edge-darkening) was the wrong lever.
  • anchor.c (UNCOMMITTED): FT_Outline_Embolden in the glyph raster (load outline, embolden, render), scaled by face->size->metrics.y_ppem so it's a fraction of the em (constant relative weight across sizes). New optional 4th arg to font_render_config(snap, interp, gamma, embolden)
    • #include <freetype/ftoutln.h>.
  • main.lua: per-theme embolden — different typefaces (Arial vs Source Sans) need different amounts. apply_theme(name,…) wrapper re-bakes (font_render_config) only when the theme's weight actually CHANGES (change-detected _theme_embolden); THEME_EMBOLDEN map; all 4 theme_apply call sites switched to apply_theme. Gamma reset to 1.4.
  • FINAL TUNED VALUES: new theme (Arial/site_dark+light) = 0.016, old theme (Source Sans/posts_old) = 0.028. (Startup font_render_config default + _theme_embolden init must match the new-theme value or the first homepage load keeps the wrong weight.)

Deploy mechanics (reusable)

  • Renderer-only change → bash tools/build-web.sh (rebuild bundle) → scp build-web/renderer.data build-web/renderer.js a327ex:/srv/a327ex/render/. Engine change → also ./build-web-engine.sh first + scp anchor.js+anchor.wasm.
  • ALWAYS verify renderer.js ↔ renderer.data are a matched pair (both same remote_package_size) after deploy, via curl --resolve new.a327ex.com:443:[REDACTED#1: infra-internal] … (bypass any CF).
  • grey-cloud + nginx no-cache means a hard-reload always gets the fresh matched bundle.

Uncommitted at session end (COMMIT on next session)

  • Anchor2 engine/src/anchor.c: the deep-link nav bindings (web_location_path/push_path/nav_pop)
    • the emboldening (FT_Outline_Embolden, ppem-scaled, 4th font_render_config arg, ftoutln.h include). → commit to Anchor2 (private repo, git push origin main).
  • a327ex-site (push prod): main.lua (nav + locateFile-side + per-theme embolden + apply_theme + emphasis-driven reconvert), theme.lua (Source Sans TTF), convert.lua (emphasis fix), tools/engine-boot.js (cover + mobile + embed-removal + locateFile), tools/serve-web.py (threading + range), deploy/nginx-new-a327ex.conf (updated: cover sub_filter + no-cache), the new assets/source-sans + assets/source-code TTFs, and the re-converted data/ (gitignored — VPS only).

▶ NEXT SESSION (remaining before a root cutover)

  1. Commit everything above.
  2. Mobile = its own project (touch input, responsive reflow, high-DPR canvas sizing).
  3. #5 prod video path — orange-cloud with a CF Cache Rule (bypass the engine bundle) OR content-hash the bundle filenames; then the deliberate, reversible root cutover.
  4. Per-post fidelity tail (the 8 dead-embed fallback cards, etc.).

🔒 Only the summary of this log is public. Private because it contains too many website internal details.