From 16f6a54651a31f2a4955363b6128b2af6267b185 Mon Sep 17 00:00:00 2001 From: Elia <83713217+eliahilse@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:09:39 +0200 Subject: [PATCH] fix: serve og image, sitemap and icon as static assets The og route rendered fine at build time but 404'd in production: takumi's native binary cannot run on Workers, and OpenNext does not ship a prerendered route-handler body as an asset. The card is now generated into public/ by a build script instead, and sitemap and icon moved to public/ for the same reason. Adds a guard so a missing pixel font fails the build rather than shipping a fallback face. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CQy3ZJ5MyUxo4qjZwA93Na --- apps/web/.gitignore | 3 +++ apps/web/app/layout.tsx | 5 +++-- apps/web/app/sitemap.ts | 13 ------------- apps/web/package.json | 6 +++--- apps/web/{app => public}/icon.svg | 0 apps/web/public/sitemap.xml | 8 ++++++++ .../{app/og/route.tsx => scripts/generate-og.tsx} | 12 +++++++++--- apps/web/tsconfig.json | 2 +- 8 files changed, 27 insertions(+), 22 deletions(-) delete mode 100644 apps/web/app/sitemap.ts rename apps/web/{app => public}/icon.svg (100%) create mode 100644 apps/web/public/sitemap.xml rename apps/web/{app/og/route.tsx => scripts/generate-og.tsx} (91%) diff --git a/apps/web/.gitignore b/apps/web/.gitignore index c4f602e..a2b88c2 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -37,3 +37,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# generated at build time by scripts/generate-og.tsx +/public/og.png diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index afb6ba4..645163e 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -33,15 +33,16 @@ export const metadata: Metadata = { siteName: "Kyora", title, description, - images: [{ url: "/og", width: 1200, height: 630, alt: title }], + images: [{ url: "/og.png", width: 1200, height: 630, alt: title }], }, twitter: { card: "summary_large_image", title, description, creator: "@eliahilse", - images: ["/og"], + images: ["/og.png"], }, + icons: { icon: "/icon.svg" }, robots: { index: true, follow: true, diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts deleted file mode 100644 index 008cbbe..0000000 --- a/apps/web/app/sitemap.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { MetadataRoute } from "next"; - -export const dynamic = "force-static"; - -export default function sitemap(): MetadataRoute.Sitemap { - return [ - { - url: "https://kyora.sh", - changeFrequency: "weekly", - priority: 1, - }, - ]; -} diff --git a/apps/web/package.json b/apps/web/package.json index 7401432..50c6ac9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,12 +5,12 @@ "private": true, "scripts": { "dev": "next dev --port 3000", - "build": "next build", + "build": "bun scripts/generate-og.tsx && next build", "start": "next start", "lint": "eslint --max-warnings 0", "check-types": "next typegen && tsc --noEmit", - "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", - "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", + "preview": "bun scripts/generate-og.tsx && opennextjs-cloudflare build && opennextjs-cloudflare preview", + "deploy": "bun scripts/generate-og.tsx && opennextjs-cloudflare build && opennextjs-cloudflare deploy", "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts" }, "dependencies": { diff --git a/apps/web/app/icon.svg b/apps/web/public/icon.svg similarity index 100% rename from apps/web/app/icon.svg rename to apps/web/public/icon.svg diff --git a/apps/web/public/sitemap.xml b/apps/web/public/sitemap.xml new file mode 100644 index 0000000..91ca544 --- /dev/null +++ b/apps/web/public/sitemap.xml @@ -0,0 +1,8 @@ + + + + https://kyora.sh/ + weekly + 1.0 + + diff --git a/apps/web/app/og/route.tsx b/apps/web/scripts/generate-og.tsx similarity index 91% rename from apps/web/app/og/route.tsx rename to apps/web/scripts/generate-og.tsx index 65ea7c5..42fcdec 100644 --- a/apps/web/app/og/route.tsx +++ b/apps/web/scripts/generate-og.tsx @@ -2,8 +2,6 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { ImageResponse } from "@takumi-rs/image-response"; -export const dynamic = "force-static"; -export const revalidate = false; const GLYPHS = " .·:-=+*ox#%@"; const COLS = 50; @@ -62,6 +60,7 @@ function fieldAt(col: number, row: number): number { } const FONT_PATH = "geist/dist/fonts/geist-pixel/GeistPixel-Square.woff2"; +const OUT = join(import.meta.dir, "..", "public", "og.png"); /** geist's exports map blocks deep paths under Node, so read the file directly. */ function pixelFont(): Uint8Array | null { @@ -72,7 +71,7 @@ function pixelFont(): Uint8Array | null { return null; } -export function GET() { +function card() { const cells = []; for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { @@ -161,3 +160,10 @@ export function GET() { }, ); } + +const response = card(); +const bytes = new Uint8Array(await response.arrayBuffer()); +if (bytes.length < 1000) throw new Error(`og render produced ${bytes.length} bytes`); +if (!pixelFont()) throw new Error("pixel font not found — og would render in a fallback face"); +await Bun.write(OUT, bytes); +console.log(`wrote ${OUT} (${bytes.length} bytes)`); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 7aef056..b0c6014 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -14,7 +14,7 @@ "next.config.js", ".next/types/**/*.ts" ], - "exclude": [ + "exclude": ["scripts", "node_modules" ] }