@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 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
- <link href={ `https://fonts.googleapis.com/css2?family=${ fontTitle }:wght@700&family=${ fontContent }:wght@300;400;700&display=swap` } rel="stylesheet" />
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 } />
@@ -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
- const script = document.createElement('script');
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
- script.id = CONTAINER_ID;
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
- document.head.appendChild(script);
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.44.1",
3
+ "version": "1.44.3",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts",