diff --git a/packages/fern-docs/bundle/package.json b/packages/fern-docs/bundle/package.json index 5ee190d816..3a63e2690b 100644 --- a/packages/fern-docs/bundle/package.json +++ b/packages/fern-docs/bundle/package.json @@ -93,6 +93,7 @@ "cssnano": "^6.0.3", "es-toolkit": "^1.27.0", "esbuild": "0.24.2", + "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "fastdom": "^1.0.12", "feed": "^4.2.2", diff --git a/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/[...slug]/page.tsx b/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/[...slug]/page.tsx index f307a365bf..ab68cf6e01 100644 --- a/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/[...slug]/page.tsx +++ b/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/[...slug]/page.tsx @@ -1,27 +1,41 @@ "use server"; import { PlaygroundEndpoint } from "@/components/playground/endpoint/PlaygroundEndpoint"; +import { conformExplorerRoute } from "@/components/playground/utils/explorer-route"; import { PlaygroundWebSocket } from "@/components/playground/websocket/PlaygroundWebSocket"; import { createCachedDocsLoader } from "@/server/docs-loader"; -import { createFindNode } from "@/server/find-node"; import { getDocsDomainApp } from "@/server/xfernhost/app"; import { FernNavigation } from "@fern-api/fdr-sdk"; import { createEndpointContext, createWebSocketContext, } from "@fern-api/fdr-sdk/api-definition"; -import { COOKIE_FERN_TOKEN } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { notFound } from "next/navigation"; +import { conformTrailingSlash, COOKIE_FERN_TOKEN } from "@fern-docs/utils"; +import { cookies, headers } from "next/headers"; +import { notFound, redirect } from "next/navigation"; export default async function Page({ params }: { params: { slug: string[] } }) { - const slug = FernNavigation.slugjoin(params.slug); + const slug = FernNavigation.slugjoin( + headers().get("x-basepath"), + params.slug + ); const fern_token = cookies().get(COOKIE_FERN_TOKEN)?.value; const loader = await createCachedDocsLoader(getDocsDomainApp(), fern_token); - const node = await createFindNode(loader)(slug); - if (node == null) { + const root = await loader.getRoot(); + if (root == null) { notFound(); } + const found = FernNavigation.utils.findNode(root, slug); + if (found.type !== "found") { + if (found.redirect) { + // follows the route path hierarchy + // e.g. /docs/foo/bar -> /docs/~/api-explorer/foo/bar + redirect(conformTrailingSlash(conformExplorerRoute(slug, root.slug))); + } + + notFound(); + } + const node = found.node; if (!FernNavigation.isApiLeaf(node)) { notFound(); } diff --git a/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/layout.tsx b/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/layout.tsx index caad63aad1..569de35744 100644 --- a/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/layout.tsx +++ b/packages/fern-docs/bundle/src/app/(docs)/~/api-explorer/layout.tsx @@ -32,6 +32,7 @@ export default async function Layout({ {children} diff --git a/packages/fern-docs/bundle/src/app/not-found.tsx b/packages/fern-docs/bundle/src/app/not-found.tsx index 7470ed4e5f..2bf7efa35c 100644 --- a/packages/fern-docs/bundle/src/app/not-found.tsx +++ b/packages/fern-docs/bundle/src/app/not-found.tsx @@ -13,6 +13,7 @@ import { conformTrailingSlash, DEFAULT_LOGO_HEIGHT, } from "@fern-docs/utils"; +import { Undo2 } from "lucide-react"; export default async function NotFound() { const loader = await createCachedDocsLoader(getDocsDomainApp()); @@ -45,6 +46,7 @@ export default async function NotFound() { )} variant="filled" intent="primary" + rightIcon={} > Return Home diff --git a/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorContent.tsx b/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorContent.tsx index 4eb4bdc2f4..835ca10b2e 100644 --- a/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorContent.tsx +++ b/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorContent.tsx @@ -12,15 +12,9 @@ import { removeTrailingSlash } from "@fern-docs/utils"; import cn, { clsx } from "clsx"; import { Search, Slash, Xmark } from "iconoir-react"; import { usePathname } from "next/navigation"; -import { - Fragment, - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from "react"; +import { Fragment, forwardRef, useEffect, useRef, useState } from "react"; import { BuiltWithFern } from "../../sidebar/BuiltWithFern"; +import { conformExplorerRoute } from "../utils/explorer-route"; import { ApiGroup } from "../utils/flatten-apis"; import { PlaygroundEndpointSelectorLeafNode } from "./PlaygroundEndpointSelectorLeafNode"; @@ -28,6 +22,7 @@ export interface PlaygroundEndpointSelectorContentProps { apiGroups: ApiGroup[]; className?: string; shallow?: boolean; + rootslug: FernNavigation.Slug; } function matchesEndpoint( @@ -49,11 +44,8 @@ function matchesEndpoint( export const PlaygroundEndpointSelectorContent = forwardRef< HTMLDivElement, PlaygroundEndpointSelectorContentProps ->(({ apiGroups, className, shallow }, ref) => { +>(({ apiGroups, className, shallow, rootslug }, forwardedRef) => { const pathname = usePathname(); - const scrollRef = useRef(null); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - useImperativeHandle(ref, () => scrollRef.current!); const [filterValue, setFilterValue] = useState(""); @@ -94,7 +86,8 @@ export const PlaygroundEndpointSelectorContent = forwardRef<
    {apiLeafNodes.map((node) => { const active = - removeTrailingSlash(pathname) === `/~/api-explorer/${node.slug}`; + removeTrailingSlash(pathname) === + conformExplorerRoute(node.slug, rootslug); return ( ); })} @@ -118,7 +112,7 @@ export const PlaygroundEndpointSelectorContent = forwardRef<
      {renderedListItems} diff --git a/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx b/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx index 6ae3056d29..775a9eaab0 100644 --- a/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx +++ b/packages/fern-docs/bundle/src/components/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx @@ -8,18 +8,20 @@ import { ReactElement, forwardRef } from "react"; import { useMemoOne } from "use-memo-one"; import { getApiDefinitionAtom } from "../../atoms"; import { Markdown } from "../../mdx/Markdown"; +import { conformExplorerRoute } from "../utils/explorer-route"; interface PlaygroundEndpointSelectorLeafNodeProps { node: FernNavigation.EndpointNode | FernNavigation.WebSocketNode; filterValue: string; active: boolean; shallow?: boolean; + rootslug: FernNavigation.Slug; } export const PlaygroundEndpointSelectorLeafNode = forwardRef< HTMLLIElement, PlaygroundEndpointSelectorLeafNodeProps ->(({ node, filterValue, active, shallow }, ref) => { +>(({ node, filterValue, active, shallow, rootslug }, ref) => { const text = renderTextWithHighlight(node.title, filterValue); const description = useAtomValue( @@ -49,7 +51,7 @@ export const PlaygroundEndpointSelectorLeafNode = forwardRef<
    • { ); } + /** + * Rewrite .../~/... to /~/... + */ + if (!pathname.startsWith("/~") && pathname.includes("/~/")) { + const index = pathname.indexOf("/~/"); + pathname = pathname.slice(index); + const basepath = pathname.slice(0, index); + headers.set("x-basepath", basepath); + return NextResponse.rewrite(withPathname(request, pathname), { + request: { headers }, + }); + } + return NextResponse.next({ request: { headers }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8102f1c7c5..6ffbee3908 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -873,6 +873,9 @@ importers: esbuild: specifier: 0.24.2 version: 0.24.2 + escape-string-regexp: + specifier: ^5.0.0 + version: 5.0.0 estree-walker: specifier: ^3.0.3 version: 3.0.3