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