From 13b2db40a16d5da9dacc4e0c1afdad004cb58d6d Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 27 Aug 2026 17:43:49 -0700 Subject: [PATCH] Canonicalize trailing-slash routes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redirect GET and HEAD requests to one slash-free URL so shared links do not 404 or create duplicate Knowledge routes. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- src/index.tsx | 3 +++ src/knowledge.test.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 6140660..04e3067 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import "./env.ts"; import { Hono } from "hono"; import { renderToReadableStream } from "hono/jsx/streaming"; +import { trimTrailingSlash } from "hono/trailing-slash"; import { serveStatic } from "@hono/node-server/serve-static"; import { serve } from "@hono/node-server"; import { Site } from "./components/site.tsx"; @@ -43,6 +44,8 @@ import { const app = new Hono(); +app.use(trimTrailingSlash({ alwaysRedirect: true })); + // --- Static files --- app.use( "/public/fonts/*", diff --git a/src/knowledge.test.ts b/src/knowledge.test.ts index d66a27f..afb91ae 100644 --- a/src/knowledge.test.ts +++ b/src/knowledge.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { Hono } from "hono"; +import { trimTrailingSlash } from "hono/trailing-slash"; import { KnowledgeVideo } from "./components/knowledge-entry.tsx"; import { knowledgeContentDigest, @@ -177,6 +178,21 @@ test("captures multiple knowledge path segments in Hono", async () => { assert.equal(await response.text(), "language-models/gemini"); }); +test("redirects trailing slashes to canonical Knowledge URLs", async () => { + const app = new Hono(); + app.use(trimTrailingSlash({ alwaysRedirect: true })); + app.get("/knowledge", (context) => context.text("knowledge")); + app.get("/knowledge/:route{.+}", (context) => context.text(context.req.param("route"))); + + const landing = await app.request("https://cameron.stream/knowledge/?view=map"); + assert.equal(landing.status, 301); + assert.equal(landing.headers.get("location"), "https://cameron.stream/knowledge?view=map"); + + const entry = await app.request("https://cameron.stream/knowledge/agent-memory/"); + assert.equal(entry.status, 301); + assert.equal(entry.headers.get("location"), "https://cameron.stream/knowledge/agent-memory"); +}); + test("indexes canonical nested routes and rejects route collisions", async () => { const root = await mkdtemp(join(tmpdir(), "knowledge-routes-")); try { -- 2.51.2