Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.34.2
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
75 changes: 75 additions & 0 deletions source/hooks/plausible.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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("Discarding unusable allow list at %s: %s", 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("Fetched %s allowed referrers", len(referrers))
return referrers


def allowed_referrers() -> list[str]:
"""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
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("Could not refresh the allow list, reusing %s: %s", ALLOWLIST_FILE, exception)
return cached
log.info("Could not fetch the allow list (%s), Plausible is left out of this build", exception)
return []


def on_config(config: MkDocsConfig, **kwargs):
config.extra.setdefault("plausible", {})["allowed_referrers"] = allowed_referrers()
return config
18 changes: 12 additions & 6 deletions source/overrides/404.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{% 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")
};
});
</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>

62 changes: 62 additions & 0 deletions source/overrides/partials/plausible.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{#-
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.

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 %}
<script async src="{{ config.extra.plausible.script }}"></script>
<script>
(function () {
var allowedReferrers = {{ config.extra.plausible.allowed_referrers | tojson }};

// 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.
function unlisted(payload) {
payload.r = "https://unlisted.invalid/";
return payload;
}

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 {
// The URL parser only lowercases the host of special schemes, so
// android-app:// and other referrers still need folding.
host = new URL(payload.r).hostname.toLowerCase().replace(/\.$/, "");
} catch (error) {
return unlisted(payload);
}

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.

return unlisted(payload);
},
});
})();
</script>
{%- endif %}
Loading