chore: sync upstream — add toc-scanner.js and data-fetch.js

New files from rmdes/indiekit-eleventy-theme:
- js/toc-scanner.js: Alpine.js TOC scanner with IntersectionObserver scroll spy
- lib/data-fetch.js: shared fetch helper with 10s timeout and watch-mode cache extension

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-15 23:15:33 +01:00
parent 423dc81346
commit ac4b62d43c
2 changed files with 105 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
/**
* Alpine.js TOC scanner component.
* Scans .e-content for h2/h3/h4 headings with IDs,
* builds a table of contents, and highlights the
* current section via IntersectionObserver scroll spy.
*/
document.addEventListener("alpine:init", () => {
Alpine.data("tocScanner", () => ({
items: [],
_observer: null,
init() {
const content = document.querySelector(".e-content");
if (!content) { this._hideWrapper(); return; }
const headings = content.querySelectorAll("h2[id], h3[id], h4[id]");
if (headings.length < 3) { this._hideWrapper(); return; }
this.items = Array.from(headings).map((h) => ({
id: h.id,
text: h.textContent.replace(/^#\s*/, "").trim(),
level: parseInt(h.tagName[1]),
active: false,
}));
this._observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
this.items.forEach((item) => {
item.active = item.id === entry.target.id;
});
}
}
},
{ rootMargin: "0px 0px -70% 0px" },
);
headings.forEach((h) => this._observer.observe(h));
},
_hideWrapper() {
const wrapper = this.$root.closest(".widget-collapsible");
if (wrapper) wrapper.style.display = "none";
},
destroy() {
if (this._observer) this._observer.disconnect();
},
}));
});
+54
View File
@@ -0,0 +1,54 @@
/**
* Shared data-fetching helper for _data files.
*
* Wraps @11ty/eleventy-fetch with two protections:
* 1. Hard timeout — 10-second AbortController ceiling on every request
* 2. Watch-mode cache extension — uses "4h" TTL during watch/serve,
* keeping the original (shorter) TTL only for production builds
*
* Usage:
* import { cachedFetch } from "../lib/data-fetch.js";
* const data = await cachedFetch(url, { duration: "15m", type: "json" });
*/
import EleventyFetch from "@11ty/eleventy-fetch";
const FETCH_TIMEOUT_MS = 10_000; // 10 seconds
// In watch/serve mode, extend cache to avoid re-fetching on every rebuild.
// Production builds use the caller's original TTL for fresh data.
const isWatchMode = process.env.ELEVENTY_RUN_MODE !== "build";
const WATCH_MODE_DURATION = "4h";
/**
* Fetch with timeout and watch-mode cache extension.
*
* @param {string} url - URL to fetch
* @param {object} options - EleventyFetch options (duration, type, fetchOptions, etc.)
* @returns {Promise<any>} Parsed response
*/
export async function cachedFetch(url, options = {}) {
// Extend cache in watch mode
const duration = isWatchMode ? WATCH_MODE_DURATION : (options.duration || "15m");
// Create abort controller for hard timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const fetchOptions = {
...options.fetchOptions,
signal: controller.signal,
};
const result = await EleventyFetch(url, {
...options,
duration,
fetchOptions,
});
return result;
} finally {
clearTimeout(timeoutId);
}
}