@autobusal/providers 1.44.1 → 1.44.3
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/Setup/Preload.tsx +41 -1
- package/Setup/analytics.ts +72 -5
- package/package.json +1 -1
package/Setup/Preload.tsx
CHANGED
|
@@ -8,6 +8,42 @@ interface Props {
|
|
|
8
8
|
children: JSX.Element
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Whether this page already carries the brand's fonts, served from its own
|
|
13
|
+
* domain.
|
|
14
|
+
*
|
|
15
|
+
* Claude - Date: 2026-09-16
|
|
16
|
+
*
|
|
17
|
+
* WHY GOOGLE'S STYLESHEET IS NOW THE FALLBACK. Measured with Lighthouse on a
|
|
18
|
+
* throttled phone against busmagus.com: the stylesheet below blocked the
|
|
19
|
+
* first paint for 854ms, before a single character of the page could draw,
|
|
20
|
+
* because it lives on a third origin that needs its own DNS, TCP and TLS
|
|
21
|
+
* before the request is even sent - and then a fourth origin for the font
|
|
22
|
+
* files it names. It was the largest single cost PageSpeed reported, ahead
|
|
23
|
+
* of every script and image on the page.
|
|
24
|
+
*
|
|
25
|
+
* A brand's build can now download the pair named in its theme and inline
|
|
26
|
+
* the @font-face rules into index.html, marked
|
|
27
|
+
* `<style data-fonts="Title|Content">`, with the files served from its own
|
|
28
|
+
* domain and the most-used ones preloaded (see magus's scripts/fonts.js).
|
|
29
|
+
* When that tag is present AND names the fonts this theme asks for, this
|
|
30
|
+
* component has nothing to add.
|
|
31
|
+
*
|
|
32
|
+
* The family check is not decoration. Fonts are chosen in the admin and a
|
|
33
|
+
* build only knows the pair it downloaded: change them afterwards and the
|
|
34
|
+
* inlined faces describe the OLD fonts until the next deploy. Comparing the
|
|
35
|
+
* names means the page falls back to Google for the new pair instead of
|
|
36
|
+
* quietly rendering in the wrong typeface. A brand whose build does not do
|
|
37
|
+
* this at all has no tag, and gets exactly the stylesheet it always had.
|
|
38
|
+
*/
|
|
39
|
+
const hasSelfHostedFonts = (title: string, content: string): boolean => {
|
|
40
|
+
if (typeof document === 'undefined') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return document.querySelector('style[data-fonts]')?.getAttribute('data-fonts') === `${ title }|${ content }`;
|
|
45
|
+
};
|
|
46
|
+
|
|
11
47
|
// React 19 automatically hoists any <title>/<meta>/<link> rendered anywhere
|
|
12
48
|
// in the tree into <head> - no wrapper component needed. Replaced
|
|
13
49
|
// react-helmet, which had gone silently non-functional under React 19 (see
|
|
@@ -51,6 +87,8 @@ const Preload = ({ data, children }: Props): JSX.Element => {
|
|
|
51
87
|
const fontTitle = data.theme.fontFamily.title.replace(/ /g, '+');
|
|
52
88
|
const fontContent = data.theme.fontFamily.content.replace(/ /g, '+');
|
|
53
89
|
|
|
90
|
+
const selfHosted = hasSelfHostedFonts(data.theme.fontFamily.title, data.theme.fontFamily.content);
|
|
91
|
+
|
|
54
92
|
const theme = useSystemTheme();
|
|
55
93
|
|
|
56
94
|
const primaryColor = data.colors[theme].primary.normal;
|
|
@@ -101,7 +139,9 @@ const Preload = ({ data, children }: Props): JSX.Element => {
|
|
|
101
139
|
<meta name="msapplication-config" content={ `${ data.url }/favicons/browserconfig.xml` } />
|
|
102
140
|
<meta name="theme-color" content={ primaryColor } />
|
|
103
141
|
|
|
104
|
-
|
|
142
|
+
{ !selfHosted && (
|
|
143
|
+
<link href={ `https://fonts.googleapis.com/css2?family=${ fontTitle }:wght@700&family=${ fontContent }:wght@300;400;700&display=swap` } rel="stylesheet" />
|
|
144
|
+
) }
|
|
105
145
|
|
|
106
146
|
<JsonLd data={ organizationLd } />
|
|
107
147
|
<JsonLd data={ websiteLd } />
|
package/Setup/analytics.ts
CHANGED
|
@@ -42,13 +42,80 @@ const analytics = (container: string | null | undefined): void => {
|
|
|
42
42
|
window.dataLayer = window.dataLayer || [];
|
|
43
43
|
window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
/*
|
|
46
|
+
* LOADED IN THE FIRST QUIET MOMENT, not while the page races for the
|
|
47
|
+
* network.
|
|
48
|
+
*
|
|
49
|
+
* Edited: Claude - Date: 2026-09-16
|
|
50
|
+
*
|
|
51
|
+
* Measured with Lighthouse against production on a throttled phone: the
|
|
52
|
+
* container and the GA tag it pulls are 287 KB of script - more than this
|
|
53
|
+
* app's own entry chunk - fetched in the same window as the bundle, the
|
|
54
|
+
* translations and the images, on a connection modelled at 1.6 Mbps. The
|
|
55
|
+
* page is no more usable for having them early, and every byte they take
|
|
56
|
+
* is one the content is waiting for.
|
|
57
|
+
*
|
|
58
|
+
* The dataLayer is filled FIRST, above, and GTM reads whatever is already
|
|
59
|
+
* in it when it boots - so a page view or a commerce event announced in
|
|
60
|
+
* the meantime still arrives. That is the only thing that could have made
|
|
61
|
+
* deferring this wrong.
|
|
62
|
+
*
|
|
63
|
+
* Safari has no requestIdleCallback, so setTimeout is the fallback rather
|
|
64
|
+
* than a polyfill: "soon, after the load" is the whole requirement.
|
|
65
|
+
*/
|
|
66
|
+
const load = (): void => {
|
|
67
|
+
if (document.getElementById(CONTAINER_ID)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
script.async = true;
|
|
49
|
-
script.src = `https://www.googletagmanager.com/gtm.js?id=${ encodeURIComponent(container) }`;
|
|
71
|
+
const script = document.createElement('script');
|
|
50
72
|
|
|
51
|
-
|
|
73
|
+
script.id = CONTAINER_ID;
|
|
74
|
+
script.async = true;
|
|
75
|
+
script.src = `https://www.googletagmanager.com/gtm.js?id=${ encodeURIComponent(container) }`;
|
|
76
|
+
|
|
77
|
+
document.head.appendChild(script);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* AFTER THE PAGE HAS LOADED, then in the first quiet moment.
|
|
82
|
+
*
|
|
83
|
+
* Edited: Claude - Date: 2026-09-16
|
|
84
|
+
*
|
|
85
|
+
* The idle wait above was not enough on its own, because "idle" arrives
|
|
86
|
+
* early on a page that is still downloading: requestIdleCallback fires
|
|
87
|
+
* whenever the main thread has a gap, and a phone waiting on the network
|
|
88
|
+
* has plenty of gaps. Lighthouse on a throttled phone still found Tag
|
|
89
|
+
* Manager and the Google Analytics tag it pulls in - 287 KB between them,
|
|
90
|
+
* 131 KB of it never executed - arriving inside the page's own loading
|
|
91
|
+
* window, competing for the same connection as the bundle and the images.
|
|
92
|
+
*
|
|
93
|
+
* Waiting for `load` first puts them after everything the page itself
|
|
94
|
+
* asked for. The idle wait then keeps them off the main thread while the
|
|
95
|
+
* page settles, with a ceiling so they are never deferred indefinitely.
|
|
96
|
+
*
|
|
97
|
+
* What this costs is measurement, not function, and it is worth naming: a
|
|
98
|
+
* visitor who leaves within a couple of seconds of the page finishing
|
|
99
|
+
* loading may not be counted. Everything else is kept. The dataLayer is
|
|
100
|
+
* created and filled BEFORE any of this, so page views and commerce
|
|
101
|
+
* events announced while the container is on its way are queued and
|
|
102
|
+
* processed when it boots, not dropped.
|
|
103
|
+
*
|
|
104
|
+
* Safari has no requestIdleCallback, so a short timeout stands in for it.
|
|
105
|
+
*/
|
|
106
|
+
const whenIdle = (): void => {
|
|
107
|
+
if (typeof window.requestIdleCallback === 'function') {
|
|
108
|
+
window.requestIdleCallback(load, { timeout: 3000 });
|
|
109
|
+
} else {
|
|
110
|
+
window.setTimeout(load, 1500);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (document.readyState === 'complete') {
|
|
115
|
+
whenIdle();
|
|
116
|
+
} else {
|
|
117
|
+
window.addEventListener('load', whenIdle, { once: true });
|
|
118
|
+
}
|
|
52
119
|
};
|
|
53
120
|
|
|
54
121
|
/**
|