UNPKG

metapatcher

Version:

HTML document head management library with convenient api. Manage social media tags, icons, device specific tags and event structured data with html meta tags and JSONLD documents.

879 lines (726 loc) 31.9 kB
export class Metapatcher { features: MetapatcherFeatures[] = [] // all ids when setting meta tags will be prefixed with this to prevent collision with other tags idPrefix = 'metapatcher' idCounters = { preload: 0, prefetch: 0, preconnect: 0, dnsPrefetch: 0 } // it uses memory instead of dom when dom is not available isDomAvailable = typeof document !== 'undefined' memory: string[] = [] htmlVoidElements = ['base', 'link', 'meta'] mimeTypesByExtension = { 'svg': 'image/svg+xml', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'ico': 'image/x-icon', 'gif': 'image/gif', 'webp': 'image/webp', 'bmp': 'image/bmp' } reImageSizeFromStr = /[0-9]{2,3}x[0-9]{2,3}/g appleTouchIconSizes = ['120x120', '180x180', '152x152', '167x167', '1024x1024'] webAppManifestIconSizes = ['72x72', '96x96', '128x128', '144x144', '152x152', '192x192', '384x384', '512x512'] msTilesNamingMap = { '70x70': 'msapplication-square70x70logo', '150x150': 'msapplication-square150x150logo', '310x310': 'msapplication-square310x310logo', '310x150': 'msapplication-wide310x150logo' } // all features except browserconfig.xml are enabled constructor () { this.features = ['structuredData', 'msTags', 'appleTags', 'openGraphTags', 'twitterTags', 'webAppManifest'] // disable default requests made by browsers this.setMsApplicationConfig('none') } configure (features?: MetapatcherFeatures[], settings?: MetapatcherSettings) { if (settings?.idPrefix) this.idPrefix = settings.idPrefix if (features) this.features = features if (this.features.includes('appleTags')) { const id = this.idPrefix + '-apple-mobile-web-app-capable' this.set('meta', { id, name: 'apple-mobile-web-app-capable', content: 'yes' }) } if (this.features.includes('twitterTags')) { const idTw = this.idPrefix + '-twitter-card' this.set('meta', { id: idTw, name: 'twitter:card', content: 'summary' }) } } setIcons(list: string[]): this { const msTileIconSizes = Object.keys(this.msTilesNamingMap) for (const url of list) { const mimeType = this.findMimeType(url) const sizeMatches = url.match(this.reImageSizeFromStr) const size = sizeMatches && sizeMatches.length > 0 ? sizeMatches[0] : undefined if (!size || !mimeType) continue if (this.features.includes('webAppManifest') && this.webAppManifestIconSizes.includes(size)) { const idWam = this.idPrefix + '-icon-' + size + '-wam' this.removeOne('link', { id: idWam }) this.set('link', { id: idWam, rel: 'icon', href: url, sizes: size, type: mimeType }) } if (this.features.includes('appleTags') && this.appleTouchIconSizes.includes(size)) { const idApple = this.idPrefix + '-icon-' + size + '-apple' this.removeOne('link', { id: idApple }) this.set('link', { id: idApple, rel: 'apple-touch-icon', href: url, sizes: size }) } if (this.features.includes('msTags') && msTileIconSizes.includes(size)) { const idMs = this.idPrefix + '-icon-' + size + '-ms' this.removeOne('meta', { id: idMs }) this.set('meta', { id: idMs, name: this.msTilesNamingMap[size as keyof typeof this.msTilesNamingMap], content: url }) } } return this } setPageDetails (params: MetapatcherPageParams): this { if (params.title) this.setPageTitle(params.title) if (params.description) this.setPageDescription(params.description) if (params.path) this.setPageUrl(params.path) if (params.image) this.setPageImage(params.image) if (params.robots) this.setRobots(params.robots) if (params.locale) this.setPageLocale(params.locale) if (params.canonical) this.setCanonical(params.canonical) if (params.localVersions) this.setLocalVersions(params.localVersions, params.locale ?? '') if (params.breadcrumb) this.setBreadcrumb(params.breadcrumb) return this } setPageTitle (title: string): this { this.setDocumentTitle(title) if (this.features.includes('openGraphTags')) { const idOg = this.idPrefix + 'og-title' this.removeOne('meta', { id: idOg }) this.set('meta', { id: idOg, property: 'og:title', content: title }) } if (this.features.includes('twitterTags')) { const idTw = this.idPrefix + 'tw-title' this.removeOne('meta', { id: idTw }) this.set('meta', { id: idTw, name: 'twitter:title', content: title }) } return this } setPageDescription (description: string): this { const id = this.idPrefix + '-description' this.removeOne('meta', { id: id }) this.set('meta', { id, name: 'description', content: description }) if (this.features.includes('openGraphTags')) { const idOg = this.idPrefix + '-description-og' this.removeOne('meta', { id: idOg }) this.set('meta', { id: idOg, property: 'og:description', content: description }) } if (this.features.includes('twitterTags')) { const idTw = this.idPrefix + '-description-tw' this.removeOne('meta', { id: idTw }) this.set('meta', { id: idTw, name: 'twitter:description', content: description }) } return this } setPageUrl (url: string): this { if (this.features.includes('openGraphTags')) { const idOg = this.idPrefix + '-url-og' this.removeOne('meta', { id: idOg }) this.set('meta', { id: idOg, property: 'og:url', content: url }) } return this } setPageImage (param: string | MetapatcherPageImage): this { const img: MetapatcherPageImage = typeof param === 'string' ? { path: param } : param if (this.features.includes('openGraphTags')) { const idOg = this.idPrefix + '-image-og' this.removeOne('meta', { id: idOg }) this.set('meta', { id: idOg, property: 'og:image', content: img.path }) const idOgw = this.idPrefix + '-image-w-og' this.removeOne('meta', { id: idOgw }) if (img.width) { this.set('meta', { id: idOgw, property: 'og:image:width', content: img.width.toString() }) } const idOgh = this.idPrefix + '-image-h-og' this.removeOne('meta', { id: idOgh }) if (img.height) { this.set('meta', { id: idOgh, property: 'og:image:height', content: img.height.toString() }) } } if (this.features.includes('twitterTags')) { const idTw = this.idPrefix + '-image-tw' this.removeOne('meta', { id: idTw }) this.set('meta', { id: idTw, property: 'twitter:image', content: img.path }) } return this } setPageLocale (locale: string) { locale = locale.replace('_', '-') if (this.isDomAvailable) { document.documentElement.setAttribute('lang', locale) } if (this.features.includes('openGraphTags')) { const id = this.idPrefix + '-locale-og' this.removeOne('meta', { id: id }) this.set('meta', { id, property: 'og:locale', content: locale }) } } setProjectDetails (params: MetapatcherProjectParams): this { if (params.favicon) this.setFavicon(params.favicon) if (params.name) this.setProjectName(params.name) if (params.url) this.setProjectUrl(params.url) if (params.robots) this.setRobots(params.robots) if (params.themeColor) this.setThemeColor(params.themeColor) if (params.twitterSite) this.setTwitterSite(params.twitterSite) if (params.safariPinnedTab) this.setSafariPinnedTab(params.safariPinnedTab) if (params.icons) this.setIcons(params.icons) if (this.features.includes('structuredData')) { const id = this.idPrefix + '-project-org' const json: Record<string, string | Record<string, string>> = { '@context': 'https://schema.org', '@type': 'Organization' } if (params.logo) json['logo'] = params.logo if (params.url) json['url'] = params.url if (params.name) json['name'] = params.name if (params.email) json['email'] = params.email if (params.phone) json['telephone'] = params.phone if (params.description) json['description'] = params.description if (params.image) json['image'] = params.image if (params.legalName) json['legalName'] = params.legalName if (params.address) { json['address'] = { '@type': 'PostalAddress' } if (params.address.country) json['address']['addressCountry'] = params.address.country if (params.address.region) json['address']['addressRegion'] = params.address.region if (params.address.city) json['address']['addressLocality'] = params.address.city if (params.address.postalCode) json['address']['postalCode'] = params.address.postalCode if (params.address.street) json['address']['streetAddress'] = params.address.street } const _data = JSON.stringify(json) this.removeOne('script', { id: id }) if (this.isDomAvailable) this.setJsonLdDom(id, _data) else this.setJsonLdMemory(id, _data) } return this } setProjectName (name: string): this { const idMs = this.idPrefix + '-project-name' this.removeOne('meta', { id: idMs }) this.set('meta', { id: idMs, name: 'application-name', content: name }) if (this.features.includes('openGraphTags')) { const idOg = this.idPrefix + '-project-name-og' this.removeOne('meta', { id: idOg }) this.set('meta', { id: idOg, property: 'og:site_name', content: name }) } else if (this.features.includes('appleTags')) { const idApple = this.idPrefix + '-project-name-apple' this.removeOne('meta', { id: idApple }) this.set('meta', { id: idApple, name: 'apple-mobile-web-app-title', content: name }) } return this } setProjectUrl (url: string): this { if (this.features.includes('msTags')) { const id = this.idPrefix + '-project-url' this.removeOne('meta', { id: id }) this.set('meta', { id, name: 'msapplication-starturl', content: url }) } return this } setThemeColor (colorHexCode: string): this { const id = this.idPrefix + '-theme-color' this.removeOne('meta', { id: id }) this.set('meta', { id, name: 'theme-color', content: colorHexCode }) if (this.features.includes('msTags')) { const idMs = this.idPrefix + '-theme-color-ms' this.removeOne('meta', { id: idMs }) this.set('meta', { id: idMs, name: 'msapplication-TileColor', content: colorHexCode }) } return this } setTwitterSite (username: string): this { const id = this.idPrefix + '-twitter-site' this.removeOne('meta', { id: id }) this.set('meta', { id, name: 'twitter:site', content: username }) return this } addDnsPrefetch (param: string | MetapatcherDnsPrefetchAttrs): this { const id = this.idPrefix + '-dns-prefetch-' + this.idCounters.dnsPrefetch.toString() this.idCounters.dnsPrefetch += 1 const attrs: MetapatcherDnsPrefetchAttrs = typeof param === 'string' ? { id, rel: 'dns-prefetch', href: param } : Object.assign({}, param, { id, rel: 'dns-prefetch' }) this.removeOne('link', { rel: 'dns-prefetch', href: attrs.href }) this.set('link', attrs) return this } addPreconnect (param: string | MetapatcherPreconnectAttrs): this { const id = this.idPrefix + '-preconnect-' + this.idCounters.preconnect.toString() this.idCounters.preconnect += 1 const attrs: MetapatcherPreconnectAttrs = typeof param === 'string' ? { id, rel: 'preconnect', href: param } : Object.assign({}, param, { id, rel: 'preconnect' }) this.removeOne('link', { rel: 'preconnect', href: attrs.href }) this.set('link', attrs) return this } addPrefetch (param: string | MetapatcherPrefetchAttrs): this { const id = this.idPrefix + '-prefetch-' + this.idCounters.prefetch.toString() this.idCounters.prefetch += 1 const attrs: MetapatcherPrefetchAttrs = typeof param === 'string' ? { id, rel: 'prefetch', href: param } : Object.assign({}, param, { id, rel: 'prefetch' }) this.removeOne('link', { rel: 'prefetch', href: attrs.href }) this.set('link', attrs) return this } addPreload (param: string | MetapatcherPreloadAttrs): this { const id = this.idPrefix + '-preload-' + this.idCounters.preload.toString() this.idCounters.preload += 1 const attrs: MetapatcherPreloadAttrs = typeof param === 'string' ? { id, rel: 'preload', href: param } : Object.assign({}, param, { id, rel: 'preload' }) this.removeOne('link', { rel: 'preload', href: attrs.href }) this.set('link', attrs) return this } setRobots(param: string | MetapatcherRobotsAttrs): this { const id = this.idPrefix + '-robots' const attrs: MetapatcherRobotsAttrs = typeof param === 'string' ? { id, name: 'robots', content: param } : Object.assign({}, param, { id, name: 'robots' }) this.removeOne('meta', { id }) this.set('meta', attrs) return this } setDocumentTitle(title: string): this { if (!this.isDomAvailable) { const predicate = (line: string) => line.includes('<title>') && line.includes('</title>') if (this.memory.some(predicate)) { this.memory = this.memory.map((line) => predicate(line) ? `<title>${title}</title>` : line) } else this.memory.push(`<title>${title}</title>`) } else document.title = title return this } setFavicon(param: string | MetapatcherFaviconAttrs): this { const mime = this.findMimeType(typeof param === 'string' ? param : param.href) if (!mime) return this const id = this.idPrefix + '-favicon' const attrs: MetapatcherFaviconAttrs = typeof param === 'string' ? { id, rel: 'shortcut icon', href: param } : Object.assign({}, param, { id, rel: 'shortcut icon' }) this.removeOne('link', { id }) this.set('link', attrs) return this } setMsApplicationConfig (param: string | MetapatcherMsApplicationConfigAttrs) { const id = this.idPrefix + '-msapplication-config' const attrs: MetapatcherMsApplicationConfigAttrs = typeof param === 'string' ? { id, name: 'msapplication-config', content: param } : Object.assign({}, param, { id, name: 'msapplication-config' }) this.removeOne('meta', { id }) this.set('meta', attrs) return this } setSafariPinnedTab(attrs: MetapatcherSafariPinnedTabAttrs): this { const id = this.idPrefix + '-safari-pinned-tab' const _attrs: MetapatcherSafariPinnedTabAttrs = Object.assign({}, attrs, { id, rel: 'mask-icon' }) this.removeOne('link', { id }) this.set('link', _attrs) return this } setAppleStatusBarStyle (content: 'default' | 'black' | 'black-translucent' = 'default'): this { const id = this.idPrefix + '-apple-status-bar-style' this.removeOne('meta', { id }) this.set('meta', { id, name: 'apple-mobile-web-app-status-bar-style', content }) return this } setBreadcrumb (data: MetapatcherBreadcrumb[]): HTMLScriptElement | string { if (!this.features.includes('structuredData')) return '' const id = this.idPrefix + '-breadcrumb' const json = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', 'itemListElement': data.map(({title, url}, ind) => ({ '@type': 'ListItem', 'position': ind + 1, 'name': title, 'item': url })) } const _data = JSON.stringify(json) this.removeOne('script', { id }) return this.isDomAvailable ? this.setJsonLdDom(id, _data) : this.setJsonLdMemory(id, _data) } setJsonLdDom (id: string, data: string) { const elem = document.createElement('script') elem.id = id elem.type = 'application/ld+json' elem.text = data document.head.insertBefore(elem, null) return elem } setJsonLdMemory (id: string, data: string) { const _data = `<script id="${id}" type="application/ld+json">${data}</script>` this.memory.push(_data) return _data } setCanonical (param: string | MetapatcherCanonicalLinkAttrs): this { const id = this.idPrefix + '-canonical' const attrs: MetapatcherCanonicalLinkAttrs = typeof param === 'string' ? { id, rel: 'canonical', href: param } : Object.assign({}, param, { id, rel: 'canonical' }) this.removeOne('link', { id }) this.set('link', attrs) return this } setMeta (name: string, content: string): this { this.removeOne('meta', { name }) this.set('meta', { name, content }) return this } setMobileVariant (param: string | MetapatcherMobileVariantLinkAttrs): this { const id = this.idPrefix + '-mobile-variant' const attrs: MetapatcherMobileVariantLinkAttrs = typeof param === 'string' ? { id, rel: 'alternate', href: param, media: 'only screen and (max-width: 640px)' } : Object.assign({}, param, { id, rel: 'alternate' }) this.removeOne('link', { id }) this.set('link', attrs) return this } setLocalVersions (param: MetapatcherLocalVersionLinkAttrs[], currentLang = ''): this { this.removeMany('link', { rel: 'alternate', hreflang: true }) if (this.features.includes('openGraphTags')) { currentLang = currentLang.replace('_', '-') this.removeMany('meta', { property: 'og:locale:alternate' }) this.removeOne('meta', { property: 'og:locale' }) } for (const _attrs of param) { const id = this.idPrefix + '-local-version-' + _attrs.hreflang _attrs.hreflang = _attrs.hreflang.replace('_', '-') const attrs: MetapatcherLocalVersionLinkAttrs = Object.assign({}, _attrs, { id, rel: 'alternate' }) this.set('link', attrs) if (this.features.includes('openGraphTags')) { const _id = this.idPrefix + '-local-version-og-' + _attrs.hreflang const suffix = currentLang === _attrs.hreflang ? '' : ':alternate' const __attrs = { id: _id, property: 'og:locale' + suffix, content: _attrs.hreflang.replace('-', '_') } this.set('meta', __attrs) } } return this } setJsonLd (id: string, data: Record<string, never>): HTMLScriptElement | string { const str = JSON.stringify(data) this.removeOne('script', { id }) return this.isDomAvailable ? this.setJsonLdDom(id, str) : this.setJsonLdMemory(id, str) } removeOne (tagName: string, attrs: Record<string, string>): this { if (!this.isDomAvailable) { const queries = [`<${tagName}`].concat(Object.keys(attrs).map((key) => `${key}="${attrs[key]}"`)) this.memory = this.memory.filter((item) => !queries.every((query) => item.includes(query))) return this } const params = Object.keys(attrs).reduce((memo: string, key: string) => { memo += `[${key}="${attrs[key]}"]` return memo }, '') const query = `${tagName}${params}` const elem = document.head.querySelector(query) if (elem) { elem.parentNode!.removeChild(elem) } return this } removeMany (tagName: string, attrs: Record<string, string | boolean>): this { if (!this.isDomAvailable) { const queries = [`<${tagName}`].concat(Object.keys(attrs).map((key) => typeof attrs[key] === 'boolean' ? `${key}` : `${key}="${attrs[key]}"`)) this.memory = this.memory.filter((item) => !queries.every((query) => item.includes(query))) return this } const params = Object.keys(attrs).reduce((memo: string, key: string) => { memo += typeof attrs[key] === 'boolean' ? `[${key}]` : `[${key}="${attrs[key]}"]` return memo }, '') const query = `${tagName}${params}` const elems = document.head.querySelectorAll(query) if (elems && elems.length > 0) { elems.forEach((elem) => { elem.parentNode!.removeChild(elem) }) } return this } dump(): string { if (this.isDomAvailable) return '' return this.memory.join('\n') } flushMemory(): this { this.memory = [] return this } set (tagName: string, attrs: MetapatcherHtmlTagAttrs = {}, settings?: MetapatcherSetSettings): string | HTMLElement { tagName = tagName.toLowerCase() const isVoid = settings && Object.hasOwn(settings, 'void') ? settings.void! : this.htmlVoidElements.includes(tagName) return this.isDomAvailable ? this.setDom(tagName, attrs, { void: isVoid }) : this.setMemory(tagName, attrs, { void: isVoid }) } // eslint-disable-next-line @typescript-eslint/no-unused-vars setDom (tagName: string, attrs: MetapatcherHtmlTagAttrs = {}, _settings: MetapatcherSetSettings): HTMLElement { const elem = document.createElement(tagName) this.setElementAttrs(elem, attrs) document.head.insertBefore(elem, null) return elem } setMemory (tagName: string, attrs: MetapatcherHtmlTagAttrs = {}, settings: MetapatcherSetSettings): string { const closingTag = settings.void ? ` />` : `></${tagName}>` const html = `<${tagName}${this.serializeAttrs(attrs)}${closingTag}` this.memory.push(html) return html } setElementAttrs (elem: HTMLElement, attrs: MetapatcherHtmlTagAttrs = {}) { for (const name of Object.keys(attrs)) { const v = attrs[name]! if (typeof v === 'string') elem.setAttribute(name, v) else if (v === true) elem.setAttribute(name, '') } } async setScript (attrs: MetapatcherSetJsAttrs, settings: MetapatcherSetJsSettings = {}): Promise<HTMLScriptElement | string> { attrs = Object.assign({}, { type: 'text/javascript' }, attrs) settings = Object.assign({}, { location: 'headEnd', waitForLoad: '', timeout: 10000 }, settings) return new Promise((resolve, reject) => { if (!this.isDomAvailable) { return resolve(this.setMemory('script', attrs, { void: this.htmlVoidElements.includes('script') })) } const timeout = setTimeout(() => { reject(new Error('Timeout.')) }, settings.timeout) const elem = document.createElement('script') const { src, ...rest } = attrs this.setElementAttrs(elem, rest) function onDone () { clearTimeout(timeout) if (settings.waitForLoad!.length === 0) { return resolve(elem) } else { const interval = setInterval(() => { if (Object.hasOwn(window, settings.waitForLoad!)) { clearInterval(interval) return resolve(elem) } }, 100) } } // eslint-disable-next-line @typescript-eslint/no-unused-vars function onError (_event: Event) { clearTimeout(timeout) reject(new Error('Error loading resource')) } elem.addEventListener('load', onDone) elem.addEventListener('error', onError) elem.src = src if (settings.location === 'headEnd') document.head.insertBefore(elem, null) else if (settings.location === 'bodyEnd') document.body.insertBefore(elem, null) else if (settings.location === 'bodyStart') document.body.insertBefore(elem, document.body.firstChild) else document.body.insertBefore(elem, null) }) } async setStylesheet (attrs: MetapatcherSetStylesheetAttrs, settings: MetapatcherSetStylesheetSettings = {}): Promise<HTMLLinkElement | string> { attrs = Object.assign({}, { rel: 'stylesheet' }, attrs) settings = Object.assign({}, { location: 'headEnd', timeout: 10000 }, settings) return new Promise((resolve, reject) => { if (!this.isDomAvailable) { return resolve(this.setMemory('link', attrs, { void: this.htmlVoidElements.includes('link') })) } const timeout = setTimeout(() => { reject(new Error('Timeout.')) }, settings.timeout) const elem = document.createElement('link') const { href, ...rest } = attrs this.setElementAttrs(elem, Object.assign({}, rest, { media: 'only x' })) function onDone () { elem.media = attrs.media ?? 'all' clearTimeout(timeout) return resolve(elem) } // eslint-disable-next-line @typescript-eslint/no-unused-vars function onError (_event: Event) { clearTimeout(timeout) return reject(new Error('Error loading resource')) } elem.addEventListener('load', onDone) elem.addEventListener('error', onError) elem.href = href if (settings.location === 'headEnd') document.head.insertBefore(elem, null) else if (settings.location === 'bodyEnd') document.body.insertBefore(elem, null) else if (settings.location === 'bodyStart') document.body.insertBefore(elem, document.body.firstChild) else document.body.insertBefore(elem, null) }) } serializeAttrs (attrs: MetapatcherHtmlTagAttrs = {}): string { return Object.keys(attrs).reduce((memo, name) => { memo += ` ${name}="${attrs[name]}"` return memo }, '') } findMimeType(path: string): string | undefined { const lastind = path.lastIndexOf('.') if (lastind < 1) return undefined const ext = path.slice(lastind + 1) if (!ext) return undefined return Object.hasOwn(this.mimeTypesByExtension, ext) ? this.mimeTypesByExtension[ext as keyof typeof this.mimeTypesByExtension] : undefined } } export type MetapatcherFeatures = 'structuredData' | 'webAppManifest' | 'msTags' | 'appleTags' | 'openGraphTags' | 'twitterTags' export interface MetapatcherSettings { idPrefix?: string } export type MetapatcherHtmlTagAttrs = Record<string, string | boolean> export interface MetapatcherSetStylesheetAttrs { id: string readonly rel?: 'stylesheet' href: string media?: string [index: string]: string | boolean } export interface MetapatcherSetStylesheetSettings { location?: 'headEnd' | 'bodyEnd' | 'bodyStart' timeout?: number } export interface MetapatcherSetJsAttrs { id: string type?: string src: string async?: boolean [index: string]: string | boolean } export interface MetapatcherSetJsSettings { location?: 'headEnd' | 'bodyEnd' | 'bodyStart' waitForLoad?: string timeout?: number } export interface MetapatcherSetSettings { void?: boolean } export interface MetapatcherCanonicalLinkAttrs { readonly rel?: 'canonical' id?: string href: string [index: string]: string | boolean } export interface MetapatcherMobileVariantLinkAttrs { readonly rel?: 'alternate' id?: string media: string href: string [index: string]: string | boolean } export interface MetapatcherLocalVersionLinkAttrs { readonly rel?: 'alternate' id?: string hreflang: string href: string [index: string]: string | boolean } export interface MetapatcherBreadcrumb { title: string url: string } export interface MetapatcherSafariPinnedTabAttrs { readonly rel?: 'mask-icon' id?: string href: string color: string [index: string]: string | boolean } export interface MetapatcherFaviconAttrs { readonly rel?: 'shortcut icon' id?: string href: string [index: string]: string | boolean } export interface MetapatcherRobotsAttrs { readonly name?: 'robots' id?: string content: string [index: string]: string | boolean } export interface MetapatcherPreloadAttrs { readonly rel?: 'preload' id?: string href: string as?: MetapatcherPreloadAs type?: string media?: string crossorigin?: boolean [index: string]: string | boolean } export type MetapatcherPreloadAs = 'audio' | 'document' | 'embed' | 'fetch' | 'font' | 'image' | 'object' | 'script' | 'style' | 'track' | 'worker' | 'video' export interface MetapatcherPrefetchAttrs { readonly rel?: 'prefetch' id?: string href: string [index: string]: string | boolean } export interface MetapatcherPreconnectAttrs { readonly rel?: 'preconnect' id?: string href: string [index: string]: string | boolean } export interface MetapatcherDnsPrefetchAttrs { readonly rel?: 'dns-prefetch' id?: string href: string [index: string]: string | boolean } export interface MetapatcherProjectParams { favicon?: string | MetapatcherFaviconAttrs name?: string url?: string robots?: string | MetapatcherRobotsAttrs logo?: string themeColor?: string primaryColor?: string backgroundColor?: string twitterSite?: string safariPinnedTab?: MetapatcherSafariPinnedTabAttrs icons?: string[] email?: string phone?: string description?: string image?: string legalName?: string address?: { country: string region?: string city?: string postalCode?: string street?: string } } export interface MetapatcherMsApplicationConfigAttrs { readonly name?: 'msapplication-config' id?: string content: string [index: string]: string | boolean } export interface MetapatcherPageParams { title?: string description?: string path?: string image?: string robots?: string | MetapatcherRobotsAttrs locale?: string canonical?: string mobileVariant?: string localVersions?: MetapatcherLocalVersionLinkAttrs[], breadcrumb?: MetapatcherBreadcrumb[] } export interface MetapatcherPageImage { path: string width?: string | number height?: string | number } export const metapatcher = new Metapatcher()