diff --git a/src/index.tsx b/src/index.tsx index bba8268..e68f35e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,6 +14,7 @@ import { isServer, spread, escape, useAssets, ssr } from "solid-js/web"; export const MetaContext = createContext(); interface TagDescription { + key?: string; tag: string; props: Record; setting?: { close?: boolean; escape?: boolean }; @@ -38,22 +39,31 @@ const metaTagProperties: string[] = // additional properties .concat(["property"]); -const getTagKey = (tag: TagDescription, properties: string[]) => { - // pick allowed properties and sort them - const tagProps = Object.fromEntries( - Object.entries(tag.props) - .filter(([k]) => properties.includes(k)) - .sort() - ); - - // treat `property` as `name` for meta tags - if (Object.hasOwn(tagProps, "name") || Object.hasOwn(tagProps, "property")) { - tagProps.name = tagProps.name || tagProps.property; - delete tagProps.property; - } +const getTagKey = (tag: TagDescription, properties: string[]) => +{ + if (tag.tag === "title") { return tag.tag; } + if (tag.tag === "meta") + { + if(tag.props.key) { return tag.tag + "|" + tag.props.key; } + else + { + // pick allowed properties and sort them + const tagProps = Object.fromEntries( + Object.entries(tag.props) + .filter(([k]) => properties.includes(k)) + .sort() + ); + + // treat `property` as `name` for meta tags + if (Object.hasOwn(tagProps, "name") || Object.hasOwn(tagProps, "property")) { + tagProps.name = tagProps.name || tagProps.property; + delete tagProps.property; + } - // concat tag name and properties as unique key for this tag - return tag.tag + JSON.stringify(tagProps); + // concat tag name and properties as unique key for this tag + return tag + JSON.stringify(tagProps); + } + } }; function initClientProvider() { @@ -95,6 +105,8 @@ function initClientProvider() { const properties = tag.tag === "title" ? titleTagProperties : metaTagProperties; const tagKey = getTagKey(tag, properties); + if (!tagKey) return -1; + // only cascading tags need to be kept as singletons if (!cascadedTagInstances.has(tagKey)) { cascadedTagInstances.set(tagKey, []); @@ -147,7 +159,7 @@ function initClientProvider() { const tagKey = getTagKey(tag, properties); if (tag.ref) { - const t = cascadedTagInstances.get(tagKey); + const t = tagKey && cascadedTagInstances.get(tagKey); if (t) { if (tag.ref.parentNode) { tag.ref.parentNode.removeChild(tag.ref); @@ -180,9 +192,14 @@ function initServerProvider() { if (cascadingTags.indexOf(tagDesc.tag) !== -1) { const properties = tagDesc.tag === "title" ? titleTagProperties : metaTagProperties; const tagDescKey = getTagKey(tagDesc, properties); - const index = tags.findIndex( - prev => prev.tag === tagDesc.tag && getTagKey(prev, properties) === tagDescKey - ); + + const index = tags.findIndex(prev => { + if (!tagDescKey) return false; + const prevKey = getTagKey(prev, properties); + if (!prevKey) return false; + return prev.tag === tagDesc.tag && prevKey === tagDescKey; + }); + if (index !== -1) { tags.splice(index, 1); } @@ -262,13 +279,20 @@ function renderTags(tags: Array) { .join(""); } +type KeyProp = { + /** + * If set, this element will override previous meta elements with the same `key` value. + * */ + key?: string; +}; + export const Title: Component> = props => MetaTag("title", props, { escape: true, close: true }); export const Style: Component> = props => MetaTag("style", props, { close: true }); -export const Meta: Component> = props => +export const Meta: Component & KeyProp> = props => MetaTag("meta", props); export const Link: Component> = props => diff --git a/test/index.spec.tsx b/test/index.spec.tsx index c247ad5..2f64e9b 100644 --- a/test/index.spec.tsx +++ b/test/index.spec.tsx @@ -4,6 +4,7 @@ import { hydrate, render, Show } from "solid-js/web"; import { MetaProvider, Title, Style, Meta, Link, Base } from "../src"; import { hydrationScript, removeScript } from "./hydration_script"; +//@ts-ignore global.queueMicrotask = setImmediate; test("renders into document.head portal", () => { @@ -332,4 +333,24 @@ test("Escaping the title meta", () => { ); expect(document.head.innerHTML).toBe(snapshot); dispose(); +}); + +test("Prevent duplicate meta tags with the same key", () => { + let div = document.createElement("div"); + const snapshot = ''; + + const dispose = render( + () => ( + +
+ + + +
+
+ ), + div + ); + expect(document.head.innerHTML).toBe(snapshot); + dispose(); }); \ No newline at end of file