diff --git a/frontend/src/app/[lang]/docs/layout.tsx b/frontend/src/app/[lang]/docs/layout.tsx
index 895da1da8..12b7d4fbd 100644
--- a/frontend/src/app/[lang]/docs/layout.tsx
+++ b/frontend/src/app/[lang]/docs/layout.tsx
@@ -1,7 +1,7 @@
-import type { PageMapItem } from "nextra";
import { getPageMap } from "nextra/page-map";
import { Layout } from "nextra-theme-docs";
+import { buildLocalizedDocsPageMap } from "@/components/docs/docs-page-map";
import { Footer } from "@/components/landing/footer";
import { Header } from "@/components/landing/header";
import { getLocaleByLang } from "@/core/i18n/locale";
@@ -12,23 +12,11 @@ const i18n = [
{ locale: "zh", name: "中文" },
];
-function formatPageRoute(base: string, items: PageMapItem[]): PageMapItem[] {
- return items.map((item) => {
- if ("route" in item && !item.route.startsWith(base)) {
- item.route = `${base}${item.route}`;
- }
- if ("children" in item && item.children) {
- item.children = formatPageRoute(base, item.children);
- }
- return item;
- });
-}
-
export default async function DocLayout({ children, params }) {
const { lang } = await params;
const locale = getLocaleByLang(lang);
const pages = await getPageMap(`/${lang}`);
- const pageMap = formatPageRoute(`/${lang}/docs`, pages);
+ const pageMap = buildLocalizedDocsPageMap(`/${lang}/docs`, pages);
return (
}
pageMap={pageMap}
- docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/content"
+ docsRepositoryBase="https://github.com/bytedance/deer-flow/tree/main/frontend"
footer={}
i18n={i18n}
// ... Your additional layout options
diff --git a/frontend/src/app/blog/layout.tsx b/frontend/src/app/blog/layout.tsx
index 94fe01037..8a5766451 100644
--- a/frontend/src/app/blog/layout.tsx
+++ b/frontend/src/app/blog/layout.tsx
@@ -13,7 +13,7 @@ export default async function BlogLayout({ children }) {
navbar={}
pageMap={pageMap}
sidebar={{ defaultOpen: true }}
- docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/content"
+ docsRepositoryBase="https://github.com/bytedance/deer-flow/tree/main/frontend"
footer={}
>
{children}
diff --git a/frontend/src/components/docs/docs-page-map.ts b/frontend/src/components/docs/docs-page-map.ts
new file mode 100644
index 000000000..5b82c7c99
--- /dev/null
+++ b/frontend/src/components/docs/docs-page-map.ts
@@ -0,0 +1,49 @@
+import type { PageMapItem } from "nextra";
+
+// Nextra's generated page map also contains unrelated App Router pages. Only
+// these top-level content directories belong under /{lang}/docs.
+export const DOCS_CONTENT_ROOTS = [
+ "application",
+ "harness",
+ "introduction",
+ "posts",
+ "reference",
+ "tutorials",
+] as const;
+
+const DOCS_CONTENT_ROOT_SET = new Set(DOCS_CONTENT_ROOTS);
+
+function isLocalizedDocsRoute(route: string, base: string): boolean {
+ return route === base || route.startsWith(`${base}/`);
+}
+
+function isDocsContentRoute(route: string): boolean {
+ const root = route.split("/").find(Boolean);
+ return root === undefined || DOCS_CONTENT_ROOT_SET.has(root);
+}
+
+export function buildLocalizedDocsPageMap(
+ base: string,
+ items: PageMapItem[],
+): PageMapItem[] {
+ return items.flatMap((item) => {
+ if (!("route" in item)) {
+ return [item];
+ }
+
+ const alreadyLocalized = isLocalizedDocsRoute(item.route, base);
+ if (!alreadyLocalized && !isDocsContentRoute(item.route)) {
+ return [];
+ }
+
+ return [
+ {
+ ...item,
+ route: alreadyLocalized ? item.route : `${base}${item.route}`,
+ ...("children" in item
+ ? { children: buildLocalizedDocsPageMap(base, item.children) }
+ : {}),
+ },
+ ];
+ });
+}
diff --git a/frontend/src/content/en/harness/lead-agent.mdx b/frontend/src/content/en/harness/lead-agent.mdx
index 0363ddba6..3da01bc4d 100644
--- a/frontend/src/content/en/harness/lead-agent.mdx
+++ b/frontend/src/content/en/harness/lead-agent.mdx
@@ -33,7 +33,7 @@ The Lead Agent does not hardcode a specific workflow. It uses the model's reason
The Lead Agent is built on **LangGraph** and **LangChain Agent** primitives. Specifically:
-- [`create_agent`](https://python.langchain.com/docs/concepts/agents/) from `langchain.agents` wraps the LLM into a tool-calling agent loop.
+- [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents) from `langchain.agents` wraps the LLM into a tool-calling agent loop.
- LangGraph manages the `ThreadState` and provides the checkpointing, streaming, and graph execution model.
- A **middleware chain** wraps every turn of the agent loop, providing cross-cutting capabilities like memory, summarization, and clarification.
diff --git a/frontend/src/content/en/harness/subagents.mdx b/frontend/src/content/en/harness/subagents.mdx
index b132099b0..ab595af77 100644
--- a/frontend/src/content/en/harness/subagents.mdx
+++ b/frontend/src/content/en/harness/subagents.mdx
@@ -96,7 +96,7 @@ If the agent tries to call more subagents than the limits allow, the middleware
## ACP agents (external agents)
-In addition to the built-in subagents, DeerFlow supports delegating to external agents through the **Agent Connect Protocol (ACP)**. ACP allows DeerFlow to invoke agents running as separate processes (including third-party CLI tools wrapped with an ACP adapter).
+In addition to the built-in subagents, DeerFlow supports delegating to external agents through the **Agent Client Protocol (ACP)**. ACP allows DeerFlow to invoke agents running as separate processes (including third-party CLI tools wrapped with an ACP adapter).
Configure ACP agents in `config.yaml`:
diff --git a/frontend/src/content/en/index.mdx b/frontend/src/content/en/index.mdx
index ba9fd7117..58735970a 100644
--- a/frontend/src/content/en/index.mdx
+++ b/frontend/src/content/en/index.mdx
@@ -38,9 +38,9 @@ Start with the Harness section. This path is for teams who want to integrate Dee
Start with the App section. This path is for teams who want to run DeerFlow as a complete application and understand how to configure, operate, and use it in practice.
- [DeerFlow App](./docs/application)
-- [Quick Start](./docs/app/quick-start)
-- [Deployment Guide](./docs/app/deployment-guide)
-- [Workspace Usage](./docs/app/workspace-usage)
+- [Quick Start](./docs/application/quick-start)
+- [Deployment Guide](./docs/application/deployment-guide)
+- [Workspace Usage](./docs/application/workspace-usage)
## Documentation structure
@@ -79,17 +79,17 @@ The App section is written for teams who want to deploy DeerFlow as a usable pro
The Tutorials section is for hands-on, task-oriented learning.
-- [Tutorials](./docs/tutorials)
+- [Tutorials](./docs/tutorials/first-conversation)
### Reference
The Reference section is for detailed lookup material, including configuration, runtime modes, APIs, and source-oriented mapping.
-- [Reference](./docs/reference)
+- [Reference](./docs/reference/model-providers)
## Choose the right path
- If you are **evaluating the project**, start with [Introduction](./docs/introduction).
- If you are **building your own agent system**, start with [DeerFlow Harness](./docs/harness).
- If you are **deploying DeerFlow for users**, start with [DeerFlow App](./docs/application).
-- If you want to **learn by doing**, go to [Tutorials](./docs/tutorials).
+- If you want to **learn by doing**, go to [Tutorials](./docs/tutorials/first-conversation).
diff --git a/frontend/src/content/en/introduction/harness-vs-app.mdx b/frontend/src/content/en/introduction/harness-vs-app.mdx
index 2f6f39454..3423c7fd2 100644
--- a/frontend/src/content/en/introduction/harness-vs-app.mdx
+++ b/frontend/src/content/en/introduction/harness-vs-app.mdx
@@ -95,7 +95,7 @@ The two layers are complementary. The Harness gives you the agent runtime. The A
-
+
## One foundation, two entry points
@@ -105,4 +105,4 @@ DeerFlow is not split into two unrelated products. It is one system with two ent
The **Harness** is the core runtime for developers.
The **App** is the best-practice Super Agent application built on top of that runtime.
-If you want to go deeper into the runtime itself, continue with the [DeerFlow Harness](/docs/harness). If you want to run DeerFlow as a product, continue with the [DeerFlow App](/docs/app).
+If you want to go deeper into the runtime itself, continue with the [DeerFlow Harness](/docs/harness). If you want to run DeerFlow as a product, continue with the [DeerFlow App](/docs/application).
diff --git a/frontend/src/content/en/posts/provider-safety-termination-in-tool-agents.mdx b/frontend/src/content/en/posts/provider-safety-termination-in-tool-agents.mdx
index f72c57770..0520d91cf 100644
--- a/frontend/src/content/en/posts/provider-safety-termination-in-tool-agents.mdx
+++ b/frontend/src/content/en/posts/provider-safety-termination-in-tool-agents.mdx
@@ -14,7 +14,7 @@ When a large model provider decides that an input or output has triggered a safe
These partial tool calls must not be executed as normal intent. A truncated `write_file` call may write an incomplete report. A truncated `bash` call may enter the sandbox with incomplete arguments. After seeing the failed result, the Agent may retry and trigger the same safety rule repeatedly.
-[PR #3035](https://github.com/bytedance/deer-flow/pull/3035) addresses this boundary: when a provider stops generation with a safety signal while the response still contains tool calls, DeerFlow should suppress those tool calls first and record the turn as a safety termination event.
+[the implementation commit](https://github.com/bytedance/deer-flow/commit/be0eae9825619b63ca0c67b253d40b5eee76a2d6) addresses this boundary: when a provider stops generation with a safety signal while the response still contains tool calls, DeerFlow should suppress those tool calls first and record the turn as a safety termination event.
## Why Safety Termination Needs Dedicated Handling
diff --git a/frontend/src/content/zh/harness/lead-agent.mdx b/frontend/src/content/zh/harness/lead-agent.mdx
index f284ba4b8..3a3c8ce28 100644
--- a/frontend/src/content/zh/harness/lead-agent.mdx
+++ b/frontend/src/content/zh/harness/lead-agent.mdx
@@ -33,7 +33,7 @@ Lead Agent 不硬编码特定的工作流。它使用模型的推理能力来适
Lead Agent 基于 **LangGraph** 和 **LangChain Agent** 原语构建:
-- `langchain.agents` 的 [`create_agent`](https://python.langchain.com/docs/concepts/agents/) 将 LLM 封装成工具调用 Agent 循环。
+- `langchain.agents` 的 [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents) 将 LLM 封装成工具调用 Agent 循环。
- LangGraph 管理 `ThreadState`,提供检查点、流式传输和图执行模型。
- **中间件链**包裹 Agent 循环的每一轮,提供记忆、摘要和澄清等跨领域能力。
diff --git a/frontend/src/content/zh/harness/subagents.mdx b/frontend/src/content/zh/harness/subagents.mdx
index f9488f97d..80397a203 100644
--- a/frontend/src/content/zh/harness/subagents.mdx
+++ b/frontend/src/content/zh/harness/subagents.mdx
@@ -95,7 +95,7 @@ subagents:
## ACP Agent(外部 Agent)
-除内置子 Agent 外,DeerFlow 还通过 **Agent Connect Protocol (ACP)** 支持委派给外部 Agent。ACP 允许 DeerFlow 调用作为独立进程运行的 Agent(包括用 ACP 适配器包装的第三方 CLI 工具)。
+除内置子 Agent 外,DeerFlow 还通过 **Agent Client Protocol (ACP)** 支持委派给外部 Agent。ACP 允许 DeerFlow 调用作为独立进程运行的 Agent(包括用 ACP 适配器包装的第三方 CLI 工具)。
在 `config.yaml` 中配置 ACP Agent:
diff --git a/frontend/src/content/zh/posts/provider-safety-termination-in-tool-agents.mdx b/frontend/src/content/zh/posts/provider-safety-termination-in-tool-agents.mdx
index 4979fa397..b407be7ba 100644
--- a/frontend/src/content/zh/posts/provider-safety-termination-in-tool-agents.mdx
+++ b/frontend/src/content/zh/posts/provider-safety-termination-in-tool-agents.mdx
@@ -14,7 +14,7 @@ tags:
这类半截工具调用不应被当成正常意图执行。一个被截断的 `write_file` 可能写出不完整的报告;一个被截断的 `bash` 调用可能带着残缺参数进入沙箱;Agent 看到失败结果后还可能继续重试,反复触发同一条安全规则。
-[PR #3035](https://github.com/bytedance/deer-flow/pull/3035) 处理的就是这个边界:当 provider 用安全信号中止生成,同时响应仍带有工具调用时,DeerFlow 应先压制这些工具调用,再把这一轮作为安全中止事件记录下来。
+[对应的实现提交](https://github.com/bytedance/deer-flow/commit/be0eae9825619b63ca0c67b253d40b5eee76a2d6) 处理的就是这个边界:当 provider 用安全信号中止生成,同时响应仍带有工具调用时,DeerFlow 应先压制这些工具调用,再把这一轮作为安全中止事件记录下来。
## 为什么需要单独处理安全中止
diff --git a/frontend/tests/e2e/docs-localized-links.spec.ts b/frontend/tests/e2e/docs-localized-links.spec.ts
index 4f1b59dee..476c00256 100644
--- a/frontend/tests/e2e/docs-localized-links.spec.ts
+++ b/frontend/tests/e2e/docs-localized-links.spec.ts
@@ -51,4 +51,33 @@ test.describe("Localized documentation links", () => {
);
await expect(page.locator("main h1")).toContainText("Agents and Threads");
});
+
+ test("excludes non-documentation app routes from the docs navigation", async ({
+ page,
+ }) => {
+ await page.goto("/en/docs");
+
+ const invalidDocsLinks = page.locator(
+ ["auth", "blog", "login", "setup", "workspace"]
+ .map((root) => `a[href^="/en/docs/${root}"]`)
+ .join(", "),
+ );
+ await expect(invalidDocsLinks).toHaveCount(0);
+ });
+
+ test("uses valid repository links for documentation feedback and edits", async ({
+ page,
+ }) => {
+ await page.goto("/en/docs/application/quick-start");
+
+ await expect(
+ page.getByRole("link", { name: "Question? Give us feedback" }),
+ ).toHaveAttribute("href", /github\.com\/bytedance\/deer-flow\/issues\/new/);
+ await expect(
+ page.getByRole("link", { name: "Edit this page" }),
+ ).toHaveAttribute(
+ "href",
+ "https://github.com/bytedance/deer-flow/tree/main/frontend/src/content/en/application/quick-start.mdx",
+ );
+ });
});
diff --git a/frontend/tests/unit/components/docs/docs-page-map.test.ts b/frontend/tests/unit/components/docs/docs-page-map.test.ts
new file mode 100644
index 000000000..5a4125556
--- /dev/null
+++ b/frontend/tests/unit/components/docs/docs-page-map.test.ts
@@ -0,0 +1,99 @@
+import { readdirSync } from "node:fs";
+import { join } from "node:path";
+
+import { describe, expect, it } from "@rstest/core";
+import type { PageMapItem } from "nextra";
+
+import {
+ buildLocalizedDocsPageMap,
+ DOCS_CONTENT_ROOTS,
+} from "@/components/docs/docs-page-map";
+
+describe("buildLocalizedDocsPageMap", () => {
+ it.each(["en", "zh"])(
+ "keeps the %s content roots in the docs page map",
+ (lang) => {
+ const contentRoots = readdirSync(
+ join(process.cwd(), "src/content", lang),
+ {
+ withFileTypes: true,
+ },
+ )
+ .filter((entry) => entry.isDirectory())
+ .map((entry) => entry.name)
+ .sort();
+
+ expect(contentRoots).toEqual([...DOCS_CONTENT_ROOTS].sort());
+ },
+ );
+
+ it("localizes content routes and removes unrelated App Router pages", () => {
+ const source: PageMapItem[] = [
+ { name: "index", route: "/" },
+ {
+ name: "application",
+ route: "/application",
+ children: [{ name: "quick-start", route: "/application/quick-start" }],
+ },
+ {
+ name: "posts",
+ route: "/posts",
+ children: [{ name: "weekly", route: "/posts/weekly/2026-04-06" }],
+ },
+ {
+ name: "workspace",
+ route: "/workspace",
+ children: [
+ { name: "chats", route: "/workspace/chats" },
+ {
+ name: "scheduled-tasks",
+ route: "/workspace/scheduled-tasks",
+ },
+ ],
+ },
+ {
+ name: "blog",
+ route: "/blog",
+ children: [{ name: "posts", route: "/blog/posts" }],
+ },
+ { name: "login", route: "/login" },
+ { name: "callback", route: "/auth/callback" },
+ ];
+
+ expect(buildLocalizedDocsPageMap("/en/docs", source)).toEqual([
+ { name: "index", route: "/en/docs/" },
+ {
+ name: "application",
+ route: "/en/docs/application",
+ children: [
+ {
+ name: "quick-start",
+ route: "/en/docs/application/quick-start",
+ },
+ ],
+ },
+ {
+ name: "posts",
+ route: "/en/docs/posts",
+ children: [
+ {
+ name: "weekly",
+ route: "/en/docs/posts/weekly/2026-04-06",
+ },
+ ],
+ },
+ ]);
+ expect(source[0]).toEqual({ name: "index", route: "/" });
+ });
+
+ it("preserves routes already localized to the requested docs base", () => {
+ const source: PageMapItem[] = [
+ { name: "harness", route: "/zh/docs/harness" },
+ { name: "not-docs", route: "/zh/docs-extra" },
+ ];
+
+ expect(buildLocalizedDocsPageMap("/zh/docs", source)).toEqual([
+ { name: "harness", route: "/zh/docs/harness" },
+ ]);
+ });
+});
diff --git a/frontend/tests/unit/content/docs-links.test.ts b/frontend/tests/unit/content/docs-links.test.ts
new file mode 100644
index 000000000..e88b16381
--- /dev/null
+++ b/frontend/tests/unit/content/docs-links.test.ts
@@ -0,0 +1,89 @@
+import { readFileSync, readdirSync } from "node:fs";
+import { join, relative, sep } from "node:path";
+
+import { describe, expect, it } from "@rstest/core";
+
+const CONTENT_ROOT = join(process.cwd(), "src/content");
+const DOC_LANGUAGES = ["en", "zh"] as const;
+const DOCS_ORIGIN = "https://docs.example";
+const LINK_PATTERN =
+ /(?]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)|\bhref\s*=\s*(?:\{\s*)?["']([^"']+)["'](?:\s*\})?/g;
+const UNLOCALIZED_DOCS_PATH = /^\/docs(?=\/|[?#]|$)/;
+
+function findMdxFiles(directory: string): string[] {
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
+ const path = join(directory, entry.name);
+ return entry.isDirectory()
+ ? findMdxFiles(path)
+ : entry.name.endsWith(".mdx")
+ ? [path]
+ : [];
+ });
+}
+
+function routeForMdx(path: string, lang: string): string {
+ const localeRoot = join(CONTENT_ROOT, lang);
+ const relativePath = relative(localeRoot, path).split(sep).join("/");
+ const pagePath = relativePath
+ .replace(/\.mdx$/, "")
+ .replace(/(?:^|\/)index$/, "");
+ return `/${lang}/docs${pagePath ? `/${pagePath}` : ""}`;
+}
+
+function extractLinks(source: string): Array<{ href: string; line: number }> {
+ return [...source.matchAll(LINK_PATTERN)].map((match) => ({
+ href: match[1] ?? match[2] ?? match[3] ?? "",
+ line: source.slice(0, match.index).split("\n").length,
+ }));
+}
+
+function resolveDocsPath(
+ href: string,
+ lang: string,
+ sourceRoute: string,
+): string | undefined {
+ const localizedHref = UNLOCALIZED_DOCS_PATH.test(href)
+ ? `/${lang}${href}`
+ : href;
+ const url = new URL(localizedHref, `${DOCS_ORIGIN}${sourceRoute}`);
+ if (
+ url.origin !== DOCS_ORIGIN ||
+ !/^\/(?:en|zh)\/docs(?:\/|$)/.test(url.pathname)
+ ) {
+ return undefined;
+ }
+ return url.pathname.replace(/\/$/, "");
+}
+
+describe("documentation content links", () => {
+ it("only links to documentation routes backed by MDX pages", () => {
+ const mdxFiles = DOC_LANGUAGES.flatMap((lang) =>
+ findMdxFiles(join(CONTENT_ROOT, lang)),
+ );
+ const routes = new Set(
+ DOC_LANGUAGES.flatMap((lang) =>
+ mdxFiles
+ .filter((path) => path.startsWith(join(CONTENT_ROOT, lang)))
+ .map((path) => routeForMdx(path, lang)),
+ ),
+ );
+
+ const brokenLinks = DOC_LANGUAGES.flatMap((lang) =>
+ findMdxFiles(join(CONTENT_ROOT, lang)).flatMap((path) => {
+ const source = readFileSync(path, "utf8");
+ const sourceRoute = routeForMdx(path, lang);
+ return extractLinks(source).flatMap(({ href, line }) => {
+ const targetRoute = resolveDocsPath(href, lang, sourceRoute);
+ if (!targetRoute || routes.has(targetRoute)) {
+ return [];
+ }
+ return [
+ `${relative(CONTENT_ROOT, path).split(sep).join("/")}:${line} -> ${href} (${targetRoute})`,
+ ];
+ });
+ }),
+ );
+
+ expect(brokenLinks).toEqual([]);
+ });
+});