const width = 768; const cacheImageDir = path.resolve(".clover/question_images"); // Cached browser session const getBrowser = RefCountedExpirable( () => puppeteer.launch({ // headless: false, args: ["--no-sandbox", "--disable-setuid-sandbox"], }), (b) => b.close(), ); export async function renderQuestionImage(question: Question) { const html = await renderViewToString("q+a/embed-image", { question }); // this browser session will be reused if multiple images are generated // either at the same time or within a 5-minute time span. the dispose // symbol using sharedBrowser = await getBrowser(); const b = sharedBrowser.value; const p = await b.newPage(); await p.setViewport({ width, height: 400 }); await p.setContent(html); try { await p.waitForNetworkIdle({ idleTime: 100, timeout: 500 }); } catch (e) {} const height = await p.evaluate(() => { const e = document.querySelector("main")!; return e.getBoundingClientRect().height; }); const buf = await p.screenshot({ path: "screenshot.png", type: "png", captureBeyondViewport: true, clip: { x: 0, width, y: 0, height: height, scale: 1.5 }, }); await p.close(); return Buffer.from(buf); } export async function getQuestionImage( question: Question, headOnly: boolean, ): Promise { const hash = crypto.createHash("sha1") .update(question.qmid + question.type + question.text) .digest("hex"); const headers = { "Content-Type": "image/png", "Cache-Control": "public, max-age=31536000", "ETag": `"${hash}"`, "Last-Modified": question.date.toUTCString(), }; if (headOnly) { return new Response(null, { headers }); } const cachedFilePath = path.join(cacheImageDir, `/${hash}.png`); let buf: Buffer; try { buf = await fs.readFile(cachedFilePath); } catch (e: any) { if (e.code !== "ENOENT") throw e; buf = await renderQuestionImage(question); fs.writeMkdir(cachedFilePath, buf).catch(() => {}); } return new Response(buf, { headers }); } import * as crypto from "node:crypto"; import * as fs from "#sitegen/fs"; import * as path from "node:path"; import * as puppeteer from "puppeteer"; import { Question } from "@/q+a/models/Question.ts"; import { RefCountedExpirable } from "#sitegen/async"; import { renderViewToString } from "#sitegen/view";