@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.
- package/CHANGELOG.md +33 -0
- package/Meta.tsx +46 -11
- 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 {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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={
|
|
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={
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
86
|
+
{ children }
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
55
90
|
|
|
56
91
|
export default Meta;
|