@studiocms/ui
Version:
The UI library for StudioCMS. Includes the layouts & components we use to build StudioCMS.
130 lines (129 loc) • 4.8 kB
JavaScript
import { z } from "astro/zod";
const HeadConfigSchema = () => z.array(
z.object({
/** Name of the HTML tag to add to `<head>`, e.g. `'meta'`, `'link'`, or `'script'`. */
tag: z.enum(["title", "base", "link", "style", "meta", "script", "noscript", "template"]),
/** Attributes to set on the tag, e.g. `{ rel: 'stylesheet', href: '/custom.css' }`. */
attrs: z.record(z.union([z.string(), z.boolean(), z.undefined()])).default({}),
/** Content to place inside the tag (optional). */
content: z.string().default("")
})
).default([]);
const headDefaults = (title, description, Astro, ogImage, canonical) => {
const headDefaults2 = [
{ tag: "meta", attrs: { charset: "utf-8" } },
{
tag: "meta",
attrs: { name: "viewport", content: "width=device-width, initial-scale=1" }
},
{ tag: "title", content: `${title}` },
{ tag: "meta", attrs: { name: "title", content: title } },
{ tag: "meta", attrs: { name: "description", content: description } },
{ tag: "link", attrs: { rel: "canonical", href: canonical?.href } },
{ tag: "meta", attrs: { name: "generator", content: Astro.generator } },
// Favicon
{
tag: "link",
attrs: { rel: "apple-touch-icon", href: "/apple-touch-icon.png", sizes: "180x180" }
},
{
tag: "link",
attrs: { rel: "icon", href: "/favicon-32x32.png", type: "image/png", sizes: "32x32" }
},
{
tag: "link",
attrs: { rel: "icon", href: "/favicon-16x16.png", type: "image/png", sizes: "16x16" }
},
{ tag: "link", attrs: { rel: "icon", href: "/favicon.png", type: "image/png" } },
{ tag: "link", attrs: { rel: "manifest", href: "/site.webmanifest" } },
{ tag: "link", attrs: { rel: "mask-icon", href: "/safari-pinned-tab.svg", color: "#5bbad5" } },
{ tag: "link", attrs: { rel: "shortcut icon", href: "/favicon.ico" } },
{ tag: "meta", attrs: { name: "msapplication-TileColor", content: "#da532c" } },
{ tag: "meta", attrs: { name: "msapplication-config", content: "/browserconfig.xml" } },
{ tag: "meta", attrs: { name: "theme-color", content: "#aa87f4" } },
// OpenGraph Tags
{ tag: "meta", attrs: { property: "og:title", content: title } },
{ tag: "meta", attrs: { property: "og:type", content: "website" } },
{ tag: "meta", attrs: { property: "og:url", content: canonical?.href } },
{ tag: "meta", attrs: { property: "og:description", content: description } },
{ tag: "meta", attrs: { property: "og:site_name", content: title } },
// Twitter Tags
{
tag: "meta",
attrs: { name: "twitter:card", content: "summary_large_image" }
},
{ tag: "meta", attrs: { name: "twitter:url", content: canonical?.href } },
{ tag: "meta", attrs: { name: "twitter:title", content: title } },
{ tag: "meta", attrs: { name: "twitter:description", content: description } }
];
if (ogImage) {
headDefaults2.push(
{ tag: "meta", attrs: { property: "og:image", content: ogImage } },
{ tag: "meta", attrs: { name: "twitter:image", content: ogImage } }
);
}
return headDefaults2;
};
const HeadSchema = HeadConfigSchema();
function hasTag(head, entry) {
switch (entry.tag) {
case "title":
return head.some(({ tag }) => tag === "title");
case "meta":
return hasOneOf(head, entry, ["name", "property", "http-equiv"]);
default:
return false;
}
}
function hasOneOf(head, entry, keys) {
const attr = getAttr(keys, entry);
if (!attr) return false;
const [key, val] = attr;
return head.some(({ tag, attrs }) => tag === entry.tag && attrs[key] === val);
}
function getAttr(keys, entry) {
let attr;
for (const key of keys) {
const val = entry.attrs[key];
if (val) {
attr = [key, val];
break;
}
}
return attr;
}
function mergeHead(oldHead, newHead) {
return [...oldHead.filter((tag) => !hasTag(newHead, tag)), ...newHead];
}
function sortHead(head) {
return head.sort((a, b) => {
const aImportance = getImportance(a);
const bImportance = getImportance(b);
return aImportance > bImportance ? -1 : bImportance > aImportance ? 1 : 0;
});
}
function getImportance(entry) {
if (entry.tag === "meta" && ("charset" in entry.attrs || "http-equiv" in entry.attrs || entry.attrs.name === "viewport")) {
return 100;
}
if (entry.tag === "title") return 90;
if (entry.tag !== "meta") {
if (entry.tag === "link" && "rel" in entry.attrs && entry.attrs.rel === "shortcut icon") {
return 70;
}
return 80;
}
return 0;
}
function createHead(defaultHeaders, ...heads) {
let head = HeadSchema.parse(defaultHeaders);
for (const next of heads) {
head = mergeHead(head, next);
}
return sortHead(head);
}
export {
HeadConfigSchema,
createHead,
headDefaults
};