From 69394d1b0dfbddd5d513bd187f19a4ccab50a6ca Mon Sep 17 00:00:00 2001 From: Etienne Pichot Damon Date: Wed, 4 Nov 2026 11:36:31 -0500 Subject: [PATCH] fix(site): version stylesheet and script URLs so deploys reach returning visitors The server lets browsers keep CSS and JS files for 4 hours (cache-control: max-age=14400), and the site's own files keep the same name from one deploy to the next. After a deploy a returning visitor gets the new HTML with the old script or stylesheet. This is why the History section of the specification pages stayed on its fallback text after #68 was deployed: the cached specifications.js predated it. A new hook, hooks/asset_versions.py, links every local entry of extra_css and extra_javascript as ?v=. The suffix only changes when the file does, so browsers keep their copy between deploys and fetch the new one as soon as it changes. External URLs are left alone, and Material's own bundle already has a hash in its name. Nothing changes for visitors: the same file is served with or without the suffix. Checked with ProperDocs 1.6.7: every page links e.g. assets/javascript/specifications.js?v=4f9819e1de, and the GBFS page still loads its History section and js-yaml. Co-Authored-By: Claude Opus 5.5 --- config/en/properdocs.yml | 1 + hooks/asset_versions.py | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 hooks/asset_versions.py diff --git a/config/en/properdocs.yml b/config/en/properdocs.yml index bd4282c..ae9535a 100644 --- a/config/en/properdocs.yml +++ b/config/en/properdocs.yml @@ -22,6 +22,7 @@ theme: hooks: - ../../hooks/specifications.py + - ../../hooks/asset_versions.py plugins: - search: diff --git a/hooks/asset_versions.py b/hooks/asset_versions.py new file mode 100644 index 0000000..e2a4aba --- /dev/null +++ b/hooks/asset_versions.py @@ -0,0 +1,49 @@ +"""Give the site's own stylesheets and scripts a version in their URL. + +The server tells browsers they may keep a CSS or JS file for hours without +asking again. The files keep the same name from one deploy to the next, so +after a change a returning visitor gets the new HTML with the old script or +stylesheet, and a page that needs the new one breaks until the copy expires. + +Every local entry in `extra_css` and `extra_javascript` is linked as +`?v=` instead. The hash only changes when the file +does, so browsers keep using their copy until there is a new one, and fetch +it as soon as there is. Visitors never see the suffix; the file served is the +same with or without it. + +Material's own bundle is not touched: its file names already carry a hash. +""" + +from __future__ import annotations + +import hashlib +import os + + +def _version(path, dirs): + for base in dirs: + full = os.path.join(base, path) + if os.path.isfile(full): + with open(full, "rb") as fh: + return hashlib.sha256(fh.read()).hexdigest()[:10] + return None + + +def _versioned(path, dirs): + path = path.split("?v=", 1)[0] # the config may be reused by `serve` + if "://" in path or path.startswith("//"): + return path + version = _version(path, dirs) + return "%s?v=%s" % (path, version) if version else path + + +def on_config(config): + dirs = [config["docs_dir"]] + list(config["theme"].dirs) + config["extra_css"] = [_versioned(p, dirs) for p in config["extra_css"]] + scripts = config["extra_javascript"] + for n, script in enumerate(scripts): + if isinstance(script, str): + scripts[n] = _versioned(script, dirs) + else: # ExtraScriptValue + script.path = _versioned(script.path, dirs) + return config