From edde12f17c3ff34dc1310bbcbda333897b876c56 Mon Sep 17 00:00:00 2001 From: Jorijn Schrijvershof Date: Tue, 17 Feb 2026 11:15:43 +0100 Subject: [PATCH] feat: add configurable custom HTML head injection (#118) Allow deployers to inject custom HTML into the of every page via the CUSTOM_HEAD_HTML config option, useful for analytics scripts (Plausible, Matomo, etc.) without modifying source. Co-authored-by: Claude Opus 4.6 --- meshcore.conf.example | 8 ++++++++ src/meshmon/env.py | 3 +++ src/meshmon/html.py | 5 +++++ src/meshmon/templates/base.html | 1 + tests/html/test_jinja_env.py | 25 +++++++++++++++++++++++++ 5 files changed, 42 insertions(+) diff --git a/meshcore.conf.example b/meshcore.conf.example index 008b78f..700a709 100644 --- a/meshcore.conf.example +++ b/meshcore.conf.example @@ -148,6 +148,14 @@ RADIO_CODING_RATE=CR8 # TELEMETRY_RETRY_ATTEMPTS=2 # TELEMETRY_RETRY_BACKOFF_S=4 +# ============================================================================= +# Custom HTML (Analytics, etc.) +# ============================================================================= +# Inject custom HTML into the of every page. +# Useful for analytics scripts (Plausible, Matomo, etc.) without modifying source. +# Example for Plausible: +# CUSTOM_HEAD_HTML= + # ============================================================================= # Paths (Native installation only) # ============================================================================= diff --git a/src/meshmon/env.py b/src/meshmon/env.py index c82fe17..dc59b34 100644 --- a/src/meshmon/env.py +++ b/src/meshmon/env.py @@ -273,6 +273,9 @@ class Config: self.html_path = get_str("HTML_PATH", "") or "" + # Custom HTML injected into (e.g. analytics scripts) + self.custom_head_html = get_str("CUSTOM_HEAD_HTML", "") or "" + # Global config instance _config: Config | None = None diff --git a/src/meshmon/html.py b/src/meshmon/html.py index 85b96f7..b97be1d 100644 --- a/src/meshmon/html.py +++ b/src/meshmon/html.py @@ -727,6 +727,9 @@ def build_page_context( "page_title": page_title, "page_subtitle": page_subtitle, "chart_groups": chart_groups, + + # Custom HTML + "custom_head_html": cfg.custom_head_html, } @@ -1304,6 +1307,7 @@ def render_report_page( "monthly_links": monthly_links, "prev_report": prev_report, "next_report": next_report, + "custom_head_html": cfg.custom_head_html, } template = env.get_template("report.html") @@ -1341,6 +1345,7 @@ def render_reports_index(report_sections: list[dict]) -> str: "css_path": "../", "report_sections": report_sections, "month_abbrs": month_abbrs, + "custom_head_html": cfg.custom_head_html, } template = env.get_template("report_index.html") diff --git a/src/meshmon/templates/base.html b/src/meshmon/templates/base.html index de46f8d..8a3a39f 100644 --- a/src/meshmon/templates/base.html +++ b/src/meshmon/templates/base.html @@ -19,6 +19,7 @@ + {% if custom_head_html %}{{ custom_head_html | safe }}{% endif %} {% block body %}{% endblock %} diff --git a/tests/html/test_jinja_env.py b/tests/html/test_jinja_env.py index 7559707..6fecc6a 100644 --- a/tests/html/test_jinja_env.py +++ b/tests/html/test_jinja_env.py @@ -147,3 +147,28 @@ class TestTemplateRendering: assert "" in html assert "" in html + + def test_custom_head_html_rendered_when_set(self): + """Custom head HTML appears in rendered output when provided.""" + env = get_jinja_env() + template = env.get_template("base.html") + + snippet = '' + html = template.render( + title="Test", + custom_head_html=snippet, + ) + + assert snippet in html + assert html.index(snippet) < html.index("") + + def test_custom_head_html_absent_when_empty(self): + """No extra content in head when custom_head_html is empty.""" + env = get_jinja_env() + template = env.get_template("base.html") + + html_with = template.render(title="Test", custom_head_html="") + html_without = template.render(title="Test") + + # Both should produce identical output (no extra content) + assert html_with == html_without