Note
I already published an npm package: vike-metadata for react, solid, and vue.
Feel free to check those out instead!
Hi, this is a WIP hook I'm making for Vike to manage meta tags with similar DX as Next.js's Metadata API e.g. export const meta = {}.
I might make an npm package out of this, but for now, feel free to copy this and save it as a plain .ts file in your project.
Goals:
- Achieve typesafe, repeatable, and pleasant DX in managing meta tags.
- Seamless SSR and Client-side <title> and behaviors.
- Seamless SSR for all meta tags.
- Seamless Client-side updates of meta tags (not sure if necessary since most meta tags are only for SEO, but right now it works for title, description, and keywords. I might add a way to activate it though and disable it by default)
- Solid.js
- React (I think it works in React, just replace
useConfigimport) - Vue
- "Universal Hook" - In Vike, universal hooks basically work on the component jsx, on
+data.tsand everywhere else.useConfigis built on top ofuseConfighence it's a universal hook. - 1:1 feature parity with Next.js's metadata API but with improvements. (not there yet but covers 80% of it so far)
- Icons
- ...And others
Assuming you've copied the ts somewhere in like use-meta.tsx
- Configure defaults
// ===========================================================================
// Config
// ===========================================================================
/** Edit the default values here. Treat this as your ROOT meta config. */
const DEFAULT_CONFIG = {
title: 'Home | Solid Launch',
description: 'An awesome app template by Carlo Taleon.',
keywords: ["solid.js", "carlo", "react"]
} as UseMetaParams;- Use in your pages
export function Page() {
useMeta({
title: getTitle('About'),
description: 'Learn about why this template is cool.'
})
}// useMeta hook
// Written by Carlo Taleon (github.com/Blankeos)
import type { JSX } from 'solid-js';
import { useConfig } from 'vike-solid/useConfig';
// ===========================================================================
// Config
// ===========================================================================
/** Edit the default values here. Treat this as your ROOT meta config. */
const DEFAULT_CONFIG = {
title: 'Home | Solid Launch',
description: 'An awesome app template by Carlo Taleon.',
} as UseMetaParams;
// ===========================================================================
// Hook
// ===========================================================================
type UseMetaParams = {
/**
* If you're looking to do templates like `%s | Solid Launch` like Next.JS,
* I'd argue the better way is to just create a utility function like:
*
* ```
* const TITLE_TEMPLATE = '%s | Solid Launch';
*
* export function getTitle(title: string) {
* return TITLE_TEMPLATE.replace('%s', title);
* }
* ```
*
* @example
* 'Home'
* <title>Home</title>
*
* getTitle('Home')
* <title>Home | Solid Launch</title>
*/
title?: string;
/**
* @example
* <meta name="description" content="An awesome app template by Carlo Taleon." />
*/
description?: string;
// ===========================================================================
// Other types I copied from Next.js
// ===========================================================================
// >>> BASIC FIELDS
/**
* @example
* <meta name="generator" content="Next.js" />
*/
generator?: string;
/**
* @example
* <meta name="application-name" content="Solid Launch" />
*/
applicationName?: string;
/**
* @example
* <meta name="referrer" content="origin-when-cross-origin" />
*/
referrer?:
| 'no-referrer'
| 'origin'
| 'no-referrer-when-downgrade'
| 'origin-when-cross-origin'
| 'same-origin'
| 'strict-origin'
| 'strict-origin-when-cross-origin';
/**
* @example
* <meta name="keywords" content="Next.js,React,JavaScript" />
*/
keywords?: string | string[];
/**
* Has multiple behaviors.
*
* @example
* "Josh"
* <meta name="author" content="Josh" />
*
* { name: "Josh", url: "https://josh.com" }
* <link rel="author" href="https://josh.com" />
* <meta name="author" content="Josh" />
*/
authors?: Author | Author[];
/**
* @example
* <meta name="creator" content="Jiachi Liu" />
*/
creator?: string;
/**
* @example
* <meta name="publisher" content="Sebastian Markbåge" />
*/
publisher?: string;
/**
* @example
* <meta name="format-detection" content="telephone=no, address=no, email=no" />
*/
formatDetection?: {
address?: boolean;
date?: boolean;
email?: boolean;
telephone?: boolean;
url?: boolean;
};
// >>> OPEN GRAPH
openGraph?: OpenGraph;
/**
* The robots setting for the document.
*
* @example
* "index, follow"
* <meta name="robots" content="index, follow" />
*
* { index: false, follow: false }
* <meta name="robots" content="noindex, nofollow" />
*/
robots?:
| string
| (RobotsInfo & {
googleBot?: string | RobotsInfo;
});
// >>> Icons - has complicated internals, so we'll see if I implement this. I think you can use `manifest.json` anyway.
// icons?: {
// /** <link rel="shortcut icon" href="/shortcut-icon.png" /> */
// icon?: string;
// /** <link rel="icon" href="/icon.png" /> */
// shortcut?: string;
// /** <link rel="apple-touch-icon" href="/apple-icon.png" /> */
// apple?: string;
// /**
// * <link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
// */
// other?: {
// rel?: string;
// url?: string;
// };
// };
// >>> Manifest
/**
* Manifest.json path
* <link rel="manifest" href="https://nextjs.org/manifest.json" />
*/
manifest?: string | URL;
// >>> Twitter
twitter?: Twitter;
/** ⭐️ */
viewport?: ViewPort;
/**
*
* @example
* { verification: { google: "1234567890", yandex: "1234567890", "me": "1234567890" } }
* <meta name="google-site-verification" content="1234567890" />
* <meta name="yandex-verification" content="1234567890" />
* <meta name="me" content="@me" />
*/
verification?: { [key: string]: string };
/**
* ⭐️
*
* All metadata options should be covered using the built-in support. However,
* there may be custom metadata tags specific to your site, or brand new metadata
* tags just released. You can use the other option to render any custom metadata tag.
*
* @example
* // { "xxx": "yyy"}
* // Generates <meta name="xxx" content="yyy" />
*
* // { "xxx": [123, 456] }
* // Generates
* <meta name="xxx" content="123" />
* <meta name="xxx" content="456" />
*
*/
other?: { [key: string]: string | number | (string | number)[] };
/**
* ⭐️
*
* If you prefer not using the type-safe data format and just want to copy-paste
* whatever meta you currently have. You can also just paste it here.
*
* otherJSX: () => (
* <>
* <meta ... />
* <meta ... />
* <meta ... />
* </>
* );
*/
otherJSX?: () => JSX.Element;
};
/**
* 🎖️ This is currently, in my opinion, the BEST way to handle <head> in Vike.
* Currently, Vike has multiple ways to handle the Head leading to mix-ups for beginners
* eg. <Head />, useConfig(), +title, +description.
* as well as some gotchas like <Head /> can't change title on clientside, +title can. +description can't.
*
* This hook gives a familiar API with more expected behaviors as a developer (in my opinion).
* No gotchas, it just works. In SSR, In the browser, or if you need dynamic values.
* Until Vike gets better, please just use this.
*
* What you have to do:
* - Edit this component for default values.
* - This hook must only be used once for every page.
* - Has a very close API as NextJS's `export meta = {}`.
*/
export function useMeta(params: UseMetaParams) {
const setConfig = useConfig();
const values = {
// Sensible Title & Description Defaults for HTML, OG, and Twitter.
title: params.title ?? DEFAULT_CONFIG.title,
description: params.description ?? DEFAULT_CONFIG.description,
// Other values here:
generator: params.generator ?? DEFAULT_CONFIG.generator,
applicationName: params.applicationName ?? DEFAULT_CONFIG.applicationName,
referrer: params.referrer ?? DEFAULT_CONFIG.referrer,
keywords: params.keywords ?? DEFAULT_CONFIG.keywords,
authors: params.authors ?? DEFAULT_CONFIG.authors,
creator: params.creator ?? DEFAULT_CONFIG.creator,
publisher: params.publisher ?? DEFAULT_CONFIG.publisher,
formatDetection: params.formatDetection ?? DEFAULT_CONFIG.formatDetection,
// Open Graph
openGraph: {
title: params.openGraph?.title ?? params.title ?? DEFAULT_CONFIG.title,
description:
params.openGraph?.description ?? params.description ?? DEFAULT_CONFIG.description,
url: params.openGraph?.url ?? DEFAULT_CONFIG?.openGraph?.url,
siteName: params.openGraph?.siteName ?? DEFAULT_CONFIG?.openGraph?.siteName,
images: params.openGraph?.images ?? DEFAULT_CONFIG?.openGraph?.images,
videos: params.openGraph?.videos ?? DEFAULT_CONFIG?.openGraph?.videos,
audio: params.openGraph?.audio ?? DEFAULT_CONFIG?.openGraph?.audio,
locale: params.openGraph?.locale ?? DEFAULT_CONFIG?.openGraph?.locale,
type: ((params.openGraph as any)?.type ?? (DEFAULT_CONFIG?.openGraph as any)?.type) as
| string
| undefined,
},
// Robots
robots: params.robots ?? DEFAULT_CONFIG.robots,
// Manifest
manifest: params.manifest ?? DEFAULT_CONFIG.manifest,
// Twitter
twitter: {
// Metadata
creator: params.twitter?.creator ?? DEFAULT_CONFIG?.twitter?.creator,
creatorId: params.twitter?.creatorId ?? DEFAULT_CONFIG?.twitter?.creatorId,
description: params.twitter?.description ?? params.description ?? DEFAULT_CONFIG.description,
images: params.twitter?.images ?? DEFAULT_CONFIG?.twitter?.images,
site: params.twitter?.site ?? DEFAULT_CONFIG?.twitter?.site,
siteId: params.twitter?.siteId ?? DEFAULT_CONFIG?.twitter?.siteId,
title: params.twitter?.title ?? params.title ?? DEFAULT_CONFIG.title,
// Card
card: ((params.twitter as any)?.card ?? (DEFAULT_CONFIG?.twitter as any)?.card) as
| string
| undefined,
// Player
players:
(params.twitter as Extract<Twitter, { card: 'player' }>)?.players ??
(DEFAULT_CONFIG?.twitter as any)?.players,
// App
app:
(params?.twitter as Extract<Twitter, { card: 'app' }>)?.app ??
(DEFAULT_CONFIG?.twitter as any)?.app,
},
// ViewPort
viewport: params.viewport ?? DEFAULT_CONFIG.viewport,
// Verification
verification: params.verification ?? DEFAULT_CONFIG.verification,
// Other
other: params.other ?? DEFAULT_CONFIG.other,
// Other JSX
otherJSX: params.otherJSX ?? DEFAULT_CONFIG.otherJSX,
};
function Head_() {
return (
<>
{renderMetaName('generator', values.generator)}
{renderMetaName('application-name', values.applicationName)}
{renderMetaName('referrer', values.referrer)}
{renderMetaName(
'keywords',
values?.keywords?.length ? returnOrJoin(values.keywords) : null
)}
{renderArrayable(values?.authors, (item) => (
<>
{renderMetaName('author', item.name)}
{item.url ? <link rel="author" href={item.url?.toString()} /> : null}
</>
))}
{renderMetaName('creator', values?.creator)}
{renderMetaName('publisher', values?.publisher)}
{renderMetaName(
'format-detection',
values?.formatDetection ? parseFormatDetection(values.formatDetection) : null
)}
{renderOpenGraphMetadata(values?.openGraph)}
{renderMetaName('robots', values?.robots ? parseRobotsInfo(values.robots) : null)}
{renderMetaName(
'googlebot',
(values?.robots as any)?.googleBot
? parseRobotsInfo((values?.robots as any)?.googleBot)
: null
)}
{values?.manifest ? <link rel="manifest" href={values.manifest?.toString()} /> : null}
{renderTwitterMetadata(values.twitter)}
{renderViewPortMetadata(values.viewport)}
{renderMetaNameMap(values?.verification)}
{renderMetaNameMap(values?.other)}
{values.otherJSX}
</>
);
}
setConfig({
title: values.title,
description: values.description,
/** @ts-ignore */
Head: Head_,
});
// Special Workarounds
// > Server-side
if (typeof window === 'undefined') {
}
// > Client-side
else {
if (values.openGraph.title) {
createIfNotExistsMetaProperty('og:title', values.openGraph.title);
}
if (values.description) {
createIfNotExistsMetaName('description', values.description);
}
if (values.openGraph.description) {
createIfNotExistsMetaProperty('og:description', values.openGraph.description);
}
if (values.twitter.title) {
createIfNotExistsMetaName('twitter:title', values.twitter.title);
}
if (values.twitter.description) {
createIfNotExistsMetaName('twitter:description', values.twitter.description);
}
if (values.keywords?.length) {
createIfNotExistsMetaName('keywords', returnOrJoin(values.keywords));
}
}
}
// ===========================================================================
// Utilities
// ===========================================================================
// >>> Utilities for Server-Side (JS-framework specific)
function renderMetaName(name: string, value: any) {
return value ? <meta name={name} content={value} /> : null;
}
function renderMetaProperty(property: string, value: any) {
return value ? <meta property={property} content={value} /> : null;
}
function renderArrayable<T>(
value: T,
render: (item: _RemoveArray<T>, index: number) => JSX.Element
) {
if (!value) return null;
if (Array.isArray(value)) {
return value.map(render);
}
return render(value as any, 0);
}
function renderOpenGraphMetadata(value: UseMetaParams['openGraph']) {
if (!value) return null;
function _renderOGArticle(value: Extract<UseMetaParams['openGraph'], { type: 'article' }>) {
if (value.type !== 'article') return null;
return (
<>
{renderArrayable(value.authors, (item) => renderMetaProperty('article:author', item))}
{renderMetaProperty('article:expiration_time', value.expirationTime)}
{renderMetaProperty('article:modified_time', value.modifiedTime)}
{renderMetaProperty('article:published_time', value.publishedTime)}
{renderMetaProperty('article:section', value.section)}
{renderArrayable(value.tags, (item) => renderMetaProperty('article:tag', item))}
</>
);
}
function _renderOGBook(value: Extract<UseMetaParams['openGraph'], { type: 'book' }>) {
if (value.type !== 'book') return null;
return (
<>
{renderArrayable(value.authors, (item) => renderMetaProperty('article:author', item))}
{renderMetaProperty('book:isbn', value.isbn)}
{renderMetaProperty('book:release_date', value.releaseDate)}
{renderArrayable(value.tags, (item) => renderMetaProperty('article:tag', item))}
</>
);
}
function _renderOGProfile(value: Extract<UseMetaParams['openGraph'], { type: 'profile' }>) {
if (value.type !== 'profile') return null;
return (
<>
{renderMetaProperty('profile:first_name', value.firstName)}
{renderMetaProperty('profile:last_name', value.lastName)}
{renderMetaProperty('profile:username', value.username)}
{renderMetaProperty('profile:gender', value.gender)}
</>
);
}
function _renderOGMusicSong(value: Extract<UseMetaParams['openGraph'], { type: 'music.song' }>) {
if (value.type !== 'music.song') return null;
return (
<>
{renderArrayable(value.albums, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return renderMetaProperty('music:album', item);
}
return (
<>
{renderMetaProperty('music:album:disc', item?.disc)}
{renderMetaProperty('music:album:track', item?.track)}
{renderMetaProperty('music:album', item?.url)}
</>
);
})}
{renderMetaProperty('music:duration', value.duration)}
{renderArrayable(value.musicians, (item) => renderMetaProperty('music:musician', item))}
</>
);
}
function _renderOGMusicAlbum(
value: Extract<UseMetaParams['openGraph'], { type: 'music.album' }>
) {
if (value.type !== 'music.album') return null;
return (
<>
{renderArrayable(value.musicians, (item) => renderMetaProperty('music:musician', item))}
{renderMetaProperty('music:release_date', value.releaseDate)}
{renderArrayable(value.songs, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return renderMetaProperty('music:song', item);
}
return (
<>
{renderMetaProperty('music:song:disc', item?.disc)}
{renderMetaProperty('music:song:track', item?.track)}
{renderMetaProperty('music:song', item?.url)}
</>
);
})}
</>
);
}
function _renderOGMusicPlaylist(
value: Extract<UseMetaParams['openGraph'], { type: 'music.playlist' }>
) {
if (value.type !== 'music.playlist') return null;
return (
<>
{renderArrayable(value.creators, (item) => renderMetaProperty('music:creator', item))}
{renderArrayable(value.songs, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return renderMetaProperty('music:song', item);
}
return (
<>
{renderMetaProperty('music:song:disc', item?.disc)}
{renderMetaProperty('music:song:track', item?.track)}
{renderMetaProperty('music:song', item?.url)}
</>
);
})}
</>
);
}
function _renderOGMusicRadioStation(
value: Extract<UseMetaParams['openGraph'], { type: 'music.radio_station' }>
) {
if (value.type !== 'music.radio_station') return null;
return (
<>{renderArrayable(value.creators, (item) => renderMetaProperty('music:creator', item))}</>
);
}
function _renderOGVideoMovie(
value: Extract<UseMetaParams['openGraph'], { type: 'video.movie' }>
) {
if (value.type !== 'video.movie') return null;
return (
<>
{renderArrayable(value.actors, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return renderMetaProperty('video:actor', item);
}
return (
<>
{renderMetaProperty('video:actor:role', item?.role)}
{renderMetaProperty('video:actor', item?.url)}
</>
);
})}
{renderArrayable(value.directors, (item) => renderMetaProperty('video:director', item))}
{renderMetaProperty('video:duration', value.duration)}
{renderMetaProperty('video:release_date', value.releaseDate)}
{renderArrayable(value.tags, (item) => renderMetaProperty('video.tag', item))}
{renderArrayable(value.writers, (item) => renderMetaProperty('video:writer', item))}
</>
);
}
function _renderOGVideoEpisode(
value: Extract<UseMetaParams['openGraph'], { type: 'video.episode' }>
) {
if (value.type !== 'video.episode') return null;
return (
<>
{renderArrayable(value.actors, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return renderMetaProperty('video:actor', item);
}
return (
<>
{renderMetaProperty('video:actor:role', item?.role)}
{renderMetaProperty('video:actor', item?.url)}
</>
);
})}
{renderArrayable(value.directors, (item) => renderMetaProperty('video:director', item))}
{renderMetaProperty('video:duration', value.duration)}
{renderMetaProperty('video:release_date', value.releaseDate)}
{renderMetaProperty('video:series', value.series)}
{renderArrayable(value.tags, (item) => renderMetaProperty('video.tag', item))}
{renderArrayable(value.writers, (item) => renderMetaProperty('video:writer', item))}
</>
);
}
return (
<>
{renderArrayable(value?.alternateLocale, (item) =>
renderMetaProperty('og:locale:alternate', item)
)}
{renderArrayable(value?.audio, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return <meta property="og:audio" content={item?.toString()} />;
}
return (
<>
{renderMetaProperty('og:audio', item?.url)}
{renderMetaProperty('og:audio:type', item?.type)}
</>
);
})}
{renderMetaProperty('og:country-name', value?.countryName)}
{renderMetaProperty('og:determiner', value?.determiner)}
{renderArrayable(value?.emails, (item) => renderMetaProperty('og:email', item))}
{renderArrayable(value?.faxNumbers, (item) => renderMetaProperty('og:fax_number', item))}
{renderArrayable(value?.images, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return <meta property="og:image" content={item?.toString()} />;
}
return (
<>
{renderMetaProperty('og:image', item?.url)}
{renderMetaProperty('og:image:secure_url', item?.secureUrl)}
{renderMetaProperty('og:image:width', item?.width)}
{renderMetaProperty('og:image:height', item?.height)}
{renderMetaProperty('og:image:alt', item?.alt)}
</>
);
})}
{renderMetaProperty('og:locale', value?.locale)}
{renderArrayable(value?.phoneNumbers, (item) => renderMetaProperty('og:phone_number', item))}
{renderMetaProperty('og:site_name', value?.siteName)}
{renderMetaProperty('og:ttl', value?.ttl)}
{renderMetaProperty('og:url', value?.url)}
{renderArrayable(value?.videos, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return <meta property="og:video" content={item?.toString()} />;
}
return (
<>
{renderMetaProperty('og:video', item?.url)}
{renderMetaProperty('og:video:secure_url', item?.secureUrl)}
{renderMetaProperty('og:video:type', item?.type)}
{renderMetaProperty('og:video:width', item?.width)}
{renderMetaProperty('og:video:height', item?.height)}
</>
);
})}
{renderMetaProperty('og:type', (value as any)?.type)}
{_renderOGArticle(value as any)}
{_renderOGBook(value as any)}
{_renderOGProfile(value as any)}
{_renderOGMusicSong(value as any)}
{_renderOGMusicAlbum(value as any)}
{_renderOGMusicPlaylist(value as any)}
{_renderOGMusicRadioStation(value as any)}
{_renderOGVideoMovie(value as any)}
{_renderOGVideoEpisode(value as any)}
</>
);
}
function renderTwitterMetadata(value: UseMetaParams['twitter']) {
if (!value) return null;
function renderTwitterAppMetadata(value: Extract<Twitter, { card: 'app' }>['app']) {
if (!value) return null;
return (
<>
{renderMetaName('twitter:app:id:googleplay', value?.id?.googleplay)}
{renderMetaName('twitter:app:url:googleplay', value?.url?.googleplay)}
{renderMetaName('twitter:app:id:iphone', value?.id?.iphone)}
{renderMetaName('twitter:app:url:iphone', value?.url?.iphone)}
{renderMetaName('twitter:app:id:ipad', value?.id?.ipad)}
{renderMetaName('twitter:app:url:ipad', value?.url?.ipad)}
{renderMetaName(
'twitter:app:name:googleplay',
value?.id?.googleplay ? value?.name : undefined
)}
{renderMetaName('twitter:app:name:iphone', value?.id?.iphone ? value?.name : undefined)}
{renderMetaName('twitter:app:name:ipad', value?.id?.ipad ? value?.name : undefined)}
</>
);
}
return (
<>
{renderMetaName('twitter:creator', value.creator)}
{renderMetaName('twitter:creator:id', value.creatorId)}
{renderMetaName('twitter:description', value.description)}
{renderMetaName('twitter:site', value.site)}
{renderMetaName('twitter:site:id', value.siteId)}
{renderMetaName('twitter:title', value.title)}
{renderMetaName('twitter:card', (value as any).card)}
{renderArrayable(value?.images, (item) => {
if (typeof item === 'string' || item instanceof URL) {
return <meta name="twitter:image" content={item?.toString()} />;
}
return (
<>
<meta name="twitter:image" content={item.url?.toString()} />;
{renderMetaName('twitter:image:alt', item.alt)}
</>
);
})}
{renderArrayable((value as Extract<Twitter, { card: 'player' }>)?.players, (item) => (
<>
{renderMetaName('twitter:player', item.playerUrl)}
{renderMetaName('twitter:player:width', item.width)}
{renderMetaName('twitter:player:height', item.height)}
{renderMetaName('twitter:player:stream', item.streamUrl)}
</>
))}
{renderTwitterAppMetadata((value as any)?.app)}
</>
);
}
function renderViewPortMetadata(value: UseMetaParams['viewport']) {
if (!value) return null;
return (
<>
{renderMetaName('color-scheme', value.colorScheme)}
{renderArrayable(value.themeColor, (item) => {
if (typeof item === 'string') {
return <meta name="theme-color" content={item} />;
}
if (item.media) {
return <meta name="theme-color" media={item.media} content={item.color} />;
}
return <meta name="theme-color" content={item.color} />;
})}
{renderMetaName('viewport', parseViewport(value))}
</>
);
}
function renderMetaNameMap(records?: { [key: string]: string | number | (string | number)[] }) {
if (!records) return null;
return Object.entries(records).map(([_key, _value]) => {
if (Array.isArray(_value)) {
return renderArrayable(_value, (item) => renderMetaName(_key, item));
}
return renderMetaName(_key, _value);
});
}
// >>> Utilities for Client-Side (Vanilla friendly)
function createIfNotExistsMetaName(name: string, value: any) {
let metaElement = document.querySelector(`meta[name="${name}"]`);
if (!metaElement) {
metaElement = document.createElement('meta');
metaElement.setAttribute('name', name);
document.head.appendChild(metaElement);
}
metaElement.setAttribute('content', value);
}
function createIfNotExistsMetaProperty(property: string, value: any) {
let metaElement = document.querySelector(`meta[property="${property}"]`);
if (!metaElement) {
metaElement = document.createElement('meta');
metaElement.setAttribute('property', property);
document.head.appendChild(metaElement);
}
metaElement.setAttribute('content', value);
}
/** Used by keywords. */
function returnOrJoin(value: string | string[]) {
if (typeof value === 'string') {
return value;
}
return value.join(',');
}
function parseFormatDetection(value: UseMetaParams['formatDetection']) {
if (!value) return undefined;
let content = '';
if (typeof value?.email !== 'undefined') {
if (value.email) content += 'email=yes,';
else content += 'email=no,';
}
if (typeof value?.telephone !== 'undefined') {
if (value.telephone) content += 'telephone=yes,';
else content += 'telephone=no,';
}
if (typeof value?.address !== 'undefined') {
if (value.address) content += 'address=yes,';
else content += 'address=no,';
}
if (typeof value?.url !== 'undefined') {
if (value.url) content += 'url=yes,';
else content += 'url=no,';
}
if (typeof value?.date !== 'undefined') {
if (value.date) content += 'date=yes';
else content += 'date=no';
}
return content;
}
function parseRobotsInfo(value: UseMetaParams['robots'] | string) {
if (!value) return null;
if (typeof value === 'string') return value;
let content = [];
if (value?.follow !== undefined || value?.nofollow !== undefined) {
if (value.follow === true || value.nofollow === false) content.push('follow');
else content.push('nofollow');
}
if (value?.index !== undefined || value?.noindex !== undefined) {
if (value.index === true || value.noindex === false) content.push('index');
else content.push('noindex');
}
if (value?.indexifembedded === true) {
content.push('indexifembedded');
}
if (value?.['max-image-preview'] !== undefined) {
content.push(`max-image-preview:${value['max-image-preview']}`);
}
if (value?.['max-snippet'] !== undefined) {
content.push(`max-snippet:${value['max-snippet']}`);
}
if (value?.['max-video-preview'] !== undefined) {
content.push(`max-video-preview:${value['max-video-preview']}`);
}
if (value?.noarchive === true) {
content.push('noarchive');
}
if (value?.nocache === true) {
content.push('nocache');
}
if (value?.noimageindex === true) {
content.push('noimageindex');
}
if (value?.nositelinkssearchbox === true) {
content.push('nositelinkssearchbox');
}
if (value?.nosnippet === true) {
content.push('nosnippet');
}
if (value?.notranslate === true) {
content.push('notranslate');
}
if (value?.unavailable_after !== undefined) {
content.push(`unavailable_after: ${value.unavailable_after}`);
}
return content.join(', ');
}
function parseViewport(value: UseMetaParams['viewport']) {
if (!value) return null;
let content = [];
if (value.width !== undefined) {
content.push(`width=${value.width}`);
}
if (value.height !== undefined) {
content.push(`height=${value.height}`);
}
if (value.initialScale !== undefined) {
content.push(`initial-scale=${value.initialScale}`);
}
if (value.interactiveWidget !== undefined) {
content.push(`interactive-widget=${value.interactiveWidget}`);
}
if (value.maximumScale !== undefined) {
content.push(`maximum-scale=${value.maximumScale}`);
}
if (value.minimumScale !== undefined) {
content.push(`minimum-scale=${value.minimumScale}`);
}
if (value.userScalable !== undefined) {
if (value.userScalable === true) content.push('user-scalable=yes');
content.push('user-scalable=no');
}
if (value.viewportFit !== undefined) {
content.push(`viewport-fit=${value.viewportFit}`);
}
return content.join(', ');
}
// ===========================================================================
// Internals
// ===========================================================================
type _RemoveArray<T> = T extends any[] | undefined ? never : T;
type TwitterImageDescriptor = {
alt?: string;
/** Twitter deprecated this in 2015. */
height?: string | number;
/** Unused, not sure why NextJS has this. */
secureUrl?: string | URL;
/** Unused, not sure why NextJS has this. */
type?: string;
url: string | URL;
/** Twitter deprecated this in 2015. */
width?: string | number;
};
type TwitterImage = string | TwitterImageDescriptor | URL;
type TwitterMetadata = {
/** <meta name="twitter:creator" content="@nextjs" /> */
creator?: string;
/** <meta name="twitter:creator:id" content="1467726470533754880" /> */
creatorId?: string;
/**
* When not specified, uses `description` by default. (Vike doesn't do this)
* <meta name="twitter:description" content="The React Framework for the Web" />
*/
description?: string;
/**
* Must be absolute URLs.
*
* <meta name="twitter:image" content="https://nextjs.org/og.png" />
*/
images?: TwitterImage | TwitterImage[];
/** <meta name="twitter:site" content="" /> */
site?: string;
/** <meta name="twitter:site:id" content="1467726470533754880" /> */
siteId?: string;
/**
* When not specified, uses `title` by default (Vike doesn't do this).
* <meta name="twitter:title" content="Next.js" />
*/
title?: string;
};
type TwitterPlayerDescriptor = {
height: number;
playerUrl: string | URL;
streamUrl: string | URL;
width: number;
};
type TwitterAppDescriptor = {
id: {
googleplay?: string;
ipad?: string | number;
iphone?: string | number;
};
name?: string;
url?: {
googleplay?: string | URL;
ipad?: string | URL;
iphone?: string | URL;
};
};
type Twitter =
| (TwitterMetadata & {
/** <meta name="twitter:card" content="summary" /> */
card: 'summary';
})
| (TwitterMetadata & {
/** <meta name="twitter:card" content="summary_large_image" /> */
card: 'summary_large_image';
})
| (TwitterMetadata & {
/** <meta name="twitter:card" content="player" /> */
card: 'player';
players: TwitterPlayerDescriptor | TwitterPlayerDescriptor[];
})
| (TwitterMetadata & {
app: TwitterAppDescriptor;
/** <meta name="twitter:card" content="app" /> */
card: 'app';
})
| TwitterMetadata;
type Author = {
name?: string;
url?: string | URL;
};
type OGAudioDescriptor = {
secureUrl?: string | URL;
type?: string;
url: string | URL;
};
type OGAudio = string | OGAudioDescriptor | URL;
type OGImageDescriptor = {
alt?: string;
height?: string | number;
secureUrl?: string | URL;
type?: string;
url: string | URL;
width?: string | number;
};
type OGImage = string | OGImageDescriptor | URL;
type OGVideoDescriptor = {
height?: string | number;
secureUrl?: string | URL;
type?: string;
url: string | URL;
width?: string | number;
};
type OGVideo = string | OGVideoDescriptor | URL;
type OpenGraphMetadata = {
alternateLocale?: string | string[];
audio?: OGAudio | OGAudio[];
countryName?: string;
/**
* When not specified, uses `description` by default. (Vike does this by default)
* <meta property="og:description" content="The React Framework for the Web" />
*/
description?: string;
determiner?: 'a' | 'an' | 'the' | 'auto' | '';
emails?: string | string[];
faxNumbers?: string | string[];
/**
* <meta property="og:image" content="https://nextjs.org/og.png" />
* <meta property="og:image:width" content="800" />
* <meta property="og:image:height" content="600" />
*/
images?: OGImage | OGImage[];
/**
* <meta property="og:locale" content="en_US" />
*/
locale?: string;
phoneNumbers?: string | string[];
siteName?: string;
/**
* When not specified, uses `title` by default. (Vike does this by default).
* <meta property="og:title" content="Next.js" />
*/
title?: string;
ttl?: number;
/**
* <meta property="og:url" content="https://nextjs.org/" />
*/
url?: string | URL;
/**
* <meta property="og:video" content="https://nextjs.org/video.mp4" />
* <meta property="og:video:width" content="800" />
* <meta property="og:video:height" content="600" />
*/
videos?: OGVideo | OGVideo[];
};
type OGAlbumOrSong = {
disc?: number;
track?: number;
url: string | URL;
};
type OGActor = {
role?: string;
url: string | URL;
};
type OpenGraph =
| (OpenGraphMetadata & {
/**
* <meta property="og:type" content="website" />
*/
type: 'website';
})
| (OpenGraphMetadata & {
authors?: null | string | URL | (string | URL)[];
expirationTime?: string;
modifiedTime?: string;
publishedTime?: string;
section?: null | string;
tags?: null | string | string[];
type: 'article';
})
| (OpenGraphMetadata & {
authors?: null | string | URL | (string | URL)[];
isbn?: null | string;
releaseDate?: null | string;
tags?: null | string | string[];
type: 'book';
})
| (OpenGraphMetadata & {
firstName?: null | string;
gender?: null | string;
lastName?: null | string;
type: 'profile';
username?: null | string;
})
| (OpenGraphMetadata & {
albums?: null | string | URL | OGAlbumOrSong | (string | URL | OGAlbumOrSong)[];
duration?: null | number;
musicians?: null | string | URL | (string | URL)[];
type: 'music.song';
})
| (OpenGraphMetadata & {
musicians?: null | string | URL | (string | URL)[];
releaseDate?: null | string;
songs?: null | string | URL | OGAlbumOrSong | (string | URL | OGAlbumOrSong)[];
type: 'music.album';
})
| (OpenGraphMetadata & {
creators?: null | string | URL | (string | URL)[];
songs?: null | string | URL | OGAlbumOrSong | (string | URL | OGAlbumOrSong)[];
type: 'music.playlist';
})
| (OpenGraphMetadata & {
creators?: null | string | URL | (string | URL)[];
type: 'music.radio_station';
})
| (OpenGraphMetadata & {
actors?: null | string | URL | OGActor | (string | URL | OGActor)[];
directors?: null | string | URL | (string | URL)[];
duration?: null | number;
releaseDate?: null | string;
tags?: null | string | string[];
type: 'video.movie';
writers?: null | string | URL | (string | URL)[];
})
| (OpenGraphMetadata & {
actors?: null | string | URL | OGActor | (string | URL | OGActor)[];
directors?: null | string | URL | (string | URL)[];
duration?: null | number;
releaseDate?: null | string;
series?: null | string | URL;
tags?: null | string | string[];
type: 'video.episode';
writers?: null | string | URL | (string | URL)[];
})
| (OpenGraphMetadata & {
type: 'video.tv_show';
})
| (OpenGraphMetadata & {
type: 'video.other';
})
| OpenGraphMetadata;
type ThemeColorDescriptor = {
color: string;
media?: string;
};
type RobotsInfo = {
follow?: boolean;
index?: boolean;
indexifembedded?: boolean;
'max-image-preview'?: 'none' | 'standard' | 'large';
'max-snippet'?: number;
'max-video-preview'?: number | string;
noarchive?: boolean;
nocache?: boolean;
nofollow?: never;
noimageindex?: boolean;
noindex?: never;
nositelinkssearchbox?: boolean;
nosnippet?: boolean;
notranslate?: boolean;
unavailable_after?: string;
};
/**
* ⭐️
*/
type ViewPort = {
/**
* The color scheme for the document.
*
* @example
* "dark"
* <meta name="color-scheme" content="dark" />
*/
colorScheme?: 'normal' | 'light' | 'dark' | 'light dark' | 'dark light' | 'only light';
height?: string | number;
initialScale?: number;
interactiveWidget?: 'resizes-visual' | 'resizes-content' | 'overlays-content';
maximumScale?: number;
minimumScale?: number;
/**
* The theme color for the document.
* @example
* "#000000"
* <meta name="theme-color" content="#000000" />
* { media: "(prefers-color-scheme: dark)", color: "#000000" }
* <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000" />
*
* [
* { media: "(prefers-color-scheme: dark)", color: "#000000" },
* { media: "(prefers-color-scheme: light)", color: "#ffffff" }
* ]
* <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000" />
* <meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
*/
themeColor?: string | ThemeColorDescriptor | ThemeColorDescriptor[];
userScalable?: boolean;
viewportFit?: 'auto' | 'cover' | 'contain';
width?: string | number;
};