From fbef2414fd20ffe07d4bd0c23458121577d0701e Mon Sep 17 00:00:00 2001 From: crush Date: Sun, 15 Feb 2026 01:22:49 +0000 Subject: [PATCH 01/21] feat: refresh social and apple visuals --- packages/web/app/apple-icon.png | Bin 0 -> 1808 bytes packages/web/app/og/page.tsx | 37 ++++++++++++++++++++++++++++++++ packages/web/public/og.png | Bin 0 -> 36851 bytes 3 files changed, 37 insertions(+) create mode 100644 packages/web/app/apple-icon.png create mode 100644 packages/web/app/og/page.tsx create mode 100644 packages/web/public/og.png diff --git a/packages/web/app/apple-icon.png b/packages/web/app/apple-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5d79508cb8501f55313f37cd485fa377f30caa09 GIT binary patch literal 1808 zcmbW2X*ipS7RRG{F`5yrrE(h$P0M4eiox6JysoVYGgTR;s%jUb%GgRR34&(AOW#68*+@}#?Fa@ovj*jmBFj!)FQ9jF6(O&bW-3h&nn;CVS zLk!P?1`CBFt=xQT0!8~tXzQ;*Ehq236X}zb#QMt-rXz#Zws$AWEha5@7qHlgn%f;(=l&Mos_A6tyYhLX)#WQ+ z=sWIfUy9|NsBCbli0ms2n{j>UDMa;7(t=m7gxB0=;+#dH30?!&!xl@^l2R1faP|Q# ziFK!Dpl~Fa6JL}7yFWeJYDr~1Z5^r74BcG#@VdA9{PzPtH`H9_d8R4Add{5!;n!#| z*gt-9akBSJEMI=b5BL3ACtLj6guQZlobI9H_8tvC+6<;&bnZ$AmXsq;s0`bxe`Abi zXg4b2GUk-kP_O@WU2nWFWYB~mRmECn95`G&SSjyqK5h7RH?fkiB&g*;=LZBSEZIT` z)}rQW#b?J30(d30+v%AhGkVDX!FtD|-d>{*e4Y~errJMsH}ab;sutBGJJOGJ>fQ7a zHM;J9oduOj7hgWGLn$%Z`YA0#rfp7o3ihtF)^sZA^P(Cqr6IGG&}mNX|2~tw<0!~ zP0s784%z%=r^L9{X0%%eRc|v|l;@|5whY;vaA*^@9K5yDHk?-U%&#!@v#qr|jzlK7 zz0PfL){%N>qx*^D<1$ncdHYTvJ1XPF=0S(7qz8_d`8ZAF;zE~?E;>D=q^iC}i}dt% zVAIi;BI!Z5tfZ7G3>~6ipUv{X9n$`yKyZ3LZvoQ;kCT-JsG+NgC88VnS{daeyZmtg zY7DmzxhP+eP{7z8hPsbN&<-BMu37fUnZp#o4>0fbK88ZDrlVAw0dS+25k09-evda| zA6qSYw}_#`4|LEbAm}O9boFEk0*vJ`1~EF^_jq%5&45TpSepU=;#8+_o2*F-JZ&ID z)k9~nru^~xc`?B?i&*wadj$GL$fcI_7@maSo+Z3%@BogsB3|kv6#VdxaPiz*u}8g| zcgMGpYaiPl%b%6rLW-ERE9DQ<1SvwHsJ?Rj#n^OJ%4~!;vREsdzY;88+cos6DoJMQ zc3l4$f7d$*S*(-I7diHpkQ?s!WC^)}lAT26FpsebM4SeVdl;%f#1NRG)Q2Mj$Jgvn^)S(Cg|obYo(k!(T+TR6B_>#L`<~m8?Z{PD9 zA+WWU)1CP5i#)YohK|uNjg)kb#D~>=TjiD1-lC3_PRoKir9BTPkIwZe%<9 z7G`xyi~>K*Q9v4)j;IG>Np!sq1O#BooMTMH4Pb*jeY^?b545E27&IVoKx~?ZQ4PWz zFeaIqlp_uUZ%8<^Vn_`9LJl;~hq{39)D(+chz1y^6yrrJXGcC1q@KKRFL6 z*pM|=uvjdl;dA6x(h{kp-97U62&drah-m>UQCPM?2v`4=zd2OgVCLy_^2iXQ`9dGg z3dy<$a{Cy~mR(1s1dnrT3TQTQd_6rzne16S^l*)x3714?Gi3m)+3(7^`{oWZ^iuTR z?Y(sMQ9)drJzm-O)pvi`W25uosN;&tz5mg+$M60;#B}@8Km${z?e%| + +
+
+
+ +
+
+
chaos engineering
+

+ cruel +

+

+ chaos testing with zero mercy +

+

+ inject failures, latency, and timeouts into any async function, model, provider, or + tool before production. +

+
+
+
cruel.dev
+
+ $ + bun add cruel +
+
+
+
+ + ) +} diff --git a/packages/web/public/og.png b/packages/web/public/og.png new file mode 100644 index 0000000000000000000000000000000000000000..a044d2a9f2458fbe85b9b520172775d629c7f42e GIT binary patch literal 36851 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 literal 0 HcmV?d00001 From 7380cda532a714c16065564afbc8a12400077c87 Mon Sep 17 00:00:00 2001 From: crush Date: Sun, 15 Feb 2026 01:22:55 +0000 Subject: [PATCH 02/21] feat: add seo metadata routes --- packages/web/app/layout.tsx | 21 +++++++++++++++++++-- packages/web/app/robots.ts | 11 +++++++++++ packages/web/app/sitemap.ts | 20 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/web/app/robots.ts create mode 100644 packages/web/app/sitemap.ts diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx index e80611a..4dd83d1 100644 --- a/packages/web/app/layout.tsx +++ b/packages/web/app/layout.tsx @@ -1,26 +1,43 @@ import { RootProvider } from "fumadocs-ui/provider/next" import { GeistMono } from "geist/font/mono" -import type { Metadata } from "next" +import type { Metadata, Viewport } from "next" import "./globals.css" export const metadata: Metadata = { title: "cruel", description: "chaos testing with zero mercy", metadataBase: new URL("https://cruel.dev"), + icons: { + icon: "/icon.svg", + apple: "/apple-icon.png", + }, openGraph: { title: "cruel", description: "chaos testing with zero mercy", url: "https://cruel.dev", siteName: "cruel", type: "website", + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "cruel", + }, + ], }, twitter: { - card: "summary", + card: "summary_large_image", title: "cruel", description: "chaos testing with zero mercy", + images: ["/og.png"], }, } +export const viewport: Viewport = { + themeColor: "#0a0a0a", +} + export default function Layout({ children }: { children: React.ReactNode }) { return ( diff --git a/packages/web/app/robots.ts b/packages/web/app/robots.ts new file mode 100644 index 0000000..6f99087 --- /dev/null +++ b/packages/web/app/robots.ts @@ -0,0 +1,11 @@ +import type { MetadataRoute } from "next" + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: "https://cruel.dev/sitemap.xml", + } +} diff --git a/packages/web/app/sitemap.ts b/packages/web/app/sitemap.ts new file mode 100644 index 0000000..e91f7b7 --- /dev/null +++ b/packages/web/app/sitemap.ts @@ -0,0 +1,20 @@ +import type { MetadataRoute } from "next" +import { source } from "@/lib/source" + +export default function sitemap(): MetadataRoute.Sitemap { + const base = "https://cruel.dev" + const time = new Date() + const set = new Set([`${base}/`, `${base}/docs`, `${base}/story`]) + + for (const item of source.generateParams()) { + const slug = item.slug?.join("/") + set.add(slug ? `${base}/docs/${slug}` : `${base}/docs`) + } + + return Array.from(set).map((url) => ({ + url, + lastModified: time, + changeFrequency: "weekly", + priority: url === `${base}/` ? 1 : 0.8, + })) +} From e745d58585729294f972f67e938161197573e117 Mon Sep 17 00:00:00 2001 From: crush Date: Sun, 15 Feb 2026 01:23:03 +0000 Subject: [PATCH 03/21] feat: add agent docs endpoints --- packages/web/app/docs.md/route.ts | 48 ++++++++++++++++++++++++++++ packages/web/app/llms.txt/route.ts | 51 ++++++++++++++++++++++++++++++ packages/web/app/page.tsx | 28 ++++++++++++++-- 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 packages/web/app/docs.md/route.ts create mode 100644 packages/web/app/llms.txt/route.ts diff --git a/packages/web/app/docs.md/route.ts b/packages/web/app/docs.md/route.ts new file mode 100644 index 0000000..b521787 --- /dev/null +++ b/packages/web/app/docs.md/route.ts @@ -0,0 +1,48 @@ +import { readFile, readdir } from "node:fs/promises" +import { join } from "node:path" + +function roots(): string[] { + return [join(process.cwd(), "content/docs"), join(process.cwd(), "packages/web/content/docs")] +} + +async function files(): Promise { + for (const root of roots()) { + try { + const list = await readdir(root) + const out = list.filter((item) => item.endsWith(".mdx")).sort() + if (out.length > 0) return out.map((item) => join(root, item)) + } catch {} + } + + return [] +} + +function clean(text: string): string { + return text.replace(/^---[\s\S]*?---\s*/m, "").trim() +} + +function name(path: string): string { + const item = path.split("/").pop() ?? "doc" + return item.replace(/\.mdx$/, "") +} + +export async function GET(): Promise { + const paths = await files() + const chunks: string[] = ["# cruel docs", ""] + + for (const path of paths) { + const raw = await readFile(path, "utf8") + const key = name(path) + const title = key === "index" ? "getting started" : key + chunks.push(`## ${title}`) + chunks.push("") + chunks.push(clean(raw)) + chunks.push("") + } + + return new Response(chunks.join("\n"), { + headers: { + "content-type": "text/markdown; charset=utf-8", + }, + }) +} diff --git a/packages/web/app/llms.txt/route.ts b/packages/web/app/llms.txt/route.ts new file mode 100644 index 0000000..45536a0 --- /dev/null +++ b/packages/web/app/llms.txt/route.ts @@ -0,0 +1,51 @@ +import { readdir } from "node:fs/promises" +import { join } from "node:path" + +function roots(): string[] { + return [join(process.cwd(), "content/docs"), join(process.cwd(), "packages/web/content/docs")] +} + +async function pages(): Promise { + for (const root of roots()) { + try { + const list = await readdir(root) + return list + .filter((item) => item.endsWith(".mdx")) + .map((item) => item.replace(/\.mdx$/, "")) + .sort() + } catch {} + } + + return [] +} + +function url(name: string): string { + if (name === "index") return "https://cruel.dev/docs" + return `https://cruel.dev/docs/${name}` +} + +export async function GET(): Promise { + const list = await pages() + const lines: string[] = [ + "project: cruel", + "site: https://cruel.dev", + "summary: chaos engineering for ai sdk and async apis", + "", + "docs_markdown: https://cruel.dev/docs.md", + "docs_root: https://cruel.dev/docs", + "", + "docs_pages:", + ] + + for (const item of list) { + lines.push(`- ${url(item)}`) + } + + lines.push("") + + return new Response(lines.join("\n"), { + headers: { + "content-type": "text/plain; charset=utf-8", + }, + }) +} diff --git a/packages/web/app/page.tsx b/packages/web/app/page.tsx index 2548240..3ae5101 100644 --- a/packages/web/app/page.tsx +++ b/packages/web/app/page.tsx @@ -1,3 +1,5 @@ +import { readFile } from "node:fs/promises" +import { join } from "node:path" import Link from "next/link" const code = `import { cruel } from "cruel" @@ -10,7 +12,29 @@ const api = cruel(fetch, { const res = await api("https://api.example.com")` -export default function Page() { +async function version(): Promise { + const paths = [ + join(process.cwd(), "../cruel/package.json"), + join(process.cwd(), "packages/cruel/package.json"), + ] + + for (const path of paths) { + try { + const file = await readFile(path, "utf8") + const data: unknown = JSON.parse(file) + if (typeof data === "object" && data !== null) { + const value = Reflect.get(data, "version") + if (typeof value === "string" && value.length > 0) return `v${value}` + } + } catch {} + } + + return "v0.0.0" +} + +export default async function Page() { + const tag = await version() + return (
@@ -81,7 +105,7 @@ export default function Page() {
-
v1.0.1
+
{tag}
$ bun add cruel From 37ee024e2f5c387615f04d616448a07870da25bc Mon Sep 17 00:00:00 2001 From: crush Date: Sun, 15 Feb 2026 01:23:40 +0000 Subject: [PATCH 04/21] chore: apply biome formatting --- packages/web/app/docs.md/route.ts | 2 +- packages/web/app/og/page.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/web/app/docs.md/route.ts b/packages/web/app/docs.md/route.ts index b521787..d825e25 100644 --- a/packages/web/app/docs.md/route.ts +++ b/packages/web/app/docs.md/route.ts @@ -1,4 +1,4 @@ -import { readFile, readdir } from "node:fs/promises" +import { readdir, readFile } from "node:fs/promises" import { join } from "node:path" function roots(): string[] { diff --git a/packages/web/app/og/page.tsx b/packages/web/app/og/page.tsx index 1975648..c6dc490 100644 --- a/packages/web/app/og/page.tsx +++ b/packages/web/app/og/page.tsx @@ -11,7 +11,9 @@ export default function Page() {
-
chaos engineering
+
+ chaos engineering +

cruel

From 2e6d5784b4efd80c344aa9882f5fd0c60c1a83c6 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:32:16 +0000 Subject: [PATCH 05/21] feat: improve docs toc controls and install tabs --- packages/web/app/docs/[[...slug]]/page.tsx | 36 +++++++++++++- packages/web/components/copy.tsx | 55 ++++++++++++++++++++++ packages/web/content/docs/index.mdx | 24 ++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 packages/web/components/copy.tsx diff --git a/packages/web/app/docs/[[...slug]]/page.tsx b/packages/web/app/docs/[[...slug]]/page.tsx index ca14884..732b8ff 100644 --- a/packages/web/app/docs/[[...slug]]/page.tsx +++ b/packages/web/app/docs/[[...slug]]/page.tsx @@ -1,9 +1,37 @@ +import { readFile } from "node:fs/promises" +import { join } from "node:path" import defaultMdxComponents from "fumadocs-ui/mdx" import { DocsBody, DocsPage } from "fumadocs-ui/page" import type { MDXContent } from "mdx/types" import { notFound } from "next/navigation" +import { Copy } from "@/components/copy" import { source } from "@/lib/source" +function clean(text: string): string { + return text.replace(/^---[\s\S]*?---\s*/m, "").trim() +} + +function name(slug?: string[]): string { + if (!slug || slug.length === 0) return "index" + return slug.join("/") +} + +async function markdown(slug?: string[]): Promise { + const file = `${name(slug)}.mdx` + const paths = [ + join(process.cwd(), "content/docs", file), + join(process.cwd(), "packages/web/content/docs", file), + ] + + for (const path of paths) { + try { + return clean(await readFile(path, "utf8")) + } catch {} + } + + return "" +} + export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params const page = source.getPage(params.slug) @@ -17,9 +45,15 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }> const resolved = data.load ? await data.load() : data const MDX = resolved.body + const text = await markdown(params.slug) return ( - + , + }} + >

{page.data.title}

{page.data.description}

diff --git a/packages/web/components/copy.tsx b/packages/web/components/copy.tsx new file mode 100644 index 0000000..1a2893e --- /dev/null +++ b/packages/web/components/copy.tsx @@ -0,0 +1,55 @@ +"use client" + +import { ArrowUp, Check, Copy as Duplicate } from "lucide-react" +import { useState } from "react" + +type copyprops = { + text: string +} + +export function Copy({ text }: copyprops) { + const [copied, setcopied] = useState(false) + + async function copy() { + if (!text) return + await navigator.clipboard.writeText(text) + setcopied(true) + setTimeout(() => setcopied(false), 1200) + } + + function top() { + const page = document.getElementById("nd-page") + if (page) { + page.scrollTo({ top: 0, behavior: "smooth" }) + return + } + window.scrollTo({ top: 0, behavior: "smooth" }) + } + + return ( +
+ + +
+ ) +} diff --git a/packages/web/content/docs/index.mdx b/packages/web/content/docs/index.mdx index 18b0238..4c604c4 100644 --- a/packages/web/content/docs/index.mdx +++ b/packages/web/content/docs/index.mdx @@ -9,14 +9,38 @@ for the base `cruel(...)` function wrappers (fetch/services/core api), see the [ ## install + + + bun + npm + pnpm + + + + ```bash bun add cruel ``` + + + + ```bash npm install cruel ``` + + + + +```bash +pnpm add cruel +``` + + + + ## wrap a model ```ts From dbd189b40bf2a8c642c1392a4792dcacaa5ca7dd Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:33:18 +0000 Subject: [PATCH 06/21] feat: enable clerk toc style --- packages/web/app/docs/[[...slug]]/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/web/app/docs/[[...slug]]/page.tsx b/packages/web/app/docs/[[...slug]]/page.tsx index 732b8ff..fb6309a 100644 --- a/packages/web/app/docs/[[...slug]]/page.tsx +++ b/packages/web/app/docs/[[...slug]]/page.tsx @@ -51,8 +51,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }> , }} + tableOfContentPopover={{ + style: "clerk", + }} >

{page.data.title}

From 3389339cde6e6b8e5057cd1294c6a915f630bfbc Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:37:45 +0000 Subject: [PATCH 07/21] fix: refine clerk toc styling --- packages/web/app/docs/docs.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 6f8c00e..8462105 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -23,8 +23,7 @@ left: 20px !important; } -.docs-panel [data-radix-scroll-area-viewport], -.docs-panel [style] { +.docs-panel [data-radix-scroll-area-viewport] { mask-image: none !important; -webkit-mask-image: none !important; } @@ -35,6 +34,10 @@ -webkit-mask: none !important; } +.docs-panel #nd-toc [style*="--fd-top"] { + background: rgba(255, 255, 255, 0.22) !important; +} + [data-state="open"][class*="backdrop-blur"], [data-state="closed"][class*="backdrop-blur"] { background: rgba(0, 0, 0, 0.6) !important; From b4f20e12dd87ba3519317377fb449c3971cf3936 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:01:28 +0000 Subject: [PATCH 08/21] fix: improve docs anchor and toc actions --- packages/web/app/docs/docs.css | 16 ++++++++++++++++ packages/web/components/copy.tsx | 26 +++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 8462105..7d33008 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -38,6 +38,22 @@ background: rgba(255, 255, 255, 0.22) !important; } +.docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer { + display: inline-flex; + align-items: center; + padding-inline-end: 14px; + margin-inline-end: -14px; +} + +.docs-panel .prose :is(h2, h3, h4, h5, h6) > svg { + pointer-events: none; +} + +.docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg, +.docs-panel .prose :is(h2, h3, h4, h5, h6):focus-within > svg { + opacity: 1 !important; +} + [data-state="open"][class*="backdrop-blur"], [data-state="closed"][class*="backdrop-blur"] { background: rgba(0, 0, 0, 0.6) !important; diff --git a/packages/web/components/copy.tsx b/packages/web/components/copy.tsx index 1a2893e..bee24af 100644 --- a/packages/web/components/copy.tsx +++ b/packages/web/components/copy.tsx @@ -1,6 +1,6 @@ "use client" -import { ArrowUp, Check, Copy as Duplicate } from "lucide-react" +import { ArrowUp, Check, Copy as Duplicate, Link } from "lucide-react" import { useState } from "react" type copyprops = { @@ -9,6 +9,7 @@ type copyprops = { export function Copy({ text }: copyprops) { const [copied, setcopied] = useState(false) + const [linked, setlinked] = useState(false) async function copy() { if (!text) return @@ -26,6 +27,12 @@ export function Copy({ text }: copyprops) { window.scrollTo({ top: 0, behavior: "smooth" }) } + async function link() { + await navigator.clipboard.writeText(window.location.href) + setlinked(true) + setTimeout(() => setlinked(false), 1200) + } + return (
+
) } From d7ec064fc2bf7b7b44f08afd81795cfd1c00aa99 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:04:17 +0000 Subject: [PATCH 09/21] feat: copy heading links with tick feedback --- packages/web/app/docs/[[...slug]]/page.tsx | 2 + packages/web/app/docs/docs.css | 16 ++++++++ packages/web/components/anchor.tsx | 44 ++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 packages/web/components/anchor.tsx diff --git a/packages/web/app/docs/[[...slug]]/page.tsx b/packages/web/app/docs/[[...slug]]/page.tsx index fb6309a..0283404 100644 --- a/packages/web/app/docs/[[...slug]]/page.tsx +++ b/packages/web/app/docs/[[...slug]]/page.tsx @@ -4,6 +4,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx" import { DocsBody, DocsPage } from "fumadocs-ui/page" import type { MDXContent } from "mdx/types" import { notFound } from "next/navigation" +import { Anchor } from "@/components/anchor" import { Copy } from "@/components/copy" import { source } from "@/lib/source" @@ -58,6 +59,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }> style: "clerk", }} > +

{page.data.title}

{page.data.description}

diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 7d33008..fc5c750 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -41,6 +41,7 @@ .docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer { display: inline-flex; align-items: center; + position: relative; padding-inline-end: 14px; margin-inline-end: -14px; } @@ -49,6 +50,21 @@ pointer-events: none; } +.docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer[data-copied="true"] + svg { + opacity: 0 !important; +} + +.docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer[data-copied="true"]::after { + content: "✓"; + position: absolute; + inset-inline-end: -14px; + top: 50%; + transform: translateY(-50%); + color: rgba(255, 255, 255, 0.82); + font-size: 14px; + line-height: 1; +} + .docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg, .docs-panel .prose :is(h2, h3, h4, h5, h6):focus-within > svg { opacity: 1 !important; diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx new file mode 100644 index 0000000..34a1a75 --- /dev/null +++ b/packages/web/components/anchor.tsx @@ -0,0 +1,44 @@ +"use client" + +import { useEffect } from "react" + +export function Anchor() { + useEffect(() => { + const page = document.getElementById("nd-page") + if (!page) return + const timers = new WeakMap() + + function copy(event: MouseEvent) { + const node = event.target + if (!(node instanceof Element)) return + + const link = node.closest("h2 > a.peer, h3 > a.peer, h4 > a.peer, h5 > a.peer, h6 > a.peer") + if (!(link instanceof HTMLAnchorElement)) return + + const hash = link.getAttribute("href") + if (!hash || !hash.startsWith("#")) return + + event.preventDefault() + + const url = new URL(window.location.href) + url.hash = hash.slice(1) + void navigator.clipboard.writeText(url.toString()) + + const timer = timers.get(link) + if (timer) window.clearTimeout(timer) + link.dataset.copied = "true" + + const next = window.setTimeout(() => { + delete link.dataset.copied + timers.delete(link) + }, 1200) + + timers.set(link, next) + } + + page.addEventListener("click", copy) + return () => page.removeEventListener("click", copy) + }, []) + + return null +} From e827f02b90a2d4781f106d4dc73583d35df9b907 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:05:51 +0000 Subject: [PATCH 10/21] fix: support heading icon copy feedback --- packages/web/app/docs/docs.css | 18 ++--------- packages/web/components/anchor.tsx | 49 ++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index fc5c750..3084dfc 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -47,22 +47,8 @@ } .docs-panel .prose :is(h2, h3, h4, h5, h6) > svg { - pointer-events: none; -} - -.docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer[data-copied="true"] + svg { - opacity: 0 !important; -} - -.docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer[data-copied="true"]::after { - content: "✓"; - position: absolute; - inset-inline-end: -14px; - top: 50%; - transform: translateY(-50%); - color: rgba(255, 255, 255, 0.82); - font-size: 14px; - line-height: 1; + pointer-events: auto; + cursor: pointer; } .docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg, diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index 34a1a75..5dc1a2f 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -7,13 +7,46 @@ export function Anchor() { const page = document.getElementById("nd-page") if (!page) return const timers = new WeakMap() + const icons = new WeakMap() + + function find(node: Element): HTMLAnchorElement | null { + const direct = node.closest("h2 > a.peer, h3 > a.peer, h4 > a.peer, h5 > a.peer, h6 > a.peer") + if (direct instanceof HTMLAnchorElement) return direct + + const icon = node.closest("h2 > svg, h3 > svg, h4 > svg, h5 > svg, h6 > svg") + if (!(icon instanceof SVGSVGElement)) return null + + const sibling = icon.previousElementSibling + if (!(sibling instanceof HTMLAnchorElement)) return null + if (!sibling.matches("a.peer")) return null + return sibling + } + + function mark(link: HTMLAnchorElement) { + const icon = link.nextElementSibling + if (!(icon instanceof SVGSVGElement)) return + + const current = icons.get(icon) ?? icon.innerHTML + if (!icons.has(icon)) icons.set(icon, current) + + icon.innerHTML = '' + + const timer = window.setTimeout(() => { + const value = icons.get(icon) + if (value) icon.innerHTML = value + }, 1200) + + const previous = timers.get(link) + if (previous) window.clearTimeout(previous) + timers.set(link, timer) + } function copy(event: MouseEvent) { const node = event.target if (!(node instanceof Element)) return - const link = node.closest("h2 > a.peer, h3 > a.peer, h4 > a.peer, h5 > a.peer, h6 > a.peer") - if (!(link instanceof HTMLAnchorElement)) return + const link = find(node) + if (!link) return const hash = link.getAttribute("href") if (!hash || !hash.startsWith("#")) return @@ -23,17 +56,7 @@ export function Anchor() { const url = new URL(window.location.href) url.hash = hash.slice(1) void navigator.clipboard.writeText(url.toString()) - - const timer = timers.get(link) - if (timer) window.clearTimeout(timer) - link.dataset.copied = "true" - - const next = window.setTimeout(() => { - delete link.dataset.copied - timers.delete(link) - }, 1200) - - timers.set(link, next) + mark(link) } page.addEventListener("click", copy) From ba016a4cd10781cfd630b5e7a765bdef041066e7 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:07:41 +0000 Subject: [PATCH 11/21] fix: show copied state on heading anchor icon --- packages/web/app/docs/docs.css | 5 +++++ packages/web/components/anchor.tsx | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 3084dfc..c2c3542 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -51,6 +51,11 @@ cursor: pointer; } +.docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer[data-copied="true"] + svg { + opacity: 1 !important; + color: rgba(255, 255, 255, 0.88); +} + .docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg, .docs-panel .prose :is(h2, h3, h4, h5, h6):focus-within > svg { opacity: 1 !important; diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index 5dc1a2f..aee3ea7 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -29,11 +29,14 @@ export function Anchor() { const current = icons.get(icon) ?? icon.innerHTML if (!icons.has(icon)) icons.set(icon, current) - icon.innerHTML = '' + link.dataset.copied = "true" + icon.innerHTML = + '' const timer = window.setTimeout(() => { const value = icons.get(icon) if (value) icon.innerHTML = value + delete link.dataset.copied }, 1200) const previous = timers.get(link) From 8ce2b77d680dbd89055f96013518fe0dec96a51f Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:10:27 +0000 Subject: [PATCH 12/21] fix: hide heading icon after copy state --- packages/web/app/docs/docs.css | 3 +-- packages/web/components/anchor.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index c2c3542..7aa5ae3 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -56,8 +56,7 @@ color: rgba(255, 255, 255, 0.88); } -.docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg, -.docs-panel .prose :is(h2, h3, h4, h5, h6):focus-within > svg { +.docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg { opacity: 1 !important; } diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index aee3ea7..6a3a98c 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -60,6 +60,7 @@ export function Anchor() { url.hash = hash.slice(1) void navigator.clipboard.writeText(url.toString()) mark(link) + link.blur() } page.addEventListener("click", copy) From 03df1775d6207ed3550eff771c8093cacc16267f Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:12:33 +0000 Subject: [PATCH 13/21] fix: hide docs search result scrollbar --- packages/web/app/globals.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/web/app/globals.css b/packages/web/app/globals.css index 47c7790..d1f2ebc 100644 --- a/packages/web/app/globals.css +++ b/packages/web/app/globals.css @@ -69,3 +69,14 @@ radial-gradient(140% 120% at 90% 90%, rgba(140, 100, 100, 0.03) 0%, transparent 55%); background-color: #0a0a0a; } + +[class*="shadow-2xl"] [class*="max-h-[460px]"][class*="overflow-y-auto"] { + scrollbar-width: none; + -ms-overflow-style: none; +} + +[class*="shadow-2xl"] [class*="max-h-[460px]"][class*="overflow-y-auto"]::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} From 628cbfd4fa0d1cf7b4ee74fb695fa9f9e324d2d8 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:14:12 +0000 Subject: [PATCH 14/21] fix: keep heading icon hidden after copy until leave --- packages/web/app/docs/docs.css | 6 +++++- packages/web/components/anchor.tsx | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 7aa5ae3..91de626 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -56,7 +56,11 @@ color: rgba(255, 255, 255, 0.88); } -.docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg { +.docs-panel .prose :is(h2, h3, h4, h5, h6)[data-lock="true"] > svg { + opacity: 0 !important; +} + +.docs-panel .prose :is(h2, h3, h4, h5, h6):hover:not([data-lock="true"]) > svg { opacity: 1 !important; } diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index 6a3a98c..58248ad 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -25,6 +25,7 @@ export function Anchor() { function mark(link: HTMLAnchorElement) { const icon = link.nextElementSibling if (!(icon instanceof SVGSVGElement)) return + const heading = link.parentElement const current = icons.get(icon) ?? icon.innerHTML if (!icons.has(icon)) icons.set(icon, current) @@ -37,6 +38,17 @@ export function Anchor() { const value = icons.get(icon) if (value) icon.innerHTML = value delete link.dataset.copied + + if (heading instanceof HTMLElement) { + heading.dataset.lock = "true" + heading.addEventListener( + "pointerleave", + () => { + delete heading.dataset.lock + }, + { once: true }, + ) + } }, 1200) const previous = timers.get(link) From 4e6eaecfdd6560cbe3e2621bdbdfef07f1c761b5 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:16:33 +0000 Subject: [PATCH 15/21] fix: remove post-copy heading icon flicker --- packages/web/components/anchor.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index 58248ad..3280d22 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -35,10 +35,6 @@ export function Anchor() { '' const timer = window.setTimeout(() => { - const value = icons.get(icon) - if (value) icon.innerHTML = value - delete link.dataset.copied - if (heading instanceof HTMLElement) { heading.dataset.lock = "true" heading.addEventListener( @@ -49,6 +45,10 @@ export function Anchor() { { once: true }, ) } + + const value = icons.get(icon) + if (value) icon.innerHTML = value + delete link.dataset.copied }, 1200) const previous = timers.get(link) From 939933e119271df2b49705c7213b9a789b915cd1 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:17:57 +0000 Subject: [PATCH 16/21] fix: remove heading anchor icon fade --- packages/web/app/docs/docs.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 91de626..3f2e2a1 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -49,6 +49,7 @@ .docs-panel .prose :is(h2, h3, h4, h5, h6) > svg { pointer-events: auto; cursor: pointer; + transition: none !important; } .docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer[data-copied="true"] + svg { From d3f147a1d05181f5ad1e89a0d4f1b39e6eadf05c Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:19:35 +0000 Subject: [PATCH 17/21] fix: smooth heading anchor hover fade --- packages/web/app/docs/docs.css | 8 ++------ packages/web/components/anchor.tsx | 12 ------------ 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 3f2e2a1..4be8210 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -49,7 +49,7 @@ .docs-panel .prose :is(h2, h3, h4, h5, h6) > svg { pointer-events: auto; cursor: pointer; - transition: none !important; + transition: opacity 140ms ease; } .docs-panel .prose :is(h2, h3, h4, h5, h6) > a.peer[data-copied="true"] + svg { @@ -57,11 +57,7 @@ color: rgba(255, 255, 255, 0.88); } -.docs-panel .prose :is(h2, h3, h4, h5, h6)[data-lock="true"] > svg { - opacity: 0 !important; -} - -.docs-panel .prose :is(h2, h3, h4, h5, h6):hover:not([data-lock="true"]) > svg { +.docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg { opacity: 1 !important; } diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index 3280d22..6a3a98c 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -25,7 +25,6 @@ export function Anchor() { function mark(link: HTMLAnchorElement) { const icon = link.nextElementSibling if (!(icon instanceof SVGSVGElement)) return - const heading = link.parentElement const current = icons.get(icon) ?? icon.innerHTML if (!icons.has(icon)) icons.set(icon, current) @@ -35,17 +34,6 @@ export function Anchor() { '' const timer = window.setTimeout(() => { - if (heading instanceof HTMLElement) { - heading.dataset.lock = "true" - heading.addEventListener( - "pointerleave", - () => { - delete heading.dataset.lock - }, - { once: true }, - ) - } - const value = icons.get(icon) if (value) icon.innerHTML = value delete link.dataset.copied From 7844ee88ff40ad2daa61ee04d95c7a118102d1f8 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:20:51 +0000 Subject: [PATCH 18/21] fix: keep offscreen heading anchors hidden after copy --- packages/web/app/docs/docs.css | 6 +++++- packages/web/components/anchor.tsx | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 4be8210..0e6ca08 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -57,7 +57,11 @@ color: rgba(255, 255, 255, 0.88); } -.docs-panel .prose :is(h2, h3, h4, h5, h6):hover > svg { +.docs-panel .prose :is(h2, h3, h4, h5, h6)[data-lock="true"] > svg { + opacity: 0 !important; +} + +.docs-panel .prose :is(h2, h3, h4, h5, h6):hover:not([data-lock="true"]) > svg { opacity: 1 !important; } diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index 6a3a98c..ba56b6a 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -25,6 +25,7 @@ export function Anchor() { function mark(link: HTMLAnchorElement) { const icon = link.nextElementSibling if (!(icon instanceof SVGSVGElement)) return + const heading = link.parentElement const current = icons.get(icon) ?? icon.innerHTML if (!icons.has(icon)) icons.set(icon, current) @@ -34,9 +35,31 @@ export function Anchor() { '' const timer = window.setTimeout(() => { + let reveal = false + if (heading instanceof HTMLElement) { + const rect = heading.getBoundingClientRect() + const visible = + rect.bottom > 0 && + rect.top < window.innerHeight && + rect.right > 0 && + rect.left < window.innerWidth + reveal = visible && heading.matches(":hover") + if (!reveal) heading.dataset.lock = "true" + } + const value = icons.get(icon) if (value) icon.innerHTML = value delete link.dataset.copied + + if (heading instanceof HTMLElement && !reveal) { + heading.addEventListener( + "pointerenter", + () => { + delete heading.dataset.lock + }, + { once: true }, + ) + } }, 1200) const previous = timers.get(link) From 079bf28f3a18f8061062b72a274c6d228643bb8e Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:23:25 +0000 Subject: [PATCH 19/21] fix: prevent offscreen heading icon flash after copy --- packages/web/app/docs/docs.css | 1 + packages/web/components/anchor.tsx | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/web/app/docs/docs.css b/packages/web/app/docs/docs.css index 0e6ca08..9c2ad9e 100644 --- a/packages/web/app/docs/docs.css +++ b/packages/web/app/docs/docs.css @@ -59,6 +59,7 @@ .docs-panel .prose :is(h2, h3, h4, h5, h6)[data-lock="true"] > svg { opacity: 0 !important; + transition: none !important; } .docs-panel .prose :is(h2, h3, h4, h5, h6):hover:not([data-lock="true"]) > svg { diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index ba56b6a..98a50c7 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -5,7 +5,8 @@ import { useEffect } from "react" export function Anchor() { useEffect(() => { const page = document.getElementById("nd-page") - if (!page) return + if (!(page instanceof HTMLElement)) return + const root = page const timers = new WeakMap() const icons = new WeakMap() @@ -37,12 +38,13 @@ export function Anchor() { const timer = window.setTimeout(() => { let reveal = false if (heading instanceof HTMLElement) { + const frame = root.getBoundingClientRect() const rect = heading.getBoundingClientRect() const visible = - rect.bottom > 0 && - rect.top < window.innerHeight && - rect.right > 0 && - rect.left < window.innerWidth + rect.bottom > frame.top && + rect.top < frame.bottom && + rect.right > frame.left && + rect.left < frame.right reveal = visible && heading.matches(":hover") if (!reveal) heading.dataset.lock = "true" } @@ -86,8 +88,8 @@ export function Anchor() { link.blur() } - page.addEventListener("click", copy) - return () => page.removeEventListener("click", copy) + root.addEventListener("click", copy) + return () => root.removeEventListener("click", copy) }, []) return null From eb67a2b39b8b7b05c1e86eaefe9eedd6a1e27175 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:23:58 +0000 Subject: [PATCH 20/21] chore: format heading anchor handler --- packages/web/components/anchor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/anchor.tsx b/packages/web/components/anchor.tsx index 98a50c7..4ab5699 100644 --- a/packages/web/components/anchor.tsx +++ b/packages/web/components/anchor.tsx @@ -38,7 +38,7 @@ export function Anchor() { const timer = window.setTimeout(() => { let reveal = false if (heading instanceof HTMLElement) { - const frame = root.getBoundingClientRect() + const frame = root.getBoundingClientRect() const rect = heading.getBoundingClientRect() const visible = rect.bottom > frame.top && From 361eda2f417223156120e8c71d1f1cbcf9a37d39 Mon Sep 17 00:00:00 2001 From: keypad <248731869+keypad@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:37:21 +0000 Subject: [PATCH 21/21] perf: harden web caching and remove og route --- packages/web/app/api/search/route.ts | 17 +++++++++- packages/web/app/docs.md/route.ts | 5 +++ packages/web/app/docs/[[...slug]]/page.tsx | 4 +++ packages/web/app/llms.txt/route.ts | 5 +++ packages/web/app/og/page.tsx | 39 ---------------------- packages/web/package.json | 10 +++--- 6 files changed, 35 insertions(+), 45 deletions(-) delete mode 100644 packages/web/app/og/page.tsx diff --git a/packages/web/app/api/search/route.ts b/packages/web/app/api/search/route.ts index e3a708d..1566ed6 100644 --- a/packages/web/app/api/search/route.ts +++ b/packages/web/app/api/search/route.ts @@ -1,4 +1,19 @@ import { createFromSource } from "fumadocs-core/search/server" import { source } from "@/lib/source" -export const { GET } = createFromSource(source) +const search = createFromSource(source) +const cache = "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800" + +export const runtime = "nodejs" + +export async function GET(request: Request): Promise { + const response = await search.GET(request) + const headers = new Headers(response.headers) + headers.set("cache-control", cache) + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }) +} diff --git a/packages/web/app/docs.md/route.ts b/packages/web/app/docs.md/route.ts index d825e25..52a3d5c 100644 --- a/packages/web/app/docs.md/route.ts +++ b/packages/web/app/docs.md/route.ts @@ -1,6 +1,10 @@ import { readdir, readFile } from "node:fs/promises" import { join } from "node:path" +export const runtime = "nodejs" +export const dynamic = "force-static" +export const revalidate = false + function roots(): string[] { return [join(process.cwd(), "content/docs"), join(process.cwd(), "packages/web/content/docs")] } @@ -43,6 +47,7 @@ export async function GET(): Promise { return new Response(chunks.join("\n"), { headers: { "content-type": "text/markdown; charset=utf-8", + "cache-control": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800", }, }) } diff --git a/packages/web/app/docs/[[...slug]]/page.tsx b/packages/web/app/docs/[[...slug]]/page.tsx index 0283404..1e45e27 100644 --- a/packages/web/app/docs/[[...slug]]/page.tsx +++ b/packages/web/app/docs/[[...slug]]/page.tsx @@ -8,6 +8,10 @@ import { Anchor } from "@/components/anchor" import { Copy } from "@/components/copy" import { source } from "@/lib/source" +export const dynamic = "force-static" +export const dynamicParams = false +export const revalidate = false + function clean(text: string): string { return text.replace(/^---[\s\S]*?---\s*/m, "").trim() } diff --git a/packages/web/app/llms.txt/route.ts b/packages/web/app/llms.txt/route.ts index 45536a0..9bd6620 100644 --- a/packages/web/app/llms.txt/route.ts +++ b/packages/web/app/llms.txt/route.ts @@ -1,6 +1,10 @@ import { readdir } from "node:fs/promises" import { join } from "node:path" +export const runtime = "nodejs" +export const dynamic = "force-static" +export const revalidate = false + function roots(): string[] { return [join(process.cwd(), "content/docs"), join(process.cwd(), "packages/web/content/docs")] } @@ -46,6 +50,7 @@ export async function GET(): Promise { return new Response(lines.join("\n"), { headers: { "content-type": "text/plain; charset=utf-8", + "cache-control": "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800", }, }) } diff --git a/packages/web/app/og/page.tsx b/packages/web/app/og/page.tsx deleted file mode 100644 index c6dc490..0000000 --- a/packages/web/app/og/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -export default function Page() { - return ( - <> - -
-
-
- -
-
-
- chaos engineering -
-

- cruel -

-

- chaos testing with zero mercy -

-

- inject failures, latency, and timeouts into any async function, model, provider, or - tool before production. -

-
-
-
cruel.dev
-
- $ - bun add cruel -
-
-
-
- - ) -} diff --git a/packages/web/package.json b/packages/web/package.json index e7090e2..d6c2573 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -15,11 +15,11 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "geist": "^1.4.0", - "fumadocs-core": "latest", - "fumadocs-mdx": "latest", - "fumadocs-ui": "latest", - "@orama/core": "latest", - "@types/mdx": "latest" + "fumadocs-core": "16.6.0", + "fumadocs-mdx": "14.2.7", + "fumadocs-ui": "16.6.0", + "@orama/core": "1.2.18", + "@types/mdx": "2.0.13" }, "devDependencies": { "@types/node": "^22.0.0",