interface SetPageMetaProps { /** Sets title, meta:og:title. Is postfixed by ` - {siteName}` **/ title: string /** Sets meta:og:site_name **/ siteName?: string /** Sets meta:description **/ description?: string /** Sets meta:og:image **/ image?: string /** Sets meta:og:locale **/ locale?: string } interface SetPageMetaReturn { /** The current page title, meta:og:title. Is postfixed by ` - {siteName}` **/ title: string /** The current meta:og:site_name **/ siteName: string /** The current meta:description **/ description: string /** The current meta:og:image **/ image: string /** The current meta:og:locale **/ locale: string } /** * Allows setting common page attrs. * * - Intelligently use the attrs, only setting if changed * - Resets back to initial if omitted, based on initial introspection * - Stores element handles in memory to remove need to query the dom * on every update * * > Note: Set `setPageMeta.testMode=true` to disable setPageMeta for testing * * @param * object: an object of key/val pairs of meta attrs to set * * @returns * object: an object of key/val pairs of meta attrs that are currently set * * @dependency * * The page should already have default meta tags. Example: * * ```html * React Template * * * * * * * * * ``` * * @example * * ```typescript * const {description} = setPageMeta({ * title: `Hello World`, * description: 'This page is awesome', * }) * ``` */ export function setPageMeta(p: SetPageMetaProps): SetPageMetaReturn { const { getLink, siteNameE, ogTitleMc, localeMc, descriptionMc, ogDescriptionMc, ogUrlMc, ogSiteNameMc, ogImageMc, } = getHeadHandles() const title = p.title ? `${p.title} - ${siteNameE}` : siteNameE if (title !== document.title) document.title = title const link = getLink() if (link.href !== location.href) link.href = location.href const locale = localeMc.upsert(p.locale) const description = descriptionMc.upsert(p.description) const siteName = ogSiteNameMc.upsert(p.siteName) const image = ogImageMc.upsert(p.image) ogTitleMc.upsert(p.title) ogDescriptionMc.upsert(p.description) ogUrlMc.upsert(location.href) return { description, image, locale, siteName, title: p.title || title, } } setPageMeta.testMode = !("document" in globalThis) /** Wrapper class on meta elements to simplify usage and make more DRY **/ class MetaClass { get: () => string orig: string last?: string set: (val: string) => string constructor(getter: () => Element) { this.get = () => setPageMeta.testMode ? "" : this.last || getter().getAttribute("content") || throwError(`No content for ${getter}`) this.set = (v: string) => { if (setPageMeta.testMode) return v getter().setAttribute("content", v) return (this.last = v) } this.orig = this.last = this.get() } upsert(val?: string): string { if (setPageMeta.testMode) return val || "" if (!val) return (val = this.orig) if (this.last !== val) return this.set(val) return this.last } } const byName = (name: string) => { return find(`meta[name="${name}"]`) } const byProp = (prop: string) => { return find(`meta[property="${prop}"]`) } const find = (selector: string): HTMLMetaElement => { if (setPageMeta.testMode) { return { getAttribute: () => "", setAttribute: (v: any) => v, } as unknown as HTMLMetaElement } return document.head.querySelector(selector) || throwError(`Missing: ${selector}`) } interface HeadHandles { getLink: () => HTMLLinkElement siteNameE: string ogTitleMc: InstanceType localeMc: InstanceType descriptionMc: InstanceType ogDescriptionMc: InstanceType ogUrlMc: InstanceType ogSiteNameMc: InstanceType ogImageMc: InstanceType } /** Helper handles with cache */ function getHeadHandles(): HeadHandles { if (getHeadHandles.last) return getHeadHandles.last return (getHeadHandles.last = { getLink: () => find('link[rel="canonical"]') as unknown as HTMLLinkElement, siteNameE: byProp("og:site_name").getAttribute("content") || "", ogTitleMc: new MetaClass(() => byProp("og:title")), localeMc: new MetaClass(() => byProp("og:locale")), descriptionMc: new MetaClass(() => byName("description")), ogDescriptionMc: new MetaClass(() => byProp("og:description")), ogUrlMc: new MetaClass(() => byProp("og:url")), ogSiteNameMc: new MetaClass(() => byProp("og:site_name")), ogImageMc: new MetaClass(() => byProp("og:image")), }) } getHeadHandles.last = null as unknown as HeadHandles /** Convenience function to throw an error */ function throwError(msg: string): never { throw new Error(msg) }