finish q+a

This commit is contained in:
chloe caruso 2025-06-15 13:11:21 -07:00
parent db244583d7
commit 7f5011bace
20 changed files with 259 additions and 68 deletions

View file

@ -23,7 +23,8 @@ export async function bundleClientJavaScript(
}
const clientPlugins: esbuild.Plugin[] = [
// There are currently no plugins needed by 'paperclover.net'
projectRelativeResolution(),
markoViaBuildCache(incr),
];
const bundle = await esbuild.build({
@ -42,6 +43,9 @@ export async function bundleClientJavaScript(
jsx: "automatic",
jsxImportSource: "#ssr",
jsxDev: dev,
define: {
"ASSERT": "console.assert",
},
});
if (bundle.errors.length || bundle.warnings.length) {
throw new AggregateError(
@ -50,11 +54,15 @@ export async function bundleClientJavaScript(
);
}
const publicScriptRoutes = extraPublicScripts.map((file) =>
path.basename(file).replace(/\.client\.[tj]sx?/, "")
"/js/" +
path.relative(hot.projectSrc, file).replaceAll("\\", "/").replace(
/\.client\.[tj]sx?/,
".js",
)
);
const { metafile } = bundle;
const { metafile, outputFiles } = bundle;
const promises: Promise<void>[] = [];
for (const file of bundle.outputFiles) {
for (const file of outputFiles) {
const { text } = file;
let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");
const { inputs } = UNWRAP(metafile.outputs["out!" + route]);
@ -63,8 +71,8 @@ export async function bundleClientJavaScript(
// Register non-chunks as script entries.
const chunk = route.startsWith("/js/c.");
if (!chunk) {
const key = hot.getScriptId(path.resolve(sources[0]));
route = "/js/" + key + ".js";
const key = hot.getScriptId(path.resolve(sources[sources.length - 1]));
route = "/js/" + key.replace(/\.client\.tsx?/, ".js");
incr.put({
sources,
kind: "script",
@ -123,27 +131,7 @@ export async function bundleServerJavaScript(
"$views": viewSource,
}),
projectRelativeResolution(),
{
name: "marko via build cache",
setup(b) {
b.onLoad(
{ filter: /\.marko$/ },
async ({ path: file }) => {
const key = path.relative(hot.projectRoot, file)
.replaceAll("\\", "/");
const cacheEntry = incr.out.serverMarko.get(key);
if (!cacheEntry) {
throw new Error("Marko file not in cache: " + file);
}
return ({
loader: "ts",
contents: cacheEntry.src,
resolveDir: path.dirname(file),
});
},
);
},
},
markoViaBuildCache(incr),
{
name: "replace client references",
setup(b) {
@ -168,6 +156,9 @@ export async function bundleServerJavaScript(
},
},
];
const pkg = await fs.readJson("package.json") as {
dependencies: Record<string, string>;
};
const { metafile, outputFiles } = await esbuild.build({
bundle: true,
chunkNames: "c.[hash]",
@ -186,6 +177,8 @@ export async function bundleServerJavaScript(
jsx: "automatic",
jsxImportSource: "#ssr",
jsxDev: false,
external: Object.keys(pkg.dependencies)
.filter((x) => !x.startsWith("@paperclover")),
});
const files: Record<string, Buffer> = {};
@ -279,6 +272,29 @@ export async function finalizeServerJavaScript(
});
}
function markoViaBuildCache(incr: Incremental): esbuild.Plugin {
return {
name: "marko via build cache",
setup(b) {
b.onLoad(
{ filter: /\.marko$/ },
async ({ path: file }) => {
const key = path.relative(hot.projectRoot, file)
.replaceAll("\\", "/");
const cacheEntry = incr.out.serverMarko.get(key);
if (!cacheEntry) {
throw new Error("Marko file not in cache: " + file);
}
return ({
loader: "ts",
contents: cacheEntry.src,
resolveDir: path.dirname(file),
});
},
);
},
};
}
import * as esbuild from "esbuild";
import * as path from "node:path";
import process from "node:process";

View file

@ -52,7 +52,6 @@ export const dynamicTag = (
if (typeof tag === "function") {
clover: {
const unwrapped = (tag as any).unwrapped;
console.log({ tag, unwrapped });
if (unwrapped) {
tag = unwrapped;
break clover;
@ -130,7 +129,7 @@ export function escapeXML(input: unknown) {
) {
throw new Error(
`Unexpected object in template placeholder: '` +
util.inspect({ name: "clover" }) + "'. " +
engine.inspect({ name: "clover" }) + "'. " +
`To emit a literal '[object Object]', use \${String(value)}`,
);
}
@ -145,4 +144,3 @@ import * as engine from "./ssr.ts";
import type { ServerRenderer } from "marko/html/template";
import { type Accessor } from "marko/common/types";
import * as marko from "#marko/html";
import * as util from "node:util";

View file

@ -8,9 +8,9 @@ export function main() {
successText,
failureText: () => "sitegen FAIL",
}, async (spinner) => {
const incr = Incremental.fromDisk();
await incr.statAllFiles();
// const incr = new Incremental();
// const incr = Incremental.fromDisk();
// await incr.statAllFiles();
const incr = new Incremental();
const result = await sitegen(spinner, incr);
incr.toDisk(); // Allows picking up this state again
return result;
@ -122,7 +122,6 @@ export async function sitegen(
}
}
}
scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
const globalCssPath = join("global.css");
// TODO: make sure that `static` and `pages` does not overlap
@ -311,8 +310,12 @@ export async function sitegen(
maxJobs: 2,
});
viewQueue.addMany(neededViews);
await pageQueue.done({ method: "stop" });
await viewQueue.done({ method: "stop" });
const pageAndViews = [
pageQueue.done({ method: "stop" }),
viewQueue.done({ method: "stop" }),
];
await Promise.allSettled(pageAndViews);
await Promise.all(pageAndViews);
status.format = spinnerFormat;
// -- bundle server javascript (backend and views) --

View file

@ -63,7 +63,7 @@ Module.prototype._compile = function (
if (shouldTrackPath(filename)) {
const cssImportsMaybe: string[] = [];
const imports: string[] = [];
for (const { filename: file } of this.children) {
for (const { filename: file, cloverClientRefs } of this.children) {
if (file.endsWith(".css")) cssImportsMaybe.push(file);
else {
const child = fileStats.get(file);
@ -71,6 +71,10 @@ Module.prototype._compile = function (
const { cssImportsRecursive } = child;
if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive);
imports.push(file);
if (cloverClientRefs && cloverClientRefs.length > 0) {
(this.cloverClientRefs ??= [])
.push(...cloverClientRefs);
}
}
}
fileStats.set(filename, {

View file

@ -148,7 +148,7 @@ export class Incremental {
for (const key of map.keys()) {
if (!this.round.referenced.has(`${kind}\0${key}`)) {
unreferenced.push({ kind: kind as ArtifactKind, key });
this.out[kind as ArtifactKind].delete(key);
// this.out[kind as ArtifactKind].delete(key);
}
}
}

View file

@ -11,7 +11,6 @@ export async function reload() {
fs.readFile(path.join(import.meta.dirname, "static.json"), "utf8"),
fs.readFile(path.join(import.meta.dirname, "static.blob")),
]);
console.log("new buffer loaded");
assets = {
map: JSON.parse(map),
buf,
@ -110,7 +109,6 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
}
process.on("message", (msg: any) => {
console.log({ msg });
if (msg?.type === "clover.assets.reload") reload();
});

View file

@ -58,8 +58,8 @@ export async function main() {
successText: generate.successText,
failureText: () => "sitegen FAIL",
}, async (spinner) => {
console.log("---");
console.log(
console.info("---");
console.info(
"Updated" +
(changed.length === 1
? " " + changed[0]

152
package-lock.json generated
View file

@ -8,6 +8,8 @@
"@hono/node-server": "^1.14.3",
"@mdx-js/mdx": "^3.1.0",
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
"codemirror": "^6.0.1",
"devalue": "^5.1.1",
"esbuild": "^0.25.5",
"hls.js": "^1.6.5",
"hono": "^4.7.11",
@ -404,6 +406,87 @@
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz",
"integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.37.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz",
"integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@ -864,6 +947,30 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@luxass/strip-json-comments": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@luxass/strip-json-comments/-/strip-json-comments-1.4.0.tgz",
@ -873,6 +980,12 @@
"node": ">=18"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@marko/compiler": {
"version": "5.39.21",
"resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.21.tgz",
@ -1458,6 +1571,21 @@
"node": ">=8"
}
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/collapse-white-space": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
@ -1537,6 +1665,12 @@
}
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -1605,6 +1739,12 @@
"node": ">=6"
}
},
"node_modules/devalue": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"license": "MIT"
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@ -3748,6 +3888,12 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/style-to-js": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
@ -4020,6 +4166,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View file

@ -4,6 +4,8 @@
"@hono/node-server": "^1.14.3",
"@mdx-js/mdx": "^3.1.0",
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
"codemirror": "^6.0.1",
"devalue": "^5.1.1",
"esbuild": "^0.25.5",
"hls.js": "^1.6.5",
"hono": "^4.7.11",

View file

@ -127,9 +127,9 @@ app.get("/q+a/:id", async (c, next) => {
const question = Question.getByDate(timestamp);
if (!question) return next();
// if (image) {
// return getQuestionImage(question, c.req.method === "HEAD");
// }
if (image) {
return getQuestionImage(question, c.req.method === "HEAD");
}
return renderView(c, "q+a/permalink", { question });
});
@ -193,11 +193,6 @@ app.get("/q+a/things/random", async (c) => {
c.res = await renderView(c, "q+a/things-random", {});
});
// 404
app.get("/q+a/*", async (c) => {
return serveAsset(c, "/q+a/404", 404);
});
async function questionFailure(
c: Context,
status: ContentfulStatusCode,
@ -229,5 +224,5 @@ import {
} from "./models/PendingQuestion.ts";
import { Question, QuestionType } from "./models/Question.ts";
import { renderView } from "#sitegen/view";
// import { getQuestionImage } from "./question_image";
import { getQuestionImage } from "./image.tsx";
import { formatQuestionId, questionIdToTimestamp } from "./format.ts";

View file

@ -211,7 +211,7 @@ function renderNode(node: string | ASTNode | ASTNode[]): any {
if (typeof node === "string") {
return node;
} else if (Array.isArray(node)) {
return node.flatMap((item) => renderNode(item));
return node.map((item) => renderNode(item));
} else if (node.type === "text") {
return node.content;
} else {

View file

@ -1,4 +1,12 @@
const h1 = document.querySelector("h1")!;
const key = "net.paperclover.q+a.header";
const state = localStorage?.getItem(key);
if (state === "detrevni ton") h1.classList.toggle("invert");
h1.addEventListener("click", () => {
h1.classList.toggle("invert");
localStorage?.setItem(
key,
(localStorage.getItem?.(key) ?? "not inverted")
.split("").reverse().join(""),
);
});

View file

@ -5,14 +5,13 @@ const cacheImageDir = path.resolve(".clover/question_images");
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 });
const html = await renderViewToString("q+a/image-embed", { 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

View file

@ -6,6 +6,9 @@ export const meta: Metadata = {
title: "paper clover q+a",
description: "ask clover a question",
};
export const regenerate = {
manual: true,
};
<const/inboxSize = PendingQuestion.getAll().length />
<if=(inboxSize > 0)>

View file

@ -4,6 +4,7 @@ export const meta: Metadata = {
};
<h2>sound the alarms</h2>
<p>this page doesn't exist</p>
<p>
<a href="/q+a">return to the questions list</a>
</p>

View file

@ -1,4 +1,7 @@
export * as layout from "../layout.tsx";
export const regenerate = {
manual: true,
};
export interface Input {
admin?: boolean;
@ -11,7 +14,7 @@ export const meta: Metadata = {
<const/{ admin = false } = input />
<const/questions = [...Question.getAll()] />
<if=!admin>
<if=true>
<question-form />
</>
<for|question| of=questions>

View file

@ -1,9 +1,8 @@
import { basicSetup, EditorState, EditorView } from "codemirror";
import { EditorState } from "@codemirror/state";
import { basicSetup, EditorView } from "codemirror";
import { ssrSync } from "#ssr";
// @ts-ignore
import type { ScriptPayload } from "../views/editor.marko";
// @ts-ignore
import QuestionRender from "@/q+a/components/Question.marko";
import { ScriptPayload } from "@/q+a/view/editor.marko";
import QuestionRender from "@/q+a/tags/question.marko";
declare const payload: ScriptPayload;
const date = new Date(payload.date);
@ -21,9 +20,11 @@ function updatePreview(text: string) {
type: payload.type,
date,
}}
editor
/>,
).text;
}
updatePreview(payload.text);
const startState = EditorState.create({
doc: payload.text,

View file

@ -2,12 +2,13 @@
export interface Input {
question: Question;
admin?: boolean;
editor?: boolean;
}
// 2024-12-31 05:00:00 EST
export const transitionDate = 1735639200000;
<const/{ question, admin } = input />
<const/{ question, admin = false, editor = false } = input />
<const/{ id, date } = question/>
<${"e-"}
@ -30,8 +31,11 @@ export const transitionDate = 1735639200000;
</>
// this singleton script will make all the '<time>' tags clickable.
client import "./clickable-links.client.ts";
<if=!editor>
client import "./clickable-links.client.ts";
</>
import type { Question } from "@/q+a/models/Question.ts";
import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
import { addScript as Script } from "#sitegen"

View file

@ -56,10 +56,12 @@ export interface ScriptPayload {
</div>
<div#editor />
<main#preview>
<question ...{question} />
<question editor ...{ question } />
</main>
</div>
<html-script src="/js/edit_frontend.js" type="module" />
<html-script>self.payload=${devalue.uneval(payload)}</html-script>
<html-script src="/js/q+a/scripts/editor.js" type="module" />
import * as devalue from 'devalue';
import { type PendingQuestion } from "@/q+a/models/PendingQuestion.ts";
import { Question, QuestionType } from "@/q+a/models/Question.ts";

View file

@ -1,23 +1,25 @@
export * as layout from "@/q+a/layout.tsx";
export interface Input {
question: Question;
}
server export function meta({ context: { req }, question }) {
const isDiscord = req.get("user-agent")
server export function meta({ context, question }) {
const isDiscord = context.get("user-agent")
?.toLowerCase()
.includes("discordbot");
if (question.type === QuestionType.normal) {
return {
title: "question permalink",
openGraph: {
images: [{ url: `https://paperclover.net/q+a/${q.id}.png` }],
images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],
},
twitter: { card: "summary_large_image" },
themeColor: isDiscord
? q.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
: undefined,
};
}
return { title: 'question permalink' };
}
<const/{ question }=input/>