Skip to content

Commit a6bde17

Browse files
committed
feat: add contact_card macro and bot-protected contact config
- Add main.py with contact_card() macro (base64 encode at build, JS decode at runtime) - Add render_macros frontmatter to index.md - Add contact_methods config to mkdocs.sample.yml (empty by default) - Add on_pre_page_macros for frontmatter variable resolution
1 parent b5b27ee commit a6bde17

3 files changed

Lines changed: 176 additions & 0 deletions

File tree

plex-guide/docs/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: "{{ server_name }} Streaming"
33
description: "Watch movies and TV shows on {{ server_name }}"
4+
render_macros: true
45
tags:
56
- plex
67
- media
@@ -543,3 +544,5 @@ If you need to move to a new Plex account (new email, etc.), {{ admin_name }} ca
543544
{% endif %}{% if has_request_system %}- A movie/show has the wrong audio, bad quality, or missing subtitles (use [Report Issue](https://{{ request_url }}) first)
544545
{% else %}- A movie/show has the wrong audio, bad quality, or missing subtitles
545546
{% endif %}- You're seeing payment prompts when you shouldn't be
547+
548+
{{ contact_card() }}

plex-guide/main.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""MkDocs Macros hook — contact card with bot-protection and metadata resolution.
2+
3+
Provides:
4+
- contact_card(): Bot-protected contact info (base64-encoded, JS-decoded)
5+
- Frontmatter variable resolution ({{ server_name }} in title/description)
6+
"""
7+
8+
import base64
9+
import re
10+
11+
from markupsafe import Markup
12+
13+
14+
def define_env(env):
15+
"""Register macros environment."""
16+
17+
@env.macro
18+
def contact_card(admin_name=None, contact_methods=None):
19+
"""Render bot-protected contact card from config.
20+
21+
Reads admin_name and contact_methods from extra: config if not passed.
22+
Base64-encodes values at build time; JavaScript decodes at runtime.
23+
24+
Args:
25+
admin_name: Display name for the admin. Falls back to extra.admin_name.
26+
contact_methods: List of contact method dicts. Falls back to extra.contact_methods.
27+
28+
Returns:
29+
Markup with encoded contact info and JS decoder.
30+
"""
31+
extra = env.conf.get("extra", {})
32+
admin_name = admin_name or extra.get("admin_name", "Admin")
33+
contact_methods = contact_methods or extra.get("contact_methods", [])
34+
35+
if not contact_methods:
36+
return Markup("")
37+
38+
items_html = []
39+
js_decoders = []
40+
41+
for i, method in enumerate(contact_methods):
42+
method_type = method.get("type", "link")
43+
label = method.get("label", "Contact")
44+
value = method.get("value", "")
45+
preferred = method.get("preferred", False)
46+
47+
if not value:
48+
continue
49+
50+
encoded = base64.b64encode(value.encode()).decode("ascii")
51+
elem_id = f"contact-{i}"
52+
badge = (
53+
' <span class="preferred-badge">preferred</span>' if preferred else ""
54+
)
55+
56+
href = _build_href(method_type, value)
57+
display_value = _display_value(method_type, value)
58+
59+
# noscript fallback with plain value
60+
noscript_link = (
61+
f'<a href="{href}" rel="nofollow noopener noreferrer">{display_value}</a>'
62+
if href != value
63+
else f"<span>{display_value}</span>"
64+
)
65+
66+
items_html.append(
67+
f'<li id="{elem_id}">'
68+
f"<strong>{label}</strong>{badge}: "
69+
f"<noscript>{noscript_link}</noscript>"
70+
f"</li>"
71+
)
72+
73+
js_decoders.append(
74+
f' decode("{elem_id}", "{encoded}", "{label}", "{method_type}", {str(preferred).lower()});'
75+
)
76+
77+
if not items_html:
78+
return Markup("")
79+
80+
items = "\n".join(items_html)
81+
decoders = "\n".join(js_decoders)
82+
83+
html = f"""\
84+
<div class="contact-card">
85+
<ul>
86+
{items}
87+
</ul>
88+
</div>
89+
<script>
90+
(function() {{
91+
function decode(id, encoded, label, type, preferred) {{
92+
var elem = document.getElementById(id);
93+
if (!elem) return;
94+
try {{
95+
var decoded = atob(encoded);
96+
var badge = preferred ? ' <span class="preferred-badge">preferred</span>' : '';
97+
var href, display;
98+
if (type === 'email') {{
99+
href = 'mailto:' + decoded;
100+
display = decoded;
101+
}} else if (type === 'phone') {{
102+
href = 'tel:' + decoded.replace(/[^+\\d]/g, '');
103+
display = decoded;
104+
}} else if (type === 'signal_url') {{
105+
href = decoded;
106+
display = label;
107+
}} else {{
108+
href = decoded;
109+
display = label;
110+
}}
111+
elem.innerHTML = '<strong>' + label + '</strong>' + badge + ': '
112+
+ '<a href="' + href + '" rel="nofollow noopener noreferrer">' + display + '</a>';
113+
}} catch (e) {{
114+
// Decoding failed — noscript fallback remains
115+
}}
116+
}}
117+
{decoders}
118+
}})();
119+
</script>"""
120+
121+
return Markup(html)
122+
123+
124+
def _build_href(method_type, value):
125+
"""Build the appropriate href for a contact method type."""
126+
if method_type == "email":
127+
return f"mailto:{value}"
128+
if method_type == "phone":
129+
digits = re.sub(r"[^\d+]", "", value)
130+
return f"tel:{digits}"
131+
return value
132+
133+
134+
def _display_value(method_type, value):
135+
"""Build a display string for noscript fallback."""
136+
if method_type in ("email", "phone"):
137+
return value
138+
if method_type == "signal_url":
139+
return "Signal"
140+
return "Link"
141+
142+
143+
def on_pre_page_macros(env):
144+
"""Resolve Jinja2 expressions in page metadata before rendering."""
145+
extra = env.conf.get("extra", {})
146+
page = env.page
147+
148+
for key in ("title", "description"):
149+
value = page.meta.get(key, "")
150+
if "{{" in str(value):
151+
resolved = re.sub(
152+
r"\{\{\s*(\w+)\s*\}\}",
153+
lambda m: str(extra.get(m.group(1), m.group(0))),
154+
str(value),
155+
)
156+
page.meta[key] = resolved
157+
if key == "title":
158+
page.title = resolved

plex-guide/mkdocs.sample.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,18 @@ extra:
7777
show_costs: false # Whether to show server cost information
7878
server_cost: "" # Monthly server cost (e.g., "~$50/month")
7979
speedtest_url: "" # Speed test URL (e.g., "speedtest.example.com"). Falls back to fast.com if empty
80+
81+
# --- Contact card (bot-protected, decoded via JavaScript) ---
82+
# Each method: type (email|phone|signal_url), label, value, preferred (optional)
83+
contact_methods: []
84+
# contact_methods:
85+
# - type: signal_url
86+
# label: "Message on Signal"
87+
# value: "https://signal.me/#eu/..."
88+
# preferred: true
89+
# - type: phone
90+
# label: "Text"
91+
# value: "+15551234567"
92+
# - type: email
93+
# label: "Email"
94+
# value: "admin@example.com"

0 commit comments

Comments
 (0)