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(fonttoolsSubsetter) scans the SAME text the renderer reads (data/*.lua+media-cache/subs/*.vtt) for codepoints routing to the two FALLBACK-ONLY fonts (matchingelements.luais_cjk/is_symbol), subsets each fromC:/Windows/Fontsto only those glyphs (keeps outlines + TT hinting → render-identical; keeps.notdefbox). Yu Gothic R+M.ttc27.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.luaCJK_REG/CJK_BOLD →assets/YuGoth{R,M}.ttf;.ttcdeleted from assets;.gitignoreupdated.renderer.data39 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: grepricochet/+anchor/too. All those fonts are git-tracked (open-licensed); restored viagit checkout. - ⚠ Pre-existing gap: Yu Gothic has no Hangul, so
kosubtitle 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_alluploads its current frame into the engine GL texture viatexImage2D(video)each frame (EM_JS glue + a thin CWebVideomanager). Samevideo_*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_textureUV-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'sio.openfailed synchronously);video_load_subsnow kicksweb_file_status+ anupdate()poll parses it when it lands. - The real blocker (cost a deep bisect):
tools/serve-web.pywas single-threadedsocketserver.TCPServer— a playing<video>holds a long-lived range request, so the dev server couldn't serve the subtitle fetch fired MID-PLAYBACK (stuckpendingforever). 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):/datastatic,/media-cache/→ proxy media.a327ex.com (R2) incl. video 206 range,/media+/staticfrom 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_filterinjects<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 (canvasid="canvas",html,body{height:100%}, same Module/fit) — a divergent setup brokeweb_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).
7. Deep-link nav (#1) — ENGINE + renderer
- 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_homepush the URL (withno_pushfor URL-driven nav), update() pollsweb_nav_popfor back/forward. URL scheme =/<section>/<slug>,/home. Module.locateFilein engine-boot.js forces absolute URLs for renderer.data/anchor.wasm — without it a deep link like/posts/xfetched/posts/renderer.data→ 404 → WebGL crash.
8. Links (#3): external → new tab (SDL_OpenURL = window.open on web), internal → in-app (via #1).
9. CF cache bug (diagnosed + fixed)
- Owner orange-clouded new.a327ex.com (for #5 video edge-cache) → Cloudflare edge-cached
renderer.js(stale) whilerenderer.datawas fresh → manifest/payload mismatch → WebGLglUseProgramon undefined context crash. Fix: reverted to grey-cloud + set the engine bundle toCache-Control: no-cachein 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 fromrenderer/: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 webfile_existsfailed → Arial fallback → "significantly changed." Converted the 4 used WOFFs → TTF intoassets/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_Emboldenin the glyph raster (load outline, embolden, render), scaled byface->size->metrics.y_ppemso it's a fraction of the em (constant relative weight across sizes). New optional 4th arg tofont_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_EMBOLDENmap; all 4theme_applycall sites switched toapply_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_configdefault +_theme_emboldeninit 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.shfirst + scpanchor.js+anchor.wasm. - ALWAYS verify renderer.js ↔ renderer.data are a matched pair (both same
remote_package_size) after deploy, viacurl --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).
- the emboldening (FT_Outline_Embolden, ppem-scaled, 4th font_render_config arg, ftoutln.h include).
→ commit to Anchor2 (private repo,
- 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)
- Commit everything above.
- Mobile = its own project (touch input, responsive reflow, high-DPR canvas sizing).
- #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.
- 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.