Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
682fbb6
docs(style[toc,body]): refine right-panel TOC and body typography
tony Mar 14, 2026
c8a6380
docs(style[toc,content]): flexible TOC width with inner-panel padding
tony Mar 14, 2026
57728a1
docs(style[toc]): increase TOC font size from 81.25% to 87.5%
tony Mar 14, 2026
14efa69
docs(style[headings]): refine heading hierarchy — scale, spacing, eye…
tony Mar 14, 2026
56236fc
docs(fonts): self-host IBM Plex via Fontsource CDN
tony Mar 14, 2026
33a720b
docs(fonts[css]): fix variable specificity — use body instead of :root
tony Mar 14, 2026
3f74fb5
docs(fonts[preload]): add <link rel="preload"> for critical font weights
tony Mar 14, 2026
7ff9ca9
docs(fonts[css]): add kerning, ligatures, and code rendering overrides
tony Mar 14, 2026
039d2ab
docs(images[cls]): prevent layout shift and add non-blocking loading
tony Mar 14, 2026
00f55b9
docs(nav[spa]): add SPA-like navigation to avoid full page reloads
tony Mar 14, 2026
190230a
docs(fonts[fallback]): add fallback font metrics to eliminate FOUT re…
tony Mar 14, 2026
318b575
docs(images[badges]): add placeholder sizing for external badge images
tony Mar 14, 2026
d87e937
docs(sidebar[projects]): prevent active link flash with visibility gate
tony Mar 14, 2026
10470f6
docs(nav[spa]): wrap DOM swap in View Transitions API for smooth cros…
tony Mar 14, 2026
bbeceae
docs(css[structure]): move view transitions section after image rules
tony Mar 14, 2026
4f44133
docs(fonts[loading]): switch to font-display block with inline CSS
tony Mar 14, 2026
d3a5910
docs(fonts[lint]): add docstrings to sphinx_fonts extension
tony Mar 14, 2026
53345b2
test(docs[sphinx_fonts]): add tests for sphinx_fonts extension
tony Mar 14, 2026
42ae9ad
test(docs[sphinx_fonts]): fix ruff lint errors in test_sphinx_fonts
tony Mar 14, 2026
28a9659
test(docs[sphinx_fonts]): apply ruff format to test_sphinx_fonts
tony Mar 14, 2026
10d3369
test(docs[sphinx_fonts]): remove type: ignore comments for mypy compat
tony Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ jobs:
python -V
uv run python -V

- name: Cache sphinx fonts
if: env.PUBLISH == 'true'
uses: actions/cache@v5
with:
path: ~/.cache/sphinx-fonts
key: sphinx-fonts-${{ hashFiles('docs/conf.py') }}
restore-keys: |
sphinx-fonts-

- name: Build documentation
if: env.PUBLISH == 'true'
run: |
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ pip-wheel-metadata/
# Monkeytype
monkeytype.sqlite3


# Generated by sphinx_fonts extension (downloaded at build time)
docs/_static/fonts/
docs/_static/css/fonts.css

# Claude code
**/CLAUDE.local.md
**/CLAUDE.*.md
Expand Down
153 changes: 153 additions & 0 deletions docs/_ext/sphinx_fonts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""Sphinx extension for self-hosted fonts via Fontsource CDN.

Downloads font files at build time, caches them locally, and passes
structured font data to the template context for inline @font-face CSS.
"""

from __future__ import annotations

import logging
import pathlib
import shutil
import typing as t
import urllib.error
import urllib.request

if t.TYPE_CHECKING:
from sphinx.application import Sphinx

logger = logging.getLogger(__name__)

CDN_TEMPLATE = (
"https://cdn.jsdelivr.net/npm/{package}@{version}"
"/files/{font_id}-{subset}-{weight}-{style}.woff2"
)


class SetupDict(t.TypedDict):
"""Return type for Sphinx extension setup()."""

version: str
parallel_read_safe: bool
parallel_write_safe: bool


def _cache_dir() -> pathlib.Path:
return pathlib.Path.home() / ".cache" / "sphinx-fonts"


def _cdn_url(
package: str,
version: str,
font_id: str,
subset: str,
weight: int,
style: str,
) -> str:
return CDN_TEMPLATE.format(
package=package,
version=version,
font_id=font_id,
subset=subset,
weight=weight,
style=style,
)


def _download_font(url: str, dest: pathlib.Path) -> bool:
if dest.exists():
logger.debug("font cached: %s", dest.name)
return True
dest.parent.mkdir(parents=True, exist_ok=True)
try:
urllib.request.urlretrieve(url, dest)
logger.info("downloaded font: %s", dest.name)
except (urllib.error.URLError, OSError):
if dest.exists():
dest.unlink()
logger.warning("failed to download font: %s", url)
return False
return True


def _on_builder_inited(app: Sphinx) -> None:
if app.builder.format != "html":
return

fonts: list[dict[str, t.Any]] = app.config.sphinx_fonts
variables: dict[str, str] = app.config.sphinx_font_css_variables
if not fonts:
return

cache = _cache_dir()
static_dir = pathlib.Path(app.outdir) / "_static"
fonts_dir = static_dir / "fonts"
fonts_dir.mkdir(parents=True, exist_ok=True)

font_faces: list[dict[str, str]] = []
for font in fonts:
font_id = font["package"].split("/")[-1]
version = font["version"]
package = font["package"]
subset = font.get("subset", "latin")
for weight in font["weights"]:
for style in font["styles"]:
filename = f"{font_id}-{subset}-{weight}-{style}.woff2"
cached = cache / filename
url = _cdn_url(package, version, font_id, subset, weight, style)
if _download_font(url, cached):
shutil.copy2(cached, fonts_dir / filename)
font_faces.append(
{
"family": font["family"],
"style": style,
"weight": str(weight),
"filename": filename,
}
)

preload_hrefs: list[str] = []
preload_specs: list[tuple[str, int, str]] = app.config.sphinx_font_preload
for family_name, weight, style in preload_specs:
for font in fonts:
if font["family"] == family_name:
font_id = font["package"].split("/")[-1]
subset = font.get("subset", "latin")
filename = f"{font_id}-{subset}-{weight}-{style}.woff2"
preload_hrefs.append(filename)
break

fallbacks: list[dict[str, str]] = app.config.sphinx_font_fallbacks

app._font_preload_hrefs = preload_hrefs # type: ignore[attr-defined]
app._font_faces = font_faces # type: ignore[attr-defined]
app._font_fallbacks = fallbacks # type: ignore[attr-defined]
app._font_css_variables = variables # type: ignore[attr-defined]


def _on_html_page_context(
app: Sphinx,
pagename: str,
templatename: str,
context: dict[str, t.Any],
doctree: t.Any,
) -> None:
context["font_preload_hrefs"] = getattr(app, "_font_preload_hrefs", [])
context["font_faces"] = getattr(app, "_font_faces", [])
context["font_fallbacks"] = getattr(app, "_font_fallbacks", [])
context["font_css_variables"] = getattr(app, "_font_css_variables", {})


def setup(app: Sphinx) -> SetupDict:
"""Register config values, events, and return extension metadata."""
app.add_config_value("sphinx_fonts", [], "html")
app.add_config_value("sphinx_font_fallbacks", [], "html")
app.add_config_value("sphinx_font_css_variables", {}, "html")
app.add_config_value("sphinx_font_preload", [], "html")
app.connect("builder-inited", _on_builder_inited)
app.connect("html-page-context", _on_html_page_context)
return {
"version": "1.0",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
Loading
Loading