Skip to content

Commit 2c48908

Browse files
kennyderekdevin-ai-integration[bot]kenny@buildwithfern.com
authored
fix(docs): landing page markdown (#4712)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent 95328a4 commit 2c48908

File tree

5 files changed

+561
-25
lines changed

5 files changed

+561
-25
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { NextRequest } from "next/server";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
vi.mock("@fern-api/docs-loader", () => ({
5+
createCachedDocsLoader: vi.fn()
6+
}));
7+
8+
vi.mock("@fern-api/docs-server/analytics/posthog", () => ({
9+
track: vi.fn()
10+
}));
11+
12+
vi.mock("next/headers", () => ({
13+
cookies: vi.fn()
14+
}));
15+
16+
vi.mock("@/server/getSectionRoot", () => ({
17+
getSectionRoot: vi.fn()
18+
}));
19+
20+
vi.mock("@/server/getMarkdownForPath", () => ({
21+
getMarkdownForPath: vi.fn()
22+
}));
23+
24+
vi.mock("@fern-api/fdr-sdk", () => ({
25+
FernNavigation: {
26+
traverseDF: vi.fn(),
27+
hasMetadata: vi.fn(),
28+
isPage: vi.fn(),
29+
getPageId: vi.fn()
30+
}
31+
}));
32+
33+
vi.mock("@fern-api/fdr-sdk/traversers", () => ({
34+
CONTINUE: "CONTINUE",
35+
SKIP: "SKIP"
36+
}));
37+
38+
import { createCachedDocsLoader } from "@fern-api/docs-loader";
39+
import { track } from "@fern-api/docs-server/analytics/posthog";
40+
import { FernNavigation } from "@fern-api/fdr-sdk";
41+
import { cookies } from "next/headers";
42+
import { getMarkdownForPath } from "@/server/getMarkdownForPath";
43+
import { getSectionRoot } from "@/server/getSectionRoot";
44+
45+
import { GET } from "./route";
46+
47+
const mockCreateCachedDocsLoader = vi.mocked(createCachedDocsLoader);
48+
const mockTrack = vi.mocked(track);
49+
const mockCookies = vi.mocked(cookies);
50+
const mockGetSectionRoot = vi.mocked(getSectionRoot);
51+
const mockGetMarkdownForPath = vi.mocked(getMarkdownForPath);
52+
const mockTraverseDF = vi.mocked(FernNavigation.traverseDF);
53+
const mockHasMetadata = vi.mocked(FernNavigation.hasMetadata);
54+
const mockIsPage = vi.mocked(FernNavigation.isPage);
55+
const mockGetPageId = vi.mocked(FernNavigation.getPageId);
56+
57+
describe("llms-full.txt route - no accessible nodes behavior", () => {
58+
beforeEach(() => {
59+
vi.clearAllMocks();
60+
61+
mockCookies.mockResolvedValue({
62+
get: vi.fn().mockReturnValue(undefined)
63+
} as any);
64+
});
65+
66+
it("should return 'User is not logged in' when there are no accessible nodes", async () => {
67+
const mockRoot = {
68+
type: "root",
69+
title: "Test Docs",
70+
authed: false,
71+
hidden: false,
72+
child: {
73+
type: "unversioned",
74+
landingPage: null
75+
}
76+
};
77+
78+
mockGetSectionRoot.mockReturnValue(mockRoot as any);
79+
mockCreateCachedDocsLoader.mockResolvedValue({
80+
getRoot: vi.fn().mockResolvedValue(mockRoot)
81+
} as any);
82+
83+
mockTraverseDF.mockImplementation((root, callback) => {});
84+
85+
const request = new NextRequest("https://example.com/llms-full.txt");
86+
const params = Promise.resolve({ host: "example.com", domain: "example.com" });
87+
88+
const response = await GET(request, { params });
89+
90+
expect(response.status).toBe(200);
91+
expect(response.headers.get("Content-Type")).toBe("text/plain; charset=utf-8");
92+
93+
const reader = response.body?.getReader();
94+
const decoder = new TextDecoder();
95+
let content = "";
96+
97+
if (reader) {
98+
while (true) {
99+
const { done, value } = await reader.read();
100+
if (done) break;
101+
content += decoder.decode(value, { stream: true });
102+
}
103+
}
104+
105+
expect(content).toBe("User is not logged in");
106+
107+
expect(mockTrack).not.toHaveBeenCalled();
108+
});
109+
110+
it("should return 'User is not logged in' when root is authed (resulting in no accessible nodes)", async () => {
111+
const mockRoot = {
112+
type: "root",
113+
title: "Test Docs",
114+
authed: true,
115+
hidden: false,
116+
child: {
117+
type: "unversioned",
118+
landingPage: null
119+
}
120+
};
121+
122+
mockGetSectionRoot.mockReturnValue(mockRoot as any);
123+
mockCreateCachedDocsLoader.mockResolvedValue({
124+
getRoot: vi.fn().mockResolvedValue(mockRoot)
125+
} as any);
126+
127+
mockTraverseDF.mockImplementation((root, callback) => {});
128+
129+
const request = new NextRequest("https://example.com/llms-full.txt");
130+
const params = Promise.resolve({ host: "example.com", domain: "example.com" });
131+
132+
const response = await GET(request, { params });
133+
134+
expect(response.status).toBe(200);
135+
136+
const reader = response.body?.getReader();
137+
const decoder = new TextDecoder();
138+
let content = "";
139+
140+
if (reader) {
141+
while (true) {
142+
const { done, value } = await reader.read();
143+
if (done) break;
144+
content += decoder.decode(value, { stream: true });
145+
}
146+
}
147+
148+
expect(content).toBe("User is not logged in");
149+
150+
expect(mockTrack).not.toHaveBeenCalled();
151+
});
152+
153+
it("should return content and track when there are accessible nodes", async () => {
154+
const mockRoot = {
155+
type: "root",
156+
title: "Test Docs",
157+
authed: false,
158+
hidden: false,
159+
child: {
160+
type: "unversioned",
161+
landingPage: null
162+
}
163+
};
164+
165+
const mockPage = {
166+
type: "page",
167+
title: "Test Page",
168+
slug: "test-page",
169+
authed: false,
170+
hidden: false,
171+
id: "page-1"
172+
};
173+
174+
mockGetSectionRoot.mockReturnValue(mockRoot as any);
175+
mockCreateCachedDocsLoader.mockResolvedValue({
176+
getRoot: vi.fn().mockResolvedValue(mockRoot)
177+
} as any);
178+
179+
mockHasMetadata.mockImplementation((node: any) => {
180+
return node.authed !== undefined || node.hidden !== undefined;
181+
});
182+
mockIsPage.mockImplementation((node: any) => {
183+
return node.type === "page";
184+
});
185+
mockGetPageId.mockReturnValue("page-1");
186+
mockGetMarkdownForPath.mockResolvedValue({
187+
content: "# Test Page Content"
188+
} as any);
189+
190+
mockTraverseDF.mockImplementation((root, callback) => {
191+
const result = callback(mockPage as any, []);
192+
return result;
193+
});
194+
195+
const request = new NextRequest("https://example.com/llms-full.txt");
196+
const params = Promise.resolve({ host: "example.com", domain: "example.com" });
197+
198+
const response = await GET(request, { params });
199+
200+
expect(response.status).toBe(200);
201+
202+
const reader = response.body?.getReader();
203+
const decoder = new TextDecoder();
204+
let content = "";
205+
206+
if (reader) {
207+
while (true) {
208+
const { done, value } = await reader.read();
209+
if (done) break;
210+
content += decoder.decode(value, { stream: true });
211+
}
212+
}
213+
214+
expect(content).not.toBe("User is not logged in");
215+
expect(content).toContain("# Test Page Content");
216+
217+
expect(mockTrack).toHaveBeenCalledWith(
218+
"static_content_served",
219+
expect.objectContaining({
220+
domain: "example.com",
221+
host: "example.com",
222+
staticContentType: "llms-full.txt",
223+
streaming: true
224+
})
225+
);
226+
});
227+
228+
it("should return 'User is not logged in' when all nodes are hidden", async () => {
229+
const mockRoot = {
230+
type: "root",
231+
title: "Test Docs",
232+
authed: false,
233+
hidden: false,
234+
child: {
235+
type: "unversioned",
236+
landingPage: null
237+
}
238+
};
239+
240+
mockGetSectionRoot.mockReturnValue(mockRoot as any);
241+
mockCreateCachedDocsLoader.mockResolvedValue({
242+
getRoot: vi.fn().mockResolvedValue(mockRoot)
243+
} as any);
244+
245+
mockTraverseDF.mockImplementation((root, callback) => {});
246+
247+
const request = new NextRequest("https://example.com/llms-full.txt");
248+
const params = Promise.resolve({ host: "example.com", domain: "example.com" });
249+
250+
const response = await GET(request, { params });
251+
252+
expect(response.status).toBe(200);
253+
254+
const reader = response.body?.getReader();
255+
const decoder = new TextDecoder();
256+
let content = "";
257+
258+
if (reader) {
259+
while (true) {
260+
const { done, value } = await reader.read();
261+
if (done) break;
262+
content += decoder.decode(value, { stream: true });
263+
}
264+
}
265+
266+
expect(content).toBe("User is not logged in");
267+
268+
expect(mockTrack).not.toHaveBeenCalled();
269+
});
270+
});

packages/fern-docs/bundle/src/app/[host]/[domain]/api/fern-docs/llms-full.txt/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ export async function GET(
6666

6767
const uniqueNodes = uniqBy(nodes, (a) => FernNavigation.getPageId(a) ?? a.canonicalSlug ?? a.slug);
6868

69+
// Don't log to PostHog if no accessible nodes
70+
if (uniqueNodes.length === 0) {
71+
controller.enqueue(encoder.encode("User is not logged in"));
72+
controller.close();
73+
return;
74+
}
75+
6976
const markdownStartTime = performance.now();
7077

7178
for (const node of uniqueNodes) {

0 commit comments

Comments
 (0)