type MetadataLike = { title?: string description?: string manifest?: string alternates?: { canonical?: string languages?: Record } icons?: { icon?: Array<{ url: string; sizes?: string; type?: string }> apple?: Array<{ url: string; sizes?: string }> } openGraph?: { title?: string description?: string url?: string siteName?: string type?: string images?: Array<{ url: string; alt?: string }> } twitter?: { card?: string title?: string description?: string images?: string[] } robots?: { index?: boolean follow?: boolean nocache?: boolean googleBot?: { index?: boolean follow?: boolean noimageindex?: boolean } } } const pushMeta = ( meta: Array>, key: string, value?: string ) => { if (!value) return meta.push({ [key]: value }) } export function metadataToHead(metadata: MetadataLike) { const meta: Array> = [ { charSet: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, ] const links: Array> = [] if (metadata.title) { meta.push({ title: metadata.title }) meta.push({ property: 'og:title', content: metadata.title }) meta.push({ name: 'twitter:title', content: metadata.title }) } pushMeta(meta, 'name', metadata.description ? 'description' : undefined) if (metadata.description) { meta.push({ name: 'description', content: metadata.description }) meta.push({ property: 'og:description', content: metadata.description }) meta.push({ name: 'twitter:description', content: metadata.description }) } if (metadata.manifest) { links.push({ rel: 'manifest', href: metadata.manifest }) } for (const icon of metadata.icons?.icon ?? []) { links.push({ rel: 'icon', href: icon.url, ...(icon.sizes ? { sizes: icon.sizes } : {}), ...(icon.type ? { type: icon.type } : {}), }) } for (const icon of metadata.icons?.apple ?? []) { links.push({ rel: 'apple-touch-icon', href: icon.url, ...(icon.sizes ? { sizes: icon.sizes } : {}), }) } if (metadata.alternates?.canonical) { links.push({ rel: 'canonical', href: metadata.alternates.canonical, }) } for (const [hreflang, href] of Object.entries( metadata.alternates?.languages ?? {} )) { links.push({ rel: 'alternate', hrefLang: hreflang, href, }) } if (metadata.openGraph?.url) { meta.push({ property: 'og:url', content: metadata.openGraph.url }) } if (metadata.openGraph?.siteName) { meta.push({ property: 'og:site_name', content: metadata.openGraph.siteName }) } if (metadata.openGraph?.type) { meta.push({ property: 'og:type', content: metadata.openGraph.type }) } for (const image of metadata.openGraph?.images ?? []) { meta.push({ property: 'og:image', content: image.url }) if (image.alt) { meta.push({ property: 'og:image:alt', content: image.alt }) } } if (metadata.twitter?.card) { meta.push({ name: 'twitter:card', content: metadata.twitter.card }) } for (const image of metadata.twitter?.images ?? []) { meta.push({ name: 'twitter:image', content: image }) } if (metadata.robots) { const robots = [ metadata.robots.index === false ? 'noindex' : 'index', metadata.robots.follow === false ? 'nofollow' : 'follow', metadata.robots.nocache ? 'nocache' : undefined, ] .filter(Boolean) .join(', ') if (robots) { meta.push({ name: 'robots', content: robots }) } if (metadata.robots.googleBot) { const googleBot = [ metadata.robots.googleBot.index === false ? 'noindex' : 'index', metadata.robots.googleBot.follow === false ? 'nofollow' : 'follow', metadata.robots.googleBot.noimageindex ? 'noimageindex' : undefined, ] .filter(Boolean) .join(', ') if (googleBot) { meta.push({ name: 'googlebot', content: googleBot }) } } } return { meta, links, } }