fastify-prerender-plugin
Version:
Prerender SPA pages for bots
105 lines (100 loc) • 3.85 kB
JavaScript
;
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;