From c209d4f8bd01b8263aac6882c90383136f8a99c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:22:09 +0000 Subject: [PATCH 1/6] Reimplement Plausible analytics The tracker was inlined into main.html with a bare init, which meant the `privacy` plugin downloaded it at build time and served a frozen self-hosted copy, and every referrer reached Plausible untouched. Visitors arriving from their own Home Assistant or ESPHome instance send that private URL as the referrer, so those had to be filtered before Plausible ever recorded them. - Move the tracker into a `partials/plausible.html` partial, included from the `extrahead` block, and read the script and dashboard URLs from `extra` in mkdocs.yml. - Exclude the Plausible host from the `privacy` plugin so the tracker is loaded from the Plausible instance and stays current. - Add a `source/hooks/plausible.py` hook that fetches the Open Home Foundation referrer allow list once per build, caches it under `.cache` alongside the translations, and exposes it to the templates. Fetch failures fall back to the cached copy and never fail the build. - Initialize the tracker with a `transformRequest` that replaces any referrer outside the allow list with a single `unlisted.invalid` bucket, so we can see how much is filtered without learning anything about individual visitors. - Guard the 404 event on `window.plausible` and point the footer dashboard link at the configured URL. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UkMvZDbHQRYGY1u1P53uFd --- mkdocs.yml | 10 +++- source/assets/stylesheets/extra.css | 1 - source/hooks/plausible.py | 74 ++++++++++++++++++++++++ source/overrides/404.html | 16 +++-- source/overrides/main.html | 2 +- source/overrides/partials/footer.html | 3 +- source/overrides/partials/plausible.html | 50 ++++++++++++++++ 7 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 source/hooks/plausible.py create mode 100644 source/overrides/partials/plausible.html diff --git a/mkdocs.yml b/mkdocs.yml index b7cb28c4..747a3733 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,7 +62,11 @@ theme: icon: annotation: material/information plugins: - - privacy + - privacy: + assets_exclude: + # The tracker has to be loaded from the Plausible instance, so that it stays + # current instead of being frozen into a self-hosted copy at build time. + - plausible.openhomefoundation.org/* - macros: on_undefined: strict include_dir: source/includes @@ -76,8 +80,12 @@ plugins: - tags hooks: - source/hooks/html_tag_modifier.py + - source/hooks/plausible.py - source/hooks/shortcodes.py extra: + plausible: + script: https://plausible.openhomefoundation.org/js/pa-GYrJt4yZ1JGFK2tTFRKPd.js + dashboard: https://plausible.openhomefoundation.org/hacs.xyz resources: - link: https://github.com/hacs/.github/blob/master/CODE_OF_CONDUCT.md title: Code of Conduct diff --git a/source/assets/stylesheets/extra.css b/source/assets/stylesheets/extra.css index 24778b20..3886356c 100644 --- a/source/assets/stylesheets/extra.css +++ b/source/assets/stylesheets/extra.css @@ -172,7 +172,6 @@ code { text-decoration: underline; } - ol:not(.no-styling) { list-style: none; counter-reset: markdown-ordered-list; diff --git a/source/hooks/plausible.py b/source/hooks/plausible.py new file mode 100644 index 00000000..449ba9e8 --- /dev/null +++ b/source/hooks/plausible.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json +import logging +import time +from pathlib import Path + +import requests +from mkdocs.config.defaults import MkDocsConfig + +ALLOWLIST_URL = "https://www.openhomefoundation.org/allowed-referrers.json" +ALLOWLIST_FILE = Path(".cache/plausible/allowed-referrers.json") +ALLOWLIST_MAX_AGE = 3600 + +# `strict: true` aborts the build on anything logged at WARNING or above under the +# "mkdocs" logger, and an unreachable allow list must never break the site, so the +# fallbacks below report at INFO instead. +log = logging.getLogger("mkdocs.hooks.plausible") + + +def normalize_referrers(payload: object) -> list[str]: + """Reduce the allow list payload to bare, lowercase domains.""" + if not isinstance(payload, list) or not all(isinstance(entry, str) for entry in payload): + raise ValueError("payload is not an array of strings") + return [domain for entry in payload if (domain := entry.strip().lower().removesuffix("."))] + + +def cached_referrers() -> list[str] | None: + """The allow list left behind by an earlier build, if it is still usable.""" + try: + return normalize_referrers(json.loads(ALLOWLIST_FILE.read_text())) + except FileNotFoundError: + return None + except (OSError, ValueError) as exception: + log.info(f"Discarding unusable allow list at {ALLOWLIST_FILE}: {exception}") + return None + + +def download_referrers() -> list[str]: + """Download the allow list and cache it for subsequent builds.""" + response = requests.get(ALLOWLIST_URL, timeout=30) + response.raise_for_status() + referrers = normalize_referrers(response.json()) + ALLOWLIST_FILE.parent.mkdir(parents=True, exist_ok=True) + ALLOWLIST_FILE.write_text(json.dumps(referrers, indent=4, sort_keys=True) + "\n") + log.info(f"Fetched {len(referrers)} allowed referrers") + return referrers + + +def allowed_referrers() -> list[str]: + """The allow list, downloaded at most once per build.""" + if ( + ALLOWLIST_FILE.exists() + and time.time() - ALLOWLIST_FILE.stat().st_mtime < ALLOWLIST_MAX_AGE + and (cached := cached_referrers()) is not None + ): + return cached + + try: + return download_referrers() + except (OSError, ValueError, requests.RequestException) as exception: + if (cached := cached_referrers()) is not None: + log.info(f"Could not refresh the allow list, reusing {ALLOWLIST_FILE}: {exception}") + return cached + log.info( + f"Could not fetch the allow list ({exception}), " + "every referrer will be reported to Plausible as unlisted" + ) + return [] + + +def on_config(config: MkDocsConfig, **kwargs): + config.extra.setdefault("plausible", {})["allowed_referrers"] = allowed_referrers() + return config diff --git a/source/overrides/404.html b/source/overrides/404.html index db0d5c35..8c7054cc 100644 --- a/source/overrides/404.html +++ b/source/overrides/404.html @@ -1,6 +1,10 @@ -{% extends "main.html" %} - -{% block content %} -

404 - Not found

- -{% endblock %} +{% extends "main.html" %} + +{% block content %} +

404 - Not found

+ +{% endblock %} diff --git a/source/overrides/main.html b/source/overrides/main.html index cdab94cc..c0567482 100644 --- a/source/overrides/main.html +++ b/source/overrides/main.html @@ -2,5 +2,5 @@ {% block extrahead %} {{ super() }} - + {% include "partials/plausible.html" %} {% endblock %} diff --git a/source/overrides/partials/footer.html b/source/overrides/partials/footer.html index 0240c62c..2c464e5a 100644 --- a/source/overrides/partials/footer.html +++ b/source/overrides/partials/footer.html @@ -1,6 +1,5 @@ - diff --git a/source/overrides/partials/plausible.html b/source/overrides/partials/plausible.html new file mode 100644 index 00000000..756e070b --- /dev/null +++ b/source/overrides/partials/plausible.html @@ -0,0 +1,50 @@ +{#- + Plausible analytics. + + Visitors arriving from their own Home Assistant or ESPHome instance send that + private URL as the referrer, so the tracker is initialized with a transformRequest + that replaces every referrer outside the Open Home Foundation allow list with a + single aggregate bucket. source/hooks/plausible.py fetches the allow list once per + build and the loop below is the only thing that ever sees the real referrer. +-#} + + From e3ef6e835e3bbecb5a8e48f4410ec2da69b91fbf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:21:26 +0000 Subject: [PATCH 2/6] Declare requests and restore the updated Plausible script src Merging main into this branch silently dropped the script src bump from #702, because that commit edited the tracker line in main.html while this branch had replaced that whole file with an include. Move the new ID into the `extra` value the partial reads. Also declare `requests` explicitly. source/macros.py has always imported it and both pinned plugins depend on it, so builds were never actually broken, but the hook makes a second first-party use of a package the project never asked for. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UkMvZDbHQRYGY1u1P53uFd --- mkdocs.yml | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 747a3733..448fc1fd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,7 +84,7 @@ hooks: - source/hooks/shortcodes.py extra: plausible: - script: https://plausible.openhomefoundation.org/js/pa-GYrJt4yZ1JGFK2tTFRKPd.js + script: https://plausible.openhomefoundation.org/js/pa-yXO_VcjbsD8Bs4w6PgY5_.js dashboard: https://plausible.openhomefoundation.org/hacs.xyz resources: - link: https://github.com/hacs/.github/blob/master/CODE_OF_CONDUCT.md diff --git a/requirements.txt b/requirements.txt index 70d27a99..27e8c469 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ mkdocs-material[imaging]==9.7.5 mkdocs-macros-plugin==1.5.0 +requests==2.33.1 From c699b4267b0113cd3591e038924055913d9ce20b Mon Sep 17 00:00:00 2001 From: Darren Griffin Date: Mon, 17 Aug 2026 14:17:05 +0100 Subject: [PATCH 3/6] Update source/overrides/404.html MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- source/overrides/404.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/overrides/404.html b/source/overrides/404.html index 8c7054cc..5ce1c38a 100644 --- a/source/overrides/404.html +++ b/source/overrides/404.html @@ -4,7 +4,9 @@

404 - Not found

{% endblock %} From 6655f95c08c181707f88548ba075f7aac33169b3 Mon Sep 17 00:00:00 2001 From: Darren Griffin Date: Mon, 17 Aug 2026 14:18:24 +0100 Subject: [PATCH 4/6] Update source/hooks/plausible.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- source/hooks/plausible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/hooks/plausible.py b/source/hooks/plausible.py index 449ba9e8..2ce3d0fc 100644 --- a/source/hooks/plausible.py +++ b/source/hooks/plausible.py @@ -32,7 +32,7 @@ def cached_referrers() -> list[str] | None: except FileNotFoundError: return None except (OSError, ValueError) as exception: - log.info(f"Discarding unusable allow list at {ALLOWLIST_FILE}: {exception}") + log.info("Discarding unusable allow list at %s: %s", ALLOWLIST_FILE, exception) return None From cdec7674ae13a55050f8b2f004d6b84572179bb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 13:36:30 +0000 Subject: [PATCH 5/6] Address review: early return, host folding, and no-allow-list behaviour - Return early when the referrer cannot be parsed, instead of falling through to a loop that cannot match an empty host. - Lowercase the host. The URL parser only folds the host of special schemes, so an `android-app://com.Google.Android/` referrer kept its case and could miss an allow list entry. - Leave Plausible out of the build entirely when the allow list is unavailable. Reporting every visit as unlisted was data without value; failing the build would turn any transient network blip into a red deploy. - Use the current requests release. - Apply the lazy logging style to the remaining log calls for consistency. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UkMvZDbHQRYGY1u1P53uFd --- requirements.txt | 2 +- source/hooks/plausible.py | 15 +++++++------ source/overrides/partials/plausible.html | 28 +++++++++++++++++------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/requirements.txt b/requirements.txt index 27e8c469..2e7aa6f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ mkdocs-material[imaging]==9.7.5 mkdocs-macros-plugin==1.5.0 -requests==2.33.1 +requests==2.34.2 diff --git a/source/hooks/plausible.py b/source/hooks/plausible.py index 2ce3d0fc..cc1370f6 100644 --- a/source/hooks/plausible.py +++ b/source/hooks/plausible.py @@ -43,12 +43,16 @@ def download_referrers() -> list[str]: referrers = normalize_referrers(response.json()) ALLOWLIST_FILE.parent.mkdir(parents=True, exist_ok=True) ALLOWLIST_FILE.write_text(json.dumps(referrers, indent=4, sort_keys=True) + "\n") - log.info(f"Fetched {len(referrers)} allowed referrers") + log.info("Fetched %s allowed referrers", len(referrers)) return referrers def allowed_referrers() -> list[str]: - """The allow list, downloaded at most once per build.""" + """The allow list, downloaded at most once per build. + + An empty result means no referrer can be checked, which leaves Plausible out of + the build entirely rather than reporting every visit as unlisted. + """ if ( ALLOWLIST_FILE.exists() and time.time() - ALLOWLIST_FILE.stat().st_mtime < ALLOWLIST_MAX_AGE @@ -60,12 +64,9 @@ def allowed_referrers() -> list[str]: return download_referrers() except (OSError, ValueError, requests.RequestException) as exception: if (cached := cached_referrers()) is not None: - log.info(f"Could not refresh the allow list, reusing {ALLOWLIST_FILE}: {exception}") + log.info("Could not refresh the allow list, reusing %s: %s", ALLOWLIST_FILE, exception) return cached - log.info( - f"Could not fetch the allow list ({exception}), " - "every referrer will be reported to Plausible as unlisted" - ) + log.info("Could not fetch the allow list (%s), Plausible is left out of this build", exception) return [] diff --git a/source/overrides/partials/plausible.html b/source/overrides/partials/plausible.html index 756e070b..2afec098 100644 --- a/source/overrides/partials/plausible.html +++ b/source/overrides/partials/plausible.html @@ -6,12 +6,25 @@ that replaces every referrer outside the Open Home Foundation allow list with a single aggregate bucket. source/hooks/plausible.py fetches the allow list once per build and the loop below is the only thing that ever sees the real referrer. + + If the allow list could not be fetched there is nothing to check referrers + against, so analytics stays off for that build rather than reporting every visit + as unlisted. -#} +{%- if config.extra.plausible.allowed_referrers %} +{%- endif %} From 6b8e5092335ea51e7645a659c4af230d08892e15 Mon Sep 17 00:00:00 2001 From: Darren Griffin Date: Wed, 19 Aug 2026 14:23:55 +0100 Subject: [PATCH 6/6] Re-trigger CI