Skip to content

Commit 0d173a6

Browse files
rorarclaude
andcommitted
docs: add prev/next navigation, move IP replacer to template
- Enable navigation.footer for previous/next page links at bottom - Move IP replacer from JS-injected DOM to MkDocs Material template override (announce block) — persistent across page navigation - Add ? info button with tooltip explaining the IP replacer feature - Add custom_dir: overrides to mkdocs.yml - Simplify ip-replacer.js (no more DOM injection, template handles UI) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3df406c commit 0d173a6

4 files changed

Lines changed: 109 additions & 111 deletions

File tree

docs/javascripts/ip-replacer.js

Lines changed: 38 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* Replaces all occurrences of <your-unraid-ip> in text and code blocks
55
* with a user-provided IP address. Persists via localStorage.
66
*
7-
* Security: User input is escaped via escapeHtml() before insertion.
8-
* Original DOM content is preserved from MkDocs-rendered pages only.
7+
* The input bar is rendered via MkDocs Material template override (announce block),
8+
* so it persists across page navigations without re-injection.
99
*
10+
* Security: User input is escaped via escapeHtml() before insertion.
1011
* Browser-only — no data is stored or transmitted.
1112
*/
1213
(function () {
@@ -15,84 +16,30 @@
1516
var STORAGE_KEY = "immich-guide-unraid-ip";
1617
var PLACEHOLDER_RE = /&lt;your-unraid-ip&gt;|<your-unraid-ip>/g;
1718

18-
/** Escape HTML special characters to prevent XSS from user input. */
1919
function escapeHtml(str) {
2020
var div = document.createElement("div");
2121
div.appendChild(document.createTextNode(str));
2222
return div.innerHTML;
2323
}
2424

25-
/** Build and inject the input bar below the header. */
26-
function createInputBar() {
27-
var bar = document.createElement("div");
28-
bar.className = "ip-replacer";
29-
30-
var label = document.createElement("label");
31-
label.setAttribute("for", "ip-input");
32-
label.textContent = "Your Unraid IP:";
33-
34-
var input = document.createElement("input");
35-
input.id = "ip-input";
36-
input.type = "text";
37-
input.placeholder = "192.168.1.x";
38-
input.autocomplete = "off";
39-
input.spellcheck = false;
40-
41-
var resetBtn = document.createElement("button");
42-
resetBtn.id = "ip-reset";
43-
resetBtn.type = "button";
44-
resetBtn.title = "Reset to placeholder";
45-
resetBtn.textContent = "Reset";
46-
47-
var disclaimer = document.createElement("span");
48-
disclaimer.className = "ip-replacer-disclaimer";
49-
disclaimer.textContent = "Browser-only \u2014 no data is stored or transmitted.";
50-
51-
bar.appendChild(label);
52-
bar.appendChild(input);
53-
bar.appendChild(resetBtn);
54-
bar.appendChild(disclaimer);
55-
56-
var header = document.querySelector(".md-header");
57-
if (header && header.nextSibling) {
58-
header.parentNode.insertBefore(bar, header.nextSibling);
59-
} else {
60-
document.body.prepend(bar);
61-
}
62-
63-
return bar;
64-
}
65-
66-
/**
67-
* Snapshot original innerHTML of elements that contain the placeholder.
68-
* We store originals from the MkDocs-rendered DOM (trusted content).
69-
*/
7025
function snapshotOriginals() {
7126
var selectors =
7227
".md-content p, .md-content li, .md-content td, .md-content th, " +
7328
".md-content code, .md-content pre, .md-content a, .md-content h1, " +
7429
".md-content h2, .md-content h3, .md-content h4";
7530
var elements = document.querySelectorAll(selectors);
7631
var targets = [];
77-
7832
for (var i = 0; i < elements.length; i++) {
7933
var el = elements[i];
8034
PLACEHOLDER_RE.lastIndex = 0;
8135
if (PLACEHOLDER_RE.test(el.innerHTML)) {
82-
if (!el.dataset.ipOriginal) {
83-
el.dataset.ipOriginal = el.innerHTML;
84-
}
36+
el.dataset.ipOriginal = el.innerHTML;
8537
targets.push(el);
8638
}
8739
}
8840
return targets;
8941
}
9042

91-
/**
92-
* Replace placeholder with given IP in all target elements.
93-
* User input is escaped to prevent XSS before being placed into a <span>.
94-
* The base content comes from dataset.ipOriginal (trusted MkDocs DOM).
95-
*/
9643
function applyReplacement(targets, ip) {
9744
var safeIp = escapeHtml(ip.trim());
9845
for (var i = 0; i < targets.length; i++) {
@@ -111,7 +58,6 @@
11158
}
11259
}
11360

114-
/** Restore all originals from trusted snapshots. */
11561
function restoreOriginals(targets) {
11662
for (var i = 0; i < targets.length; i++) {
11763
var el = targets[i];
@@ -121,79 +67,60 @@
12167
}
12268
}
12369

124-
/** Initialize on page load and MkDocs instant navigation. */
70+
function applyFromStorage() {
71+
var saved = "";
72+
try { saved = localStorage.getItem(STORAGE_KEY) || ""; } catch (e) {}
73+
var targets = snapshotOriginals();
74+
if (saved) applyReplacement(targets, saved);
75+
return { targets: targets, saved: saved };
76+
}
77+
12578
function init() {
126-
var bar = document.querySelector(".ip-replacer") || createInputBar();
12779
var input = document.getElementById("ip-input");
12880
var resetBtn = document.getElementById("ip-reset");
81+
var infoBtn = document.getElementById("ip-info-btn");
82+
var tooltip = document.getElementById("ip-tooltip");
12983

130-
// Restore saved value
131-
var saved = "";
132-
try {
133-
saved = localStorage.getItem(STORAGE_KEY) || "";
134-
} catch (e) {
135-
// localStorage unavailable (private browsing etc.)
136-
}
137-
if (input) {
138-
input.value = saved;
139-
}
84+
if (!input) return;
14085

141-
// Snapshot and apply
142-
var targets = snapshotOriginals();
143-
if (saved) {
144-
applyReplacement(targets, saved);
145-
}
86+
// Restore saved IP into input field
87+
var state = applyFromStorage();
88+
input.value = state.saved;
14689

147-
// Input handler
148-
if (input) {
149-
input.addEventListener("input", function () {
150-
var val = input.value;
151-
try {
152-
localStorage.setItem(STORAGE_KEY, val);
153-
} catch (e) {
154-
// ignore
155-
}
156-
targets = snapshotOriginals();
157-
applyReplacement(targets, val);
90+
// Tooltip toggle
91+
if (infoBtn && tooltip) {
92+
infoBtn.addEventListener("click", function (e) {
93+
e.stopPropagation();
94+
tooltip.classList.toggle("ip-replacer-tooltip--visible");
95+
});
96+
document.addEventListener("click", function () {
97+
tooltip.classList.remove("ip-replacer-tooltip--visible");
15898
});
15999
}
160100

101+
// IP input handler
102+
input.addEventListener("input", function () {
103+
var val = input.value;
104+
try { localStorage.setItem(STORAGE_KEY, val); } catch (e) {}
105+
var targets = snapshotOriginals();
106+
applyReplacement(targets, val);
107+
});
108+
161109
// Reset handler
162110
if (resetBtn) {
163111
resetBtn.addEventListener("click", function () {
164-
if (input) {
165-
input.value = "";
166-
}
167-
try {
168-
localStorage.removeItem(STORAGE_KEY);
169-
} catch (e) {
170-
// ignore
171-
}
112+
input.value = "";
113+
try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
114+
var targets = snapshotOriginals();
172115
restoreOriginals(targets);
173116
});
174117
}
175118
}
176119

177-
// Run on initial load
120+
// Run on initial page load
178121
if (document.readyState === "loading") {
179122
document.addEventListener("DOMContentLoaded", init);
180123
} else {
181124
init();
182125
}
183-
184-
// Re-run on MkDocs Material instant navigation (SPA page changes)
185-
document.addEventListener("DOMContentSwap", function () {
186-
setTimeout(function () {
187-
var saved = "";
188-
try {
189-
saved = localStorage.getItem(STORAGE_KEY) || "";
190-
} catch (e) {
191-
// ignore
192-
}
193-
var targets = snapshotOriginals();
194-
if (saved) {
195-
applyReplacement(targets, saved);
196-
}
197-
}, 50);
198-
});
199126
})();

docs/stylesheets/extra.css

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,56 @@
5959
font-style: italic;
6060
}
6161

62+
/* Info button (?) */
63+
.ip-replacer-info-wrapper {
64+
position: relative;
65+
display: inline-flex;
66+
}
67+
68+
.ip-replacer-info-btn {
69+
width: 1.5rem;
70+
height: 1.5rem;
71+
padding: 0;
72+
border-radius: 50%;
73+
font-weight: 700;
74+
font-size: 0.75rem;
75+
line-height: 1;
76+
display: inline-flex;
77+
align-items: center;
78+
justify-content: center;
79+
border: 1px solid var(--md-default-fg-color--lighter);
80+
background: var(--md-default-bg-color);
81+
color: var(--md-default-fg-color);
82+
cursor: pointer;
83+
}
84+
85+
.ip-replacer-info-btn:hover {
86+
background: var(--md-accent-fg-color--transparent);
87+
}
88+
89+
/* Tooltip */
90+
.ip-replacer-tooltip {
91+
display: none;
92+
position: absolute;
93+
top: 2rem;
94+
left: 0;
95+
z-index: 100;
96+
width: 20rem;
97+
padding: 0.6rem 0.8rem;
98+
border-radius: 6px;
99+
font-size: 0.78rem;
100+
font-style: normal;
101+
font-weight: 400;
102+
line-height: 1.5;
103+
background: var(--md-default-fg-color);
104+
color: var(--md-default-bg-color);
105+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
106+
}
107+
108+
.ip-replacer-tooltip--visible {
109+
display: block;
110+
}
111+
62112
/* Highlighted replaced IP values */
63113
.ip-replaced {
64114
background: var(--md-accent-fg-color--transparent);

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ repo_name: rorar/immich-unraid-manual
66

77
theme:
88
name: material
9+
custom_dir: overrides
910
palette:
1011
- scheme: default
1112
primary: indigo
@@ -25,6 +26,7 @@ theme:
2526
- navigation.expand
2627
- navigation.top
2728
- navigation.indexes
29+
- navigation.footer
2830
- content.code.copy
2931
- search.suggest
3032
- search.highlight

overrides/main.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% extends "base.html" %}
2+
3+
{% block announce %}
4+
<div class="ip-replacer">
5+
<span class="ip-replacer-info-wrapper">
6+
<button type="button" class="ip-replacer-info-btn" id="ip-info-btn" title="What is this?">?</button>
7+
<div class="ip-replacer-tooltip" id="ip-tooltip">
8+
Enter your Unraid server IP address here. All placeholders like
9+
&ldquo;&lt;your-unraid-ip&gt;&rdquo; throughout the guide will be replaced
10+
with your IP &mdash; including in copy-able code blocks.
11+
Your IP is saved in your browser only and never transmitted.
12+
</div>
13+
</span>
14+
<label for="ip-input">Your Unraid IP:</label>
15+
<input id="ip-input" type="text" placeholder="192.168.1.x" autocomplete="off" spellcheck="false" />
16+
<button id="ip-reset" type="button" title="Reset to placeholder">Reset</button>
17+
<span class="ip-replacer-disclaimer">Browser-only &mdash; no data is sent.</span>
18+
</div>
19+
{% endblock %}

0 commit comments

Comments
 (0)