UNPKG

fastify-prerender-plugin

Version:
105 lines (100 loc) 3.85 kB
'use strict'; var Crypto = require('node:crypto'); var Fs = require('node:fs'); var Path = require('node:path'); var fastifyPlugin = require('fastify-plugin'); var isbot = require('isbot'); var sanitize = require('sanitize-filename'); var tmp = require('tmp'); var node_url = require('node:url'); var node_worker_threads = require('node:worker_threads'); var browser = require('@lightpanda/browser'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; async function requestFromBrowser(url) { return new Promise((resolve, reject) => { const worker = new node_worker_threads.Worker(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))), { workerData: { url } }); worker.on("message", resolve); worker.on("error", reject); }); } if (!node_worker_threads.isMainThread) { const options = { dump: true, disableHostVerification: false }; browser.lightpanda.fetch(node_worker_threads.workerData.url, options).then((response) => { node_worker_threads.parentPort?.postMessage(response.toString()); }); } const prerenderPlugin = fastifyPlugin.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.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" } ); exports.prerenderPlugin = prerenderPlugin; exports.requestFromBrowser = requestFromBrowser;