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
JavaScript
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;
}