UNPKG

@m-media/vue3-meta-tags

Version:

Add support for meta tags in your Vue3 Router

281 lines (244 loc) 7.93 kB
import type { RouteLocationNormalized, Router } from "vue-router"; import type { App } from "vue"; export const RTL_LOCALES = [ "ar", "arc", "dv", "fa", "ha", "he", "khw", "ks", "ku", "ps", "ur", "yi", ]; const preconnect = [] as string[]; let defaultName = "Web App"; let defaultLocale = "en_US"; let locales: string[] = []; let textCallback = null as ((text: string) => string) | null; // Taken from https://www.digitalocean.com/community/tutorials/vuejs-vue-router-modify-head export const setMetaAttributes = ( to: RouteLocationNormalized, from: RouteLocationNormalized ) => { if (typeof to.meta.description === "string") { setDescription(setText(to.meta.description)); } if (typeof to.meta.image === "string") { updateOrCreateMetaTag("og:image", to.meta.image, "meta", "property"); updateOrCreateMetaTag("twitter:image", to.meta.image); } updateOrCreateMetaTag("og:site_name", defaultName); if (typeof to.meta.locale === "string") { setLocale(to.meta.locale ?? defaultLocale); } else { setLocale(defaultLocale); } setFollow(true); setCurrentUrl(); updateOrCreateSchema(); setPreconnect(preconnect); setLocaleAlternate(locales); // This goes through the matched routes from last to first, finding the closest route with a title. // e.g., if we have `/some/deep/nested/route` and `/some`, `/deep`, and `/nested` have titles, // `/nested`'s will be chosen. const nearestWithTitle = to.matched .slice() .reverse() .find((r) => r.meta && r.meta.title); // Find the nearest route element with meta tags. const nearestWithMeta = to.matched .slice() .reverse() .find((r) => r.meta && r.meta.metaTags); const previousNearestWithMeta = from.matched .slice() .reverse() .find((r) => r.meta && r.meta.metaTags); // If a route with a title was found, set the document (page) title to that value. if (nearestWithTitle && typeof nearestWithTitle.meta.title === "string") { setTitle(setText(nearestWithTitle.meta.title)); } else if ( previousNearestWithMeta && typeof previousNearestWithMeta.meta.title === "string" ) { setTitle(setText(previousNearestWithMeta.meta.title)); } else if (typeof to.name === "string") { setTitle( setText(to.name) + " - " + defaultName ); } else { setTitle(defaultName); } // Remove any stale meta tags from the document using the key attribute we set below. Array.from(document.querySelectorAll("[data-vue-router-controlled]")).map( (el) => el.parentNode?.removeChild(el) ); // Skip rendering meta tags if there are none. if (!nearestWithMeta || !(nearestWithMeta.meta.metaTags instanceof Array)) return; // Turn the meta tag definitions into actual elements in the head. nearestWithMeta.meta.metaTags .map((tagDef: { [x: string]: string }) => { const tag = document.createElement("meta"); Object.keys(tagDef).forEach((key) => { tag.setAttribute(key, tagDef[key]); }); // We use this to track which meta tags we create so we don't interfere with other ones. tag.setAttribute("data-vue-router-controlled", ""); return tag; }) // Add the meta tags to the document head. .forEach((tag: HTMLMetaElement) => document.head.appendChild(tag)); }; export const setLocaleToUse = (locale: string) => { defaultLocale = locale; }; export const setTitle = (title: string) => { document.title = title; updateOrCreateMetaTag("og:title", title, "meta", "property"); updateOrCreateMetaTag("twitter:title", title); }; export const setFollow = (follow = true) => { updateOrCreateMetaTag("robots", follow ? "index,follow" : "noindex,nofollow"); updateOrCreateMetaTag( "googlebot", follow ? "index,follow" : "noindex,nofollow" ); }; export const setDescription = (description: string) => { updateOrCreateMetaTag("description", description); updateOrCreateMetaTag("og:description", description, "meta", "property"); updateOrCreateMetaTag("twitter:description", description); }; export const setCurrentUrl = (url = null as null | string) => { // If the URL is null, set it to the current URL. if (url === null) { url = window.location.href; } updateOrCreateMetaTag("og:url", url); updateOrCreateMetaTag("twitter:url", url); }; export const updateOrCreateMetaTag = ( tagName: string, content: string, type = "meta", tagNameSelector = "name", contentSelector = "content", additionalAttributes = [] as { name: string; value: string }[] ) => { const metaTag = document.querySelector( `${type}[${tagNameSelector}="${tagName}"][${contentSelector}]` ); if (metaTag) { metaTag.setAttribute(contentSelector, content); additionalAttributes.forEach((attribute) => { metaTag.setAttribute(attribute.name, attribute.value); }); } else { const newMetaTag = document.createElement(type); newMetaTag.setAttribute(tagNameSelector, tagName); newMetaTag.setAttribute(contentSelector, content); additionalAttributes.forEach((attribute) => { newMetaTag.setAttribute(attribute.name, attribute.value); }); document.head.appendChild(newMetaTag); } }; export const setDefaultSchema = () => { updateOrCreateSchema({ "@context": "https://schema.org", "@type": "SoftwareApplication", name: defaultName, applicationCategory: "BrowserApplication", offers: { "@type": "Offer", price: "0", }, }); }; export const updateOrCreateSchema = (json = null as null | Object) => { if (!json) { return setDefaultSchema(); } const schema = document.querySelector("script[type='application/ld+json']"); if (schema) { schema.innerHTML = JSON.stringify(json); } else { const newSchema = document.createElement("script"); newSchema.setAttribute("type", "application/ld+json"); newSchema.innerHTML = JSON.stringify(json); document.head.appendChild(newSchema); } }; export const setLocale = (locale: string) => { updateOrCreateMetaTag("og:locale", locale, "meta", "property"); document.documentElement.lang = locale; document.documentElement.dir = RTL_LOCALES.includes(locale) ? "rtl" : "ltr"; }; export const setLocaleAlternate = (locales: string[]) => { updateOrCreateMetaTag("x-default", "alternate", "link", "hreflang", "rel", [ { name: "href", value: window.location.href, }, ]); locales.forEach((locale) => { updateOrCreateMetaTag(locale, "alternate", "link", "hreflang", "rel", [ { name: "href", value: window.location.href, }, ]); }); }; export const setPreconnect = (urls: string[]) => { urls.forEach((url) => { const preconnectLink = document.querySelector( `link[rel="preconnect"][href="${url}"]` ); if (!preconnectLink) { const newPreconnect = document.createElement("link"); newPreconnect.setAttribute("rel", "preconnect"); newPreconnect.setAttribute("href", url); document.head.appendChild(newPreconnect); } }); }; export const setText = (text: string) => { if (!textCallback) { return text; } else { return textCallback(text); } }; export const setupMetaTagsHandler = (router: Router) => { router.afterEach((to, from, failure) => { if (!failure && !to.meta.skipMetaTagsHandler) { setMetaAttributes(to, from); } }); }; export const metaTagPlugin = { install: (app: App, options: any, router: Router) => { if (options.preconnect) { preconnect.push(...options.preconnect); } if (options.defaultLocale) { setLocaleToUse(options.defaultLocale); } if (options.textCallback) { textCallback = options.textCallback; } if (options.locales) { locales = options.locales; } if (options.defaultName) { defaultName = options.defaultName; } setupMetaTagsHandler(router); }, };