@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 CHANGED
@@ -18,6 +18,10 @@ Standalone images written as `![Image description](url)` 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 explicit pageviews. Umami disables automatic pageviews while retaining click tracking, capturing each page's URL, title, and referrer before the SDK resolves so rapid navigation cannot mix visits. An existing legacy Partytown session performs one full reload when it encounters the new runtime to prevent duplicate listeners and tracking.
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 loads the tracking code asynchronously and preserves one loader across soft navigation. The SDK handles history navigation itself. Do not install the same project again through `anyhead`, a tag manager, or another plugin.
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.38.0",
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",
@@ -42,7 +42,7 @@ const ldJson = JSON.stringify(seo.schema).replace(/<\//g, "<\\/");
42
42
 
43
43
  <head>
44
44
  <meta charset="utf-8">
45
- <ClientRouter fallback="animate" />
45
+ <ClientRouter fallback="swap" />
46
46
  <SEO
47
47
  title={seo.title}
48
48
  description={seo.description}
@@ -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-text-shadow": "--s1r",
292
- "--surface-glass": "--s1s",
293
- "--surface-glass-shadow": "--s1t",
294
- "--tag-bg": "--s1u",
295
- "--tag-bg-hover": "--s1v",
296
- "--tl-line": "--s1w",
297
- "--tl-pad": "--s1x",
298
- "--transition-fast": "--s1y",
299
- "--transition-medium": "--s1z",
300
- "--transition-normal": "--s20",
301
- "--transition-slow": "--s21",
302
- "--transition-slower": "--s22",
303
- "--transition-slowest": "--s23",
304
- "--un-rotate": "--s24",
305
- "--un-rotate-x": "--s25",
306
- "--un-rotate-y": "--s26",
307
- "--un-rotate-z": "--s27",
308
- "--un-scale-x": "--s28",
309
- "--un-scale-y": "--s29",
310
- "--un-scale-z": "--s2a",
311
- "--un-skew-x": "--s2b",
312
- "--un-skew-y": "--s2c",
313
- "--un-translate-x": "--s2d",
314
- "--un-translate-y": "--s2e",
315
- "--un-translate-z": "--s2f",
316
- "--updated-bg": "--s2g",
317
- "--updated-border": "--s2h",
318
- "--white-05p": "--s2i",
319
- "--white-10p": "--s2j",
320
- "--white-15p": "--s2k",
321
- "--white-20p": "--s2l",
322
- "--white-30p": "--s2m",
323
- "--white-50p": "--s2n",
324
- "--white-60p": "--s2o",
325
- "--white-70p": "--s2p",
326
- "--white-75p": "--s2q",
327
- "--white-78p": "--s2r",
328
- "--white-80p": "--s2s",
329
- "--white-85p": "--s2t",
330
- "--white-90p": "--s2u",
331
- "--white-95p": "--s2v",
332
- "--white-full": "--s2w"
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",
@@ -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 { getRouteAnimation, getRouteKind } from "@utils/view-transitions";
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 lang={htmlLang} style={getAccentColorStyle(siteConfig.accentColor)}>
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={routeKind === "article" ? "stalux-article-page" : "stalux-page"}
119
- transition:animate={routeKind === "home" || routeKind === "article"
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) & { q: unknown[] };
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
- /** Never await analytics in a router callback; failed loads can retry on a later page. */
34
- function loadTrackerScript(src: string, attributes: Record<string, string> = {}): Promise<void> {
35
- const existing = state.scripts?.get(src);
36
- if (existing) return existing;
37
- const script = document.createElement("script");
38
- script.async = true;
39
- script.fetchPriority = "low";
40
- script.src = src;
41
- for (const [name, value] of Object.entries(attributes)) script.setAttribute(name, value);
42
- const loaded = new Promise<void>((resolve, reject) => {
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 && state.clarityId !== 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
- host.gtag?.("event", "page_view", {
107
- page_location: window.location.href,
108
- page_path: window.location.pathname + window.location.search,
109
- page_title: document.title,
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 !== 2)) {
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 = 2;
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
- const transition = (event as Event & { viewTransition?: ViewTransition }).viewTransition;
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
  }