Skip to content
Merged
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
4 changes: 2 additions & 2 deletions LLM_RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ selectors: data-\*-mount ONLY for JS mount points. no generic IDs except well-kn
responsive: use static/js/core/responsive.js helpers. no ad-hoc breakpoints in JS.
daisyUI: data-tip XOR title. never both.
typography: HTML class="not-prose". NO CSS @apply not-prose.
access-keys: SEMANTIC_RULES in access-keys.js = single source of truth for keyboard hints. no data-hint in templates except override cases. elements inside [data-toc-sidebar] are excluded from semantic rules (internal anchors).
access-keys: SEMANTIC_RULES in access-keys.js = single source of truth for keyboard hints. no data-hint in templates except override cases. elements inside [data-toc-sidebar] are excluded from semantic rules (internal anchors). if a force-rail page needs a shortcut for a rail item like Archive, set an explicit `data-hint` on that template link.
TOC: single initToc() from toc.js covers ALL pages (blog, archive, taxonomy). getTocLink uses href$=#ID for full-URL zola children AND data-toc-ID for summary parents AND bare #ID for archive anchors.
notify-banner: path is js/ui/notify-banner.js (not root-level js/).

Expand All @@ -62,7 +62,7 @@ macros: import separate files. no deep same-file chaining.
404 routing: templates/404.html ONLY. no content/404.md.
offline page: templates/offline.html (no content/offline.md).
404 + offline layout: \_force_rail=true in base.html → use_fit_shell=true + has_layout_rail=true + empty #toc-sidebar nav (aria-label="Page tools") for full structural parity. Toggle button label is "Hide/Show tools rail" (not "table of contents"). offline page must have force_rail=true in [extra].
CSP: no inline style="" attributes. use CSS classes. JS DOM manipulation toggles classes only.
CSP: no inline style="" attributes. use CSS classes. JS DOM manipulation toggles classes only. deferred style sheets use `data-defer-css="true"` and get activated by `static/js/core/boot.js`; avoid inline event-handler attributes on style sheet links.

SEO / SCHEMA
article JSON-LD: author Person must include both name AND URL (→ /about/).
Expand Down
7 changes: 6 additions & 1 deletion scripts/clean-pagination-redirects.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ if (fs.existsSync(sitemapPath) && removedPaths.length > 0) {
}
}
if (purged > 0) {
fs.writeFileSync(sitemapPath, sitemap, "utf8");
// Write atomically to avoid race conditions: write to a temp file
// and rename into place. This prevents other processes from reading
// a partially-written file and addresses file-system race warnings.
const tmpPath = sitemapPath + ".tmp";
fs.writeFileSync(tmpPath, sitemap, "utf8");
fs.renameSync(tmpPath, sitemapPath);
console.log(`Purged ${purged} stale page/1 URL(s) from sitemap.xml.`);
}
}
30 changes: 25 additions & 5 deletions scripts/sync-project-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ function parseRepoFromUrl(url) {
const match = value.match(/github\.com\/([^/]+)\/([^/#?]+)/i);
if (!match) return null;

return {
owner: match[1],
repo: match[2].replace(/\.git$/i, ""),
};
const owner = match[1];
const repo = match[2].replace(/\.git$/i, "");

// Validate owner and repo against a strict whitelist of characters to
// prevent crafted file data from being used to build unexpected URLs.
// GitHub owners: alphanumeric and hyphens; repos: allow dot/underscore/hyphen.
if (!/^[A-Za-z0-9-]+$/.test(owner)) return null;
if (!/^[A-Za-z0-9._-]+$/.test(repo)) return null;

return { owner, repo };
}

function getGitHubToken() {
Expand Down Expand Up @@ -60,8 +66,22 @@ function githubRequest(endpoint, token) {
if (token) headers.Authorization = `Bearer ${token}`; // lgtm[js/file-access-to-http] token sourced from env or gh CLI

return new Promise((resolve, reject) => {
// Construct the full URL via the URL API and validate the hostname so
// injected or unexpected endpoint values cannot cause requests to other
// origins. This ensures outbound requests are constrained to GitHub.
let urlObj;
try {
urlObj = new URL(endpoint, API_BASE);
} catch {
return reject(new Error("Invalid GitHub API endpoint"));
}

if (urlObj.hostname !== new URL(API_BASE).hostname) {
return reject(new Error("Disallowed GitHub API host"));
}

const req = https.request(
`${API_BASE}${endpoint}`,
urlObj,
{
method: "GET",
headers,
Expand Down
26 changes: 26 additions & 0 deletions static/js/core/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,36 @@
: raw;

d.setAttribute("data-theme", mode);
// Also set a theme class immediately so CSS prepaint rules using
// `html.dark` / `html.light` apply before the JS modules load. This
// prevents a flash/flicker of the wrong logo variant during navigation.
try {
d.classList.remove("light", "dark");
d.classList.add(mode);
} catch {
// Ignore failures (e.g. if classList isn't supported). The data-theme
// attribute will still be set correctly.
}
d.setAttribute("data-ui-init", "0");
d.setAttribute(
"data-sidebar-collapsed",
getL("sidebar-collapsed") ? "1" : "0",
);
d.setAttribute("data-toc-collapsed", getL("toc-collapsed") ? "1" : "0");

function activateDeferredCss() {
var links = document.querySelectorAll('link[data-defer-css="true"]');
for (var i = 0; i < links.length; i += 1) {
links[i].media = "all";
links[i].removeAttribute("data-defer-css");
}
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", activateDeferredCss, {
once: true,
});
} else {
activateDeferredCss();
}
})();
5 changes: 3 additions & 2 deletions static/js/features/access-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,9 @@ function showHints() {
for (const el of targets) {
let explicitHint = null;

// Semantic rules — skip TOC sidebar: those are page-internal anchors
// (#skills, #section-heading) that must not inherit site-nav shortcuts.
// Semantic rules — skip TOC sidebar: those are page-internal anchors that
// must not inherit site-nav shortcuts. If a TOC entry needs a shortcut,
// it should opt in explicitly with data-hint in the template.
if (!el.closest("[data-toc-sidebar]")) {
for (const rule of SEMANTIC_RULES) {
if (
Expand Down
32 changes: 32 additions & 0 deletions static/js/system/offline-reload.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
if (!el) return;
var ATTEMPT_KEY = "offlineReloadLastAttempt";
var ATTEMPT_WINDOW_MS = 6000;
var POLL_INTERVAL_MS = 5000;
var PING_TIMEOUT_MS = 2500;
var isChecking = false;
var pollInterval = null;

el.textContent = "Waiting for connection";
el.classList.add("status-pill", "status-pill--pending");
Expand All @@ -16,6 +19,8 @@
el.classList.remove("status-pill--pending");
el.classList.add("status-pill--online");

if (pollInterval) clearInterval(pollInterval);

setTimeout(function () {
var path = window.location.pathname || "/";
if (path === "/offline" || path === "/offline/") {
Expand Down Expand Up @@ -45,16 +50,25 @@

function probeNetwork() {
// Requests under /__runtime/ are excluded from SW handling in static/sw.js.
var controller = new AbortController();
var timeoutId = window.setTimeout(function () {
controller.abort();
}, PING_TIMEOUT_MS);

return fetch("/__runtime/ping?ts=" + Date.now(), {
method: "GET",
cache: "no-store",
credentials: "same-origin",
signal: controller.signal,
})
.then(function () {
return true;
})
.catch(function () {
return false;
})
.finally(function () {
window.clearTimeout(timeoutId);
});
}

Expand Down Expand Up @@ -84,4 +98,22 @@
}

window.addEventListener("online", maybeRecover);

function stopPolling() {
if (pollInterval) {
window.clearInterval(pollInterval);
pollInterval = null;
}
}

// Periodic polling: keep checking network status every POLL_INTERVAL_MS
// even if 'online' event doesn't fire. This ensures recovery on reconnect.
pollInterval = setInterval(function () {
if (navigator.onLine) {
maybeRecover();
}
}, POLL_INTERVAL_MS);
Comment on lines +109 to +115

window.addEventListener("pagehide", stopPolling, { once: true });
window.addEventListener("beforeunload", stopPolling, { once: true });
})();
2 changes: 1 addition & 1 deletion templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ <h2 class="text-[11px] font-semibold uppercase tracking-wider text-base-content/
<li><a href="/blog/" class="toc-link">Blog</a></li>
<li><a href="/links/" class="toc-link">Links</a></li>
{% endif %}
<li><a href="/archive/" class="toc-link">
<li><a href="/archive/" class="toc-link" data-hint="x">
<i class="fa-solid fa-calendar-days text-[11px] opacity-60 mr-1"></i>
Archive
</a></li>
Expand Down
Loading
Loading