Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@
"devDependencies": {
"@cloudflare/workers-types": "^4.20250327.0",
"wrangler": "^4.14.4"
},
"dependencies": {
"hono": "^4.13.2"
}
}
135 changes: 135 additions & 0 deletions router/src/app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { createApp } from "./app.ts";

/** App with a scripted upstream; unmatched paths 404 like the assets Worker. */
function appWith(handler: (url: URL) => Response | undefined) {
const calls: URL[] = [];
const app = createApp({
upstream: async (url) => {
calls.push(url);
return handler(url) ?? new Response("not found", { status: 404 });
},
passthrough: async (request) =>
new Response(`passthrough:${request.url}`),
});
return { app, calls };
}

const html = () =>
new Response("<html>page</html>", {
status: 200,
headers: { "Content-Type": "text/html" },
});

const markdown = () =>
new Response("# Page\n", {
status: 200,
headers: { "Content-Type": "text/markdown; charset=utf-8" },
});

test("redirects bare project roots to their landing page", async () => {
const { app } = appWith(() => undefined);
const res = await app.request("https://bomb.sh/docs/clack", {
headers: { Accept: "text/markdown" },
});
assert.equal(res.status, 308);
assert.equal(
res.headers.get("Location"),
"https://bomb.sh/docs/clack/basics/getting-started/",
);
});

test("serves the markdown twin on Accept: text/markdown", async () => {
const { app, calls } = appWith((url) =>
url.pathname === "/docs/args/api/index.md" ? markdown() : undefined,
);
const res = await app.request("https://bomb.sh/docs/args/api/", {
headers: { Accept: "text/markdown" },
});
assert.equal(res.status, 200);
assert.equal(calls[0].href, "https://docs.bomb.sh/docs/args/api/index.md");
assert.match(res.headers.get("Content-Type") ?? "", /text\/markdown/);
assert.equal(res.headers.get("Vary"), "Accept");
assert.equal(
res.headers.get("Link"),
'<https://bomb.sh/docs/args/api/>; rel="canonical"',
);
assert.equal(await res.text(), "# Page\n");
});

test("serves markdown for explicit .md paths without an Accept header", async () => {
const { app, calls } = appWith((url) =>
url.pathname === "/docs/args/api/index.md" ? markdown() : undefined,
);
const res = await app.request("https://bomb.sh/docs/args/api.md");
assert.equal(res.status, 200);
assert.equal(calls[0].href, "https://docs.bomb.sh/docs/args/api/index.md");
});

test("negotiates the docs root to the markdown index", async () => {
const { app, calls } = appWith((url) =>
url.pathname === "/docs/index.md" ? markdown() : undefined,
);
const res = await app.request("https://bomb.sh/docs", {
headers: { Accept: "text/markdown" },
});
assert.equal(res.status, 200);
assert.equal(calls[0].href, "https://docs.bomb.sh/docs/index.md");
});

test("returns markdown 404 guidance when the twin is missing", async () => {
const { app } = appWith(() => undefined);
const res = await app.request("https://bomb.sh/docs/nope/", {
headers: { Accept: "text/markdown" },
});
assert.equal(res.status, 404);
assert.match(res.headers.get("Content-Type") ?? "", /text\/markdown/);
assert.match(await res.text(), /https:\/\/bomb\.sh\/docs\/index\.md/);
});

test("proxies HTML with security headers and a markdown alternate link", async () => {
const { app } = appWith((url) =>
url.pathname === "/docs/args/api/" ? html() : undefined,
);
const res = await app.request("https://bomb.sh/docs/args/api/");
assert.equal(res.status, 200);
assert.equal(
res.headers.get("Cross-Origin-Embedder-Policy"),
"require-corp",
);
assert.equal(
res.headers.get("Link"),
'<https://bomb.sh/docs/args/api/index.md>; rel="alternate"; type="text/markdown"',
);
assert.equal(await res.text(), "<html>page</html>");
});

test("maps missing pages to the Starlight 404 page with status 404", async () => {
const { app } = appWith((url) =>
url.pathname === "/docs/404.html"
? new Response("<html>404</html>", {
status: 200,
headers: { "Content-Type": "text/html" },
})
: undefined,
);
const res = await app.request("https://bomb.sh/docs/missing/");
assert.equal(res.status, 404);
assert.equal(await res.text(), "<html>404</html>");
});

test("does not negotiate asset paths", async () => {
const { app, calls } = appWith(() => html());
await app.request("https://bomb.sh/docs/og-docs.png", {
headers: { Accept: "text/markdown" },
});
assert.equal(calls[0].href, "https://docs.bomb.sh/docs/og-docs.png");
});

test("passes non-docs requests through untouched", async () => {
const { app, calls } = appWith(() => undefined);
const res = await app.request("https://bomb.sh/other");
assert.equal(await res.text(), "passthrough:https://bomb.sh/other");
assert.equal(calls.length, 0);
});
27 changes: 27 additions & 0 deletions router/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Hono } from "hono/tiny";
import { projectRedirects } from "./redirects.ts";
import { markdownNegotiation } from "./markdown.ts";
import { docsProxy } from "./proxy.ts";
import { defaultUpstream, type Upstream } from "./upstream.ts";

export interface AppOptions {
/** Fetches from the docs assets Worker. */
upstream?: Upstream;
/** Handles non-docs requests. */
passthrough?: (request: Request) => Promise<Response>;
}

export function createApp(options: AppOptions = {}) {
const upstream = options.upstream ?? defaultUpstream;
const passthrough =
options.passthrough ?? ((request: Request) => fetch(request));

const app = new Hono();
for (const path of ["/docs", "/docs/*"]) {
app.use(path, projectRedirects());
app.use(path, markdownNegotiation(upstream));
app.all(path, docsProxy(upstream));
}
app.all("*", (c) => passthrough(c.req.raw));
return app;
}
12 changes: 12 additions & 0 deletions router/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Public origin used in Link headers (canonical/alternate) so agents always
// discover the proxied bomb.sh URLs, never the internal workers.dev ones.
export const SITE = "https://bomb.sh";

// Bare project roots don't have their own index page and should redirect to
// their actual landing page instead of 404ing.
export const PROJECT_LANDING_PAGES: Record<string, string> = {
clack: "/docs/clack/basics/getting-started/",
tab: "/docs/tab/",
args: "/docs/args/getting-started/",
tty: "/docs/tty/basics/getting-started/",
};
124 changes: 2 additions & 122 deletions router/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,123 +1,3 @@
import { prefersMarkdown, toMarkdownPath } from "./negotiate";
import { createApp } from "./app.ts";

export interface Env { }

// Public origin used in Link headers (canonical/alternate) so agents always
// discover the proxied bomb.sh URLs, never the internal workers.dev ones.
const SITE = "https://bomb.sh";

const MARKDOWN_404 = `# 404: Not Found

This page does not exist. An index of all Bombshell documentation is
available at ${SITE}/docs/index.md
`;

// Where to proxy docs requests. In production this is the live site. On
// Cloudflare branch previews both Workers share the same branch slug
// (e.g. `fix-404-bombsh-docs-router` ↔ `fix-404-bombshell-docs`), so we point
// the router at the matching docs preview by rewriting our own hostname.
// Per-version previews use an 8-char hex id that differs per Worker and can't
// be mapped, so those fall back to production.
function docsOrigin(host: string): string {
const match = host.match(/^(.+)-bombsh-docs-router\.(.+\.workers\.dev)$/);
if (match) {
const [, slug, zone] = match;
if (!/^[0-9a-f]{8}$/.test(slug)) {
return `https://${slug}-bombshell-docs.${zone}/`;
}
}
return "https://docs.bomb.sh/";
}

// Bare project roots don't have their own index page and should redirect to
// their actual landing page instead of 404ing.
const PROJECT_LANDING_PAGES: Record<string, string> = {
clack: "/docs/clack/basics/getting-started/",
tab: "/docs/tab/",
args: "/docs/args/getting-started/",
tty: "/docs/tty/basics/getting-started/",
};

export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);

const projectMatch = url.pathname.match(/^\/docs\/([^/]+)\/?$/);
if (projectMatch) {
const landingPage = PROJECT_LANDING_PAGES[projectMatch[1]];
if (landingPage && url.pathname !== landingPage) {
return Response.redirect(new URL(landingPage, url).toString(), 308);
}
}

if (url.pathname.startsWith("/docs")) {
const origin = docsOrigin(url.host);

// Agent-facing markdown: explicit `.md` paths always serve markdown;
// extensionless page routes negotiate on `Accept: text/markdown`.
// Negotiation rewrites to a distinct origin URL, so HTML and markdown
// variants get distinct cache keys — Cloudflare's cache ignores `Vary`.
const markdownPath = toMarkdownPath(url.pathname);
const wantsMarkdown =
markdownPath !== null &&
(url.pathname.endsWith(".md") ||
prefersMarkdown(request.headers.get("Accept")));

if (wantsMarkdown && markdownPath) {
const response = await fetch(new URL(markdownPath, origin));
if (response.status === 404) {
return new Response(MARKDOWN_404, {
status: 404,
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Vary": "Accept",
},
});
}
const headers = new Headers(response.headers);
headers.set("Content-Type", "text/markdown; charset=utf-8");
headers.set("Vary", "Accept");
const htmlPath = markdownPath.slice(0, -"index.md".length);
headers.set("Link", `<${SITE}${htmlPath}>; rel="canonical"`);
return new Response(response.body, {
status: response.status,
headers,
});
}

let response = await fetch(new URL(url.pathname, docsOrigin(url.host)));
console.log({ from: url, to: new URL(url.pathname, docsOrigin(url.host)) });


// Special case for Starlight's 404 page
let status = response.status;
if (status === 404) {
response = await fetch(new URL("/docs/404.html", origin))
}

const headers = new Headers(response.headers);
headers.set("Cross-Origin-Embedder-Policy", "require-corp");
headers.set("Cross-Origin-Opener-Policy", "same-origin");
headers.set("Cross-Origin-Resource-Policy", "cross-origin");
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");

// Advertise the markdown twin to agents crawling the HTML variant.
if (markdownPath && status === 200) {
headers.set(
"Link",
`<${SITE}${markdownPath}>; rel="alternate"; type="text/markdown"`,
);
}

// If we got 404, return the HTML, but set status to 404 manually,
// because the response status would be 200
return new Response(response.body, {
status: status,
statusText: status === 404 ? "Not Found" : response.statusText,
headers,
});
}

return fetch(request);
},
} satisfies ExportedHandler<Env>;
export default createApp();
50 changes: 50 additions & 0 deletions router/src/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { MiddlewareHandler } from "hono";
import { prefersMarkdown, toMarkdownPath } from "./negotiate.ts";
import { docsOrigin } from "./origin.ts";
import { SITE } from "./config.ts";
import type { Upstream } from "./upstream.ts";

export const MARKDOWN_404 = `# 404: Not Found

This page does not exist. An index of all Bombshell documentation is
available at ${SITE}/docs/index.md
`;

// Agent-facing markdown: explicit `.md` paths always serve markdown;
// extensionless page routes negotiate on `Accept: text/markdown`.
// Negotiation rewrites to a distinct origin URL, so HTML and markdown
// variants get distinct cache keys — Cloudflare's cache ignores `Vary`.
export function markdownNegotiation(upstream: Upstream): MiddlewareHandler {
return async (c, next) => {
const url = new URL(c.req.url);
const markdownPath = toMarkdownPath(url.pathname);
const wantsMarkdown =
markdownPath !== null &&
(url.pathname.endsWith(".md") ||
prefersMarkdown(c.req.header("Accept") ?? null));
if (!wantsMarkdown || !markdownPath) return next();

const response = await upstream(
new URL(markdownPath, docsOrigin(url.host)),
);
if (response.status === 404) {
return new Response(MARKDOWN_404, {
status: 404,
headers: {
"Content-Type": "text/markdown; charset=utf-8",
Vary: "Accept",
},
});
}

const headers = new Headers(response.headers);
headers.set("Content-Type", "text/markdown; charset=utf-8");
headers.set("Vary", "Accept");
const htmlPath = markdownPath.slice(0, -"index.md".length);
headers.set("Link", `<${SITE}${htmlPath}>; rel="canonical"`);
return new Response(response.body, {
status: response.status,
headers,
});
};
}
Loading