Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/astro-theme/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/astro-theme",
"version": "0.1.109",
"version": "0.1.111",
"description": "Astro UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
"keywords": [
"astro",
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/astro",
"version": "0.1.109",
"version": "0.1.111",
"description": "Astro adapter for @farming-labs/docs — content loading and navigation utilities",
"keywords": [
"astro",
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/docs",
"version": "0.1.109",
"version": "0.1.111",
"description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
"keywords": [
"docs",
Expand Down
2 changes: 1 addition & 1 deletion packages/fumadocs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/theme",
"version": "0.1.109",
"version": "0.1.111",
"description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
"keywords": [
"docs",
Expand Down
72 changes: 72 additions & 0 deletions packages/fumadocs/src/docs-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2514,6 +2514,78 @@ The changelog now has its own dedicated route.
);
});

it.each(["", "/", "///"])(
"serves markdown from root-mounted docsPath value %s",
async (docsPath) => {
const rootDir = mkdtempSync(join(tmpdir(), "fumadocs-root-docspath-markdown-"));
tempDirs.push(rootDir);

mkdirSync(join(rootDir, "app", "docs", "quickstart"), { recursive: true });
writeFileSync(
join(rootDir, "app", "docs", "quickstart", "page.mdx"),
`---
title: "Quickstart"
description: "Start here"
---

# Quickstart

Welcome to the docs.
`,
);

process.chdir(rootDir);

const { GET } = createDocsAPI({
rootDir,
entry: "docs",
docsPath,
});

const response = await GET(new Request("http://localhost/quickstart.md"));
expect(response.status).toBe(200);
expect(response.headers.get("link")).toBe('<http://localhost/quickstart>; rel="canonical"');
expect(await response.text()).toContain("# Quickstart\nURL: /quickstart");
},
);

it.each(["docs", "/docs", "docs/", "/docs/"])(
"serves markdown from default docsPath value %s",
async (docsPath) => {
const rootDir = mkdtempSync(join(tmpdir(), "fumadocs-default-docspath-markdown-"));
tempDirs.push(rootDir);

mkdirSync(join(rootDir, "app", "docs", "quickstart"), { recursive: true });
writeFileSync(
join(rootDir, "app", "docs", "quickstart", "page.mdx"),
`---
title: "Quickstart"
description: "Start here"
---

# Quickstart

Welcome to the docs.
`,
);

process.chdir(rootDir);

const { GET } = createDocsAPI({
rootDir,
entry: "docs",
docsPath,
});

const response = await GET(new Request("http://localhost/docs/quickstart.md"));
expect(response.status).toBe(200);
expect(response.headers.get("link")).toBe(
'<http://localhost/docs/quickstart>; rel="canonical"',
);
expect(await response.text()).toContain("# Quickstart\nURL: /docs/quickstart");
},
);

it("skips changelog indexing when reading the changelog directory fails", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "fumadocs-changelog-search-read-failure-"));
tempDirs.push(rootDir);
Expand Down
9 changes: 6 additions & 3 deletions packages/fumadocs/src/docs-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1644,10 +1644,13 @@ function resolveMarkdownRequest(entry: string, url: URL, request: Request): Mark
function normalizeDocsPublicPath(value: string | undefined, entry: string): string {
if (typeof value !== "string") return `/${normalizePathSegment(entry)}`;

const cleaned = value.trim();
if (cleaned === "" || cleaned === "/") return "";
const cleaned = value
.trim()
.replace(/^\/+|\/+$/g, "")
.replace(/\/+/g, "/");
if (cleaned === "") return "";

return `/${cleaned.replace(/^\/+|\/+$/g, "")}`;
return `/${cleaned}`;
}

function publicDocsRoute(publicPath: string, slugParts: string[] = []): string {
Expand Down
9 changes: 6 additions & 3 deletions packages/fumadocs/src/docs-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,13 @@ function resolveDocsLocaleContext(config: DocsConfig, locale?: string): DocsLoca
function normalizeDocsPublicPath(value: string | undefined, entry: string): string {
if (typeof value !== "string") return `/${entry.replace(/^\/+|\/+$/g, "") || "docs"}`;

const cleaned = value.trim();
if (cleaned === "" || cleaned === "/") return "";
const cleaned = value
.trim()
.replace(/^\/+|\/+$/g, "")
.replace(/\/+/g, "/");
if (cleaned === "") return "";

return `/${cleaned.replace(/^\/+|\/+$/g, "")}`;
return `/${cleaned}`;
}

function publicDocsRoute(ctx: DocsLocaleContext, slugParts: string[] = []): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/next",
"version": "0.1.109",
"version": "0.1.111",
"description": "Next.js adapter for @farming-labs/docs — MDX config wrapper",
"keywords": [
"docs",
Expand Down
118 changes: 118 additions & 0 deletions packages/next/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ const DOCS_CONFIG_WITH_ROOT_DOCS_PATH = `export default {
};
`;

const DOCS_CONFIG_WITH_SLASH_DOCS_PATH = `export default {
entry: "docs",
docsPath: "/",
};
`;

const MARKDOWN_ACCEPT_HEADER = {
type: "header",
key: "accept",
Expand Down Expand Up @@ -684,6 +690,118 @@ describe("withDocs (app dir: src/app vs app)", () => {
);
});

it.each([DOCS_CONFIG_WITH_ROOT_DOCS_PATH, DOCS_CONFIG_WITH_SLASH_DOCS_PATH])(
"treats root docsPath values as the site root",
async (configSource) => {
writeFileSync(join(tmpDir, "docs.config.ts"), configSource, "utf-8");
mkdirSync(join(tmpDir, "app"), { recursive: true });
process.chdir(tmpDir);

const nextConfig = withDocs({});
const beforeFiles = getBeforeFilesRewrites(await readRewrites(nextConfig));

expect(beforeFiles).toEqual(
expect.arrayContaining([
expect.objectContaining({
source: "/",
destination: "/docs/",
}),
expect.objectContaining({
source: expect.stringContaining(":slug((?!"),
destination: "/docs/:slug",
}),
expect.objectContaining({
source: "/",
has: [MARKDOWN_ACCEPT_HEADER],
destination: "/api/docs?format=markdown",
}),
]),
);
},
);

it.each(["docs", "/docs", "docs/", "/docs/"])(
"treats %s as the default docsPath",
async (docsPath) => {
writeFileSync(
join(tmpDir, "docs.config.ts"),
`export default {
entry: "docs",
docsPath: ${JSON.stringify(docsPath)},
};
`,
"utf-8",
);
mkdirSync(join(tmpDir, "app"), { recursive: true });
process.chdir(tmpDir);

const nextConfig = withDocs({});
const beforeFiles = getBeforeFilesRewrites(await readRewrites(nextConfig));

expect(beforeFiles).toEqual(
expect.arrayContaining([
expect.objectContaining({
source: "/docs",
has: [MARKDOWN_ACCEPT_HEADER],
destination: "/api/docs?format=markdown",
}),
expect.objectContaining({
source: "/docs/:slug*",
has: [MARKDOWN_ACCEPT_HEADER],
destination: "/api/docs?format=markdown&path=:slug*",
}),
]),
);
expect(beforeFiles).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
source: "/docs",
destination: "/docs",
}),
expect.objectContaining({
source: "/docs/:slug*",
destination: "/docs/:slug*",
}),
]),
);
},
);

it("normalizes duplicate slashes inside docsPath", async () => {
writeFileSync(
join(tmpDir, "docs.config.ts"),
`export default {
entry: "docs",
docsPath: "/guides//docs/",
};
`,
"utf-8",
);
mkdirSync(join(tmpDir, "app"), { recursive: true });
process.chdir(tmpDir);

const nextConfig = withDocs({});
const beforeFiles = getBeforeFilesRewrites(await readRewrites(nextConfig));

expect(beforeFiles).toEqual(
expect.arrayContaining([
expect.objectContaining({
source: "/guides/docs",
destination: "/docs",
}),
expect.objectContaining({
source: "/guides/docs/:slug*",
destination: "/docs/:slug*",
}),
expect.objectContaining({
source: "/guides/docs",
has: [MARKDOWN_ACCEPT_HEADER],
destination: "/api/docs?format=markdown",
}),
]),
);
});

it("adds agent feedback rewrites through the shared docs api handler by default", async () => {
mkdirSync(join(tmpDir, "app"), { recursive: true });
process.chdir(tmpDir);
Expand Down
9 changes: 6 additions & 3 deletions packages/next/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1267,10 +1267,13 @@ function normalizeRouteSegment(value: string | undefined, fallback = "docs"): st
function normalizeDocsPath(value: string | undefined, entry: string): string {
if (typeof value !== "string") return `/${normalizeRouteSegment(entry)}`;

const cleaned = value.trim();
if (cleaned === "" || cleaned === "/") return "";
const cleaned = value
.trim()
.replace(/^\/+|\/+$/g, "")
.replace(/\/+/g, "/");
if (cleaned === "") return "";

return `/${cleaned.replace(/^\/+|\/+$/g, "")}`;
return `/${cleaned}`;
}

function docsRootSource(entry: string): string {
Expand Down
33 changes: 33 additions & 0 deletions packages/next/src/mdx-plugins/remark-markdown-alternate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,39 @@ describe("remarkMarkdownAlternate", () => {
expect(tree.children[0]?.value).toContain('text/markdown: "/docs.md"');
});

it.each(["", "/", "///"])("treats docsPath %s as root-mounted docs", (docsPath) => {
const tree = {
children: [{ type: "yaml", value: 'title: "Install"' }],
};
const transform = remarkMarkdownAlternate({
entry: "docs",
contentDir: "app/docs",
docsPath,
});

transform(tree, { path: "/repo/app/docs/install/page.mdx" });

expect(tree.children[0]?.value).toContain('text/markdown: "/install.md"');
});

it.each(["docs", "/docs", "docs/", "/docs/"])(
"normalizes docsPath %s to the default docs route",
(docsPath) => {
const tree = {
children: [{ type: "yaml", value: 'title: "Install"' }],
};
const transform = remarkMarkdownAlternate({
entry: "docs",
contentDir: "app/docs",
docsPath,
});

transform(tree, { path: "/repo/app/docs/install/page.mdx" });

expect(tree.children[0]?.value).toContain('text/markdown: "/docs/install.md"');
},
);

it("handles relative source paths", () => {
const tree = {
children: [{ type: "yaml", value: 'title: "Quickstart"' }],
Expand Down
9 changes: 6 additions & 3 deletions packages/next/src/mdx-plugins/remark-markdown-alternate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ function normalizeSegment(value: string | undefined, fallback: string): string {
function normalizeDocsPath(value: string | undefined, entry: string): string {
if (typeof value !== "string") return `/${entry}`;

const cleaned = value.trim();
if (cleaned === "" || cleaned === "/") return "";
const cleaned = value
.trim()
.replace(/^\/+|\/+$/g, "")
.replace(/\/+/g, "/");
if (cleaned === "") return "";

return `/${cleaned.replace(/^\/+|\/+$/g, "")}`;
return `/${cleaned}`;
}

function joinDocsPath(docsPath: string, slug: string): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt-theme/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/nuxt-theme",
"version": "0.1.109",
"version": "0.1.111",
"description": "Nuxt/Vue UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
"keywords": [
"docs",
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/nuxt",
"version": "0.1.109",
"version": "0.1.111",
"description": "Nuxt adapter for @farming-labs/docs — content loading and navigation utilities",
"keywords": [
"docs",
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte-theme/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/svelte-theme",
"version": "0.1.109",
"version": "0.1.111",
"description": "Svelte UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
"keywords": [
"docs",
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/svelte",
"version": "0.1.109",
"version": "0.1.111",
"description": "SvelteKit adapter for @farming-labs/docs — content loading and navigation utilities",
"keywords": [
"docs",
Expand Down
2 changes: 1 addition & 1 deletion packages/tanstack-start/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@farming-labs/tanstack-start",
"version": "0.1.109",
"version": "0.1.111",
"description": "TanStack Start adapter for @farming-labs/docs — content loading, search, and AI utilities",
"keywords": [
"docs",
Expand Down
Loading