@autobusal/common 1.4.7 → 1.5.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/Meta.tsx +46 -11
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,39 @@
3
3
  All notable changes to `@autobusal/common` are documented here. This project follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [1.5.1] - 2026-07-30
7
+
8
+ ### Fixed
9
+
10
+ - **`Meta.tsx` and `@autobusal/providers`'s `Preload.tsx` both render a
11
+ `<title>`, and React 19's native hoisting (added in 1.5.0, see above) does
12
+ NOT dedupe `<title>` between separate component instances.** Both ended up
13
+ as real, simultaneous DOM nodes - technically invalid HTML (a document must
14
+ not have more than one title element), even though `document.title` itself
15
+ still resolved correctly by spec ("first title in tree order" happened to
16
+ always be this one). `Meta` now takes exclusive ownership on mount: it
17
+ removes any other `<title>` element in `<head>` the instant its own real
18
+ page title exists, so exactly one survives.
19
+
20
+ ## [1.5.0] - 2026-07-30
21
+
22
+ ### Fixed
23
+
24
+ - **`Meta.tsx` migrated off `react-helmet` to React 19's native document
25
+ metadata support.** react-helmet 6.1.0 (unmaintained since ~2021) was
26
+ discovered to be completely non-functional under React 19: zero of its DOM
27
+ writes were ever committing on any route, on any fresh page load, in either
28
+ consuming app - confirmed via the absence of `[data-react-helmet]` in the
29
+ DOM and by bisecting back through the app's dependency history to before
30
+ any of today's changes. Every page's `<title>`, `description`, `robots`,
31
+ `og:*`/`twitter:*` tags were silently frozen at whatever the static
32
+ `index.html` shipped, for every real user and JS-executing crawler landing
33
+ directly on a URL. `<title>`/`<meta>`/`<link>` are now rendered as plain
34
+ elements - React 19 auto-hoists and deduplicates them into `<head>` with no
35
+ library needed, and the same "deepest/last-rendered wins" semantics Helmet
36
+ was supposed to provide (a page's own `<Meta>` overriding
37
+ `@autobusal/providers`'s app-shell fallback title) now actually work.
38
+
6
39
  ## [1.4.7] - 2026-07-30
7
40
 
8
41
  ### Fixed
package/Meta.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Helmet } from 'react-helmet';
1
+ import { useEffect } from 'react';
2
2
 
3
3
  interface Props {
4
4
  title: string
@@ -21,10 +21,45 @@ const brand = import.meta.env.VITE_APP_NAME as string | undefined;
21
21
 
22
22
  const withBrand = (value: string): string => (brand ? `${ value } - ${ brand }` : value);
23
23
 
24
- const Meta = ({ title, keywords, description, image, url, type = 'website', noIndex = false, children }: Props): JSX.Element => (
25
- <>
26
- <Helmet>
27
- <title>{ withBrand(title) }</title>
24
+ // React 19 automatically hoists any <title>/<meta>/<link> rendered ANYWHERE in
25
+ // the component tree into <head> - no wrapper component needed. This replaced
26
+ // react-helmet (6.1.0, unmaintained since ~2021), which had gone completely
27
+ // non-functional under React 19: none of its DOM writes were ever committing
28
+ // (verified: zero [data-react-helmet] tags in the DOM on any route, on any
29
+ // page load, even before this session's changes).
30
+ //
31
+ // React does NOT dedupe <title> between separate component instances -
32
+ // @autobusal/providers' <Preload> (the app-shell fallback title, rendered
33
+ // above the router) and this component's own <title> both end up as real,
34
+ // separate DOM nodes at once - technically invalid HTML (a document must not
35
+ // have more than one title element), even though `document.title` itself
36
+ // still resolved correctly (per spec it's the FIRST title in tree order,
37
+ // which empirically was always this one - but relying on that ordering
38
+ // instead of guaranteeing it was fragile). The effect below makes this Meta
39
+ // the sole owner of <title>: on mount, it removes every OTHER <title> element
40
+ // in <head> - Preload's fallback, or any stray leftover - so exactly one
41
+ // title exists once a page has its own Meta.
42
+ const useSoleTitleOwnership = (fullTitle: string): void => {
43
+ useEffect(() => {
44
+ const mine = [...document.querySelectorAll('head > title')]
45
+ .find(el => el.textContent === fullTitle);
46
+
47
+ document.querySelectorAll('head > title').forEach(el => {
48
+ if (el !== mine) {
49
+ el.remove();
50
+ }
51
+ });
52
+ });
53
+ };
54
+
55
+ const Meta = ({ title, keywords, description, image, url, type = 'website', noIndex = false, children }: Props): JSX.Element => {
56
+ const fullTitle = withBrand(title);
57
+
58
+ useSoleTitleOwnership(fullTitle);
59
+
60
+ return (
61
+ <>
62
+ <title>{ fullTitle }</title>
28
63
 
29
64
  {/* Robots */}
30
65
  <meta name="robots" content={ noIndex ? 'noindex, nofollow' : 'index, follow' } />
@@ -36,7 +71,7 @@ const Meta = ({ title, keywords, description, image, url, type = 'website', noIn
36
71
 
37
72
  {/* Open Graph / Facebook */}
38
73
  <meta property="og:type" content={ type } />
39
- <meta property="og:title" content={ withBrand(title) } />
74
+ <meta property="og:title" content={ fullTitle } />
40
75
  { description && <meta property="og:description" content={ description } /> }
41
76
  { image && <meta property="og:image" content={ image } /> }
42
77
  { url && <meta property="og:url" content={ url } /> }
@@ -44,13 +79,13 @@ const Meta = ({ title, keywords, description, image, url, type = 'website', noIn
44
79
 
45
80
  {/* Twitter */}
46
81
  <meta name="twitter:card" content="summary_large_image" />
47
- <meta name="twitter:title" content={ withBrand(title) } />
82
+ <meta name="twitter:title" content={ fullTitle } />
48
83
  { description && <meta name="twitter:description" content={ description } /> }
49
84
  { image && <meta name="twitter:image" content={ image } /> }
50
- </Helmet>
51
85
 
52
- { children }
53
- </>
54
- );
86
+ { children }
87
+ </>
88
+ );
89
+ };
55
90
 
56
91
  export default Meta;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.4.7",
3
+ "version": "1.5.1",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"