@decocms/apps-website 8.0.0 → 8.1.0-next.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 +26 -0
- package/package.json +4 -4
- package/src/components/OneDollarStats.test.ts +41 -0
- package/src/components/OneDollarStats.tsx +52 -15
- package/src/components/Stats.tsx +16 -0
- package/src/loaders/fonts/googleFonts.ts +5 -2
- package/src/mod.ts +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# `@decocms/apps-website` (v7 only)
|
|
2
|
+
|
|
3
|
+
This package is the v7 website app. It has no next-major surface: the next
|
|
4
|
+
major ships no website package, and nothing new is added here.
|
|
5
|
+
|
|
6
|
+
Website features move to the platform templates (starter sites you copy and
|
|
7
|
+
own) or to built-ins:
|
|
8
|
+
|
|
9
|
+
| v7 (code in this package) | Next major |
|
|
10
|
+
|---|---|
|
|
11
|
+
| SEO components and sections (`Seo`, `SeoV2`) | SEO helpers in the platform templates. |
|
|
12
|
+
| `Analytics` section (Google Tag Manager, GA4) | A tag manager block in the platform template, owned by the site. |
|
|
13
|
+
| `Stats`, `OneDollarStats` | The built-in `analytics` block. |
|
|
14
|
+
| `website/loaders/secret.ts` | The built-in `secret` block; the migration re-encrypts each v7 secret. |
|
|
15
|
+
| Fonts, theme, video and environment loaders | Platform templates and site code. |
|
|
16
|
+
|
|
17
|
+
v7 saved-content keys and features that are not code in this package:
|
|
18
|
+
|
|
19
|
+
| v7 | Next major |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `website/pages/Page.tsx`, `website/flags/multivariate.ts` | The built-in `page` and `multivariate`, through the CLI alias table. |
|
|
22
|
+
| Matchers | The built-in `always`, `never` and `date` matchers; others are site code. |
|
|
23
|
+
| Sitemaps, redirect logic | Platform templates and site code. |
|
|
24
|
+
|
|
25
|
+
See the next-major docs: *Calling APIs* (what is not in a client),
|
|
26
|
+
*Built-in blocks* and *Migrating from v7*.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps-website",
|
|
3
|
-
"version": "8.0.0",
|
|
3
|
+
"version": "8.1.0-next.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Deco
|
|
5
|
+
"description": "v7 only: Deco website app. The next major ships no website package; website features live in the platform templates.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/decocms/blocks.git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"lint:unused": "knip"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@decocms/blocks": "8.0.0",
|
|
36
|
-
"@decocms/apps-commerce": "8.0.0"
|
|
35
|
+
"@decocms/blocks": "8.1.0-next.0",
|
|
36
|
+
"@decocms/apps-commerce": "8.1.0-next.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^19.0.0",
|
|
@@ -216,3 +216,44 @@ describe("initOneDollarStats", () => {
|
|
|
216
216
|
expect(event).toHaveBeenCalledWith("ev", { keep: "x" });
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
describe("async tracker readiness", () => {
|
|
221
|
+
// The tracker <script> is `async`, so it can resolve after the 10s poll
|
|
222
|
+
// window closes. Its `load` event is the second, unbounded trigger.
|
|
223
|
+
let tracker: HTMLScriptElement;
|
|
224
|
+
|
|
225
|
+
beforeEach(() => {
|
|
226
|
+
tracker = document.createElement("script");
|
|
227
|
+
tracker.id = "onedollarstats-tracker";
|
|
228
|
+
document.head.appendChild(tracker);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
afterEach(() => {
|
|
232
|
+
tracker.remove();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("fires the initial pageview on the tracker's load event after the poll window closes", () => {
|
|
236
|
+
const view = vi.fn();
|
|
237
|
+
initOneDollarStats();
|
|
238
|
+
|
|
239
|
+
// Poll window (200 * 50ms) elapses with no stonks — pageview would be lost.
|
|
240
|
+
vi.advanceTimersByTime(11_000);
|
|
241
|
+
expect(view).not.toHaveBeenCalled();
|
|
242
|
+
|
|
243
|
+
(window as Window & { stonks?: unknown }).stonks = { view };
|
|
244
|
+
tracker.dispatchEvent(new Event("load"));
|
|
245
|
+
|
|
246
|
+
expect(view).toHaveBeenCalledTimes(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("does not double-fire when both the poll and the load event resolve", () => {
|
|
250
|
+
const view = vi.fn();
|
|
251
|
+
initOneDollarStats();
|
|
252
|
+
|
|
253
|
+
(window as Window & { stonks?: unknown }).stonks = { view };
|
|
254
|
+
vi.advanceTimersByTime(100); // poll wins
|
|
255
|
+
tracker.dispatchEvent(new Event("load"));
|
|
256
|
+
|
|
257
|
+
expect(view).toHaveBeenCalledTimes(1);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
* 2. **`useEffect` for client logic.** All side-effects (initial pageview,
|
|
25
25
|
* pushState wrap, DECO event subscribe) run inside a `useEffect`,
|
|
26
26
|
* which fires after hydration. By then `<ScriptOnce>` in
|
|
27
|
-
* `DecoRootLayout` has bootstrapped `window.DECO.events
|
|
28
|
-
* `<script>` (rendered as a sibling)
|
|
29
|
-
*
|
|
30
|
-
* fragile script-execution-order
|
|
27
|
+
* `DecoRootLayout` has bootstrapped `window.DECO.events`. The SDK
|
|
28
|
+
* `<script>` (rendered as a sibling) is `async`, so `window.stonks`
|
|
29
|
+
* may or may not exist yet — see (4) and (5). No inline
|
|
30
|
+
* `dangerouslySetInnerHTML` snippet, no fragile script-execution-order
|
|
31
|
+
* dependency.
|
|
31
32
|
*
|
|
32
33
|
* 3. **Module-level guards.** `window.DECO.events.subscribe()` returns no
|
|
33
34
|
* unsubscribe handle, so we cannot clean up on unmount. We use a
|
|
@@ -39,6 +40,18 @@
|
|
|
39
40
|
* load). We poll every 50 ms for up to 10 s. Production: resolves
|
|
40
41
|
* within one tick.
|
|
41
42
|
*
|
|
43
|
+
* 5. **The tracker `<script>` is `async`, never `defer`.** A third-party
|
|
44
|
+
* analytics tag must not be able to hold the host page's lifecycle
|
|
45
|
+
* hostage: with `defer`, an unreachable `s.lilstts.com` (adblock,
|
|
46
|
+
* blocked route, DNS/CDN outage) delays DOMContentLoaded/load until
|
|
47
|
+
* the TCP timeout — a real client saw the browser spin for ~5 min
|
|
48
|
+
* with the page already painted. `async` is non-blocking for both
|
|
49
|
+
* parsing and DOMContentLoaded. Because the tracker can then resolve
|
|
50
|
+
* at any point, the first pageview is fired from whichever comes
|
|
51
|
+
* first: the readiness poll, or the tracker's own `load` event
|
|
52
|
+
* (which also covers a tracker that arrives after the 10 s poll
|
|
53
|
+
* window closes). See `deco-cx/apps@e0af145` for the Fresh-side fix.
|
|
54
|
+
*
|
|
42
55
|
* ## Behavioural parity vs Fresh `deco-cx/apps`
|
|
43
56
|
*
|
|
44
57
|
* Mirrors the Path B snippet (`analytics/loaders/OneDollarScript.ts`):
|
|
@@ -69,6 +82,9 @@ export interface Props {
|
|
|
69
82
|
staticScriptUrl?: string;
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
/** DOM id of the tracker `<script>`; we listen to its `load` event. */
|
|
86
|
+
const TRACKER_ID = "onedollarstats-tracker";
|
|
87
|
+
|
|
72
88
|
export const DEFAULT_COLLECTOR_ADDRESS = "https://d.lilstts.com/events";
|
|
73
89
|
export const DEFAULT_ANALYTICS_SCRIPT_URL = "https://s.lilstts.com/deco.js";
|
|
74
90
|
|
|
@@ -91,12 +107,12 @@ function OneDollarStats({ collectorAddress, staticScriptUrl }: Props) {
|
|
|
91
107
|
<link rel="dns-prefetch" href={collector} />
|
|
92
108
|
<link rel="preconnect" href={collector} crossOrigin="anonymous" />
|
|
93
109
|
<script
|
|
94
|
-
id=
|
|
110
|
+
id={TRACKER_ID}
|
|
95
111
|
data-autocollect="false"
|
|
96
112
|
data-hash-routing="true"
|
|
97
113
|
data-url={collector}
|
|
98
114
|
src={staticScript}
|
|
99
|
-
|
|
115
|
+
async
|
|
100
116
|
/>
|
|
101
117
|
<OneDollarStatsClient />
|
|
102
118
|
</>
|
|
@@ -199,6 +215,14 @@ function whenReady<T>(
|
|
|
199
215
|
}, intervalMs);
|
|
200
216
|
}
|
|
201
217
|
|
|
218
|
+
type StonksView = NonNullable<NonNullable<Window["stonks"]>["view"]>;
|
|
219
|
+
|
|
220
|
+
function getStonksView(): StonksView | undefined {
|
|
221
|
+
return typeof window.stonks?.view === "function"
|
|
222
|
+
? window.stonks.view.bind(window.stonks)
|
|
223
|
+
: undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
202
226
|
/**
|
|
203
227
|
* Wire up the analytics integration. Idempotent — only the first call has
|
|
204
228
|
* any effect.
|
|
@@ -212,16 +236,29 @@ export function initOneDollarStats(): void {
|
|
|
212
236
|
const flags = readFlagsFromCookie();
|
|
213
237
|
|
|
214
238
|
// 1) Initial pageview + SPA nav tracking, with flag enrichment.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
//
|
|
240
|
+
// The tracker is `async`, so `window.stonks` may land before hydration,
|
|
241
|
+
// after it, or after the poll window closes. Two independent triggers,
|
|
242
|
+
// first one wins (`started` guard) — otherwise a slow tracker silently
|
|
243
|
+
// drops the landing pageview, since `data-autocollect="false"` means the
|
|
244
|
+
// SDK will never send one for us.
|
|
245
|
+
let started = false;
|
|
246
|
+
const start = (view: StonksView) => {
|
|
247
|
+
if (started) return;
|
|
248
|
+
started = true;
|
|
249
|
+
view(flags);
|
|
250
|
+
wrapHistoryPushState(() => view(flags));
|
|
251
|
+
addEventListener("popstate", () => view(flags));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
whenReady(getStonksView, start);
|
|
255
|
+
document.getElementById(TRACKER_ID)?.addEventListener(
|
|
256
|
+
"load",
|
|
257
|
+
() => {
|
|
258
|
+
const view = getStonksView();
|
|
259
|
+
if (view) start(view);
|
|
224
260
|
},
|
|
261
|
+
{ once: true },
|
|
225
262
|
);
|
|
226
263
|
|
|
227
264
|
// 2) Forward DECO events to stonks.event with flag enrichment.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moved to `@decocms/blocks/hooks`, and re-exported here so nothing that already imports
|
|
3
|
+
* `@decocms/apps-website/components/Stats` breaks.
|
|
4
|
+
*
|
|
5
|
+
* It moved because `DecoRootLayout` mounts it now: `@decocms/tanstack` importing from an apps
|
|
6
|
+
* package would add an edge to a dependency graph this repo keeps one-way on purpose, and the
|
|
7
|
+
* component never needed to be here — it is a `<link>` and a `<script>` pointing at our own
|
|
8
|
+
* collector, not a vendor integration like `OneDollarStats`.
|
|
9
|
+
*
|
|
10
|
+
* Sites do not need to import it at all any more. Set `DECO_ANALYTICS_ENABLED=true`.
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
Stats as default,
|
|
14
|
+
STATS_DEFAULT_ORIGIN as DEFAULT_ORIGIN,
|
|
15
|
+
type StatsProps as Props,
|
|
16
|
+
} from "@decocms/blocks/hooks";
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { withFetchTimeout } from "@decocms/blocks/sdk/fetchTimeout";
|
|
1
2
|
import type { Font } from "../../types";
|
|
2
3
|
|
|
4
|
+
const timeoutFetch = withFetchTimeout();
|
|
5
|
+
|
|
3
6
|
interface Props {
|
|
4
7
|
fonts: GoogleFont[];
|
|
5
8
|
}
|
|
@@ -94,13 +97,13 @@ const loader = async (props: Props): Promise<Font> => {
|
|
|
94
97
|
};
|
|
95
98
|
|
|
96
99
|
const sheets = await Promise.all([
|
|
97
|
-
|
|
100
|
+
timeoutFetch(url, { headers: OLD_BROWSER_KEY })
|
|
98
101
|
.then((res) => res.text())
|
|
99
102
|
.catch((e) => {
|
|
100
103
|
logFontError("OLD_UA", url, e);
|
|
101
104
|
return "";
|
|
102
105
|
}),
|
|
103
|
-
|
|
106
|
+
timeoutFetch(url, { headers: NEW_BROWSER_KEY })
|
|
104
107
|
.then((res) => res.text())
|
|
105
108
|
.catch((e) => {
|
|
106
109
|
logFontError("NEW_UA", url, e);
|
package/src/mod.ts
CHANGED
|
@@ -108,6 +108,18 @@ export interface Props {
|
|
|
108
108
|
|
|
109
109
|
/** @title Whilelist URL Patterns */
|
|
110
110
|
whilelistURLs?: string[];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @title renderJson
|
|
114
|
+
* @description Structured JSON rendering of pages for the mobile app (?renderJson).
|
|
115
|
+
*/
|
|
116
|
+
renderJson?: {
|
|
117
|
+
/**
|
|
118
|
+
* @title Ignored Sections
|
|
119
|
+
* @description App-owned sections excluded from the ?renderJson response, matched by resolveType suffix (e.g. "SeoV2.tsx"). Site-owned sections should prefer `export const renderJson = false` in their own file.
|
|
120
|
+
*/
|
|
121
|
+
sectionsToIgnore?: string[];
|
|
122
|
+
};
|
|
111
123
|
}
|
|
112
124
|
|
|
113
125
|
/** Alias for site app bridges that extend website Props. */
|