"""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)