From 497ed71c29cad820445af6210c2ba4f82f6d5378 Mon Sep 17 00:00:00 2001 From: Elia <83713217+eliahilse@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:53:44 +0200 Subject: [PATCH 1/2] feat: og image and seo metadata Build-time OG card rendering the same domain-warped field as the live background, sampled per character cell into the site's ASCII ramp and set in Geist Pixel Square, with the middle cleared for the wordmark. Adds full metadata (openGraph, twitter, canonical, robots, keywords), a sitemap, robots.txt, and a pixel favicon. Two things the font needed: geist's exports map blocks deep paths under Node, so the woff2 is read by path; and takumi ignores a raw Buffer, so it is passed as FontDetails over a Uint8Array. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CQy3ZJ5MyUxo4qjZwA93Na --- apps/web/app/icon.svg | 11 +++ apps/web/app/layout.tsx | 47 ++++++++++- apps/web/app/og/route.tsx | 163 ++++++++++++++++++++++++++++++++++++++ apps/web/app/robots.ts | 11 +++ apps/web/app/sitemap.ts | 13 +++ apps/web/next.config.js | 4 +- apps/web/package.json | 1 + bun.lock | 1 + 8 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 apps/web/app/icon.svg create mode 100644 apps/web/app/og/route.tsx create mode 100644 apps/web/app/robots.ts create mode 100644 apps/web/app/sitemap.ts diff --git a/apps/web/app/icon.svg b/apps/web/app/icon.svg new file mode 100644 index 0000000..f90e0da --- /dev/null +++ b/apps/web/app/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 7b86657..afb6ba4 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -2,10 +2,51 @@ import type { Metadata } from "next"; import { GeistPixelSquare } from "geist/font/pixel"; import "./globals.css"; +const title = "Kyora — unlocking the full potential of coding agents"; +const description = + "Queryable runtime state, multi-model code review, and councils of other model families — on the coding subscriptions you already pay for."; + export const metadata: Metadata = { - title: "kyora — unlocking the full potential of coding agents", - description: - "Queryable runtime state, multi-model code review, and councils of other model families — on the coding subscriptions you already pay for.", + metadataBase: new URL("https://kyora.sh"), + title: { + default: title, + template: "%s — Kyora", + }, + description, + applicationName: "Kyora", + keywords: [ + "coding agents", + "AI code review", + "multi-model code review", + "runtime observability", + "MCP server", + "Claude Code", + "Codex", + "agent tooling", + ], + authors: [{ name: "Elia Hilse", url: "https://x.com/eliahilse" }], + creator: "Elia Hilse", + alternates: { canonical: "/" }, + openGraph: { + type: "website", + url: "https://kyora.sh", + siteName: "Kyora", + title, + description, + images: [{ url: "/og", width: 1200, height: 630, alt: title }], + }, + twitter: { + card: "summary_large_image", + title, + description, + creator: "@eliahilse", + images: ["/og"], + }, + robots: { + index: true, + follow: true, + googleBot: { index: true, follow: true, "max-image-preview": "large" }, + }, }; export default function RootLayout({ diff --git a/apps/web/app/og/route.tsx b/apps/web/app/og/route.tsx new file mode 100644 index 0000000..4c96d2b --- /dev/null +++ b/apps/web/app/og/route.tsx @@ -0,0 +1,163 @@ +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; +const ROWS = 27; +const CELL = 24; + +function hash(x: number, y: number): number { + const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453123; + return n - Math.floor(n); +} + +function noise(x: number, y: number): number { + const ix = Math.floor(x); + const iy = Math.floor(y); + const fx = x - ix; + const fy = y - iy; + const ux = fx * fx * (3 - 2 * fx); + const uy = fy * fy * (3 - 2 * fy); + const a = hash(ix, iy); + const b = hash(ix + 1, iy); + const c = hash(ix, iy + 1); + const d = hash(ix + 1, iy + 1); + return (a + (b - a) * ux) * (1 - uy) + (c + (d - c) * ux) * uy; +} + +function fbm(x: number, y: number): number { + let value = 0; + let amplitude = 0.5; + let px = x; + let py = y; + for (let i = 0; i < 5; i++) { + value += amplitude * noise(px, py); + px *= 2.02; + py *= 2.02; + amplitude *= 0.5; + } + return value; +} + +/** Same domain-warped field as the live background, sampled once per character cell. */ +function fieldAt(col: number, row: number): number { + const x = (col / COLS) * 6.4; + const y = (row / ROWS) * 3.4; + const qx = fbm(x, y); + const qy = fbm(x + 5.2, y + 1.3); + const rx = fbm(x + 3.4 * qx + 1.7, y + 3.4 * qy + 9.2); + const ry = fbm(x + 3.4 * qx + 8.3, y + 3.4 * qy + 2.8); + const field = fbm(x + 3.4 * rx, y + 3.4 * ry); + + const dx = (col / COLS - 0.5) * 1.9; + const dy = row / ROWS - 0.5; + const distance = Math.sqrt(dx * dx + dy * dy); + const clearing = Math.min(1, Math.max(0, (distance - 0.26) / 0.38)); + + return Math.min(1, Math.max(0, (field - 0.34) * 2.6)) * clearing; +} + +const FONT_PATH = "geist/dist/fonts/geist-pixel/GeistPixel-Square.woff2"; + +/** geist's exports map blocks deep paths under Node, so read the file directly. */ +function pixelFont(): Uint8Array | null { + for (const base of ["node_modules", "../../node_modules", "../node_modules"]) { + const candidate = join(process.cwd(), base, FONT_PATH); + if (existsSync(candidate)) return new Uint8Array(readFileSync(candidate)); + } + return null; +} + +export function GET() { + const cells = []; + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const intensity = fieldAt(col, row); + if (intensity < 0.06) continue; + const index = Math.min(GLYPHS.length - 1, Math.floor(intensity * GLYPHS.length)); + const glyph = GLYPHS[index]!; + if (glyph === " ") continue; + cells.push( +
+ {glyph} +
, + ); + } + } + + const font = pixelFont(); + + return new ImageResponse( + ( +
+
{cells}
+
+
Kyora
+
+ Unlocking the full potential of coding agents. +
+
+ {["state", "review", "council"].map((name) => ( +
+ {name} +
+ ))} +
+
+
+ ), + { + width: 1200, + height: 630, + format: "png", + ...(font ? { fonts: [{ name: "KyoraPixel", data: font }] } : {}), + }, + ); +} diff --git a/apps/web/app/robots.ts b/apps/web/app/robots.ts new file mode 100644 index 0000000..3895915 --- /dev/null +++ b/apps/web/app/robots.ts @@ -0,0 +1,11 @@ +import type { MetadataRoute } from "next"; + +export const dynamic = "force-static"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { userAgent: "*", allow: "/" }, + sitemap: "https://kyora.sh/sitemap.xml", + host: "https://kyora.sh", + }; +} diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts new file mode 100644 index 0000000..008cbbe --- /dev/null +++ b/apps/web/app/sitemap.ts @@ -0,0 +1,13 @@ +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/next.config.js b/apps/web/next.config.js index a42659c..6acc8d7 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,7 +1,9 @@ import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + serverExternalPackages: ["@takumi-rs/image-response"], +}; initOpenNextCloudflareForDev(); diff --git a/apps/web/package.json b/apps/web/package.json index 74b77a2..7401432 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@opennextjs/cloudflare": "latest", + "@takumi-rs/image-response": "^0.70.4", "geist": "^1.7.0", "next": "16.1.5", "react": "^19.2.0", diff --git a/bun.lock b/bun.lock index bd02a43..9a2f289 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,7 @@ "version": "0.1.0", "dependencies": { "@opennextjs/cloudflare": "latest", + "@takumi-rs/image-response": "^0.70.4", "geist": "^1.7.0", "next": "16.1.5", "react": "^19.2.0", From 92784004f9071827c07b123bedd229de2ade1958 Mon Sep 17 00:00:00 2001 From: Elia <83713217+eliahilse@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:57:03 +0200 Subject: [PATCH 2/2] fix: scale up og type for small-screen legibility Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CQy3ZJ5MyUxo4qjZwA93Na --- apps/web/app/og/route.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/app/og/route.tsx b/apps/web/app/og/route.tsx index 4c96d2b..65ea7c5 100644 --- a/apps/web/app/og/route.tsx +++ b/apps/web/app/og/route.tsx @@ -129,20 +129,20 @@ export function GET() { gap: 24, }} > -
Kyora
-
+
Kyora
+
Unlocking the full potential of coding agents.
-
+
{["state", "review", "council"].map((name) => (