"""MkDocs hooks for the publishing layer. Two jobs, both publishing-layer only — neither edits the documents or the authored stylesheets in the repository. 1. Directory-style links. The documents sometimes link to a directory the way Gitea renders it, e.g. infrastructure.md contains [`incidents/`](incidents/). On a static site with directory URLs, that href resolves relative to the *page* (giving /infrastructure/incidents/ and a 404). This hook rewrites such links to the generated landing page for that directory (build.sh creates /index.md for documented directories that lack one). 2. Content-hashed stylesheet URLs. The site's own stylesheets (brand.css, typography.css) are listed in mkdocs.yml under extra_css, so every page links them at a FIXED URL (publishing/brand.css). A fixed URL means a browser with a warm cache keeps serving the old stylesheet after a deploy — the URL never changes even when the content does. Material's own assets (assets/stylesheets/main..min.css) are content-hashed and immune; ours were not. This hook gives ours the same property: at build time it copies each to ..css in the output and rewrites the links in the built HTML, so any change to a stylesheet's content necessarily yields a new URL. The authored sources keep their names and locations. """ import hashlib import os import posixpath import re import shutil from mkdocs.utils import get_relative_url _HREF = re.compile(r'(href=)("([^"]+)/")') def on_page_content(html, page, config, files): index_urls = {} for f in files: src = f.src_path if src.endswith("/index.md"): index_urls[posixpath.dirname(src)] = f.url page_dir = posixpath.dirname(page.file.src_path) def fix(match): href = match.group(3) if href.startswith(("http://", "https://", "/", "#")): return match.group(0) target = posixpath.normpath(posixpath.join(page_dir, href)) if target in (".", "..") or target not in index_urls: return match.group(0) return f'{match.group(1)}"{get_relative_url(index_urls[target], page.file.url)}"' return _HREF.sub(fix, html) # --- Content-hashed stylesheet URLs ---------------------------------------- # Material's own asset names use 8 hex characters (main.57a0d79d.min.css); match # that so the site's naming is uniform and the hash is short but collision-safe # for a handful of stylesheets. _HASH_LEN = 8 # A hashed sibling of brand.css is brand.<8 hex>.css. _HASHED_SIBLING = re.compile(r"^(?P.+)\.(?P[0-9a-f]{%d})(?P\.[^.]+)$" % _HASH_LEN) def _hashed_links(config, files=None): """Build each extra_css file to a content-hashed name in the site output. Returns {original-relative-path: hashed-relative-path}. The hashed copy lands in the *same directory* as the original, so relative references inside the CSS (e.g. brand.css's @font-face url("fonts/figtree-400.woff2")) keep resolving. Stale hashed siblings from an earlier build are removed, so the output does not accumulate files across builds. """ site_dir = config["site_dir"] replacements = {} for css in config.get("extra_css") or []: rel = css.lstrip("/").replace("\\", "/") original = os.path.join(site_dir, *rel.split("/")) if not os.path.isfile(original): continue with open(original, "rb") as fh: digest = hashlib.sha256(fh.read()).hexdigest()[:_HASH_LEN] stem, ext = posixpath.splitext(rel) hashed_rel = f"{stem}.{digest}{ext}" hashed = os.path.join(site_dir, *hashed_rel.split("/")) # Copy to the hashed name (a no-op if it is already correct). if os.path.abspath(hashed) != os.path.abspath(original): shutil.copyfile(original, hashed) # Drop stale hashed siblings of the original (same stem/ext, any hash). directory = os.path.dirname(original) base_stem, base_ext = posixpath.splitext(os.path.basename(rel)) keep = os.path.basename(hashed) for name in os.listdir(directory): m = _HASHED_SIBLING.match(name) if not m or name == keep: continue if m.group("stem") == base_stem and m.group("ext") == base_ext: os.remove(os.path.join(directory, name)) replacements[rel] = hashed_rel return replacements def _rewrite_stylesheet_links(site_dir, replacements): """Point every at the hashed file, in place. The href is relative to each page (root pages use "publishing/brand.css", nested pages "../publishing/brand.css"), so match any prefix up to the known relative path and swap only the path tail, preserving the page's own relative prefix. """ patterns = [ (re.compile(r'(href="[^"]*)' + re.escape(orig) + r'(")'), hashed) for orig, hashed in replacements.items() ] for root, _dirs, filenames in os.walk(site_dir): for name in filenames: if not name.endswith(".html"): continue path = os.path.join(root, name) with open(path, "r", encoding="utf-8") as fh: html = fh.read() new = html for pattern, hashed in patterns: new = pattern.sub(lambda m, h=hashed: m.group(1) + h + m.group(2), new) if new != html: with open(path, "w", encoding="utf-8") as fh: fh.write(new) def on_post_build(config): replacements = _hashed_links(config) if replacements: _rewrite_stylesheet_links(config["site_dir"], replacements)