docs: self-host fonts, eliminate layout shift, add SPA navigation#534
docs: self-host fonts, eliminate layout shift, add SPA navigation#534
Conversation
why: Furo's default TOC title (10px) is nearly invisible and smaller than its own items (12px), inverting typographic hierarchy. Body line-height (1.5) is tighter than WCAG-recommended range. what: - Bump TOC item size 75% → 81.25% (12→13px) via --toc-font-size - Bump TOC title size 62.5% → 87.5% (10→14px) via --toc-title-font-size - Increase .toc-tree line-height 1.3 → 1.4 for wrapped entries - Increase article line-height 1.5 → 1.6 for paragraph readability - Enable text-rendering: optimizeLegibility on body
why: Furo hardcodes .toc-drawer to 15em; long TOC entries overflow and code blocks in the content area are cramped. what: - Override .toc-drawer min-width to 18em with flex-shrink: 0 - Move padding to .toc-sticky inner panel (1.5em right) - Set .content to flex: 1 1 46em with max-width: 46em - Override right offset at ≤82em breakpoint for collapse
why: 81.25% (13px) is still noticeably smaller than the 14px body text; at 87.5% (14px) the TOC matches body size and reads comfortably beside it. what: - Change --toc-font-size from --font-size--small--2 to --font-size--small - Move variables from :root to body for specificity with Furo
…brow labels why: Furo headings are large and bold, crowding the page and flattening visual hierarchy. Biome-inspired medium-weight scale uses size and spacing — not boldness — to convey structure. what: - Set all article headings to font-weight: 500 - Scale: h1 1.8em, h2 1.6em, h3 1.15em, h4-h6 eyebrow treatment - Add uppercase + letter-spacing + muted color for h4-h6 - Add changelog heading extras for #history section - Revert TOC variables from body back to :root
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #534 +/- ##
=======================================
Coverage 84.24% 84.24%
=======================================
Files 29 29
Lines 3795 3795
Branches 756 756
=======================================
Hits 3197 3197
Misses 378 378
Partials 220 220 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Code reviewFound 3 issues. Checked for bugs and CLAUDE.md compliance. Issue 1 (score 90) — Bug: vcspull/docs/_ext/sphinx_fonts.py Lines 143 to 151 in e62ac30
The test Fix: only append to Issue 2 (score 85) — Temporary CI branch trigger left in workflow vcspull/.github/workflows/docs.yml Line 7 in e62ac30 The commit message for this line says "temporarily add docs-fonts branch to trigger". The branch is still listed in Issue 3 (score 82) — Missing docstrings on vcspull/docs/_ext/sphinx_fonts.py Lines 81 to 100 in e62ac30 Both functions lack docstrings. CLAUDE.md requires NumPy-style docstrings for all functions and methods. 🤖 Generated with Claude Code |
why: Standardize on IBM Plex Sans / Mono across projects without committing ~227KB of binary font files to the repo. what: - Add sphinx_fonts extension that downloads fonts at build time, caches in ~/.cache/sphinx-fonts/, and generates @font-face CSS - Configure IBM Plex Sans (400/500/600/700) and IBM Plex Mono (400) with CSS variable overrides for Furo theme - Add actions/cache step in docs workflow for font cache persistence - Gitignore generated font assets in docs/_static/
why: Furo sets --font-stack on body, so :root declarations lose in specificity. Using body selector matches Furo's own pattern. what: - Change CSS variable container from :root to body in _generate_css
why: The browser doesn't discover font URLs until it parses fonts.css, which itself must wait for the HTML to load. Preload hints in <head> tell the browser to start downloading fonts immediately. what: - Add sphinx_font_preload config option to sphinx_fonts extension - Emit <link rel="preload"> tags in page.html template's extrahead block - Preload 3 critical weights: Sans 400/700, Mono 400 - Rename layout.html → page.html (Furo extends !page.html)
why: IBM Plex Sans benefits from OpenType features that browsers disable by default; monospace blocks need opposite treatment. what: - Add font-kerning, font-variant-ligatures, letter-spacing to body - Add optimizeSpeed and disable ligatures for pre/code/kbd/samp
why: Every <img> on the docs site lacked dimension hints, causing Cumulative Layout Shift (CLS) on page load. what: - Add content-visibility: auto on all img for off-screen decode skip - Add height: auto !important for lazy-loaded images with aspect-ratio - Add CSS height: 20px for shields.io / badge / codecov badges - Add sidebar/brand.html with width/height/decoding on logo
why: Every page navigation re-downloads and re-parses CSS/JS/fonts and re-renders the entire layout. Only article content, TOC, and active sidebar link actually change between pages. what: - Create docs/_static/js/spa-nav.js (~170 lines, vanilla JS, no deps) - Intercept internal link clicks and swap three DOM regions - Preserve sidebar scroll, theme state, all CSS/JS/fonts - Register in conf.py setup() with loading_method="defer"
…flow why: When web fonts load, text reflowed because system fallbacks have different metrics. Capsize-derived overrides make fallback fonts match IBM Plex dimensions exactly. what: - Add sphinx_font_fallbacks config with size-adjust/ascent/descent overrides for Arial (sans) and Courier New (mono) - Update font stacks to include fallback font families
why: Badges from shields.io/codecov render at 0x0 until loaded, causing visible layout shift in the header area. what: - Add min-width: 60px, border-radius, and background placeholder to badge selectors for stable pre-load dimensions
why: All links render visibly then JS replaces the hostname-matching link with a bold span, causing a visible reflow. what: - Remove misleading class="current" from all project links - Hide #sidebar-projects until JS resolves active state (.ready) - Use textContent instead of innerHTML for safer DOM manipulation
…sfade why: SPA navigation instantly replaces DOM content, causing a jarring visual jump between pages instead of a smooth transition. what: - Wrap swap+reinit in document.startViewTransition() when available - Add 150ms crossfade animation via ::view-transition pseudo-elements - Progressive enhancement: unsupported browsers get instant swap
why: View transitions are not image-related — grouping them with image CLS rules is misleading. Moving to end of file keeps related sections together and matches the logical reading order. what: - Move view transitions CSS block after badge placeholder rules
why: font-display swap causes visible text reflow (FOUT). Matching the tony.nl/cv approach: block rendering until preloaded fonts arrive, and inline the @font-face CSS to eliminate the extra fonts.css request. what: - Change font-display from swap to block - Move @font-face CSS from external fonts.css to inline <style> in <head> - Use pathto() in template for correct relative font URLs - Remove _generate_css() function (CSS now generated in Jinja template)
why: ruff D101/D103 rules flag missing docstrings on SetupDict and setup(), causing CI lint failures in repos that lint docs/_ext/. what: - Add docstring to SetupDict TypedDict class - Add docstring to setup() function
why: codecov drops because docs/_ext/sphinx_fonts.py is measured for coverage but has zero tests across all repos. what: - Add test_sphinx_fonts.py with 21 tests covering all functions - Test pure functions, I/O with monkeypatch, Sphinx events with SimpleNamespace - Cover all branches: cached/success/URLError/OSError, html/non-html, empty/with fonts
why: CI ruff check fails on EM101 (string literal in exception), TRY003 (long exception message), and D403 (uncapitalized docstring). what: - Extract exception messages to variables for EM101/TRY003 - Capitalize docstrings starting with "setup" for D403
why: CI ruff format check fails on multi-line function call formatting. what: - Apply ruff format to test_sphinx_fonts.py
why: CI mypy fails with unused-ignore because sphinx_fonts is imported as an untyped module via sys.path, making arg-type suppression unnecessary. what: - Remove all type: ignore[arg-type] comments from test_sphinx_fonts.py
why: sphinx_fonts is a local docs/_ext extension, not an installed package — mypy cannot resolve it at analysis time. what: - Add sphinx_fonts to [[tool.mypy.overrides]] ignore_missing_imports
Summary
Port documentation frontend improvements from tmuxp:
Overhaul the docs frontend for faster, smoother page loads:
See tmuxp PRs for full design rationale and test plan.