Skip to content

Commit 114914c

Browse files
authored
Merge pull request #35 from GoodbyeNJN/issue-34
Fix meta overwriting when `name` is same but others are different
2 parents c9d21ef + 21e331b commit 114914c

2 files changed

Lines changed: 71 additions & 15 deletions

File tree

src/index.tsx

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,32 @@ export interface MetaContextType {
3232

3333
const cascadingTags = ["title", "meta"];
3434

35-
const getTagType = (tag: TagDescription) => tag.tag + (tag.name ? `.${tag.name}"` : "");
35+
// https://html.spec.whatwg.org/multipage/semantics.html#the-title-element
36+
const titleTagProperties: string[] = [];
37+
38+
const metaTagProperties: string[] =
39+
// https://html.spec.whatwg.org/multipage/semantics.html#the-meta-element
40+
["name", "http-equiv", "content", "charset", "media"]
41+
// additional properties
42+
.concat(["property"]);
43+
44+
const getTagKey = (tag: TagDescription, properties: string[]) => {
45+
// pick allowed properties and sort them
46+
const tagProps = Object.fromEntries(
47+
Object.entries(tag.props)
48+
.filter(([k]) => properties.includes(k))
49+
.sort()
50+
);
51+
52+
// treat `property` as `name` for meta tags
53+
if (Object.hasOwn(tagProps, "name") || Object.hasOwn(tagProps, "property")) {
54+
tagProps.name = tagProps.name || tagProps.property;
55+
delete tagProps.property;
56+
}
57+
58+
// concat tag name and properties as unique key for this tag
59+
return tag.tag + JSON.stringify(tagProps);
60+
};
3661

3762
const MetaProvider: ParentComponent<{ tags?: Array<TagDescription> }> = props => {
3863
if (!isServer && !sharedConfig.context) {
@@ -71,21 +96,22 @@ const MetaProvider: ParentComponent<{ tags?: Array<TagDescription> }> = props =>
7196

7297
const actions: MetaContextType = {
7398
addClientTag: (tag: TagDescription) => {
74-
let tagType = getTagType(tag);
75-
7699
if (cascadingTags.indexOf(tag.tag) !== -1) {
100+
const properties = tag.tag === "title" ? titleTagProperties : metaTagProperties;
101+
const tagKey = getTagKey(tag, properties);
102+
77103
// only cascading tags need to be kept as singletons
78-
if (!cascadedTagInstances.has(tagType)) {
79-
cascadedTagInstances.set(tagType, []);
104+
if (!cascadedTagInstances.has(tagKey)) {
105+
cascadedTagInstances.set(tagKey, []);
80106
}
81107

82-
let instances = cascadedTagInstances.get(tagType);
108+
let instances = cascadedTagInstances.get(tagKey);
83109
let index = instances.length;
84110

85111
instances = [...instances, tag];
86112

87113
// track indices synchronously
88-
cascadedTagInstances.set(tagType, instances);
114+
cascadedTagInstances.set(tagKey, instances);
89115

90116
if (!isServer) {
91117
let element = getElement(tag);
@@ -127,10 +153,11 @@ const MetaProvider: ParentComponent<{ tags?: Array<TagDescription> }> = props =>
127153
},
128154

129155
removeClientTag: (tag: TagDescription, index: number) => {
130-
const tagName = getTagType(tag);
156+
const properties = tag.tag === "title" ? titleTagProperties : metaTagProperties;
157+
const tagKey = getTagKey(tag, properties);
131158

132159
if (tag.ref) {
133-
const t = cascadedTagInstances.get(tagName);
160+
const t = cascadedTagInstances.get(tagKey);
134161
if (t) {
135162
if (tag.ref.parentNode) {
136163
tag.ref.parentNode.removeChild(tag.ref);
@@ -142,7 +169,7 @@ const MetaProvider: ParentComponent<{ tags?: Array<TagDescription> }> = props =>
142169
}
143170

144171
t[index] = null;
145-
cascadedTagInstances.set(tagName, t);
172+
cascadedTagInstances.set(tagKey, t);
146173
} else {
147174
if (tag.ref.parentNode) {
148175
tag.ref.parentNode.removeChild(tag.ref);
@@ -157,11 +184,11 @@ const MetaProvider: ParentComponent<{ tags?: Array<TagDescription> }> = props =>
157184
const { tags = [] } = props;
158185
// tweak only cascading tags
159186
if (cascadingTags.indexOf(tagDesc.tag) !== -1) {
160-
const index = tags.findIndex(prev => {
161-
const prevName = prev.props.name || prev.props.property;
162-
const nextName = tagDesc.props.name || tagDesc.props.property;
163-
return prev.tag === tagDesc.tag && prevName === nextName;
164-
});
187+
const properties = tagDesc.tag === "title" ? titleTagProperties : metaTagProperties;
188+
const tagDescKey = getTagKey(tagDesc, properties);
189+
const index = tags.findIndex(
190+
prev => prev.tag === tagDesc.tag && getTagKey(prev, properties) === tagDescKey
191+
);
165192
if (index !== -1) {
166193
tags.splice(index, 1);
167194
}

test/index.spec.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,35 @@ test("renders only last meta with the same property", () => {
267267
dispose();
268268
});
269269

270+
test("renders both meta with the same name/property but different other attributes", () => {
271+
let div = document.createElement("div");
272+
const snapshot =
273+
'<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff"><meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000">';
274+
const dispose = render(
275+
() => (
276+
<MetaProvider>
277+
<Meta
278+
name="theme-color"
279+
// @ts-expect-error TS2322
280+
// https://github.com/ryansolid/dom-expressions/issues/273
281+
media="(prefers-color-scheme: light)"
282+
content="#fff"
283+
/>
284+
<Meta
285+
name="theme-color"
286+
// @ts-expect-error TS2322
287+
// https://github.com/ryansolid/dom-expressions/issues/273
288+
media="(prefers-color-scheme: dark)"
289+
content="#000"
290+
/>
291+
</MetaProvider>
292+
),
293+
div
294+
);
295+
expect(document.head.innerHTML).toBe(snapshot);
296+
dispose();
297+
});
298+
270299
test("throws error if head tag is rendered without MetaProvider", () => {
271300
expect(() => {
272301
let div = document.createElement("div");

0 commit comments

Comments
 (0)