Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
10 changes: 9 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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-yXO_VcjbsD8Bs4w6PgY5_.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
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mkdocs-material[imaging]==9.7.5
mkdocs-macros-plugin==1.5.0
requests==2.33.1
Comment thread
mrdarrengriffin marked this conversation as resolved.
Outdated
1 change: 0 additions & 1 deletion source/assets/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ code {
text-decoration: underline;
}


ol:not(.no-styling) {
list-style: none;
counter-reset: markdown-ordered-list;
Expand Down
74 changes: 74 additions & 0 deletions source/hooks/plausible.py
Original file line number Diff line number Diff line change
@@ -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
Comment thread
mrdarrengriffin marked this conversation as resolved.

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}")
Comment thread
mrdarrengriffin marked this conversation as resolved.
Outdated
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")
Comment thread
mrdarrengriffin marked this conversation as resolved.
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"
Comment thread
mrdarrengriffin marked this conversation as resolved.
Outdated
)
return []


def on_config(config: MkDocsConfig, **kwargs):
config.extra.setdefault("plausible", {})["allowed_referrers"] = allowed_referrers()
return config
16 changes: 10 additions & 6 deletions source/overrides/404.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{% extends "main.html" %}

{% block content %}
<h1>404 - Not found</h1>
<script>document.addEventListener("DOMContentLoaded", function () { plausible("404"); });</script>
{% endblock %}
{% extends "main.html" %}

{% block content %}
<h1>404 - Not found</h1>
<script>
document.addEventListener("DOMContentLoaded", function () {
if (typeof window.plausible === "function") window.plausible("404");
Comment thread
mrdarrengriffin marked this conversation as resolved.
Outdated
});
</script>
{% endblock %}
2 changes: 1 addition & 1 deletion source/overrides/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

{% block extrahead %}
{{ super() }}
<script async src="https://plausible.openhomefoundation.org/js/pa-yXO_VcjbsD8Bs4w6PgY5_.js"></script><script> window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}}; plausible.init()</script>
{% include "partials/plausible.html" %}
{% endblock %}
3 changes: 1 addition & 2 deletions source/overrides/partials/footer.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<footer class="md-footer md-typeset">
<p class="plausible-attribution">
This website uses <a href="https://www.openhomefoundation.org/blog/making-our-web-analytics-open-source-with-plausible/" target="_blank" rel="noopener">privacy-first analytics</a> to help us improve the site. You can view all data in our <a href="https://plausible.openhomefoundation.org/hacs.xyz" target="_blank" rel="noopener">public dashboard</a>.
This website uses <a href="https://www.openhomefoundation.org/blog/making-our-web-analytics-open-source-with-plausible/" target="_blank" rel="noopener">privacy-first analytics</a> to help us improve the site. You can view all data in our <a href="{{ config.extra.plausible.dashboard }}" target="_blank" rel="noopener">public dashboard</a>.
</p>
</footer>

50 changes: 50 additions & 0 deletions source/overrides/partials/plausible.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{#-
Plausible analytics.

Visitors arriving from their own Home Assistant or ESPHome instance send that
Comment thread
mrdarrengriffin marked this conversation as resolved.
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.
-#}
<script async src="{{ config.extra.plausible.script }}"></script>
Comment thread
ludeeus marked this conversation as resolved.
<script>
(function () {
var allowedReferrers = {{ config.extra.plausible.allowed_referrers | tojson }};

window.plausible = window.plausible || function () {
(plausible.q = plausible.q || []).push(arguments);
};
plausible.init = plausible.init || function (options) {
plausible.o = options || {};
};

plausible.init({
transformRequest: function (payload) {
if (!payload.r) {
return payload;
}

var host = "";
try {
host = new URL(payload.r).hostname.replace(/\.$/, "");
Comment thread
mrdarrengriffin marked this conversation as resolved.
Outdated
} catch (error) {
// A referrer we cannot parse falls through and gets replaced.
Comment thread
mrdarrengriffin marked this conversation as resolved.
Outdated
}

for (var index = 0; index < allowedReferrers.length; index++) {
var domain = allowedReferrers[index];
if (host === domain || (host.length > domain.length && host.slice(-(domain.length + 1)) === "." + domain)) {
return payload;
}
}
Comment thread
mrdarrengriffin marked this conversation as resolved.

// One aggregate bucket, so we can see how much we filter without learning
// anything about individual visitors. RFC 2606 reserves .invalid, so this
// can never collide with a real domain.
payload.r = "https://unlisted.invalid/";
return payload;
},
});
})();
</script>
Loading