diff --git a/showcase/shell-docs/next.config.ts b/showcase/shell-docs/next.config.ts index 0c468ffa55..167cfaef84 100644 --- a/showcase/shell-docs/next.config.ts +++ b/showcase/shell-docs/next.config.ts @@ -294,6 +294,91 @@ const CHANNEL_REDIRECTS: PermanentRedirect[] = [ ...CHANNEL_ROOT_REDIRECTS, ]; +// ---------------------------------------------------------------- +// Retired AG-UI mirror (`/ag-ui/*`). +// +// The mirrored AG-UI docs tree was deleted in #7092; AG-UI documentation is +// canonical at docs.ag-ui.com. 96 `/ag-ui/*` URLs were in the production +// sitemap and carry accumulated search ranking, so they are handed to the +// upstream site with a 301 rather than left to 404. +// +// The mirror's slugs match upstream 1:1 for 87 of the 96 paths (verified by +// HTTP probe against docs.ag-ui.com), so a single wildcard carries the bulk +// of the tree. The nine paths with no upstream page are listed explicitly +// ABOVE the wildcard so the specific rule wins. +// +// Suffix handling: a raw Markdown request reaches redirects before the +// `.md` / `.mdx` rewrite, and upstream serves `.md` but NOT `.mdx` +// (docs.ag-ui.com/concepts/agents.md -> 200, .../concepts/agents.mdx -> 404), +// so both suffixes are collapsed onto upstream `.md`. +// ---------------------------------------------------------------- +const AG_UI_DOCS_ORIGIN = "https://docs.ag-ui.com"; + +function agUiMirrorRedirects( + mirrorPath: string, + upstreamPath: string, +): PermanentRedirect[] { + return [ + { + source: `/ag-ui${mirrorPath}.mdx`, + destination: `${AG_UI_DOCS_ORIGIN}${upstreamPath}.md`, + permanent: true, + }, + { + source: `/ag-ui${mirrorPath}.md`, + destination: `${AG_UI_DOCS_ORIGIN}${upstreamPath}.md`, + permanent: true, + }, + { + source: `/ag-ui${mirrorPath}`, + destination: `${AG_UI_DOCS_ORIGIN}${upstreamPath}`, + permanent: true, + }, + ]; +} + +// Retired mirror paths whose slug does NOT exist upstream. Each destination +// was probed and returns 200; the bare wildcard below would send these to an +// upstream 404, which is worse for readers than the page simply being gone. +const AG_UI_MIRROR_EXCEPTIONS = [ + // Both drafts graduated into the Concepts section upstream. + ["/drafts/interrupts", "/concepts/interrupts"], + ["/drafts/multimodal-messages", "/concepts/messages"], + // The Dart SDK kept every page except its client overview. + ["/sdk/dart/client/overview", "/sdk/dart/overview"], + // Upstream reduced the Rust SDK to a single overview page. + ["/sdk/rust/client/agent-trait", "/sdk/rust/overview"], + ["/sdk/rust/client/http-agent", "/sdk/rust/overview"], + ["/sdk/rust/client/subscriber", "/sdk/rust/overview"], + ["/sdk/rust/core/events", "/sdk/rust/overview"], + ["/sdk/rust/core/overview", "/sdk/rust/overview"], + ["/sdk/rust/core/types", "/sdk/rust/overview"], +] as const; + +const AG_UI_MIRROR_REDIRECTS: PermanentRedirect[] = [ + ...AG_UI_MIRROR_EXCEPTIONS.flatMap(([mirrorPath, upstreamPath]) => + agUiMirrorRedirects(mirrorPath, upstreamPath), + ), + // The mirror root rendered the AG-UI introduction page. + ...agUiMirrorRedirects("", "/introduction"), + // Everything else keeps its slug upstream. + { + source: "/ag-ui/:path*.mdx", + destination: `${AG_UI_DOCS_ORIGIN}/:path*.md`, + permanent: true, + }, + { + source: "/ag-ui/:path*.md", + destination: `${AG_UI_DOCS_ORIGIN}/:path*.md`, + permanent: true, + }, + { + source: "/ag-ui/:path*", + destination: `${AG_UI_DOCS_ORIGIN}/:path*`, + permanent: true, + }, +]; + // NEXT_PUBLIC_BASE_URL and NEXT_PUBLIC_SHELL_URL are read at REQUEST // time by the server `getRuntimeConfig()` reader and injected into the // client via `window.__SHOWCASE_CONFIG__` from the root layout. They @@ -375,6 +460,9 @@ const nextConfig: NextConfig = { // OSS-615: legacy global, scoped, generated-reference, and Bots URLs // resolve directly to the canonical Slack/Teams guide trees. ...CHANNEL_REDIRECTS, + // OSS: the retired `/ag-ui/*` mirror hands its search equity to the + // canonical upstream docs at docs.ag-ui.com (see AG_UI_MIRROR_REDIRECTS). + ...AG_UI_MIRROR_REDIRECTS, { // Built-in agent is the default framework, so its overview page // is the docs root. Avoid surfacing a redundant "Introduction" diff --git a/showcase/shell-docs/src/lib/__tests__/next-config-redirects.test.ts b/showcase/shell-docs/src/lib/__tests__/next-config-redirects.test.ts index a0b2714ee3..b5760a6b70 100644 --- a/showcase/shell-docs/src/lib/__tests__/next-config-redirects.test.ts +++ b/showcase/shell-docs/src/lib/__tests__/next-config-redirects.test.ts @@ -56,4 +56,87 @@ describe("next.config redirects", () => { ]), ); }); + + it("hands the retired /ag-ui mirror to docs.ag-ui.com", async () => { + vi.stubEnv("NEXT_PUBLIC_BASE_URL", "http://localhost:3003"); + vi.stubEnv("NEXT_PUBLIC_SHELL_URL", "http://localhost:3000"); + + const nextConfig = (await import("../../../next.config")).default; + const redirects = (await nextConfig.redirects?.()) ?? []; + + expect(redirects).toEqual( + expect.arrayContaining([ + // Bulk of the mirror keeps its slug upstream. + { + source: "/ag-ui/:path*", + destination: "https://docs.ag-ui.com/:path*", + permanent: true, + }, + // Upstream serves `.md` but not `.mdx`, so both collapse onto `.md`. + { + source: "/ag-ui/:path*.md", + destination: "https://docs.ag-ui.com/:path*.md", + permanent: true, + }, + { + source: "/ag-ui/:path*.mdx", + destination: "https://docs.ag-ui.com/:path*.md", + permanent: true, + }, + // Mirror root rendered the upstream introduction page. + { + source: "/ag-ui", + destination: "https://docs.ag-ui.com/introduction", + permanent: true, + }, + // Paths with no upstream equivalent land on the nearest live page. + { + source: "/ag-ui/drafts/interrupts", + destination: "https://docs.ag-ui.com/concepts/interrupts", + permanent: true, + }, + { + source: "/ag-ui/drafts/multimodal-messages", + destination: "https://docs.ag-ui.com/concepts/messages", + permanent: true, + }, + { + source: "/ag-ui/sdk/dart/client/overview", + destination: "https://docs.ag-ui.com/sdk/dart/overview", + permanent: true, + }, + { + source: "/ag-ui/sdk/rust/core/types", + destination: "https://docs.ag-ui.com/sdk/rust/overview", + permanent: true, + }, + ]), + ); + }); + + it("orders every /ag-ui exception ahead of the catch-all", async () => { + vi.stubEnv("NEXT_PUBLIC_BASE_URL", "http://localhost:3003"); + vi.stubEnv("NEXT_PUBLIC_SHELL_URL", "http://localhost:3000"); + + const nextConfig = (await import("../../../next.config")).default; + const redirects = (await nextConfig.redirects?.()) ?? []; + + const catchAll = redirects.findIndex( + (redirect) => redirect.source === "/ag-ui/:path*", + ); + expect(catchAll).toBeGreaterThan(-1); + + const exceptions = redirects.filter( + (redirect) => + typeof redirect.destination === "string" && + redirect.destination.startsWith("https://docs.ag-ui.com") && + !redirect.source.includes(":path*"), + ); + // 9 retired paths with no upstream equivalent + the mirror root, each + // emitted bare, `.md` and `.mdx`. + expect(exceptions).toHaveLength(30); + for (const exception of exceptions) { + expect(redirects.indexOf(exception)).toBeLessThan(catchAll); + } + }); });