@autobusal/providers 1.44.2 → 1.44.4
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 +71 -5
- package/Setup/analytics.ts +37 -3
- package/package.json +1 -1
package/Setup/Preload.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
-
import { JsonLd, setServerToday } from '@autobusal/common';
|
|
2
|
+
import { JsonLd, hasMetaMounted, setServerToday } from '@autobusal/common';
|
|
3
3
|
import { SettingsData } from '../types/settings';
|
|
4
4
|
import { useSystemTheme } from '@autobusal/hooks';
|
|
5
5
|
|
|
@@ -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
|
|
@@ -40,17 +76,45 @@ const Preload = ({ data, children }: Props): JSX.Element => {
|
|
|
40
76
|
// sold them quite happily.
|
|
41
77
|
setServerToday(data.today);
|
|
42
78
|
|
|
43
|
-
|
|
79
|
+
/*
|
|
80
|
+
* The app-shell title, for the moment before a page's own <Meta> mounts -
|
|
81
|
+
* AND MARKED, so that page can take it away again.
|
|
82
|
+
*
|
|
83
|
+
* Edited: Claude - Date: 2026-09-16
|
|
84
|
+
*
|
|
85
|
+
* Assigning document.title when <head> holds no <title> does not just set a
|
|
86
|
+
* string: the browser CREATES a <title> element for it. On a route whose
|
|
87
|
+
* chunk is still loading there is nothing else in there - main.tsx has
|
|
88
|
+
* already stripped index.html's build-time tag - so this fallback made one,
|
|
89
|
+
* and when the page's own <Meta> arrived React hoisted a second beside it.
|
|
90
|
+
* Both were then captured into the prerendered snapshot, which is why Bing's
|
|
91
|
+
* site scan reported "More than one title tag" on 112 pages. A browser reads
|
|
92
|
+
* the first, which is the right one, so nothing looked wrong; a crawler
|
|
93
|
+
* reading the HTML sees two, and the stray one says only the brand name.
|
|
94
|
+
*
|
|
95
|
+
* Meta removes any element carrying this marker as soon as it owns the head
|
|
96
|
+
* (see useSoleMetaOwnership in @autobusal/common), so the fallback lasts
|
|
97
|
+
* exactly as long as it is the only title there is.
|
|
98
|
+
*/
|
|
44
99
|
useEffect(() => {
|
|
45
|
-
|
|
46
|
-
|
|
100
|
+
// Nothing to stand in for: the page owns the title, or already put one
|
|
101
|
+
// up. hasMetaMounted is the half of this that a marker cannot cover -
|
|
102
|
+
// see the note on it in @autobusal/common's Meta.
|
|
103
|
+
if (hasMetaMounted() || document.head.querySelector('title')) {
|
|
104
|
+
return;
|
|
47
105
|
}
|
|
106
|
+
|
|
107
|
+
document.title = data.preferences.company;
|
|
108
|
+
|
|
109
|
+
document.head.querySelector('title')?.setAttribute('data-shell-title', '');
|
|
48
110
|
}, [ data.preferences.company ]);
|
|
49
111
|
|
|
50
112
|
// we arrange the font family names before inserting them
|
|
51
113
|
const fontTitle = data.theme.fontFamily.title.replace(/ /g, '+');
|
|
52
114
|
const fontContent = data.theme.fontFamily.content.replace(/ /g, '+');
|
|
53
115
|
|
|
116
|
+
const selfHosted = hasSelfHostedFonts(data.theme.fontFamily.title, data.theme.fontFamily.content);
|
|
117
|
+
|
|
54
118
|
const theme = useSystemTheme();
|
|
55
119
|
|
|
56
120
|
const primaryColor = data.colors[theme].primary.normal;
|
|
@@ -101,7 +165,9 @@ const Preload = ({ data, children }: Props): JSX.Element => {
|
|
|
101
165
|
<meta name="msapplication-config" content={ `${ data.url }/favicons/browserconfig.xml` } />
|
|
102
166
|
<meta name="theme-color" content={ primaryColor } />
|
|
103
167
|
|
|
104
|
-
|
|
168
|
+
{ !selfHosted && (
|
|
169
|
+
<link href={ `https://fonts.googleapis.com/css2?family=${ fontTitle }:wght@700&family=${ fontContent }:wght@300;400;700&display=swap` } rel="stylesheet" />
|
|
170
|
+
) }
|
|
105
171
|
|
|
106
172
|
<JsonLd data={ organizationLd } />
|
|
107
173
|
<JsonLd data={ websiteLd } />
|
package/Setup/analytics.ts
CHANGED
|
@@ -77,10 +77,44 @@ const analytics = (container: string | null | undefined): void => {
|
|
|
77
77
|
document.head.appendChild(script);
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
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();
|
|
82
116
|
} else {
|
|
83
|
-
window.
|
|
117
|
+
window.addEventListener('load', whenIdle, { once: true });
|
|
84
118
|
}
|
|
85
119
|
};
|
|
86
120
|
|