mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-12 20:50:01 -04:00
Compare commits
1 Commits
catalog-og
..
v2
| Author | SHA1 | Date | |
|---|---|---|---|
| af127a643b |
@@ -591,7 +591,6 @@
|
||||
"react-dom": "^19.2.7",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/canvas": "1.0.2",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { $ } from "bun"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { buildCliToResources, downloadCliToResources, windowsify } from "./utils"
|
||||
import { downloadCliToResources, windowsify } from "./utils"
|
||||
|
||||
type ServerSource = { type: "build" } | { type: "download"; version: string }
|
||||
type DevOptions = { server: ServerSource; electron: string[] }
|
||||
@@ -38,18 +37,9 @@ function selectOptions(): DevOptions {
|
||||
}
|
||||
|
||||
async function prepareServer(source: ServerSource) {
|
||||
const destination = windowsify("resources/opencode-cli-dev")
|
||||
if (source.type === "download") return downloadCliToResources(source.version, destination)
|
||||
return buildCliToResources(destination, developmentStateHome())
|
||||
}
|
||||
|
||||
function developmentStateHome() {
|
||||
const appData = (() => {
|
||||
if (process.platform === "darwin") return join(homedir(), "Library", "Application Support")
|
||||
if (process.platform === "win32") return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming")
|
||||
return process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config")
|
||||
})()
|
||||
return join(appData, "ai.opencode.desktop.dev")
|
||||
if (source.type === "download")
|
||||
return downloadCliToResources(source.version, windowsify("resources/opencode-cli-dev"))
|
||||
process.env.OPENCODE_DESKTOP_CLI_DEV = join(import.meta.dirname, "../../cli")
|
||||
}
|
||||
|
||||
async function startDesktop(args: string[]) {
|
||||
|
||||
@@ -86,34 +86,6 @@ export async function downloadCliToResources(version = CLI_VERSION, dest = windo
|
||||
console.log(`Copied ${cli.package}@${version} to ${dest}`)
|
||||
}
|
||||
|
||||
export async function buildCliToResources(dest = windowsify("resources/opencode-cli"), stateHome?: string) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-cli-"))
|
||||
const target = `cli-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
||||
try {
|
||||
await $`bun ${join(import.meta.dirname, "../../cli/script/build.ts")} --single --skip-install --skip-web-ui --outdir=${directory}`.env(
|
||||
{
|
||||
...process.env,
|
||||
OPENCODE_VERSION: process.env.OPENCODE_VERSION,
|
||||
},
|
||||
)
|
||||
if (stateHome && (await Bun.file(dest).exists())) {
|
||||
const child = Bun.spawn([dest, "service", "stop"], {
|
||||
env: { ...process.env, XDG_STATE_HOME: stateHome },
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
const exitCode = await child.exited
|
||||
if (exitCode !== 0) throw new Error(`Failed to stop development service: ${exitCode}`)
|
||||
}
|
||||
await copyFile(join(directory, target, "bin", windowsify("opencode2")), dest)
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
await prepareCli(dest)
|
||||
|
||||
console.log(`Built local CLI at ${dest}`)
|
||||
}
|
||||
|
||||
async function prepareCli(dest: string) {
|
||||
if (process.platform !== "win32") await chmod(dest, 0o755)
|
||||
if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") {
|
||||
|
||||
@@ -17,29 +17,31 @@ type Logger = {
|
||||
|
||||
export async function startBackgroundCli(logger: Logger) {
|
||||
const isolated = !app.isPackaged && process.env.OPENCODE_DESKTOP_ISOLATED_SERVER === "1"
|
||||
const bundled = app.isPackaged
|
||||
? join(process.resourcesPath, executableName())
|
||||
: join(root, "../../resources", isolated ? developmentExecutableName() : executableName())
|
||||
logger.log("v2 CLI executable resolved", { bundled, packaged: app.isPackaged })
|
||||
const version = parseVersion(await run(bundled, ["--version"], logger))
|
||||
const binary = app.isPackaged || isolated ? await installCli(bundled, version, logger) : bundled
|
||||
const development = !app.isPackaged && process.env.OPENCODE_DESKTOP_CLI_DEV
|
||||
const cli = development
|
||||
? {
|
||||
version: "local",
|
||||
command: ["bun", "run", "--cwd", development, "dev", "--"],
|
||||
binary: undefined,
|
||||
}
|
||||
: await resolveBundledCli(isolated, logger)
|
||||
if (isolated) process.env.XDG_STATE_HOME = app.getPath("userData")
|
||||
const service = await Service.ensure({
|
||||
file:
|
||||
isolated && process.env.OPENCODE_DESKTOP_SERVER_CHANNEL === "local"
|
||||
? join(app.getPath("userData"), "opencode", "service-local.json")
|
||||
: undefined,
|
||||
version,
|
||||
command: [binary, "serve", "--service"],
|
||||
version: cli.version,
|
||||
command: [...cli.command, "serve", "--service"],
|
||||
onStart: (reason, previousVersion) => logger.log("v2 CLI background service starting", { reason, previousVersion }),
|
||||
})
|
||||
if (service.auth?.type !== "basic") throw new Error("V2 CLI background service did not provide authentication")
|
||||
logger.log("v2 CLI background service ready", {
|
||||
username: service.auth.username,
|
||||
version,
|
||||
version: cli.version,
|
||||
...endpoint(service.url),
|
||||
})
|
||||
if (isolated) await cleanCliStages(binary, logger)
|
||||
if (isolated && cli.binary) await cleanCliStages(cli.binary, logger)
|
||||
return {
|
||||
url: service.url,
|
||||
username: service.auth.username,
|
||||
@@ -47,6 +49,16 @@ export async function startBackgroundCli(logger: Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveBundledCli(isolated: boolean, logger: Logger) {
|
||||
const bundled = app.isPackaged
|
||||
? join(process.resourcesPath, executableName())
|
||||
: join(root, "../../resources", isolated ? developmentExecutableName() : executableName())
|
||||
logger.log("v2 CLI executable resolved", { bundled, packaged: app.isPackaged })
|
||||
const version = parseVersion(await run(bundled, ["--version"], logger))
|
||||
const binary = app.isPackaged || isolated ? await installCli(bundled, version, logger) : bundled
|
||||
return { version, binary, command: [binary] }
|
||||
}
|
||||
|
||||
async function cleanCliStages(binary: string, logger: Logger) {
|
||||
const current = dirname(binary)
|
||||
const root = dirname(current)
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,21 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import wrangler from "../wrangler.jsonc"
|
||||
import { assetPath, metaTags } from "../worker"
|
||||
|
||||
const catalog = {
|
||||
variants: [
|
||||
{ id: "opencode", label: "Opencode" },
|
||||
{ id: "tokyonight", label: "Tokyo Night" },
|
||||
],
|
||||
screens: [
|
||||
{
|
||||
id: "home",
|
||||
title: "Home",
|
||||
summary: "",
|
||||
frames: [{ variantId: "opencode" }, { variantId: "tokyonight" }],
|
||||
},
|
||||
],
|
||||
}
|
||||
import { assetPath } from "../worker"
|
||||
|
||||
describe("catalog worker", () => {
|
||||
test("serves the app shell for catalog routes", () => {
|
||||
@@ -32,25 +17,4 @@ describe("catalog worker", () => {
|
||||
test("leaves HTML routing to the worker", () => {
|
||||
expect(wrangler.assets.html_handling).toBe("none")
|
||||
})
|
||||
|
||||
test("injects a per-capture Open Graph card for deep links", () => {
|
||||
const tags = metaTags(new URL("https://dev.opencode.ai/lab/catalog?screen=home&set=tokyonight"), catalog)
|
||||
expect(tags).toContain('content="Home — OpenCode Terminal Catalog"')
|
||||
expect(tags).toContain('content="https://dev.opencode.ai/lab/catalog/og/home--tokyonight.png"')
|
||||
expect(tags).toContain("Tokyo Night theme")
|
||||
expect(tags).toContain('name="twitter:card" content="summary_large_image"')
|
||||
})
|
||||
|
||||
test("defaults the theme to the first variant", () => {
|
||||
const tags = metaTags(new URL("https://dev.opencode.ai/lab/catalog?screen=home"), catalog)
|
||||
expect(tags).toContain('content="https://dev.opencode.ai/lab/catalog/og/home--opencode.png"')
|
||||
})
|
||||
|
||||
test("falls back to the default card for unknown or missing captures", () => {
|
||||
for (const search of ["", "?screen=missing", "?screen=home&set=missing"]) {
|
||||
const tags = metaTags(new URL(`https://dev.opencode.ai/lab/catalog${search}`), catalog)
|
||||
expect(tags).toContain('content="https://dev.opencode.ai/lab/catalog/og/default.png"')
|
||||
expect(tags).toContain('content="OpenCode Terminal Catalog"')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"dev": "bun ./server.ts",
|
||||
"lint": "cd ../../.. && bun run lint -- packages/lab/catalog/src packages/lab/catalog/catalog packages/lab/catalog/scripts packages/lab/catalog/scenarios packages/lab/catalog/server.ts packages/lab/catalog/worker.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "rm -rf dist && bun build ./src/index.html --outdir dist --public-path=/lab/catalog/ --minify && cp -R public/captures public/catalog.json public/drive-captures.json dist/ && bun ./scripts/generate-og.ts",
|
||||
"build": "rm -rf dist && bun build ./src/index.html --outdir dist --public-path=/lab/catalog/ --minify && cp -R public/captures public/catalog.json public/drive-captures.json dist/",
|
||||
"deploy": "bun run generate && bun run build && bunx wrangler deploy",
|
||||
"doctor": "bunx -y react-doctor@latest .",
|
||||
"bench:capture-flow": "bun ./scripts/bench-capture-flow.ts",
|
||||
@@ -28,7 +28,6 @@
|
||||
"react-dom": "^19.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/canvas": "1.0.2",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
/**
|
||||
* Generates Open Graph cards for the catalog: one 1200x630 PNG per captured
|
||||
* screen/theme pair plus a default card, written to dist/og. Runs after the
|
||||
* production build so the images deploy as plain static assets.
|
||||
*/
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { GlobalFonts, createCanvas } from "@napi-rs/canvas"
|
||||
import type { SKRSContext2D, Canvas } from "@napi-rs/canvas"
|
||||
import {
|
||||
CellHeight,
|
||||
CellWidth,
|
||||
DimAlpha,
|
||||
FontSize,
|
||||
StrikethroughOffset,
|
||||
TextStyle,
|
||||
UnderlineOffset,
|
||||
baselineOffset,
|
||||
drawBlockGlyph,
|
||||
} from "opencode-drive/frame"
|
||||
|
||||
const CardWidth = 1200
|
||||
const CardHeight = 630
|
||||
const Pad = 56
|
||||
|
||||
const FontStack = `"OpenCode Mono", "OpenCode Symbols", "OpenCode Symbols 2", "OpenCode Math"`
|
||||
|
||||
const driveRoot = new URL("../../", Bun.pathToFileURL(Bun.resolveSync("opencode-drive/frame", import.meta.dir)).href)
|
||||
for (const [file, family] of [
|
||||
["commit-mono/CommitMono-400-Regular.otf", "OpenCode Mono"],
|
||||
["commit-mono/CommitMono-700-Regular.otf", "OpenCode Mono"],
|
||||
["commit-mono/CommitMono-400-Italic.otf", "OpenCode Mono"],
|
||||
["commit-mono/CommitMono-700-Italic.otf", "OpenCode Mono"],
|
||||
["noto/NotoSansSymbols.ttf", "OpenCode Symbols"],
|
||||
["noto/NotoSansSymbols2-Regular.ttf", "OpenCode Symbols 2"],
|
||||
["noto/NotoSansMath-Regular.ttf", "OpenCode Math"],
|
||||
] as const) {
|
||||
const path = fileURLToPath(new URL(`assets/fonts/${file}`, driveRoot))
|
||||
if (!GlobalFonts.registerFromPath(path, family)) throw new Error(`Failed to register OG font: ${path}`)
|
||||
}
|
||||
|
||||
interface FrameSpan {
|
||||
readonly text: string
|
||||
readonly fg: readonly [number, number, number, number]
|
||||
readonly bg: readonly [number, number, number, number]
|
||||
readonly attributes: number
|
||||
readonly width: number
|
||||
}
|
||||
|
||||
interface FrameArtifact {
|
||||
readonly cols: number
|
||||
readonly rows: number
|
||||
readonly lines: ReadonlyArray<{ readonly spans: ReadonlyArray<FrameSpan> }>
|
||||
}
|
||||
|
||||
function color([red, green, blue, alpha]: FrameSpan["fg"], opacity = 1) {
|
||||
return `rgba(${red}, ${green}, ${blue}, ${(alpha / 255) * opacity})`
|
||||
}
|
||||
|
||||
/** Mirrors the browser canvas renderer in src/components/TerminalFrame.tsx. */
|
||||
function renderFrame(frame: FrameArtifact) {
|
||||
const canvas = createCanvas(frame.cols * CellWidth, frame.rows * CellHeight)
|
||||
const context = canvas.getContext("2d")
|
||||
context.fillStyle = "#080808"
|
||||
context.fillRect(0, 0, canvas.width, canvas.height)
|
||||
context.textBaseline = "alphabetic"
|
||||
context.textAlign = "center"
|
||||
|
||||
frame.lines.forEach((line, row) => {
|
||||
let column = 0
|
||||
line.spans.forEach((span) => {
|
||||
const attributes = span.attributes & 0xff
|
||||
const inverse = Boolean(attributes & TextStyle.inverse)
|
||||
const hidden = Boolean(attributes & TextStyle.invisible)
|
||||
const foreground = inverse ? span.bg : span.fg
|
||||
const background = inverse ? span.fg : span.bg
|
||||
const chars = [...span.text]
|
||||
let remaining = span.width
|
||||
|
||||
chars.forEach((char, index) => {
|
||||
const cells = Math.max(1, remaining - (chars.length - index - 1))
|
||||
const x = column * CellWidth
|
||||
const y = row * CellHeight
|
||||
if (background[3]) {
|
||||
context.fillStyle = color(background)
|
||||
context.fillRect(x, y, cells * CellWidth, CellHeight)
|
||||
}
|
||||
if (!hidden && char.codePointAt(0) !== 0x0a00) {
|
||||
context.fillStyle = color(foreground, attributes & TextStyle.dim ? DimAlpha : 1)
|
||||
if (!drawBlockGlyph(context, char, x, y, cells)) {
|
||||
const font = `${attributes & TextStyle.italic ? "italic " : ""}${attributes & TextStyle.bold ? "700 " : "400 "}${FontSize}px ${FontStack}`
|
||||
context.font = font
|
||||
context.fillText(char, x + (cells * CellWidth) / 2, y + baselineOffset(context, font), cells * CellWidth)
|
||||
}
|
||||
if (attributes & TextStyle.underline) context.fillRect(x, y + UnderlineOffset, cells * CellWidth, 1)
|
||||
if (attributes & TextStyle.strikethrough) context.fillRect(x, y + StrikethroughOffset, cells * CellWidth, 1)
|
||||
}
|
||||
column += cells
|
||||
remaining -= cells
|
||||
})
|
||||
while (remaining-- > 0) {
|
||||
if (background[3]) {
|
||||
context.fillStyle = color(background)
|
||||
context.fillRect(column * CellWidth, row * CellHeight, CellWidth, CellHeight)
|
||||
}
|
||||
column++
|
||||
}
|
||||
})
|
||||
})
|
||||
return canvas
|
||||
}
|
||||
|
||||
function drawChrome(context: SKRSContext2D, kicker: string, title: string, chip: string) {
|
||||
context.fillStyle = "#0a0a0a"
|
||||
context.fillRect(0, 0, CardWidth, CardHeight)
|
||||
|
||||
context.textAlign = "left"
|
||||
context.textBaseline = "alphabetic"
|
||||
context.font = `700 20px ${FontStack}`
|
||||
context.fillStyle = "#6f6e69"
|
||||
context.fillText(kicker.toUpperCase(), Pad, Pad + 16)
|
||||
|
||||
context.font = `700 46px ${FontStack}`
|
||||
context.fillStyle = "#ededed"
|
||||
context.fillText(title, Pad - 2, Pad + 74, CardWidth - Pad * 2 - 220)
|
||||
|
||||
if (chip !== "") {
|
||||
context.font = `400 20px ${FontStack}`
|
||||
const width = context.measureText(chip).width + 36
|
||||
const x = CardWidth - Pad - width
|
||||
context.strokeStyle = "#2c2c2c"
|
||||
context.lineWidth = 1
|
||||
context.beginPath()
|
||||
context.roundRect(x, Pad + 38, width, 40, 20)
|
||||
context.stroke()
|
||||
context.fillStyle = "#9c9b96"
|
||||
context.fillText(chip, x + 18, Pad + 65)
|
||||
}
|
||||
}
|
||||
|
||||
/** Frame peeks from the bottom edge like a window, cropped by the card. */
|
||||
function drawFramePeek(context: SKRSContext2D, frame: Canvas) {
|
||||
const top = 182
|
||||
const width = CardWidth - Pad * 2
|
||||
const scale = width / frame.width
|
||||
const visible = CardHeight - top
|
||||
const radius = 10
|
||||
|
||||
context.save()
|
||||
context.beginPath()
|
||||
context.roundRect(Pad, top, width, visible + radius, radius)
|
||||
context.clip()
|
||||
context.drawImage(frame, 0, 0, frame.width, Math.min(frame.height, (visible + radius) / scale), Pad, top, width, Math.min(frame.height * scale, visible + radius))
|
||||
context.restore()
|
||||
|
||||
context.strokeStyle = "#2c2c2c"
|
||||
context.lineWidth = 2
|
||||
context.beginPath()
|
||||
context.roundRect(Pad + 1, top + 1, width - 2, visible + radius, radius)
|
||||
context.stroke()
|
||||
}
|
||||
|
||||
const root = new URL("../", import.meta.url)
|
||||
const outDir = new URL("dist/og/", root)
|
||||
const catalog = await Bun.file(new URL("dist/catalog.json", root)).json()
|
||||
|
||||
const variants = catalog.variants as ReadonlyArray<{ id: string; label: string }>
|
||||
const screens = catalog.screens as ReadonlyArray<{
|
||||
id: string
|
||||
title: string
|
||||
frames: ReadonlyArray<{ variantId: string; src: string }>
|
||||
}>
|
||||
|
||||
let generated = 0
|
||||
for (const screen of screens) {
|
||||
for (const frame of screen.frames) {
|
||||
const variant = variants.find((candidate) => candidate.id === frame.variantId)
|
||||
if (!variant) continue
|
||||
const artifact = (await Bun.file(new URL(frame.src, new URL("dist/", root))).json()) as FrameArtifact
|
||||
const card = createCanvas(CardWidth, CardHeight)
|
||||
const context = card.getContext("2d")
|
||||
drawChrome(context, "OpenCode · Terminal Catalog", screen.title, variant.label)
|
||||
drawFramePeek(context, renderFrame(artifact))
|
||||
await Bun.write(new URL(`${screen.id}--${variant.id}.png`, outDir), card.toBuffer("image/png"))
|
||||
generated++
|
||||
}
|
||||
}
|
||||
|
||||
const home = screens.find((screen) => screen.id === "home")
|
||||
const homeFrame = home?.frames.find((frame) => frame.variantId === variants[0]?.id) ?? home?.frames[0]
|
||||
const card = createCanvas(CardWidth, CardHeight)
|
||||
const context = card.getContext("2d")
|
||||
drawChrome(context, "OpenCode", "Terminal Catalog", `${screens.length} screens · ${variants.length} themes`)
|
||||
if (homeFrame) {
|
||||
const artifact = (await Bun.file(new URL(homeFrame.src, new URL("dist/", root))).json()) as FrameArtifact
|
||||
drawFramePeek(context, renderFrame(artifact))
|
||||
}
|
||||
await Bun.write(new URL("default.png", outDir), card.toBuffer("image/png"))
|
||||
|
||||
console.log(`generated ${generated + 1} Open Graph cards in dist/og`)
|
||||
@@ -2,43 +2,11 @@ interface Env {
|
||||
readonly ASSETS: { fetch(request: Request): Promise<Response> }
|
||||
}
|
||||
|
||||
interface CatalogIndex {
|
||||
readonly variants: ReadonlyArray<{ readonly id: string; readonly label: string }>
|
||||
readonly screens: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly title: string
|
||||
readonly summary: string
|
||||
readonly frames: ReadonlyArray<{ readonly variantId: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
let catalogCache: Promise<CatalogIndex> | undefined
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
const path = assetPath(url.pathname)
|
||||
const assetUrl = new URL(url)
|
||||
assetUrl.pathname = path
|
||||
const response = await env.ASSETS.fetch(new Request(assetUrl, request))
|
||||
if (path !== "/index.html" || !response.ok) return response
|
||||
|
||||
catalogCache ??= env.ASSETS.fetch(new Request(new URL("/catalog.json", url.origin)))
|
||||
.then((asset) => asset.json() as Promise<CatalogIndex>)
|
||||
.catch((cause) => {
|
||||
catalogCache = undefined
|
||||
throw cause
|
||||
})
|
||||
const catalog = await catalogCache.catch(() => undefined)
|
||||
if (!catalog) return response
|
||||
|
||||
const html = await response.text()
|
||||
const headers = new Headers(response.headers)
|
||||
headers.delete("content-length")
|
||||
return new Response(html.replace("</head>", `${metaTags(url, catalog)}</head>`), {
|
||||
status: response.status,
|
||||
headers,
|
||||
})
|
||||
url.pathname = assetPath(url.pathname)
|
||||
return env.ASSETS.fetch(new Request(url, request))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,37 +14,3 @@ export function assetPath(pathname: string) {
|
||||
const path = pathname.slice("/lab/catalog".length)
|
||||
return path === "" || path === "/" || !path.includes(".") ? "/index.html" : path
|
||||
}
|
||||
|
||||
export function metaTags(url: URL, catalog: CatalogIndex) {
|
||||
const screen = catalog.screens.find((candidate) => candidate.id === url.searchParams.get("screen"))
|
||||
const variantId = url.searchParams.get("set") ?? catalog.variants[0]?.id
|
||||
const variant = catalog.variants.find((candidate) => candidate.id === variantId)
|
||||
const captured = screen && variant && screen.frames.some((frame) => frame.variantId === variant.id)
|
||||
|
||||
const title = captured ? `${screen.title} — OpenCode Terminal Catalog` : "OpenCode Terminal Catalog"
|
||||
const description = captured
|
||||
? screen.summary || `The ${screen.title} state of the OpenCode TUI, captured live in the ${variant.label} theme.`
|
||||
: "A visual catalog of OpenCode TUI states, captured from the live terminal."
|
||||
const image = `${url.origin}/lab/catalog/og/${captured ? `${screen.id}--${variant.id}` : "default"}.png`
|
||||
|
||||
const tags: ReadonlyArray<readonly [string, string]> = [
|
||||
["og:title", title],
|
||||
["og:description", description],
|
||||
["og:type", "website"],
|
||||
["og:url", url.href],
|
||||
["og:image", image],
|
||||
["og:image:width", "1200"],
|
||||
["og:image:height", "630"],
|
||||
]
|
||||
return [
|
||||
...tags.map(([property, content]) => `<meta property="${property}" content="${escapeAttribute(content)}" />`),
|
||||
`<meta name="twitter:card" content="summary_large_image" />`,
|
||||
`<meta name="twitter:title" content="${escapeAttribute(title)}" />`,
|
||||
`<meta name="twitter:image" content="${escapeAttribute(image)}" />`,
|
||||
`<meta name="description" content="${escapeAttribute(description)}" />`,
|
||||
].join("\n ")
|
||||
}
|
||||
|
||||
function escapeAttribute(value: string) {
|
||||
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user