Skip to content

Instantly share code, notes, and snippets.

@amoutonbrady
Created February 19, 2021 20:42
Show Gist options
  • Save amoutonbrady/e0ca61c8907d44b18e3631a3490a9ca8 to your computer and use it in GitHub Desktop.
Save amoutonbrady/e0ca61c8907d44b18e3631a3490a9ca8 to your computer and use it in GitHub Desktop.
import { createContext, createState, createComputed, onMount, onCleanup, splitProps, useContext, mergeProps } from "solid-js";
import { isServer, Show, Portal, Dynamic } from "solid-js/web";
const MetaContext = createContext();
const cascadingTags = ["title", "meta"];
const MetaProvider = props => {
const indices = new Map(), [state, setState] = createState({});
onMount(() => {
const ssrTags = document.head.querySelectorAll(`[data-sm=""]`);
// `forEach` on `NodeList` is not supported in Googlebot, so use a workaround
Array.prototype.forEach.call(ssrTags, (ssrTag) => ssrTag.parentNode.removeChild(ssrTag));
});
const actions = {
addClientTag: (tag, name) => {
// consider only cascading tags
if (cascadingTags.indexOf(tag) !== -1) {
setState(state => {
const names = state[tag] || [];
return { [tag]: [...names, name] };
});
// track indices synchronously
const index = indices.has(tag) ? indices.get(tag) + 1 : 0;
indices.set(tag, index);
return index;
}
return -1;
},
shouldRenderTag: (tag, index) => {
if (cascadingTags.indexOf(tag) !== -1) {
const names = state[tag];
// check if the tag is the last one of similar
return names && names.lastIndexOf(names[index]) === index;
}
return true;
},
removeClientTag: (tag, index) => {
setState(tag, (names) => {
if (names)
return { [index]: null };
return names;
});
},
addServerTag: (tagDesc) => {
const { tags = [] } = props;
// tweak only cascading tags
if (cascadingTags.indexOf(tagDesc.tag) !== -1) {
const index = tags.findIndex(prev => {
const prevName = prev.props.name || prev.props.property;
const nextName = tagDesc.props.name || tagDesc.props.property;
return prev.tag === tagDesc.tag && prevName === nextName;
});
if (index !== -1) {
tags.splice(index, 1);
}
}
tags.push(tagDesc);
}
};
if (isServer && Array.isArray(props.tags) === false) {
throw Error("tags array should be passed to <MetaProvider /> in node");
}
return <MetaContext.Provider value={actions}>{props.children}</MetaContext.Provider>;
};
const MetaTag = props => {
const c = useContext(MetaContext);
if (!c)
throw new Error("<MetaProvider /> should be in the tree");
const { addClientTag, removeClientTag, addServerTag, shouldRenderTag } = c;
let index = -1;
createComputed(() => {
index = addClientTag(props.tag, props.name || props.property);
onCleanup(() => removeClientTag(props.tag, index));
});
const [internal, rest] = splitProps(props, ["tag"]);
if (isServer) {
addServerTag({ tag: internal.tag, props: rest });
return null;
}
return (<Show when={shouldRenderTag(internal.tag, index)}>
<Portal mount={document.head}>
<Dynamic component={internal.tag} {...rest}/>
</Portal>
</Show>);
};
export { MetaProvider };
export function renderTags(tags) {
return tags
.map(tag => {
const keys = Object.keys(tag.props);
return `<${tag.tag} data-sm=""${keys.map(k => k === "children" ? "" : ` ${k}="${tag.props[k]}"`)}>${tag.props.children || ""}</${tag.tag}>`;
})
.join("");
}
export const Title = props => MetaTag(mergeProps({ tag: "title" }, props));
export const Style = props => MetaTag(mergeProps({ tag: "style" }, props));
export const Meta = props => MetaTag(mergeProps({ tag: "meta" }, props));
export const Link = props => MetaTag(mergeProps({ tag: "link" }, props));
export const Base = props => MetaTag(mergeProps({ tag: "base" }, props));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment