diff --git a/packages/docs/.env.example b/packages/docs/.env.example
new file mode 100644
index 0000000000..02d2d08332
--- /dev/null
+++ b/packages/docs/.env.example
@@ -0,0 +1,3 @@
+# Show draft documentation pages in the sidebar and allow direct URL access.
+# Pages marked `draft: true` are hidden in production builds.
+# VITE_SHOW_DRAFTS=true
diff --git a/packages/docs/app/components/DocDraftBanner.tsx b/packages/docs/app/components/DocDraftBanner.tsx
new file mode 100644
index 0000000000..74bc552a66
--- /dev/null
+++ b/packages/docs/app/components/DocDraftBanner.tsx
@@ -0,0 +1,16 @@
+import { useT } from "@agent-native/core/client/i18n";
+
+export default function DocDraftBanner() {
+ const t = useT();
+ return (
+
+ {t("docs.draftLabel")} — {t("docs.draftDescription")}
+
+ );
+}
diff --git a/packages/docs/app/components/DocsSidebar.tsx b/packages/docs/app/components/DocsSidebar.tsx
index 89615af632..cbe3eec7e3 100644
--- a/packages/docs/app/components/DocsSidebar.tsx
+++ b/packages/docs/app/components/DocsSidebar.tsx
@@ -193,6 +193,11 @@ export default function DocsSidebar() {
current === item.id ? null : item.id,
)
}
+ style={
+ item.draft
+ ? { color: "var(--approaches-warn)" }
+ : undefined
+ }
>
{item.label}
{item.label}
@@ -236,6 +246,11 @@ export default function DocsSidebar() {
tabIndex={
childrenTabbable ? undefined : -1
}
+ style={
+ child.draft
+ ? { color: "var(--approaches-warn)" }
+ : undefined
+ }
>
{child.label}
diff --git a/packages/docs/app/components/docs-content.ts b/packages/docs/app/components/docs-content.ts
index 5421b0c688..e5e10ee390 100644
--- a/packages/docs/app/components/docs-content.ts
+++ b/packages/docs/app/components/docs-content.ts
@@ -50,6 +50,7 @@ export interface DocEntry {
title: string;
description: string;
search: string;
+ draft?: boolean;
body: string; // markdown body (without frontmatter)
headings: { id: string; label: string; level: number }[];
}
@@ -150,6 +151,7 @@ function docEntryFromPath(path: string, raw: string): DocEntry {
title: data.title || slug,
description: data.description || "",
search: data.search || "",
+ draft: data.draft === "true" || undefined,
body,
headings,
};
@@ -243,6 +245,34 @@ export async function loadDoc(
return promise;
}
+/**
+ * Loads a doc and applies draft visibility, checking the canonical
+ * (default-locale) entry's draft status even when serving a localized
+ * translation. A translation's frontmatter can drift from the canonical
+ * page it was translated from, so gating on the localized doc alone lets a
+ * draft leak through any locale whose translator forgot `draft: true`.
+ */
+export async function loadDocRespectingDraftVisibility(
+ slug: string,
+ locale: unknown = DEFAULT_DOCS_LOCALE,
+): Promise {
+ const doc = await loadDoc(slug, locale);
+ if (!doc) return undefined;
+
+ const docsLocale = normalizeDocsLocale(locale);
+ const canonical =
+ docsLocale === DEFAULT_DOCS_LOCALE
+ ? doc
+ : await loadDoc(slug, DEFAULT_DOCS_LOCALE);
+ const isDraft = Boolean(doc.draft || canonical?.draft);
+
+ if (isDraft && import.meta.env.VITE_SHOW_DRAFTS !== "true") return undefined;
+ // Normalize `draft` to the resolved status so callers that render a draft
+ // banner off this flag stay correct for translations whose frontmatter
+ // omits `draft: true` even though the canonical page is a draft.
+ return isDraft === Boolean(doc.draft) ? doc : { ...doc, draft: isDraft };
+}
+
export function hasLocalizedDoc(locale: unknown, slug: string): boolean {
const docsLocale = normalizeDocsLocale(locale);
if (docsLocale === DEFAULT_DOCS_LOCALE) {
diff --git a/packages/docs/app/components/docsNavItems.ts b/packages/docs/app/components/docsNavItems.ts
index bd31f7138a..f4559bb4ec 100644
--- a/packages/docs/app/components/docsNavItems.ts
+++ b/packages/docs/app/components/docsNavItems.ts
@@ -9,6 +9,7 @@ export type NavItem = {
id: string;
label: string;
to?: string;
+ draft?: boolean;
children?: NavItem[];
};
export type NavSection = { id: string; title: string; items: NavItem[] };
@@ -19,6 +20,7 @@ type NavItemConfig = {
id: string;
labelKey: keyof typeof enUS.nav;
slug?: string;
+ draft?: boolean;
children?: NavItemConfig[];
};
@@ -892,17 +894,24 @@ function navLabel(t: Translate, key: keyof typeof enUS.nav): string {
return t(`nav.${key}`) || enMessage(`nav.${key}`);
}
+const SHOW_DRAFTS = import.meta.env.VITE_SHOW_DRAFTS === "true";
+
function toNavItem(
config: NavItemConfig,
locale: DocsLocale,
t: Translate,
-): NavItem {
+): NavItem | null {
+ if (config.draft && !SHOW_DRAFTS) return null;
const slug = config.slug;
+ const children = config.children
+ ?.map((child) => toNavItem(child, locale, t))
+ .filter((item): item is NavItem => item !== null);
return {
id: config.id,
label: navLabel(t, config.labelKey),
to: slug ? docsPathForSlug(slug, locale) : undefined,
- children: config.children?.map((child) => toNavItem(child, locale, t)),
+ draft: config.draft || undefined,
+ children,
};
}
@@ -913,8 +922,10 @@ export function getDocsNavSections(
return NAV_SECTION_CONFIG.map((section) => ({
id: section.id,
title: navLabel(t, section.titleKey),
- items: section.items.map((item) => toNavItem(item, locale, t)),
- }));
+ items: section.items
+ .map((item) => toNavItem(item, locale, t))
+ .filter((item): item is NavItem => item !== null),
+ })).filter((section) => section.items.length > 0);
}
// Flat list for prev/next navigation and current-item lookups. Nested
diff --git a/packages/docs/app/i18n/ar-SA.ts b/packages/docs/app/i18n/ar-SA.ts
index 38fa519a40..6c6e9334c4 100644
--- a/packages/docs/app/i18n/ar-SA.ts
+++ b/packages/docs/app/i18n/ar-SA.ts
@@ -53,6 +53,9 @@ const arSA = {
copyMarkdownError: "تعذر نسخ Markdown",
previous: "السابق",
next: "التالي",
+ draftLabel: "مسودة",
+ draftDescription:
+ "هذه الصفحة قيد الإنشاء. قد يكون المحتوى غير مكتمل أو عرضة للتغيير قبل النشر.",
},
search: {
dialogLabel: "البحث في الوثائق",
diff --git a/packages/docs/app/i18n/de-DE.ts b/packages/docs/app/i18n/de-DE.ts
index 1b87c971db..0f4b497609 100644
--- a/packages/docs/app/i18n/de-DE.ts
+++ b/packages/docs/app/i18n/de-DE.ts
@@ -53,6 +53,9 @@ const deDE = {
copyMarkdownError: "Markdown konnte nicht kopiert werden",
previous: "Zurück",
next: "Weiter",
+ draftLabel: "Entwurf",
+ draftDescription:
+ "Diese Seite befindet sich in Bearbeitung. Der Inhalt kann unvollständig sein oder sich vor der Veröffentlichung ändern.",
},
search: {
dialogLabel: "Dokumentation durchsuchen",
diff --git a/packages/docs/app/i18n/en-US.ts b/packages/docs/app/i18n/en-US.ts
index f86dd2ad5e..f7b6ab4d3c 100644
--- a/packages/docs/app/i18n/en-US.ts
+++ b/packages/docs/app/i18n/en-US.ts
@@ -51,6 +51,9 @@ const enUS = {
copyMarkdownError: "Couldn't copy Markdown",
previous: "Previous",
next: "Next",
+ draftLabel: "Draft",
+ draftDescription:
+ "This page is a work in progress. Content may be incomplete or subject to change before publication.",
},
common: {
copied: "Copied",
diff --git a/packages/docs/app/i18n/es-ES.ts b/packages/docs/app/i18n/es-ES.ts
index 717b1d7210..ad5c7f7f60 100644
--- a/packages/docs/app/i18n/es-ES.ts
+++ b/packages/docs/app/i18n/es-ES.ts
@@ -53,6 +53,9 @@ const esES = {
copyMarkdownError: "No se pudo copiar Markdown",
previous: "Anterior",
next: "Siguiente",
+ draftLabel: "Borrador",
+ draftDescription:
+ "Esta página está en construcción. El contenido puede estar incompleto o sujeto a cambios antes de su publicación.",
},
search: {
dialogLabel: "Buscar documentación",
diff --git a/packages/docs/app/i18n/fr-FR.ts b/packages/docs/app/i18n/fr-FR.ts
index ffc188f270..43384e91fd 100644
--- a/packages/docs/app/i18n/fr-FR.ts
+++ b/packages/docs/app/i18n/fr-FR.ts
@@ -53,6 +53,9 @@ const frFR = {
copyMarkdownError: "Impossible de copier le Markdown",
previous: "Précédent",
next: "Suivant",
+ draftLabel: "Brouillon",
+ draftDescription:
+ "Cette page est en cours de rédaction. Le contenu peut être incomplet ou sujet à modification avant publication.",
},
search: {
dialogLabel: "Rechercher la documentation",
diff --git a/packages/docs/app/i18n/hi-IN.ts b/packages/docs/app/i18n/hi-IN.ts
index 293be8ff1e..814d97248f 100644
--- a/packages/docs/app/i18n/hi-IN.ts
+++ b/packages/docs/app/i18n/hi-IN.ts
@@ -53,6 +53,9 @@ const hiIN = {
copyMarkdownError: "Markdown कॉपी नहीं हो सका",
previous: "पिछला",
next: "अगला",
+ draftLabel: "ड्राफ़्ट",
+ draftDescription:
+ "यह पेज अभी तैयार किया जा रहा है। प्रकाशन से पहले सामग्री अधूरी हो सकती है या इसमें बदलाव हो सकता है।",
},
search: {
dialogLabel: "दस्तावेज़ खोजें",
diff --git a/packages/docs/app/i18n/ja-JP.ts b/packages/docs/app/i18n/ja-JP.ts
index 021b431a08..2d9c668d1c 100644
--- a/packages/docs/app/i18n/ja-JP.ts
+++ b/packages/docs/app/i18n/ja-JP.ts
@@ -53,6 +53,9 @@ const jaJP = {
copyMarkdownError: "Markdownをコピーできませんでした",
previous: "前へ",
next: "次へ",
+ draftLabel: "ドラフト",
+ draftDescription:
+ "このページは作成中です。内容は公開前に不完全であったり変更される場合があります。",
},
search: {
dialogLabel: "ドキュメントを検索",
diff --git a/packages/docs/app/i18n/ko-KR.ts b/packages/docs/app/i18n/ko-KR.ts
index 779cb1c213..225d020cc1 100644
--- a/packages/docs/app/i18n/ko-KR.ts
+++ b/packages/docs/app/i18n/ko-KR.ts
@@ -53,6 +53,9 @@ const koKR = {
copyMarkdownError: "Markdown을 복사할 수 없음",
previous: "이전",
next: "다음",
+ draftLabel: "초안",
+ draftDescription:
+ "이 페이지는 작업 중입니다. 게시 전까지 콘텐츠가 불완전하거나 변경될 수 있습니다.",
},
search: {
dialogLabel: "문서 검색",
diff --git a/packages/docs/app/i18n/pt-BR.ts b/packages/docs/app/i18n/pt-BR.ts
index 2f5dd807cd..7ec12b23fa 100644
--- a/packages/docs/app/i18n/pt-BR.ts
+++ b/packages/docs/app/i18n/pt-BR.ts
@@ -53,6 +53,9 @@ const ptBR = {
copyMarkdownError: "Não foi possível copiar Markdown",
previous: "Anterior",
next: "Próximo",
+ draftLabel: "Rascunho",
+ draftDescription:
+ "Esta página está em andamento. O conteúdo pode estar incompleto ou sujeito a alterações antes da publicação.",
},
search: {
dialogLabel: "Pesquisar documentação",
diff --git a/packages/docs/app/i18n/zh-CN.ts b/packages/docs/app/i18n/zh-CN.ts
index 27da19bf14..027555d727 100644
--- a/packages/docs/app/i18n/zh-CN.ts
+++ b/packages/docs/app/i18n/zh-CN.ts
@@ -53,6 +53,8 @@ const zhCN = {
copyMarkdownError: "无法复制 Markdown",
previous: "上一页",
next: "下一页",
+ draftLabel: "草稿",
+ draftDescription: "此页面仍在编写中。内容在发布前可能不完整或有所变动。",
},
search: {
dialogLabel: "搜索文档",
diff --git a/packages/docs/app/i18n/zh-TW.ts b/packages/docs/app/i18n/zh-TW.ts
index 42bcf57854..c7ba8cef47 100644
--- a/packages/docs/app/i18n/zh-TW.ts
+++ b/packages/docs/app/i18n/zh-TW.ts
@@ -51,6 +51,8 @@ const messages = {
copyMarkdownError: "無法複製 Markdown",
previous: "上一頁面",
next: "下一頁面",
+ draftLabel: "草稿",
+ draftDescription: "此頁面仍在製作中。內容在發布前可能不完整或有所變動。",
},
search: {
dialogLabel: "搜尋檔案",
diff --git a/packages/docs/app/routes/docs.$locale.$slug.tsx b/packages/docs/app/routes/docs.$locale.$slug.tsx
index aa98352e18..32cb173263 100644
--- a/packages/docs/app/routes/docs.$locale.$slug.tsx
+++ b/packages/docs/app/routes/docs.$locale.$slug.tsx
@@ -6,7 +6,11 @@ import {
} from "react-router";
import DocContent from "../components/DocContent";
-import { loadDoc, type DocEntry } from "../components/docs-content";
+import DocDraftBanner from "../components/DocDraftBanner";
+import {
+ loadDocRespectingDraftVisibility,
+ type DocEntry,
+} from "../components/docs-content";
import {
DEFAULT_DOCS_LOCALE,
docsPathForSlug,
@@ -57,7 +61,7 @@ export async function loader({ params, request, url }: LoaderFunctionArgs) {
throw redirect(docsPathForSlug(slug, locale), 301);
}
- const doc = await loadDoc(slug, locale);
+ const doc = await loadDocRespectingDraftVisibility(slug, locale);
if (!doc) {
throw new Response("Not Found", { status: 404 });
}
@@ -106,6 +110,7 @@ export default function LocalizedDocPage() {
toc={toc}
markdownUrl={docsMarkdownPathForDoc(doc.slug, locale) ?? undefined}
>
+ {doc.draft && }
);
diff --git a/packages/docs/app/routes/docs.$slug.tsx b/packages/docs/app/routes/docs.$slug.tsx
index 45a59079ae..cd6a57aeb7 100644
--- a/packages/docs/app/routes/docs.$slug.tsx
+++ b/packages/docs/app/routes/docs.$slug.tsx
@@ -1,7 +1,11 @@
import { redirect, useLoaderData, type LoaderFunctionArgs } from "react-router";
import DocContent from "../components/DocContent";
-import { loadDoc, type DocEntry } from "../components/docs-content";
+import DocDraftBanner from "../components/DocDraftBanner";
+import {
+ loadDocRespectingDraftVisibility,
+ type DocEntry,
+} from "../components/docs-content";
import {
DEFAULT_DOCS_LOCALE,
docsPathForSlug,
@@ -39,7 +43,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
if (target) {
throw redirect(docsPathForSlug(target, DEFAULT_DOCS_LOCALE), 301);
}
- const doc = await loadDoc(slug);
+ const doc = await loadDocRespectingDraftVisibility(slug);
if (!doc) {
throw new Response("Not Found", { status: 404 });
}
@@ -84,6 +88,7 @@ export default function DocPage() {
docsMarkdownPathForDoc(doc.slug, DEFAULT_DOCS_LOCALE) ?? undefined
}
>
+ {doc.draft && }
);
diff --git a/packages/docs/app/routes/docs._index.tsx b/packages/docs/app/routes/docs._index.tsx
index 5bc2371d0c..0cdca010b7 100644
--- a/packages/docs/app/routes/docs._index.tsx
+++ b/packages/docs/app/routes/docs._index.tsx
@@ -5,7 +5,11 @@ import {
} from "react-router";
import DocContent from "../components/DocContent";
-import { loadDoc, type DocEntry } from "../components/docs-content";
+import DocDraftBanner from "../components/DocDraftBanner";
+import {
+ loadDocRespectingDraftVisibility,
+ type DocEntry,
+} from "../components/docs-content";
import { DEFAULT_DOCS_LOCALE, isDocsLocale } from "../components/docs-locale";
import { docsMarkdownPathForDoc } from "../components/docs-seo";
import DocsLayout from "../components/DocsLayout";
@@ -20,7 +24,10 @@ function routeLocale(params: LoaderFunctionArgs["params"]) {
export async function loader({
params,
}: LoaderFunctionArgs): Promise {
- const doc = await loadDoc(GETTING_STARTED_SLUG, routeLocale(params));
+ const doc = await loadDocRespectingDraftVisibility(
+ GETTING_STARTED_SLUG,
+ routeLocale(params),
+ );
if (!doc) throw new Response("Not Found", { status: 404 });
return doc;
}
@@ -61,6 +68,7 @@ export default function DocsIndex() {
toc={toc}
markdownUrl={docsMarkdownPathForDoc(currentDoc.slug, locale) ?? undefined}
>
+ {currentDoc.draft && }
);