@autobusal/common 1.8.1 → 1.8.2
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 +15 -0
- package/Meta.tsx +57 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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.8.2] - 2026-07-30
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **`Meta.tsx` duplicate description/canonical/OG/Twitter tags.** The
|
|
11
|
+
existing title-only dedup (`useSoleTitleOwnership`, since 1.5.1) is now
|
|
12
|
+
`useSoleMetaOwnership` and covers every tag Meta renders, not just
|
|
13
|
+
`<title>`. Surfaced by the new build-time prerender step: React doesn't
|
|
14
|
+
reconcile against tags already sitting in `<head>` from a source other
|
|
15
|
+
than its own current render (a prerendered snapshot's baked-in
|
|
16
|
+
description, a leftover from a pre-locale-detection render, ...), so
|
|
17
|
+
multiple `<meta name="description">`/`og:description`/etc could coexist
|
|
18
|
+
- crawlers commonly read the FIRST occurrence, which could be the stale
|
|
19
|
+
one, not the page's real content.
|
|
20
|
+
|
|
6
21
|
## [1.8.1] - 2026-07-30
|
|
7
22
|
|
|
8
23
|
### Fixed
|
package/Meta.tsx
CHANGED
|
@@ -31,26 +31,46 @@ const withBrand = (value: string): string => (brand ? `${ value } - ${ brand }`
|
|
|
31
31
|
// (verified: zero [data-react-helmet] tags in the DOM on any route, on any
|
|
32
32
|
// page load, even before this session's changes).
|
|
33
33
|
//
|
|
34
|
-
// React does NOT dedupe
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
34
|
+
// React does NOT dedupe these tags against ones that were already sitting in
|
|
35
|
+
// <head> from something OTHER than React's own current render pass - a
|
|
36
|
+
// static tag baked into index.html at build time, or (discovered while
|
|
37
|
+
// building the build-time prerender step) React's own PREVIOUS output
|
|
38
|
+
// baked into a prerendered dist/{path}/index.html snapshot. `createRoot`
|
|
39
|
+
// doesn't adopt/reconcile against existing markup the way `hydrateRoot`
|
|
40
|
+
// would, so every one of these tags - title, description, canonical,
|
|
41
|
+
// og:*, twitter:* - could end up duplicated: a page's real content
|
|
42
|
+
// alongside a stale prerendered description, or (worse) a different
|
|
43
|
+
// page/language's leftover copy. `document.title` happening to still
|
|
44
|
+
// resolve correctly (spec: first <title> in tree order) hid this for
|
|
45
|
+
// title alone; a description/canonical duplicate is worse, since crawlers
|
|
46
|
+
// commonly pick the FIRST occurrence, which is the potentially-stale one.
|
|
47
|
+
//
|
|
48
|
+
// This makes Meta the sole owner of every tag it manages: for each one, if
|
|
49
|
+
// it has a current value, keep the element carrying that value and remove
|
|
50
|
+
// every other element matching the same selector; if it has no value for
|
|
51
|
+
// this page (description/keywords/image are optional), remove all of
|
|
52
|
+
// them, since nothing here can claim the page has one.
|
|
53
|
+
type OwnedTag = { selector: string, attr: 'text' | string, value?: string };
|
|
54
|
+
|
|
55
|
+
const useSoleMetaOwnership = (tags: OwnedTag[]): void => {
|
|
46
56
|
useEffect(() => {
|
|
47
|
-
|
|
48
|
-
.
|
|
57
|
+
tags.forEach(({ selector, attr, value }) => {
|
|
58
|
+
const elements = [...document.querySelectorAll(`head > ${ selector }`)];
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
if (value === undefined) {
|
|
61
|
+
elements.forEach(el => el.remove());
|
|
62
|
+
return;
|
|
53
63
|
}
|
|
64
|
+
|
|
65
|
+
const mine = elements.find(el => (
|
|
66
|
+
attr === 'text' ? el.textContent === value : el.getAttribute(attr) === value
|
|
67
|
+
));
|
|
68
|
+
|
|
69
|
+
elements.forEach(el => {
|
|
70
|
+
if (el !== mine) {
|
|
71
|
+
el.remove();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
54
74
|
});
|
|
55
75
|
});
|
|
56
76
|
};
|
|
@@ -100,8 +120,25 @@ const Meta = ({ title, keywords, description, image, url, type = 'website', noIn
|
|
|
100
120
|
const fullTitle = withBrand(title);
|
|
101
121
|
const canonicalUrl = useCanonicalUrl(url);
|
|
102
122
|
const location = useLocation();
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
const robots = noIndex ? 'noindex, nofollow' : 'index, follow';
|
|
124
|
+
|
|
125
|
+
useSoleMetaOwnership([
|
|
126
|
+
{ selector: 'title', attr: 'text', value: fullTitle },
|
|
127
|
+
{ selector: 'meta[name="robots"]', attr: 'content', value: robots },
|
|
128
|
+
{ selector: 'meta[name="keywords"]', attr: 'content', value: keywords },
|
|
129
|
+
{ selector: 'meta[name="description"]', attr: 'content', value: description },
|
|
130
|
+
{ selector: 'link[rel="canonical"]', attr: 'href', value: canonicalUrl },
|
|
131
|
+
{ selector: 'meta[property="og:type"]', attr: 'content', value: type },
|
|
132
|
+
{ selector: 'meta[property="og:title"]', attr: 'content', value: fullTitle },
|
|
133
|
+
{ selector: 'meta[property="og:description"]', attr: 'content', value: description },
|
|
134
|
+
{ selector: 'meta[property="og:image"]', attr: 'content', value: image },
|
|
135
|
+
{ selector: 'meta[property="og:url"]', attr: 'content', value: canonicalUrl },
|
|
136
|
+
{ selector: 'meta[property="og:site_name"]', attr: 'content', value: brand },
|
|
137
|
+
{ selector: 'meta[name="twitter:card"]', attr: 'content', value: 'summary_large_image' },
|
|
138
|
+
{ selector: 'meta[name="twitter:title"]', attr: 'content', value: fullTitle },
|
|
139
|
+
{ selector: 'meta[name="twitter:description"]', attr: 'content', value: description },
|
|
140
|
+
{ selector: 'meta[name="twitter:image"]', attr: 'content', value: image }
|
|
141
|
+
]);
|
|
105
142
|
usePageviewTracking(location.pathname + location.search, fullTitle);
|
|
106
143
|
|
|
107
144
|
return (
|
|
@@ -109,7 +146,7 @@ const Meta = ({ title, keywords, description, image, url, type = 'website', noIn
|
|
|
109
146
|
<title>{ fullTitle }</title>
|
|
110
147
|
|
|
111
148
|
{/* Robots */}
|
|
112
|
-
<meta name="robots" content={
|
|
149
|
+
<meta name="robots" content={ robots } />
|
|
113
150
|
|
|
114
151
|
{/* Standard Meta Tags */}
|
|
115
152
|
{ keywords && <meta name="keywords" content={ keywords } /> }
|