Skip to content

Commit

Permalink
migrate endpoints to app router
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed Dec 20, 2024
1 parent c8d089c commit 9b8fdc4
Show file tree
Hide file tree
Showing 36 changed files with 652 additions and 641 deletions.
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default tseslint.config(
"**/.next",
"**/storybook-static",
"**/out",
"**/node_modules",
"fern/**",
],
},
eslint.configs.recommended,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,42 @@
import { DocsLoader } from "@/server/DocsLoader";
import { getDocsDomainNode, getHostNode } from "@/server/xfernhost/node";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import type { DocsV1Read } from "@fern-api/fdr-sdk/client/types";
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { NodeCollector } from "@fern-api/fdr-sdk/navigation";
import { assertNever, withDefaultProtocol } from "@fern-api/ui-core-utils";
import { getFrontmatter } from "@fern-docs/mdx";
import { COOKIE_FERN_TOKEN } from "@fern-docs/utils";
import { addLeadingSlash, COOKIE_FERN_TOKEN } from "@fern-docs/utils";
import { Feed, Item } from "feed";
import { NextApiRequest, NextApiResponse } from "next";
import { NextRequest, NextResponse } from "next/server";
import urlJoin from "url-join";

export const revalidate = 60 * 60 * 24;

export default async function responseApiHandler(
req: NextApiRequest,
res: NextApiResponse
): Promise<unknown> {
if (req.method !== "GET") {
return res.status(405).end();
}

const path = req.query.path;
const format = req.query.format ?? "rss";

if (typeof path !== "string" || typeof format !== "string") {
return res.status(400).end();
}

const domain = getDocsDomainNode(req);
const host = getHostNode(req) ?? domain;
const fernToken = req.cookies[COOKIE_FERN_TOKEN];
export async function handleChangelog(
req: NextRequest,
{ params }: { params: { slug?: string[]; format?: "rss" | "atom" | "json" } }
): Promise<NextResponse> {
const slug = params.slug ?? [];
const path = addLeadingSlash(slug.join("/"));
const format = params.format ?? "rss";

const domain = getDocsDomainEdge(req);
const host = getHostEdge(req);
const fernToken = req.cookies.get(COOKIE_FERN_TOKEN)?.value;
const loader = DocsLoader.for(domain, host, fernToken);

const root = await loader.root();

if (!root) {
return res.status(404).end();
return NextResponse.json(null, { status: 404 });
}

const collector = NodeCollector.collect(root);

const slug = FernNavigation.slugjoin(decodeURIComponent(path));
const node = collector.slugMap.get(slug);
const node = collector.slugMap.get(FernNavigation.slugjoin(path));

if (node?.type !== "changelog") {
return res.status(404).end();
return NextResponse.json(null, { status: 404 });
}

const link = urlJoin(withDefaultProtocol(domain), node.slug);
Expand Down Expand Up @@ -73,17 +65,18 @@ export default async function responseApiHandler(
});
});

const headers = new Headers();

if (format === "json") {
headers.set("Content-Type", "application/json");
return res.json(feed.json1());
return new NextResponse(feed.json1(), {
headers: { "Content-Type": "application/json" },
});
} else if (format === "atom") {
headers.set("Content-Type", "application/atom+xml");
return res.send(feed.atom1());
return new NextResponse(feed.atom1(), {
headers: { "Content-Type": "application/atom+xml" },
});
} else {
headers.set("Content-Type", "application/rss+xml");
return res.send(feed.rss2());
return new NextResponse(feed.rss2(), {
headers: { "Content-Type": "application/rss+xml" },
});
}
}

Expand Down
76 changes: 76 additions & 0 deletions packages/fern-docs/bundle/src/app/[[...slug]]/llms-full.txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { DocsLoader } from "@/server/DocsLoader";
import { getMarkdownForPath } from "@/server/getMarkdownForPath";
import { getSectionRoot } from "@/server/getSectionRoot";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import { FernNavigation } from "@fern-api/fdr-sdk";
import { CONTINUE, SKIP } from "@fern-api/fdr-sdk/traversers";
import { isNonNullish } from "@fern-api/ui-core-utils";
import { getFeatureFlags } from "@fern-docs/edge-config";
import { addLeadingSlash, COOKIE_FERN_TOKEN } from "@fern-docs/utils";
import { uniqBy } from "es-toolkit/array";
import { NextRequest, NextResponse } from "next/server";

export async function handleLLMSFullTxt(
req: NextRequest,
{ params }: { params: { slug?: string[] } }
): Promise<NextResponse> {
const slug = params.slug ?? [];
const path = addLeadingSlash(slug.join("/"));
const domain = getDocsDomainEdge(req);
const host = getHostEdge(req);
const fern_token = req.cookies.get(COOKIE_FERN_TOKEN)?.value;
const featureFlags = await getFeatureFlags(domain);
const loader = DocsLoader.for(domain, host, fern_token).withFeatureFlags(
featureFlags
);

const root = getSectionRoot(await loader.root(), path);

if (root == null) {
return NextResponse.json(null, { status: 404 });
}

const nodes: FernNavigation.NavigationNodePage[] = [];

FernNavigation.traverseDF(root, (node) => {
if (FernNavigation.hasMetadata(node)) {
if (node.hidden || node.authed) {
return SKIP;
}
}

if (FernNavigation.isPage(node)) {
nodes.push(node);
}

return CONTINUE;
});

const markdowns = (
await Promise.all(
uniqBy(
nodes,
(a) => FernNavigation.getPageId(a) ?? a.canonicalSlug ?? a.slug
).map(async (node) => {
const markdown = await getMarkdownForPath(node, loader, featureFlags);
if (markdown == null) {
return undefined;
}
return markdown.content;
})
)
).filter(isNonNullish);

if (markdowns.length === 0) {
return NextResponse.json(null, { status: 404 });
}

return new NextResponse(markdowns.join("\n\n"), {
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"X-Robots-Tag": "noindex",
"Cache-Control": "s-maxage=60",
},
});
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { DocsLoader } from "@/server/DocsLoader";
import { getMarkdownForPath } from "@/server/getMarkdownForPath";
import { getSectionRoot } from "@/server/getSectionRoot";
import { getStringParam } from "@/server/getStringParam";
import { getLlmTxtMetadata } from "@/server/llm-txt-md";
import { getDocsDomainNode, getHostNode } from "@/server/xfernhost/node";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { CONTINUE, SKIP } from "@fern-api/fdr-sdk/traversers";
import { isNonNullish, withDefaultProtocol } from "@fern-api/ui-core-utils";
import { getFeatureFlags } from "@fern-docs/edge-config";
import { COOKIE_FERN_TOKEN, addLeadingSlash } from "@fern-docs/utils";
import { NextApiRequest, NextApiResponse } from "next";
import { NextRequest, NextResponse } from "next/server";

/**
* This endpoint follows the https://llmstxt.org/ specification for a LLM-friendly markdown-esque page listing all the pages in the docs.
Expand All @@ -32,26 +31,14 @@ import { NextApiRequest, NextApiResponse } from "next";
* - should hidden pages be included under an `## Optional` heading?
*/

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise<unknown> {
if (req.method === "OPTIONS") {
return res
.status(200)
.setHeader("X-Robots-Tag", "noindex")
.setHeader("Allow", "OPTIONS, GET")
.end();
}

if (req.method !== "GET") {
return res.status(405).end();
}

const path = getStringParam(req, "path") ?? "/";
const domain = getDocsDomainNode(req);
const host = getHostNode(req) ?? domain;
const fern_token = req.cookies[COOKIE_FERN_TOKEN];
export async function handleLLMSTxt(
req: NextRequest,
{ params }: { params: { slug?: string[] } }
): Promise<NextResponse> {
const path = addLeadingSlash(params.slug?.join("/") ?? "");
const domain = getDocsDomainEdge(req);
const host = getHostEdge(req);
const fern_token = req.cookies.get(COOKIE_FERN_TOKEN)?.value;
const featureFlags = await getFeatureFlags(domain);
const loader = DocsLoader.for(domain, host, fern_token).withFeatureFlags(
featureFlags
Expand All @@ -61,7 +48,7 @@ export default async function handler(
const pages = await loader.pages();

if (root == null) {
return res.status(404).end();
return NextResponse.json(null, { status: 404 });
}

const pageInfos: {
Expand Down Expand Up @@ -202,27 +189,26 @@ export default async function handler(
`- ${endpoint.breadcrumb.join(" > ")} [${endpoint.title}](${endpoint.href})`
);

res
.status(200)
.setHeader("Content-Type", "text/plain; charset=utf-8")
// prevent search engines from indexing this page
.setHeader("X-Robots-Tag", "noindex")
// cannot guarantee that the content won't change, so we only cache for 60 seconds
.setHeader("Cache-Control", "s-maxage=60")
.send(
[
// if there's a landing page, use the llm-friendly markdown version instead of the ${root.title}
markdown?.content ?? `# ${root.title}`,
docs.length > 0 ? `## Docs\n\n${docs.join("\n")}` : undefined,
endpoints.length > 0
? `## API Docs\n\n${endpoints.join("\n")}`
: undefined,
]
.filter(isNonNullish)
.join("\n\n")
);

return;
return new NextResponse(
[
// if there's a landing page, use the llm-friendly markdown version instead of the ${root.title}
markdown?.content ?? `# ${root.title}`,
docs.length > 0 ? `## Docs\n\n${docs.join("\n")}` : undefined,
endpoints.length > 0
? `## API Docs\n\n${endpoints.join("\n")}`
: undefined,
]
.filter(isNonNullish)
.join("\n\n"),
{
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"X-Robots-Tag": "noindex",
"Cache-Control": "s-maxage=60",
},
}
);
}

function getLandingPage(
Expand Down
64 changes: 64 additions & 0 deletions packages/fern-docs/bundle/src/app/[[...slug]]/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { DocsLoader } from "@/server/DocsLoader";
import {
getMarkdownForPath,
getPageNodeForPath,
} from "@/server/getMarkdownForPath";
import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge";
import { getFeatureFlags } from "@fern-docs/edge-config";
import { addLeadingSlash, COOKIE_FERN_TOKEN } from "@fern-docs/utils";
import { notFound } from "next/navigation";
import { NextRequest, NextResponse } from "next/server";

/**
* This endpoint returns the markdown content of any page in the docs by adding `.md` or `.mdx` to the end of any docs page.
*/

export async function handleMarkdown(
req: NextRequest,
{ params }: { params: { slug?: string[] } }
): Promise<NextResponse> {
const slug = params.slug ?? [];
const path = addLeadingSlash(slug.join("/"));
const domain = getDocsDomainEdge(req);
const host = getHostEdge(req);
const fern_token = req.cookies.get(COOKIE_FERN_TOKEN)?.value;
const featureFlags = await getFeatureFlags(domain);
const loader = DocsLoader.for(domain, host, fern_token).withFeatureFlags(
featureFlags
);

const node = getPageNodeForPath(await loader.root(), path);
console.log(path, node);
if (node == null) {
notFound();
}

// If the page is authed, but the user is not authed, return a 403
if (node.authed && !(await loader.isAuthed())) {
return new NextResponse(null, { status: 403 });
}

const markdown = await getMarkdownForPath(node, loader, featureFlags);
if (markdown == null) {
notFound();
}

return new NextResponse(markdown.content, {
status: 200,
headers: {
"Content-Type": `text/${markdown.contentType}`,
"X-Robots-Tag": "noindex", // prevent search engines from indexing this page
"Cache-Control": "s-maxage=60", // cannot guarantee that the content won't change, so we only cache for 60 seconds
},
});
}

export async function OPTIONS(): Promise<NextResponse> {
return new NextResponse(null, {
status: 200,
headers: {
"X-Robots-Tag": "noindex",
Allow: "OPTIONS, GET",
},
});
}
Loading

0 comments on commit 9b8fdc4

Please sign in to comment.