Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

add opengraph cf worker

+692
+7
.gitignore
··· 23 23 24 24 # OS 25 25 .DS_Store 26 + 27 + # wrangler files 28 + .wrangler 29 + .dev.vars* 30 + !.dev.vars.example 31 + .env* 32 + !.env.example
+19
opengraph/README.md
··· 1 + # OpenGraph Worker 2 + 3 + Cloudflare Worker that injects dynamic `og:title`, `og:description`, `og:image`, and `og:url` into the atbbs SPA for link previews on social platforms. 4 + 5 + Handles two route patterns: 6 + 7 + - `/bbs/*` — rewrites the HTML `<meta>` tags with BBS/board/thread metadata from Slingshot 8 + - `/og/bbs/*.png` — generates a branded 1200x630 preview image on the fly 9 + 10 + ## Deploy 11 + 12 + ```bash 13 + cd opengraph 14 + npm install 15 + npx wrangler login 16 + npx wrangler deploy -c wrangler.toml 17 + ``` 18 + 19 + Requires `atbbs.xyz` to be proxied through Cloudflare.
+66
opengraph/hero-light.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="276" height="84" viewBox="0 0 276 84"> 2 + <rect x="12" y="0" width="12" height="12" fill="#d97706"/> 3 + <rect x="24" y="0" width="12" height="12" fill="#d97706"/> 4 + <rect x="36" y="0" width="12" height="12" fill="#d97706"/> 5 + <rect x="72" y="0" width="12" height="12" fill="#262626"/> 6 + <rect x="144" y="0" width="12" height="12" fill="#262626"/> 7 + <rect x="0" y="12" width="12" height="12" fill="#d97706"/> 8 + <rect x="48" y="12" width="12" height="12" fill="#d97706"/> 9 + <rect x="72" y="12" width="12" height="12" fill="#262626"/> 10 + <rect x="144" y="12" width="12" height="12" fill="#262626"/> 11 + <rect x="0" y="24" width="12" height="12" fill="#d97706"/> 12 + <rect x="24" y="24" width="12" height="12" fill="#d97706"/> 13 + <rect x="48" y="24" width="12" height="12" fill="#d97706"/> 14 + <rect x="72" y="24" width="12" height="12" fill="#262626"/> 15 + <rect x="84" y="24" width="12" height="12" fill="#262626"/> 16 + <rect x="96" y="24" width="12" height="12" fill="#262626"/> 17 + <rect x="108" y="24" width="12" height="12" fill="#262626"/> 18 + <rect x="144" y="24" width="12" height="12" fill="#262626"/> 19 + <rect x="156" y="24" width="12" height="12" fill="#262626"/> 20 + <rect x="168" y="24" width="12" height="12" fill="#262626"/> 21 + <rect x="180" y="24" width="12" height="12" fill="#262626"/> 22 + <rect x="228" y="24" width="12" height="12" fill="#262626"/> 23 + <rect x="240" y="24" width="12" height="12" fill="#262626"/> 24 + <rect x="252" y="24" width="12" height="12" fill="#262626"/> 25 + <rect x="264" y="24" width="12" height="12" fill="#262626"/> 26 + <rect x="0" y="36" width="12" height="12" fill="#d97706"/> 27 + <rect x="24" y="36" width="12" height="12" fill="#d97706"/> 28 + <rect x="36" y="36" width="12" height="12" fill="#d97706"/> 29 + <rect x="48" y="36" width="12" height="12" fill="#d97706"/> 30 + <rect x="72" y="36" width="12" height="12" fill="#262626"/> 31 + <rect x="120" y="36" width="12" height="12" fill="#262626"/> 32 + <rect x="144" y="36" width="12" height="12" fill="#262626"/> 33 + <rect x="192" y="36" width="12" height="12" fill="#262626"/> 34 + <rect x="216" y="36" width="12" height="12" fill="#262626"/> 35 + <rect x="0" y="48" width="12" height="12" fill="#d97706"/> 36 + <rect x="24" y="48" width="12" height="12" fill="#d97706"/> 37 + <rect x="36" y="48" width="12" height="12" fill="#d97706"/> 38 + <rect x="72" y="48" width="12" height="12" fill="#262626"/> 39 + <rect x="120" y="48" width="12" height="12" fill="#262626"/> 40 + <rect x="144" y="48" width="12" height="12" fill="#262626"/> 41 + <rect x="192" y="48" width="12" height="12" fill="#262626"/> 42 + <rect x="228" y="48" width="12" height="12" fill="#262626"/> 43 + <rect x="240" y="48" width="12" height="12" fill="#262626"/> 44 + <rect x="252" y="48" width="12" height="12" fill="#262626"/> 45 + <rect x="0" y="60" width="12" height="12" fill="#d97706"/> 46 + <rect x="72" y="60" width="12" height="12" fill="#262626"/> 47 + <rect x="120" y="60" width="12" height="12" fill="#262626"/> 48 + <rect x="144" y="60" width="12" height="12" fill="#262626"/> 49 + <rect x="192" y="60" width="12" height="12" fill="#262626"/> 50 + <rect x="264" y="60" width="12" height="12" fill="#262626"/> 51 + <rect x="12" y="72" width="12" height="12" fill="#d97706"/> 52 + <rect x="24" y="72" width="12" height="12" fill="#d97706"/> 53 + <rect x="36" y="72" width="12" height="12" fill="#d97706"/> 54 + <rect x="72" y="72" width="12" height="12" fill="#262626"/> 55 + <rect x="84" y="72" width="12" height="12" fill="#262626"/> 56 + <rect x="96" y="72" width="12" height="12" fill="#262626"/> 57 + <rect x="108" y="72" width="12" height="12" fill="#262626"/> 58 + <rect x="144" y="72" width="12" height="12" fill="#262626"/> 59 + <rect x="156" y="72" width="12" height="12" fill="#262626"/> 60 + <rect x="168" y="72" width="12" height="12" fill="#262626"/> 61 + <rect x="180" y="72" width="12" height="12" fill="#262626"/> 62 + <rect x="216" y="72" width="12" height="12" fill="#262626"/> 63 + <rect x="228" y="72" width="12" height="12" fill="#262626"/> 64 + <rect x="240" y="72" width="12" height="12" fill="#262626"/> 65 + <rect x="252" y="72" width="12" height="12" fill="#262626"/> 66 + </svg>
+340
opengraph/index.ts
··· 1 + import { ImageResponse, loadGoogleFont } from "workers-og"; 2 + import heroLogoSvg from "./hero-light.svg"; 3 + 4 + const SLINGSHOT_URL = "https://slingshot.microcosm.blue/xrpc"; 5 + 6 + const DEFAULT_TITLE = "atbbs"; 7 + const DEFAULT_DESCRIPTION = "Decentralized forums on the AT Protocol."; 8 + 9 + // Tailwind neutral palette — matches the site's light theme. 10 + const COLORS = { 11 + background: "#fafafa", // neutral-50 12 + title: "#171717", // neutral-900 13 + subtitle: "#525252", // neutral-600 14 + description: "#525252", // neutral-600 15 + }; 16 + 17 + // Types 18 + 19 + interface Route { 20 + type: "bbs" | "board" | "thread" | "news"; 21 + handle: string; 22 + slug?: string; 23 + did?: string; 24 + rkey?: string; 25 + } 26 + 27 + interface Metadata { 28 + title: string; 29 + subtitle: string; 30 + description: string; 31 + } 32 + 33 + interface SlingshotIdentity { 34 + did: string; 35 + handle: string; 36 + pds?: string; 37 + } 38 + 39 + interface SlingshotRecord { 40 + uri: string; 41 + cid: string; 42 + value: Record<string, string>; 43 + } 44 + 45 + // Utils 46 + 47 + function escapeHtml(text: string): string { 48 + return text 49 + .replace(/&/g, "&amp;") 50 + .replace(/</g, "&lt;") 51 + .replace(/>/g, "&gt;") 52 + .replace(/"/g, "&quot;"); 53 + } 54 + 55 + function truncate(text: string, maxLength: number): string { 56 + if (text.length <= maxLength) return text; 57 + return text.substring(0, maxLength - 3) + "..."; 58 + } 59 + 60 + // Slingshot 61 + 62 + async function resolveIdentity( 63 + handle: string, 64 + ): Promise<SlingshotIdentity | null> { 65 + const response = await fetch( 66 + `${SLINGSHOT_URL}/blue.microcosm.identity.resolveMiniDoc?identifier=${encodeURIComponent(handle)}`, 67 + ); 68 + if (!response.ok) return null; 69 + return (await response.json()) as SlingshotIdentity; 70 + } 71 + 72 + async function fetchRecord( 73 + did: string, 74 + collection: string, 75 + recordKey: string, 76 + ): Promise<SlingshotRecord | null> { 77 + const response = await fetch( 78 + `${SLINGSHOT_URL}/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(recordKey)}`, 79 + ); 80 + if (!response.ok) return null; 81 + return (await response.json()) as SlingshotRecord; 82 + } 83 + 84 + async function fetchSiteName(did: string, fallback: string): Promise<string> { 85 + const siteRecord = await fetchRecord(did, "xyz.atbbs.site", "self"); 86 + return siteRecord ? siteRecord.value.name : fallback; 87 + } 88 + 89 + // --- Route parsing --- 90 + 91 + function parseRoute(path: string): Route | null { 92 + // Strip /og prefix and .png suffix so this works for both HTML and image routes. 93 + const normalizedPath = path.replace(/^\/og/, "").replace(/\.png$/, ""); 94 + 95 + const bbsMatch = normalizedPath.match(/^\/bbs\/([^/]+)$/); 96 + if (bbsMatch) return { type: "bbs", handle: bbsMatch[1] }; 97 + 98 + const boardMatch = normalizedPath.match(/^\/bbs\/([^/]+)\/board\/([^/]+)$/); 99 + if (boardMatch) 100 + return { type: "board", handle: boardMatch[1], slug: boardMatch[2] }; 101 + 102 + const threadMatch = normalizedPath.match( 103 + /^\/bbs\/([^/]+)\/thread\/([^/]+)\/([^/]+)$/, 104 + ); 105 + if (threadMatch) 106 + return { 107 + type: "thread", 108 + handle: threadMatch[1], 109 + did: threadMatch[2], 110 + rkey: threadMatch[3], 111 + }; 112 + 113 + const newsMatch = normalizedPath.match(/^\/bbs\/([^/]+)\/news\/([^/]+)$/); 114 + if (newsMatch) 115 + return { type: "news", handle: newsMatch[1], rkey: newsMatch[2] }; 116 + 117 + return null; 118 + } 119 + 120 + // Metadata 121 + 122 + async function fetchMetadata(route: Route): Promise<Metadata | null> { 123 + const identity = await resolveIdentity(route.handle); 124 + if (!identity) return null; 125 + 126 + if (route.type === "bbs") { 127 + const siteRecord = await fetchRecord( 128 + identity.did, 129 + "xyz.atbbs.site", 130 + "self", 131 + ); 132 + if (siteRecord) { 133 + return { 134 + title: siteRecord.value.name, 135 + subtitle: "", 136 + description: siteRecord.value.description || "", 137 + }; 138 + } 139 + } else if (route.type === "board") { 140 + const siteName = await fetchSiteName(identity.did, route.handle); 141 + const boardRecord = await fetchRecord( 142 + identity.did, 143 + "xyz.atbbs.board", 144 + route.slug!, 145 + ); 146 + if (boardRecord) { 147 + return { 148 + title: boardRecord.value.name, 149 + subtitle: siteName, 150 + description: boardRecord.value.description || "", 151 + }; 152 + } 153 + } else if (route.type === "thread") { 154 + const siteName = await fetchSiteName(identity.did, route.handle); 155 + const postRecord = await fetchRecord( 156 + route.did!, 157 + "xyz.atbbs.post", 158 + route.rkey!, 159 + ); 160 + if (postRecord) { 161 + return { 162 + title: postRecord.value.title || "Thread", 163 + subtitle: siteName, 164 + description: postRecord.value.body || "", 165 + }; 166 + } 167 + } else if (route.type === "news") { 168 + const siteName = await fetchSiteName(identity.did, route.handle); 169 + const postRecord = await fetchRecord( 170 + identity.did, 171 + "xyz.atbbs.post", 172 + route.rkey!, 173 + ); 174 + if (postRecord) { 175 + return { 176 + title: postRecord.value.title || "News", 177 + subtitle: siteName, 178 + description: postRecord.value.body || "", 179 + }; 180 + } 181 + } 182 + 183 + return null; 184 + } 185 + 186 + // Generate OG Images 187 + 188 + const HERO_LOGO_DATA_URI = 189 + "data:image/svg+xml," + encodeURIComponent(heroLogoSvg); 190 + 191 + async function renderOgImage( 192 + title: string, 193 + subtitle: string, 194 + description: string, 195 + ): Promise<Response> { 196 + const displayTitle = escapeHtml(truncate(title, 40)); 197 + const displaySubtitle = escapeHtml(subtitle); 198 + const displayDescription = escapeHtml(truncate(description, 120)); 199 + 200 + const fontData = await loadGoogleFont({ 201 + family: "Geist Mono", 202 + weight: 400, 203 + }); 204 + 205 + const subtitleHtml = displaySubtitle 206 + ? `<div style="display: flex; font-size: 24px; color: ${COLORS.subtitle}; font-family: 'Geist Mono';">${displaySubtitle}</div>` 207 + : ""; 208 + 209 + const html = ` 210 + <div style="display: flex; flex-direction: column; justify-content: space-between; width: 1200px; height: 630px; background-color: ${COLORS.background}; padding: 80px 90px;"> 211 + <img src="${HERO_LOGO_DATA_URI}" width="276" height="84" style="image-rendering: pixelated;" /> 212 + <div style="display: flex; flex-direction: column; gap: 12px;"> 213 + ${subtitleHtml} 214 + <div style="display: flex; font-size: 56px; color: ${COLORS.title}; font-family: 'Geist Mono'; line-height: 1.2;">${displayTitle}</div> 215 + <div style="display: flex; font-size: 22px; color: ${COLORS.description}; font-family: 'Geist Mono'; line-height: 1.4;">${displayDescription}</div> 216 + </div> 217 + </div> 218 + `; 219 + 220 + return new ImageResponse(html, { 221 + width: 1200, 222 + height: 630, 223 + fonts: [ 224 + { 225 + name: "Geist Mono", 226 + data: fontData, 227 + style: "normal", 228 + weight: 400, 229 + }, 230 + ], 231 + }); 232 + } 233 + 234 + // Inject HTML 235 + 236 + function injectMetadata( 237 + html: string, 238 + title: string, 239 + description: string, 240 + pageUrl: string, 241 + imageUrl: string, 242 + ): string { 243 + const safeTitle = escapeHtml(title); 244 + const safeDescription = escapeHtml(description.substring(0, 200)); 245 + const safePageUrl = escapeHtml(pageUrl); 246 + const safeImageUrl = escapeHtml(imageUrl); 247 + 248 + html = html.replace( 249 + "<title>atbbs</title>", 250 + `<title>${safeTitle}</title>`, 251 + ); 252 + html = html.replace( 253 + '<meta property="og:title" content="atbbs" />', 254 + `<meta property="og:title" content="${safeTitle}" />`, 255 + ); 256 + html = html.replace( 257 + '<meta property="og:description" content="Decentralized forums on the AT Protocol." />', 258 + `<meta property="og:description" content="${safeDescription}" />`, 259 + ); 260 + html = html.replace( 261 + '<meta property="og:image" content="/og.png" />', 262 + `<meta property="og:image" content="${safeImageUrl}" />`, 263 + ); 264 + 265 + if (!html.includes("og:url")) { 266 + html = html.replace( 267 + '<meta property="og:type"', 268 + `<meta property="og:url" content="${safePageUrl}" />\n <meta property="og:type"`, 269 + ); 270 + } 271 + 272 + return html; 273 + } 274 + 275 + // Entry point 276 + 277 + export default { 278 + async fetch(request: Request): Promise<Response> { 279 + const url = new URL(request.url); 280 + const path = url.pathname; 281 + 282 + // Dynamic og:image at /og/bbs/... 283 + if (path.startsWith("/og/bbs/")) { 284 + const route = parseRoute(path); 285 + if (!route) { 286 + return renderOgImage(DEFAULT_TITLE, "", DEFAULT_DESCRIPTION); 287 + } 288 + 289 + try { 290 + const metadata = await fetchMetadata(route); 291 + if (metadata) { 292 + return renderOgImage( 293 + metadata.title, 294 + metadata.subtitle, 295 + metadata.description, 296 + ); 297 + } 298 + } catch { 299 + // Fall through to default image. 300 + } 301 + 302 + return renderOgImage(DEFAULT_TITLE, "", DEFAULT_DESCRIPTION); 303 + } 304 + 305 + // Inject metadata into HTML for /bbs/... routes. 306 + const route = parseRoute(path); 307 + const originResponse = await fetch(request); 308 + const contentType = originResponse.headers.get("content-type") || ""; 309 + 310 + if (!route || !contentType.includes("text/html")) { 311 + return originResponse; 312 + } 313 + 314 + let html = await originResponse.text(); 315 + 316 + try { 317 + const metadata = await fetchMetadata(route); 318 + if (metadata) { 319 + const fullTitle = metadata.subtitle 320 + ? `${metadata.title} \u2014 ${metadata.subtitle}` 321 + : metadata.title; 322 + const imageUrl = `${url.origin}/og${path}.png`; 323 + html = injectMetadata( 324 + html, 325 + fullTitle, 326 + metadata.description, 327 + url.toString(), 328 + imageUrl, 329 + ); 330 + } 331 + } catch { 332 + // On any error, serve the original HTML unmodified. 333 + } 334 + 335 + return new Response(html, { 336 + status: originResponse.status, 337 + headers: originResponse.headers, 338 + }); 339 + }, 340 + };
+237
opengraph/package-lock.json
··· 1 + { 2 + "name": "atbbs-opengraph", 3 + "version": "1.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "atbbs-opengraph", 9 + "version": "1.0.0", 10 + "dependencies": { 11 + "workers-og": "^0.0.27" 12 + } 13 + }, 14 + "node_modules/@resvg/resvg-wasm": { 15 + "version": "2.4.0", 16 + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", 17 + "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", 18 + "license": "MPL-2.0", 19 + "engines": { 20 + "node": ">= 10" 21 + } 22 + }, 23 + "node_modules/@shuding/opentype.js": { 24 + "version": "1.4.0-beta.0", 25 + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", 26 + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", 27 + "license": "MIT", 28 + "dependencies": { 29 + "fflate": "^0.7.3", 30 + "string.prototype.codepointat": "^0.2.1" 31 + }, 32 + "bin": { 33 + "ot": "bin/ot" 34 + }, 35 + "engines": { 36 + "node": ">= 8.0.0" 37 + } 38 + }, 39 + "node_modules/base64-js": { 40 + "version": "0.0.8", 41 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", 42 + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", 43 + "license": "MIT", 44 + "engines": { 45 + "node": ">= 0.4" 46 + } 47 + }, 48 + "node_modules/camelize": { 49 + "version": "1.0.1", 50 + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", 51 + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", 52 + "license": "MIT", 53 + "funding": { 54 + "url": "https://github.com/sponsors/ljharb" 55 + } 56 + }, 57 + "node_modules/color-name": { 58 + "version": "1.1.4", 59 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 60 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 61 + "license": "MIT" 62 + }, 63 + "node_modules/css-background-parser": { 64 + "version": "0.1.0", 65 + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", 66 + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", 67 + "license": "MIT" 68 + }, 69 + "node_modules/css-box-shadow": { 70 + "version": "1.0.0-3", 71 + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", 72 + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", 73 + "license": "MIT" 74 + }, 75 + "node_modules/css-color-keywords": { 76 + "version": "1.0.0", 77 + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", 78 + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", 79 + "license": "ISC", 80 + "engines": { 81 + "node": ">=4" 82 + } 83 + }, 84 + "node_modules/css-to-react-native": { 85 + "version": "3.2.0", 86 + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", 87 + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", 88 + "license": "MIT", 89 + "dependencies": { 90 + "camelize": "^1.0.0", 91 + "css-color-keywords": "^1.0.0", 92 + "postcss-value-parser": "^4.0.2" 93 + } 94 + }, 95 + "node_modules/emoji-regex-xs": { 96 + "version": "2.0.1", 97 + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", 98 + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", 99 + "license": "MIT", 100 + "engines": { 101 + "node": ">=10.0.0" 102 + } 103 + }, 104 + "node_modules/escape-html": { 105 + "version": "1.0.3", 106 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 107 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 108 + "license": "MIT" 109 + }, 110 + "node_modules/fflate": { 111 + "version": "0.7.4", 112 + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", 113 + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", 114 + "license": "MIT" 115 + }, 116 + "node_modules/hex-rgb": { 117 + "version": "4.3.0", 118 + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", 119 + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", 120 + "license": "MIT", 121 + "engines": { 122 + "node": ">=6" 123 + }, 124 + "funding": { 125 + "url": "https://github.com/sponsors/sindresorhus" 126 + } 127 + }, 128 + "node_modules/just-camel-case": { 129 + "version": "6.2.0", 130 + "resolved": "https://registry.npmjs.org/just-camel-case/-/just-camel-case-6.2.0.tgz", 131 + "integrity": "sha512-ICenRLXwkQYLk3UyvLQZ+uKuwFVJ3JHFYFn7F2782G2Mv2hW8WPePqgdhpnjGaqkYtSVWnyCESZhGXUmY3/bEg==", 132 + "license": "MIT" 133 + }, 134 + "node_modules/linebreak": { 135 + "version": "1.1.0", 136 + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", 137 + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", 138 + "license": "MIT", 139 + "dependencies": { 140 + "base64-js": "0.0.8", 141 + "unicode-trie": "^2.0.0" 142 + } 143 + }, 144 + "node_modules/pako": { 145 + "version": "0.2.9", 146 + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", 147 + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", 148 + "license": "MIT" 149 + }, 150 + "node_modules/parse-css-color": { 151 + "version": "0.2.1", 152 + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", 153 + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", 154 + "license": "MIT", 155 + "dependencies": { 156 + "color-name": "^1.1.4", 157 + "hex-rgb": "^4.1.0" 158 + } 159 + }, 160 + "node_modules/postcss-value-parser": { 161 + "version": "4.2.0", 162 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 163 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 164 + "license": "MIT" 165 + }, 166 + "node_modules/string.prototype.codepointat": { 167 + "version": "0.2.1", 168 + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", 169 + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", 170 + "license": "MIT" 171 + }, 172 + "node_modules/tiny-inflate": { 173 + "version": "1.0.3", 174 + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", 175 + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", 176 + "license": "MIT" 177 + }, 178 + "node_modules/unicode-trie": { 179 + "version": "2.0.0", 180 + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", 181 + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", 182 + "license": "MIT", 183 + "dependencies": { 184 + "pako": "^0.2.5", 185 + "tiny-inflate": "^1.0.0" 186 + } 187 + }, 188 + "node_modules/workers-og": { 189 + "version": "0.0.27", 190 + "resolved": "https://registry.npmjs.org/workers-og/-/workers-og-0.0.27.tgz", 191 + "integrity": "sha512-QvwptQ0twmouQHiITUi3kYxEPCLdueC/U4msQ2xMz2iktd+iseSs7zlREw3T1dAsPxPw73FQlw8cXFsfANZPlw==", 192 + "dependencies": { 193 + "@resvg/resvg-wasm": "2.4.0", 194 + "just-camel-case": "^6.2.0", 195 + "satori": "^0.15.2", 196 + "yoga-wasm-web": "0.3.3" 197 + } 198 + }, 199 + "node_modules/workers-og/node_modules/css-gradient-parser": { 200 + "version": "0.0.16", 201 + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", 202 + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", 203 + "license": "MIT", 204 + "engines": { 205 + "node": ">=16" 206 + } 207 + }, 208 + "node_modules/workers-og/node_modules/satori": { 209 + "version": "0.15.2", 210 + "resolved": "https://registry.npmjs.org/satori/-/satori-0.15.2.tgz", 211 + "integrity": "sha512-vu/49vdc8MzV5jUchs3TIRDCOkOvMc1iJ11MrZvhg9tE4ziKIEIBjBZvies6a9sfM2vQ2gc3dXeu6rCK7AztHA==", 212 + "license": "MPL-2.0", 213 + "dependencies": { 214 + "@shuding/opentype.js": "1.4.0-beta.0", 215 + "css-background-parser": "^0.1.0", 216 + "css-box-shadow": "1.0.0-3", 217 + "css-gradient-parser": "^0.0.16", 218 + "css-to-react-native": "^3.0.0", 219 + "emoji-regex-xs": "^2.0.1", 220 + "escape-html": "^1.0.3", 221 + "linebreak": "^1.1.0", 222 + "parse-css-color": "^0.2.1", 223 + "postcss-value-parser": "^4.2.0", 224 + "yoga-wasm-web": "^0.3.3" 225 + }, 226 + "engines": { 227 + "node": ">=16" 228 + } 229 + }, 230 + "node_modules/yoga-wasm-web": { 231 + "version": "0.3.3", 232 + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", 233 + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", 234 + "license": "MIT" 235 + } 236 + } 237 + }
+8
opengraph/package.json
··· 1 + { 2 + "name": "atbbs-opengraph", 3 + "version": "1.0.0", 4 + "private": true, 5 + "dependencies": { 6 + "workers-og": "^0.0.27" 7 + } 8 + }
+15
opengraph/wrangler.toml
··· 1 + name = "atbbs-opengraph" 2 + main = "index.ts" 3 + compatibility_date = "2024-01-01" 4 + 5 + rules = [ 6 + { type = "Text", globs = ["**/*.svg"] } 7 + ] 8 + 9 + [[routes]] 10 + pattern = "*atbbs.xyz/bbs/*" 11 + zone_name = "atbbs.xyz" 12 + 13 + [[routes]] 14 + pattern = "*atbbs.xyz/og/*" 15 + zone_name = "atbbs.xyz"
web/public/og.png

This is a binary file and will not be displayed.