@xingwangzhe/stalux 1.49.0 → 1.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/layouts/Stalux.astro +1 -0
- package/src/scripts/route-prefetch.ts +120 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The homepage typewriter is built into Stalux and continues to use the `items` st
|
|
|
16
16
|
|
|
17
17
|
Standalone images written as `` receive a `figure` and `figcaption` through a build-time satteri HAST plugin. Captions appear as small, centered gray text below the image, preserving image alt text and the PhotoSwipe lightbox. No configuration or browser script is required. Empty descriptions, inline images, multiple images in one paragraph, and existing figures are left unchanged. Captions are plain text with automatic escaping. External images can declare intrinsic size in the markdown title: ``. The build turns it into `width`/`height`, removes the tooltip title, and gives PhotoSwipe matching `data-pswp-width/height`; images without a size declaration keep their previous output.
|
|
18
18
|
|
|
19
|
-
Friend-link icons are requested only when they approach the viewport (400px prefetch margin), instead of the browser's large native lazy window pulling every third-party avatar on the page. A failed icon keeps a placeholder disc and never affects the text content. The Google Analytics SDK loads after the first screen settles (first input, 600ms of stable LCP, or six seconds after parsing), while `page_view` snapshots and the dataLayer queue are still written during navigation without waiting for the SDK. The random-posts sidebar likewise requests its index only near the viewport.
|
|
19
|
+
Friend-link icons are requested only when they approach the viewport (400px prefetch margin), instead of the browser's large native lazy window pulling every third-party avatar on the page. A failed icon keeps a placeholder disc and never affects the text content. The Google Analytics SDK loads after the first screen settles (first input, 600ms of stable LCP, or six seconds after parsing), while `page_view` snapshots and the dataLayer queue are still written during navigation without waiting for the SDK. The random-posts sidebar likewise requests its index only near the viewport. After the homepage settles (about one second after load, with a 2.5-second ceiling), the main navigation routes are warmed one at a time through Swup's preload cache and concurrency limit; saveData, 2G, reduced-data preferences and hidden tabs are skipped, and clicks reuse the cached pages.
|
|
20
20
|
|
|
21
21
|
Code blocks default to the single `dark-plus` theme, with system color-scheme switching and theme selectors disabled. To opt into another theme or switching behavior, explicitly override `themes`, `useDarkModeMediaQuery`, and `themeCssSelector` through `expressiveCode`. This only changes generated code styles and adds no telemetry.
|
|
22
22
|
|
package/package.json
CHANGED
package/src/layouts/Stalux.astro
CHANGED
|
@@ -80,6 +80,7 @@ const routeKind = getRouteKind(Astro.url.pathname);
|
|
|
80
80
|
// against the article and archive components that also request it.
|
|
81
81
|
import { mountListReveal } from "../scripts/list-reveal";
|
|
82
82
|
import { registerPageLifecycle } from "../scripts/page-runtime";
|
|
83
|
+
import "../scripts/route-prefetch";
|
|
83
84
|
|
|
84
85
|
registerPageLifecycle("list-reveal", mountListReveal);
|
|
85
86
|
</script>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warm the main navigation routes after the homepage has settled.
|
|
3
|
+
*
|
|
4
|
+
* Swup already preloads hovered/focused links. This adds a low-priority,
|
|
5
|
+
* idle-time preload of the visible navigation routes, so the first click from
|
|
6
|
+
* the homepage can reuse Swup's page cache instead of fetching and parsing the
|
|
7
|
+
* document during the transition. The preload plugin enforces its own throttle,
|
|
8
|
+
* skips saveData/2G connections, deduplicates cached pages and ignores routes
|
|
9
|
+
* the router cannot handle.
|
|
10
|
+
*/
|
|
11
|
+
import { createClientLogger } from "./logger";
|
|
12
|
+
import { registerPageLifecycle } from "./page-runtime";
|
|
13
|
+
|
|
14
|
+
const logger = createClientLogger("route-prefetch");
|
|
15
|
+
|
|
16
|
+
const AFTER_LOAD_DELAY_MS = 1000;
|
|
17
|
+
const LOAD_CEILING_MS = 2500;
|
|
18
|
+
const IDLE_TIMEOUT_MS = 2000;
|
|
19
|
+
|
|
20
|
+
export function collectMainRouteUrls(
|
|
21
|
+
documentLike: Pick<Document, "querySelectorAll"> = document,
|
|
22
|
+
current = new URL(window.location.href),
|
|
23
|
+
): string[] {
|
|
24
|
+
const urls = new Set<string>();
|
|
25
|
+
for (const anchor of documentLike.querySelectorAll<HTMLAnchorElement>(
|
|
26
|
+
'a[data-ref="stalux-nav-link"]',
|
|
27
|
+
)) {
|
|
28
|
+
let url: URL;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(anchor.href, current);
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (url.origin !== current.origin) continue;
|
|
35
|
+
if (url.pathname === current.pathname && url.search === current.search) continue;
|
|
36
|
+
urls.add(`${url.pathname}${url.search}`);
|
|
37
|
+
}
|
|
38
|
+
return [...urls];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mountRoutePrefetch(): (() => void) | undefined {
|
|
42
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
43
|
+
if (document.documentElement.dataset.staluxRoute !== "home") return;
|
|
44
|
+
const reducedData = window.matchMedia?.("(prefers-reduced-data: reduce)");
|
|
45
|
+
if (reducedData?.matches) return;
|
|
46
|
+
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
let scheduled = false;
|
|
49
|
+
let delayTimer: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
let ceilingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
51
|
+
let idleId: number | undefined;
|
|
52
|
+
|
|
53
|
+
const run = () => {
|
|
54
|
+
if (cancelled || document.hidden) return;
|
|
55
|
+
const swup = window.swup;
|
|
56
|
+
if (!swup || typeof swup.preload !== "function") {
|
|
57
|
+
logger.debug("swup preload API unavailable; skipping route prefetch");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const urls = collectMainRouteUrls();
|
|
61
|
+
if (!urls.length) return;
|
|
62
|
+
logger.debug(`prefetching ${urls.length} main routes`);
|
|
63
|
+
// One document at a time: keep the prefetch invisible to the current
|
|
64
|
+
// page's main thread and network instead of parsing six documents at once.
|
|
65
|
+
let chain: Promise<unknown> = Promise.resolve();
|
|
66
|
+
for (const url of urls) {
|
|
67
|
+
chain = chain
|
|
68
|
+
.then(() => {
|
|
69
|
+
if (cancelled) return;
|
|
70
|
+
return swup.preload?.(url, { priority: false });
|
|
71
|
+
})
|
|
72
|
+
.catch(() => {
|
|
73
|
+
// A failed warm-up must never affect navigation.
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
void chain;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const requestIdle = () => {
|
|
80
|
+
if (cancelled) return;
|
|
81
|
+
const idle = window.requestIdleCallback;
|
|
82
|
+
if (typeof idle === "function") {
|
|
83
|
+
idleId = idle(run, { timeout: IDLE_TIMEOUT_MS });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
idleId = setTimeout(run, 1) as unknown as number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const schedule = () => {
|
|
90
|
+
if (scheduled || cancelled) return;
|
|
91
|
+
scheduled = true;
|
|
92
|
+
delayTimer = setTimeout(requestIdle, AFTER_LOAD_DELAY_MS);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (document.readyState === "complete") {
|
|
96
|
+
schedule();
|
|
97
|
+
} else {
|
|
98
|
+
const onLoad = () => {
|
|
99
|
+
window.removeEventListener("load", onLoad);
|
|
100
|
+
schedule();
|
|
101
|
+
};
|
|
102
|
+
window.addEventListener("load", onLoad, { once: true });
|
|
103
|
+
// A slow image or third-party request must not postpone the warm-up forever.
|
|
104
|
+
ceilingTimer = setTimeout(() => {
|
|
105
|
+
window.removeEventListener("load", onLoad);
|
|
106
|
+
schedule();
|
|
107
|
+
}, LOAD_CEILING_MS);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
cancelled = true;
|
|
112
|
+
if (delayTimer !== undefined) clearTimeout(delayTimer);
|
|
113
|
+
if (ceilingTimer !== undefined) clearTimeout(ceilingTimer);
|
|
114
|
+
if (idleId === undefined) return;
|
|
115
|
+
if (typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(idleId);
|
|
116
|
+
else clearTimeout(idleId);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerPageLifecycle("route-prefetch", mountRoutePrefetch);
|