authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-12-23 14:18:14-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-14 01:08:15-08:00
loga465df835ca978dd2cd1d1ac7c75daf5a49f350a
treefcb38f08a2ef1391d952915b05607d39dc2bfffd
parent898b567540e271981e6be16f337063ba7c9df934
signature Commit is signed but in an unrecognized format.

fix(q+a): issues gernerating question embed images

this fixes multiple issues: - the font files were not avaialble - the user-agent detection was wrong - the opengraph object format changed these all happened with the move to sitegen and not testing things. i never noticed any of these bugs since i don't use discord much these days, but i really do like having this feature. now, puppeteer is set to open against localhost, which loads the exact page experience as normal.

3 files changed, 16 insertions(+), 6 deletions(-)

src/q+a/backend.ts+3
...@@ -127,6 +127,9 @@ app.get("/q+a/:id", async (c, next) => {...@@ -127,6 +127,9 @@ app.get("/q+a/:id", async (c, next) => {
127 if (image) {127 if (image) {
128 return getQuestionImage(question, c.req.method === "HEAD");128 return getQuestionImage(question, c.req.method === "HEAD");
129 }129 }
130 if (c.req.header("User-Agent")?.includes("clover-qa-image")) {
131 return view.serve(c, "q+a/image-embed", { question });
132 }
130 return view.serve(c, "q+a/permalink", { question });133 return view.serve(c, "q+a/permalink", { question });
131});134});
132135
src/q+a/image.tsx+4-3
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1const port = Number(process.env.PORT ?? 3000);
1const width = 768;2const width = 768;
2const cacheImageDir = path.resolve(".clover/question_images");3const cacheImageDir = path.resolve(".clover/question_images");
34
...@@ -11,8 +12,6 @@ const getBrowser = RefCountedExpirable(...@@ -11,8 +12,6 @@ const getBrowser = RefCountedExpirable(
11);12);
1213
13export async function renderQuestionImage(question: Question) {14export async function renderQuestionImage(question: Question) {
14 const html = await view.renderToString("q+a/image-embed", { question });
15
16 // this browser session will be reused if multiple images are generated15 // this browser session will be reused if multiple images are generated
17 // either at the same time or within a 5-minute time span. the dispose16 // either at the same time or within a 5-minute time span. the dispose
18 // symbol17 // symbol
...@@ -20,12 +19,14 @@ export async function renderQuestionImage(question: Question) {...@@ -20,12 +19,14 @@ export async function renderQuestionImage(question: Question) {
20 const b = sharedBrowser.value;19 const b = sharedBrowser.value;
2120
22 const p = await b.newPage();21 const p = await b.newPage();
22 await p.setUserAgent({ userAgent: "clover-qa-image" });
23 await p.setViewport({ width, height: 400 });23 await p.setViewport({ width, height: 400 });
24 await p.setContent(html);24 await p.goto(`http://localhost:${port}/q+a/${question.id}`);
25 try {25 try {
26 await p.waitForNetworkIdle({ idleTime: 100, timeout: 500 });26 await p.waitForNetworkIdle({ idleTime: 100, timeout: 500 });
27 } catch (e) {}27 } catch (e) {}
28 const height = await p.evaluate(() => {28 const height = await p.evaluate(() => {
29 document.body.style.background = "#1a1b1c";
29 const e = document.querySelector("main")!;30 const e = document.querySelector("main")!;
30 return e.getBoundingClientRect().height;31 return e.getBoundingClientRect().height;
31 });32 });
src/q+a/views/permalink.marko+9-3
...@@ -4,14 +4,16 @@ export interface Input {...@@ -4,14 +4,16 @@ export interface Input {
4}4}
55
6server export function meta({ context, question }) {6server export function meta({ context, question }) {
7 const isDiscord = context.get("user-agent")7 console.log(context.req.header("user-agent"));
8 const isDiscord = context.req.header("user-agent")
8 ?.toLowerCase()9 ?.toLowerCase()
9 .includes("discordbot");10 .includes("discord");
10 if (question.type === QuestionType.normal) {11 if (question.type === QuestionType.normal) {
11 return {12 return {
12 title: "question permalink",13 title: "question permalink",
13 openGraph: {14 openGraph: {
14 images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],15 title: isDiscord ? undefined : `question at ${formatQuestionTimestamp(question.date)}`,
16 image: `https://paperclover.net/q+a/${question.id}.png`
15 },17 },
16 twitter: { card: "summary_large_image" },18 twitter: { card: "summary_large_image" },
17 themeColor: isDiscord19 themeColor: isDiscord
...@@ -46,4 +48,8 @@ server export function meta({ context, question }) {...@@ -46,4 +48,8 @@ server export function meta({ context, question }) {
46 <pre>${JSON.stringify(question, null, 2)}</pre>48 <pre>${JSON.stringify(question, null, 2)}</pre>
47</>49</>
4850
51<a href="/q+a">view more questions</a>
52
49import { Question, QuestionType } from '@/q+a/models/Question.ts';53import { Question, QuestionType } from '@/q+a/models/Question.ts';
54import { transitionDate } from "@/q+a/tags/question.marko";
55import { formatQuestionTimestamp } from "@/q+a/format.ts";