UNPKG

nuxt-resource-hints

Version:

Nuxt module that adds HTTP header resource hints for resources in your Nuxt App.

101 lines (100 loc) 3.9 kB
import { appendResponseHeader } from "h3"; import { defineNitroPlugin, getResponseHeaders, setResponseHeaders, useStorage } from "#imports"; export default defineNitroPlugin(async (nitroApp) => { nitroApp.hooks.hook("render:html", (html, { event }) => { const link = generateLinkHeader(html.head, { resources: { stylesheet: true, preload: true, module_preload: true, prefetch: false, images: false, fonts: false, scripts: true, dns_prefetch: true, preconnect: true }, headerLength: 1990 }); if (link !== "") { appendResponseHeader(event, "link", link); } }); if (import.meta.prerender) { const preRenderedHeaders = {}; nitroApp.hooks.hook("render:html", (_html, { event }) => { const headers = getResponseHeaders(event); const path = event.path.split("?")[0]; preRenderedHeaders[path] = headers; }); nitroApp.hooks.hook("close", async () => { const headers = Object.fromEntries( Object.entries(preRenderedHeaders).map(([path, headers2]) => { const headersEntries = Object.entries(headers2).filter(([header]) => header === "link").map(([header, value]) => { if (Array.isArray(value)) { return [header, value.join(";")]; } else { return [header, value]; } }); return [path, Object.fromEntries(headersEntries)]; }) ); await useStorage("build:nuxt-resource-hints").setItem( "headers.json", headers ); }); } else { const preRenderedHeaders = await useStorage("assets:nuxt-resource-hints").getItem("headers.json") || {}; nitroApp.hooks.hook("beforeResponse", (event) => { const path = event.path.split("?")[0]; if (preRenderedHeaders[path]) { setResponseHeaders(event, preRenderedHeaders[path]); } }); } }); function generateLinkHeader(head, options) { const link_regex = /<link\s([^>]+)>/g; let linkHeader = ""; for (let i = 0; i < head.length; i++) { let match; while ((match = link_regex.exec(head[i])) !== null) { const attributes = match[1]; const relMatch = attributes.match(/rel="([^"]+)"/); const hrefMatch = attributes.match(/href="([^"]+)"/); const asMatch = attributes.match(/as="([^"]+)"/); const crossoriginMatch = attributes.match(/crossorigin(?:="([^"]*)")?/); const fetchpriorityMatch = attributes.match(/fetchpriority="([^"]+)"/i); let blocking = false; if (relMatch && hrefMatch) { let rel = relMatch[1]; let as = asMatch ? asMatch[1] : null; const includeResource = options.resources.stylesheet && rel === "stylesheet" || options.resources.preload && rel === "preload" || options.resources.module_preload && rel === "modulepreload" || options.resources.prefetch && rel === "prefetch" || options.resources.images && as === "image" || options.resources.fonts && as === "font" || options.resources.scripts && as === "script" || options.resources.dns_prefetch && rel === "dns-prefetch" || options.resources.preconnect && rel === "preconnect"; if (includeResource) { if (rel === "stylesheet") { as = "style"; rel = "preload"; blocking = true; } const link = `<${hrefMatch[1]}>; rel="${rel}"${as ? `; as="${as}"` : ""}${crossoriginMatch ? "; crossorigin" : ""}${fetchpriorityMatch ? `; fetchpriority="${fetchpriorityMatch[1]}"` : ""}${blocking ? `; blocking` : ""}`; if (linkHeader.length + link.length + 2 >= options.headerLength) { return linkHeader; } if (linkHeader !== "") { linkHeader += ", " + link; } else { linkHeader += link; } } } } } return linkHeader; }