@xingwangzhe/stalux 1.38.0 → 1.39.1
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 +6 -2
- package/package.json +2 -1
- package/src/components/stalux/layout/head.astro +1 -1
- package/src/components/stalux/layout/navigationProgress.astro +55 -0
- package/src/internal/style-names.generated.ts +43 -42
- package/src/layouts/Stalux.astro +10 -7
- package/src/scripts/analytics-runtime.ts +179 -35
- package/src/scripts/background.ts +2 -2
- package/src/scripts/navigation-progress.ts +147 -0
- package/src/scripts/navigation.ts +65 -42
- package/src/scripts/route-motion.ts +108 -0
- package/src/scripts/view-transitions.ts +80 -51
- package/src/styles/base.css +9 -1
- package/src/styles/generated.css +1 -1
- package/src/styles/motion.css +8 -72
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ Standalone images written as `` receive a `figure` and
|
|
|
18
18
|
|
|
19
19
|
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.
|
|
20
20
|
|
|
21
|
+
Route changes automatically show a 2px accent-colored progress bar at the top of the viewport, with no configuration. Its length follows actual milestones, with slow visual growth toward a bounded ceiling while waiting: loading begins, document/font preparation finishes, the DOM swaps, and page scripts finish; completion waits for the visual transition as well. It only reaches the end when loading and the transition both complete, and does not represent download/time percentages. It handles superseded or canceled navigation, history traversal, and reduced motion without additional requests or telemetry.
|
|
22
|
+
|
|
23
|
+
The sticky header stays at the top while the body grows with long content. Route transitions use a 100ms native Web Animations API fade on the new content, with the persisted header and footer stationary. Page-sized View Transition snapshots and geometry interpolation are disabled; Astro still handles document preparation, history, scroll restoration and scripts. Progress and background updates wait for the real content animation to finish. Reduced motion and browser-provided history gestures skip this extra animation. Navigation batches layout reads in one frame and carries the persisted header's measured dimensions across swaps, reducing style recalculation, rasterization and compositing work on mobile devices. Content, lightboxes, search, comments and analytics keep their existing behavior.
|
|
24
|
+
|
|
21
25
|
---
|
|
22
26
|
|
|
23
27
|
## 🚀 Quick Start
|
|
@@ -191,11 +195,11 @@ id: head
|
|
|
191
195
|
bingClarityId: "YOUR_CLARITY_PROJECT_ID"
|
|
192
196
|
```
|
|
193
197
|
|
|
194
|
-
Google Analytics, Clarity, and Umami use low-priority native asynchronous scripts. Each SDK loads once per session, and failures do not block reading or navigation. Vercount rebinds visible counters after each `astro:page-load`. GA sends
|
|
198
|
+
Google Analytics, Clarity, and Umami use low-priority native asynchronous scripts. Each SDK loads once per session, and failures do not block reading or navigation. Vercount rebinds visible counters after each `astro:page-load`. GA captures each URL, title, and referrer, then sends pageviews in visit order through separate background tasks after the native transition releases rendering, the content animation is installed and a paint has been yielded. A bounded timer handles task starvation; hiding or leaving the document flushes pending views immediately. The SDK still runs on the main thread; this scheduling reduces synchronous work on the route's first frame. Umami disables automatic pageviews while retaining click tracking and captures page information before the SDK resolves so rapid navigation cannot mix visits. An existing older runtime or legacy Partytown session performs one full reload when it encounters the new runtime to prevent duplicate listeners and tracking.
|
|
195
199
|
|
|
196
200
|
For Google Analytics, turn off **Page changes based on browser history events** under **Admin → Data streams → Enhanced measurement → Page views → Advanced settings**. Stalux sets `send_page_view: false` and sends the URL and title for each navigation explicitly. GA's history tracking must also be disabled in the property to prevent duplicate soft-navigation pageviews. Other enhanced measurement options can remain enabled. See [Google's pageview documentation](https://developers.google.com/analytics/devguides/collection/ga4/views).
|
|
197
201
|
|
|
198
|
-
`bingClarityId` is the historical Stalux field name for a Microsoft Clarity Project ID. Get the ID from the Clarity project under **Settings → Setup → Get tracking code**. Stalux
|
|
202
|
+
`bingClarityId` is the historical Stalux field name for a Microsoft Clarity Project ID. Get the ID from the Clarity project under **Settings → Setup → Get tracking code**. Stalux includes the official `@microsoft/clarity` npm dependency, imports it dynamically in the browser, and calls `Clarity.init()` once across soft navigation. Consumer projects such as myblog only configure the ID and upgrade the theme; no separate Clarity installation or script is needed. The theme observes tag loading failures and allows a later page to retry. Tag load, recorder readiness and successful collection are verified separately. The official npm package still loads the hosted recording engine and does not move DOM capture into a Worker. The SDK handles history navigation itself. Do not install the same project again through `anyhead`, a tag manager, or another plugin.
|
|
199
203
|
|
|
200
204
|
The Project ID is a public browser identifier. Never put a Clarity Data Export API token in this YAML or in client-side code. If the site uses a strict CSP, consent banner, or CMP, configure those host-site policies and signals separately; this theme does not make legal compliance decisions for the site.
|
|
201
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.1",
|
|
4
4
|
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"@expressive-code/plugin-line-numbers": "^0.44.2",
|
|
108
108
|
"@lucide/astro": "^1.56.0",
|
|
109
109
|
"@mcp-b/webmcp-polyfill": "^5.1.0",
|
|
110
|
+
"@microsoft/clarity": "1.0.2",
|
|
110
111
|
"@pagefind/component-ui": "^1.5.2",
|
|
111
112
|
"@waline/client": "^3.16.1",
|
|
112
113
|
"@xingwangzhe/cjk-font-split-native": "^0.4.10",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div
|
|
2
|
+
data-stalux-navigation-progress
|
|
3
|
+
aria-hidden="true"
|
|
4
|
+
transition:persist="stalux-navigation-progress"
|
|
5
|
+
transition:name="stalux-navigation-progress"
|
|
6
|
+
transition:animate="none"
|
|
7
|
+
></div>
|
|
8
|
+
|
|
9
|
+
<style is:global>
|
|
10
|
+
[data-stalux-navigation-progress] {
|
|
11
|
+
position: fixed;
|
|
12
|
+
inset: 0 0 auto;
|
|
13
|
+
height: 2px;
|
|
14
|
+
z-index: 2147483647;
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
background: var(--stalux-accent-color, #eab308);
|
|
17
|
+
transform: scaleX(0);
|
|
18
|
+
transform-origin: left;
|
|
19
|
+
opacity: 0;
|
|
20
|
+
transition:
|
|
21
|
+
transform 450ms ease-out,
|
|
22
|
+
opacity 120ms ease-out;
|
|
23
|
+
}
|
|
24
|
+
[data-stalux-navigation-progress][data-stage] {
|
|
25
|
+
opacity: 1;
|
|
26
|
+
}
|
|
27
|
+
[data-stalux-navigation-progress][data-stage="complete"] {
|
|
28
|
+
transition:
|
|
29
|
+
transform 160ms ease-out,
|
|
30
|
+
opacity 120ms ease-out;
|
|
31
|
+
}
|
|
32
|
+
[data-stalux-navigation-progress]:not([data-stage]) {
|
|
33
|
+
transition: opacity 120ms ease-out;
|
|
34
|
+
}
|
|
35
|
+
::view-transition-group(stalux-navigation-progress) {
|
|
36
|
+
z-index: 2147483647;
|
|
37
|
+
}
|
|
38
|
+
::view-transition-old(stalux-navigation-progress) {
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
::view-transition-new(stalux-navigation-progress) {
|
|
42
|
+
animation: none;
|
|
43
|
+
}
|
|
44
|
+
@media (prefers-reduced-motion: reduce) {
|
|
45
|
+
[data-stalux-navigation-progress],
|
|
46
|
+
[data-stalux-navigation-progress][data-stage] {
|
|
47
|
+
transition: none;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
@media print {
|
|
51
|
+
[data-stalux-navigation-progress] {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
@@ -288,48 +288,49 @@ export const styleNames: { classes: Record<string, string>; variables: Record<st
|
|
|
288
288
|
"--stalux-bg-filter": "--s1o",
|
|
289
289
|
"--stalux-nav-offset": "--s1p",
|
|
290
290
|
"--stalux-site-header-height": "--s1q",
|
|
291
|
-
"--stalux-
|
|
292
|
-
"--
|
|
293
|
-
"--surface-glass
|
|
294
|
-
"--
|
|
295
|
-
"--tag-bg
|
|
296
|
-
"--
|
|
297
|
-
"--tl-
|
|
298
|
-
"--
|
|
299
|
-
"--transition-
|
|
300
|
-
"--transition-
|
|
301
|
-
"--transition-
|
|
302
|
-
"--transition-
|
|
303
|
-
"--transition-
|
|
304
|
-
"--
|
|
305
|
-
"--un-rotate
|
|
306
|
-
"--un-rotate-
|
|
307
|
-
"--un-rotate-
|
|
308
|
-
"--un-
|
|
309
|
-
"--un-scale-
|
|
310
|
-
"--un-scale-
|
|
311
|
-
"--un-
|
|
312
|
-
"--un-skew-
|
|
313
|
-
"--un-
|
|
314
|
-
"--un-translate-
|
|
315
|
-
"--un-translate-
|
|
316
|
-
"--
|
|
317
|
-
"--updated-
|
|
318
|
-
"--
|
|
319
|
-
"--white-
|
|
320
|
-
"--white-
|
|
321
|
-
"--white-
|
|
322
|
-
"--white-
|
|
323
|
-
"--white-
|
|
324
|
-
"--white-
|
|
325
|
-
"--white-
|
|
326
|
-
"--white-
|
|
327
|
-
"--white-
|
|
328
|
-
"--white-
|
|
329
|
-
"--white-
|
|
330
|
-
"--white-
|
|
331
|
-
"--white-
|
|
332
|
-
"--white-
|
|
291
|
+
"--stalux-site-header-min-height": "--s1r",
|
|
292
|
+
"--stalux-text-shadow": "--s1s",
|
|
293
|
+
"--surface-glass": "--s1t",
|
|
294
|
+
"--surface-glass-shadow": "--s1u",
|
|
295
|
+
"--tag-bg": "--s1v",
|
|
296
|
+
"--tag-bg-hover": "--s1w",
|
|
297
|
+
"--tl-line": "--s1x",
|
|
298
|
+
"--tl-pad": "--s1y",
|
|
299
|
+
"--transition-fast": "--s1z",
|
|
300
|
+
"--transition-medium": "--s20",
|
|
301
|
+
"--transition-normal": "--s21",
|
|
302
|
+
"--transition-slow": "--s22",
|
|
303
|
+
"--transition-slower": "--s23",
|
|
304
|
+
"--transition-slowest": "--s24",
|
|
305
|
+
"--un-rotate": "--s25",
|
|
306
|
+
"--un-rotate-x": "--s26",
|
|
307
|
+
"--un-rotate-y": "--s27",
|
|
308
|
+
"--un-rotate-z": "--s28",
|
|
309
|
+
"--un-scale-x": "--s29",
|
|
310
|
+
"--un-scale-y": "--s2a",
|
|
311
|
+
"--un-scale-z": "--s2b",
|
|
312
|
+
"--un-skew-x": "--s2c",
|
|
313
|
+
"--un-skew-y": "--s2d",
|
|
314
|
+
"--un-translate-x": "--s2e",
|
|
315
|
+
"--un-translate-y": "--s2f",
|
|
316
|
+
"--un-translate-z": "--s2g",
|
|
317
|
+
"--updated-bg": "--s2h",
|
|
318
|
+
"--updated-border": "--s2i",
|
|
319
|
+
"--white-05p": "--s2j",
|
|
320
|
+
"--white-10p": "--s2k",
|
|
321
|
+
"--white-15p": "--s2l",
|
|
322
|
+
"--white-20p": "--s2m",
|
|
323
|
+
"--white-30p": "--s2n",
|
|
324
|
+
"--white-50p": "--s2o",
|
|
325
|
+
"--white-60p": "--s2p",
|
|
326
|
+
"--white-70p": "--s2q",
|
|
327
|
+
"--white-75p": "--s2r",
|
|
328
|
+
"--white-78p": "--s2s",
|
|
329
|
+
"--white-80p": "--s2t",
|
|
330
|
+
"--white-85p": "--s2u",
|
|
331
|
+
"--white-90p": "--s2v",
|
|
332
|
+
"--white-95p": "--s2w",
|
|
333
|
+
"--white-full": "--s2x"
|
|
333
334
|
},
|
|
334
335
|
"animations": {
|
|
335
336
|
"fadeInUpComment": "k0",
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getCollection } from "astro:content";
|
|
3
3
|
import Footer from "@components/stalux/layout/footer.astro";
|
|
4
4
|
import Head from "@components/stalux/layout/head.astro";
|
|
5
|
+
import NavigationProgress from "@components/stalux/layout/navigationProgress.astro";
|
|
5
6
|
import Navs from "@components/stalux/layout/navs.astro";
|
|
6
7
|
import { getAccentColorStyle } from "@utils/accent-color";
|
|
7
8
|
import {
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
getPromoteData,
|
|
12
13
|
getSiteData,
|
|
13
14
|
} from "@utils/config-utils";
|
|
14
|
-
import {
|
|
15
|
+
import { getRouteKind } from "@utils/view-transitions";
|
|
15
16
|
|
|
16
17
|
const props = Astro.props as Props;
|
|
17
18
|
|
|
@@ -48,11 +49,14 @@ const llmPromote = llmPromoteRaw
|
|
|
48
49
|
?.replace(/\{title\}/g, siteConfig.title)
|
|
49
50
|
?.replace(/\{cc\}/g, "CC-BY-NC-SA-4.0");
|
|
50
51
|
const routeKind = getRouteKind(Astro.url.pathname);
|
|
51
|
-
const routeAnimation = getRouteAnimation(routeKind);
|
|
52
52
|
---
|
|
53
53
|
|
|
54
54
|
<!doctype html>
|
|
55
|
-
<html
|
|
55
|
+
<html
|
|
56
|
+
lang={htmlLang}
|
|
57
|
+
style={getAccentColorStyle(siteConfig.accentColor)}
|
|
58
|
+
data-stalux-route={routeKind}
|
|
59
|
+
>
|
|
56
60
|
<Head
|
|
57
61
|
title={props.title}
|
|
58
62
|
description={props.description}
|
|
@@ -85,6 +89,7 @@ const routeAnimation = getRouteAnimation(routeKind);
|
|
|
85
89
|
data-stalux-umami-id={headConfig?.umami?.id}
|
|
86
90
|
data-stalux-umami-url={headConfig?.umami?.url}
|
|
87
91
|
>
|
|
92
|
+
<NavigationProgress />
|
|
88
93
|
<header
|
|
89
94
|
class="stalux-site-header"
|
|
90
95
|
data-stalux-font-group="header"
|
|
@@ -115,10 +120,8 @@ const routeAnimation = getRouteAnimation(routeKind);
|
|
|
115
120
|
<main
|
|
116
121
|
class="stalux-main"
|
|
117
122
|
data-stalux-route={routeKind}
|
|
118
|
-
transition:name=
|
|
119
|
-
transition:animate=
|
|
120
|
-
? "none"
|
|
121
|
-
: routeAnimation}
|
|
123
|
+
transition:name="stalux-page"
|
|
124
|
+
transition:animate="none"
|
|
122
125
|
>
|
|
123
126
|
<slot />
|
|
124
127
|
</main>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Async native SDKs keep live DOM events out of worker proxies during Astro swaps. */
|
|
2
2
|
|
|
3
3
|
import { createClientLogger } from "./logger";
|
|
4
|
+
import { prepareRouteMotion, type RouteMotionEvent } from "./route-motion";
|
|
4
5
|
|
|
5
6
|
const logger = createClientLogger("analytics");
|
|
6
7
|
|
|
@@ -22,7 +23,7 @@ declare global {
|
|
|
22
23
|
type AnalyticsHost = Window & {
|
|
23
24
|
dataLayer?: unknown[];
|
|
24
25
|
gtag?: (...args: unknown[]) => void;
|
|
25
|
-
clarity?: ((...args: unknown[]) => void) & {
|
|
26
|
+
clarity?: ((...args: unknown[]) => void) & { v?: string };
|
|
26
27
|
umami?: { track: (payload: Record<string, unknown>) => unknown };
|
|
27
28
|
};
|
|
28
29
|
const host = window as AnalyticsHost;
|
|
@@ -30,17 +31,101 @@ window.__staluxAnalyticsRuntime ??= {};
|
|
|
30
31
|
const state = window.__staluxAnalyticsRuntime;
|
|
31
32
|
state.scripts ??= new Map();
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
type GooglePage = {
|
|
35
|
+
ready: Promise<unknown>;
|
|
36
|
+
payload: {
|
|
37
|
+
page_location: string;
|
|
38
|
+
page_path: string;
|
|
39
|
+
page_title: string;
|
|
40
|
+
page_referrer: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const googlePages: GooglePage[] = [];
|
|
44
|
+
let transitionReady: Promise<unknown> = Promise.resolve();
|
|
45
|
+
let googleScheduled = false;
|
|
46
|
+
let googleSchedule = 0;
|
|
47
|
+
let googleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
let googleFrames: number[] = [];
|
|
49
|
+
|
|
50
|
+
function cancelGoogleSchedule(): void {
|
|
51
|
+
googleSchedule++;
|
|
52
|
+
googleScheduled = false;
|
|
53
|
+
clearTimeout(googleTimer);
|
|
54
|
+
for (const frame of googleFrames) window.cancelAnimationFrame(frame);
|
|
55
|
+
googleFrames = [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sendGooglePage(page: GooglePage): void {
|
|
59
|
+
try {
|
|
60
|
+
host.gtag?.("event", "page_view", page.payload);
|
|
61
|
+
} catch {
|
|
62
|
+
logger.debug("Google page view unavailable; navigation remains usable");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The loaded Google SDK processes dataLayer.push synchronously and can read layout. */
|
|
67
|
+
function scheduleGooglePages(): void {
|
|
68
|
+
const page = googlePages[0];
|
|
69
|
+
if (!page || googleScheduled) return;
|
|
70
|
+
googleScheduled = true;
|
|
71
|
+
const schedule = ++googleSchedule;
|
|
72
|
+
const run = () => {
|
|
73
|
+
if (schedule !== googleSchedule) return;
|
|
74
|
+
cancelGoogleSchedule();
|
|
75
|
+
const next = googlePages.shift();
|
|
76
|
+
if (next) sendGooglePage(next);
|
|
77
|
+
// A burst of visits stays FIFO, with a separate task for each SDK call.
|
|
78
|
+
scheduleGooglePages();
|
|
79
|
+
};
|
|
80
|
+
// A queued visit can outlive its own transition. Also wait for whichever
|
|
81
|
+
// incoming snapshot is current when this task is scheduled.
|
|
82
|
+
void Promise.all([page.ready, transitionReady]).then(() => {
|
|
83
|
+
if (schedule !== googleSchedule) return;
|
|
84
|
+
// Let the routed content paint after native rendering resumes and its own
|
|
85
|
+
// motion is installed. The timer also covers throttled/background frames.
|
|
86
|
+
googleTimer = setTimeout(run, 1000);
|
|
87
|
+
googleFrames.push(
|
|
88
|
+
window.requestAnimationFrame(() => {
|
|
89
|
+
if (schedule !== googleSchedule) return;
|
|
90
|
+
googleFrames.push(
|
|
91
|
+
window.requestAnimationFrame(() => {
|
|
92
|
+
if (schedule !== googleSchedule) return;
|
|
93
|
+
const scheduler = (
|
|
94
|
+
window as Window & {
|
|
95
|
+
scheduler?: {
|
|
96
|
+
postTask: (
|
|
97
|
+
callback: () => void,
|
|
98
|
+
options: { priority: "background" },
|
|
99
|
+
) => Promise<unknown>;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
).scheduler;
|
|
103
|
+
if (scheduler?.postTask) {
|
|
104
|
+
void scheduler.postTask(run, { priority: "background" }).catch(() => {
|
|
105
|
+
if (schedule === googleSchedule) setTimeout(run, 0);
|
|
106
|
+
});
|
|
107
|
+
} else setTimeout(run, 0);
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function flushGooglePages(): void {
|
|
116
|
+
cancelGoogleSchedule();
|
|
117
|
+
// Native gtag remains on the main thread for GA's own pagehide/engagement work.
|
|
118
|
+
// Do not leave a queued view behind when the document is hidden or unloaded.
|
|
119
|
+
for (const page of googlePages.splice(0)) sendGooglePage(page);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Monitor only a script created by this integration; a loaded tag stays intact. */
|
|
123
|
+
function monitorTrackerScript(script: HTMLScriptElement, append = false): Promise<void> {
|
|
124
|
+
return new Promise<void>((resolve, reject) => {
|
|
125
|
+
let settled = false;
|
|
43
126
|
const finish = (error?: Error) => {
|
|
127
|
+
if (settled) return;
|
|
128
|
+
settled = true;
|
|
44
129
|
clearTimeout(timer);
|
|
45
130
|
script.onload = null;
|
|
46
131
|
script.onerror = null;
|
|
@@ -52,8 +137,20 @@ function loadTrackerScript(src: string, attributes: Record<string, string> = {})
|
|
|
52
137
|
const timer = setTimeout(() => finish(new Error("Analytics SDK load timed out")), 15_000);
|
|
53
138
|
script.onload = () => finish();
|
|
54
139
|
script.onerror = () => finish(new Error("Analytics SDK load failed"));
|
|
55
|
-
document.head.appendChild(script);
|
|
140
|
+
if (append) document.head.appendChild(script);
|
|
56
141
|
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Never await analytics in a router callback; failed loads can retry on a later page. */
|
|
145
|
+
function loadTrackerScript(src: string, attributes: Record<string, string> = {}): Promise<void> {
|
|
146
|
+
const existing = state.scripts?.get(src);
|
|
147
|
+
if (existing) return existing;
|
|
148
|
+
const script = document.createElement("script");
|
|
149
|
+
script.async = true;
|
|
150
|
+
script.fetchPriority = "low";
|
|
151
|
+
script.src = src;
|
|
152
|
+
for (const [name, value] of Object.entries(attributes)) script.setAttribute(name, value);
|
|
153
|
+
const loaded = monitorTrackerScript(script, true);
|
|
57
154
|
state.scripts?.set(src, loaded);
|
|
58
155
|
void loaded.catch(() => {
|
|
59
156
|
if (state.scripts?.get(src) === loaded) state.scripts.delete(src);
|
|
@@ -61,6 +158,50 @@ function loadTrackerScript(src: string, attributes: Record<string, string> = {})
|
|
|
61
158
|
return loaded;
|
|
62
159
|
}
|
|
63
160
|
|
|
161
|
+
const CLARITY_SCRIPT_ID = "clarity-script";
|
|
162
|
+
const CLARITY_LOADER_KEY = "clarity:npm";
|
|
163
|
+
|
|
164
|
+
function hasExistingClarity(): boolean {
|
|
165
|
+
return Boolean(
|
|
166
|
+
host.clarity?.v ||
|
|
167
|
+
document.getElementById(CLARITY_SCRIPT_ID) ||
|
|
168
|
+
document.querySelector(
|
|
169
|
+
'script[data-stalux-clarity],script[src^="https://www.clarity.ms/tag/"]',
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function initializeClarity(id: string): void {
|
|
175
|
+
if (state.clarityId === id || state.scripts?.has(CLARITY_LOADER_KEY)) return;
|
|
176
|
+
state.clarityId = id;
|
|
177
|
+
// A preceding SDK or user-provided tag owns its own lifecycle. Never create
|
|
178
|
+
// a second loader or attach destructive retry handlers to that script.
|
|
179
|
+
if (hasExistingClarity()) return;
|
|
180
|
+
const initialized = import("@microsoft/clarity").then(({ default: clarity }) => {
|
|
181
|
+
// Another integration may have installed its tag while this chunk loaded.
|
|
182
|
+
if (hasExistingClarity()) return;
|
|
183
|
+
clarity.init(id);
|
|
184
|
+
const script = document.getElementById(CLARITY_SCRIPT_ID);
|
|
185
|
+
// Official init returns void and catches its own exceptions. Its inserted
|
|
186
|
+
// element is the observable loader, not the return value of init().
|
|
187
|
+
if (script?.tagName !== "SCRIPT") {
|
|
188
|
+
throw new Error("Clarity did not create its loader");
|
|
189
|
+
}
|
|
190
|
+
const tracker = script as HTMLScriptElement;
|
|
191
|
+
tracker.dataset.staluxClarity = "true";
|
|
192
|
+
tracker.fetchPriority = "low";
|
|
193
|
+
// Tag load does not establish that its remote core or collector is ready.
|
|
194
|
+
return monitorTrackerScript(tracker);
|
|
195
|
+
});
|
|
196
|
+
state.scripts?.set(CLARITY_LOADER_KEY, initialized);
|
|
197
|
+
void initialized.catch(() => {
|
|
198
|
+
if (state.scripts?.get(CLARITY_LOADER_KEY) !== initialized) return;
|
|
199
|
+
state.scripts.delete(CLARITY_LOADER_KEY);
|
|
200
|
+
if (state.clarityId === id) state.clarityId = undefined;
|
|
201
|
+
logger.debug("Clarity loader unavailable; a later navigation can retry");
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
64
205
|
function initializeTrackers(): void {
|
|
65
206
|
const config = document.body?.dataset;
|
|
66
207
|
if (!config) return;
|
|
@@ -82,32 +223,23 @@ function initializeTrackers(): void {
|
|
|
82
223
|
{ "data-stalux-google": "true" },
|
|
83
224
|
).catch(() => logger.debug("Google SDK unavailable; queued events retained"));
|
|
84
225
|
}
|
|
85
|
-
if (config.staluxClarityId
|
|
86
|
-
host.clarity ??= Object.assign(
|
|
87
|
-
function (..._args: unknown[]) {
|
|
88
|
-
// biome-ignore lint/complexity/noArguments: Match the official Clarity queue format.
|
|
89
|
-
host.clarity?.q.push(arguments);
|
|
90
|
-
},
|
|
91
|
-
{ q: [] as unknown[] },
|
|
92
|
-
);
|
|
93
|
-
const id = config.staluxClarityId;
|
|
94
|
-
state.clarityId = id;
|
|
95
|
-
void loadTrackerScript(`https://www.clarity.ms/tag/${encodeURIComponent(id)}`, {
|
|
96
|
-
"data-stalux-clarity": "true",
|
|
97
|
-
}).catch(() => {
|
|
98
|
-
if (state.clarityId === id) state.clarityId = undefined;
|
|
99
|
-
logger.debug("Clarity SDK unavailable; a later navigation can retry");
|
|
100
|
-
});
|
|
101
|
-
}
|
|
226
|
+
if (config.staluxClarityId) initializeClarity(config.staluxClarityId);
|
|
102
227
|
}
|
|
103
228
|
|
|
104
229
|
function trackGooglePage(): void {
|
|
105
230
|
if (!state.googleId) return;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
231
|
+
// Capture the visit before yielding: another navigation must not replace its
|
|
232
|
+
// URL, title or virtual referrer while the SDK is waiting for its task.
|
|
233
|
+
googlePages.push({
|
|
234
|
+
ready: transitionReady,
|
|
235
|
+
payload: {
|
|
236
|
+
page_location: window.location.href,
|
|
237
|
+
page_path: window.location.pathname + window.location.search,
|
|
238
|
+
page_title: document.title,
|
|
239
|
+
page_referrer: state.previousPage ?? document.referrer,
|
|
240
|
+
},
|
|
110
241
|
});
|
|
242
|
+
scheduleGooglePages();
|
|
111
243
|
}
|
|
112
244
|
|
|
113
245
|
function trackUmamiPage(): void {
|
|
@@ -142,14 +274,26 @@ function trackVercountPage(): void {
|
|
|
142
274
|
document.head.appendChild(script);
|
|
143
275
|
}
|
|
144
276
|
|
|
145
|
-
if (state.installed && (!state.native || state.revision !==
|
|
277
|
+
if (state.installed && (!state.native || state.revision !== 4)) {
|
|
278
|
+
// Revision 4 replaces synchronous Google page views and the motion policy.
|
|
146
279
|
// An open tab can cross a deployment boundary. Its old anonymous worker
|
|
147
280
|
// listeners cannot be detached safely; reload once to install the new runtime.
|
|
148
281
|
window.location.reload();
|
|
149
282
|
} else if (!state.installed) {
|
|
150
283
|
state.native = true;
|
|
151
|
-
state.revision =
|
|
284
|
+
state.revision = 4;
|
|
152
285
|
state.installed = true;
|
|
286
|
+
document.addEventListener("astro:before-swap", (event) => {
|
|
287
|
+
transitionReady = prepareRouteMotion(event as Event & RouteMotionEvent).ready;
|
|
288
|
+
// Invalidate an older page's frames, timer and posted task before this
|
|
289
|
+
// incoming snapshot starts. Its captured visit remains in the FIFO.
|
|
290
|
+
cancelGoogleSchedule();
|
|
291
|
+
scheduleGooglePages();
|
|
292
|
+
});
|
|
293
|
+
window.addEventListener("pagehide", flushGooglePages);
|
|
294
|
+
document.addEventListener("visibilitychange", () => {
|
|
295
|
+
if (document.hidden) flushGooglePages();
|
|
296
|
+
});
|
|
153
297
|
document.addEventListener("astro:page-load", () => {
|
|
154
298
|
initializeTrackers();
|
|
155
299
|
trackGooglePage();
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 先低优先级解码目标背景,等待内容过渡完成后再淡入,不与内容快照争抢绘制。
|
|
14
14
|
*/
|
|
15
15
|
import { createClientLogger } from "./logger";
|
|
16
|
+
import { prepareRouteMotion, type RouteMotionEvent } from "./route-motion";
|
|
16
17
|
|
|
17
18
|
const logger = createClientLogger("background");
|
|
18
19
|
|
|
@@ -115,8 +116,7 @@ if (!(window as Window & { __STALUX_BG_LISTENER__?: boolean }).__STALUX_BG_LISTE
|
|
|
115
116
|
prepared = connection?.saveData ? undefined : prepareBackground();
|
|
116
117
|
});
|
|
117
118
|
document.addEventListener("astro:before-swap", (event) => {
|
|
118
|
-
|
|
119
|
-
transitionFinished = transition?.finished.catch(() => undefined) ?? Promise.resolve();
|
|
119
|
+
transitionFinished = prepareRouteMotion(event as RouteMotionEvent).finished;
|
|
120
120
|
});
|
|
121
121
|
document.addEventListener("astro:page-load", initializeBackground);
|
|
122
122
|
}
|