UNPKG

fastify-prerender-plugin

Version:
101 lines (97 loc) 3.37 kB
import Crypto from 'node:crypto'; import Fs from 'node:fs'; import Path from 'node:path'; import { fastifyPlugin } from 'fastify-plugin'; import { isbot } from 'isbot'; import sanitize from 'sanitize-filename'; import tmp from 'tmp'; import { fileURLToPath } from 'node:url'; import { isMainThread, workerData, parentPort, Worker } from 'node:worker_threads'; import { lightpanda } from '@lightpanda/browser'; async function requestFromBrowser(url) { return new Promise((resolve, reject) => { const worker = new Worker(fileURLToPath(import.meta.url), { workerData: { url } }); worker.on("message", resolve); worker.on("error", reject); }); } if (!isMainThread) { const options = { dump: true, disableHostVerification: false }; lightpanda.fetch(workerData.url, options).then((response) => { parentPort?.postMessage(response.toString()); }); } const prerenderPlugin = fastifyPlugin( (app, options, done) => { const tmpobj = tmp.dirSync(); app.addHook("onRequest", async (request, reply) => { const userAgent = request.headers["user-agent"] ?? ""; if (userAgent.startsWith("Lightpanda/")) { return; } const requestFromBot = isbot(userAgent) || userAgent.toLowerCase().startsWith("facebookexternalhit") || userAgent.toLowerCase().startsWith("whatsapp") || userAgent.toLowerCase().startsWith("twitterbot"); if (!requestFromBot || request.method !== "GET") { return; } request.log.info({ requestFromBot }); let matches = false; const requestPathname = request.url.split("?")[0]; for (const url2 of options.urls) { if (typeof url2 === "string") { if (url2 === requestPathname) { matches = true; break; } } else { if (url2.test(request.url)) { matches = true; break; } } } if (!matches) { return; } const url = `http://${options.host ?? "localhost"}:${options.port}${request.url}`; const urlObj = new URL(url); const pathname = sanitize(urlObj.pathname.replace(/\//g, "_")); const queryString = urlObj.search; let filename = pathname || "index"; if (queryString) { const queryHash = Crypto.createHash("md5").update(queryString).digest("hex"); filename = `${filename}_${queryHash}`; } filename = `${filename}.html`; const filepath = Path.join(options.tmpPath ?? tmpobj.name, filename); if (Fs.existsSync(filepath)) { const fileStat = Fs.statSync(filepath); const fileAgeInMinutes = (Date.now() - fileStat.mtime.getTime()) / 1e3 / 60; if (fileAgeInMinutes <= 5) { return reply.status(200).type("text/html").send(Fs.readFileSync(filepath)); } Fs.rmSync(filepath); } request.log.info(`request-from-browser: ${url}`); const html = await requestFromBrowser(url).catch((error) => { console.error(error); }) ?? ""; try { Fs.writeFileSync(filepath, html); } catch (_error) { request.log.error(`Couldn't write cache in ${filepath}`); } reply.status(200).type("text/html").send(html); }); done(); }, { fastify: "^5.x", name: "fastify-prerender" } ); export { prerenderPlugin, requestFromBrowser };