Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/docs/.env.example
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions packages/docs/app/components/DocsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

const ALWAYS_OPEN_SECTION_INDEX = 0;


function normalizePath(pathname: string) {
return pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
}
Expand Down Expand Up @@ -193,6 +194,11 @@ export default function DocsSidebar() {
current === item.id ? null : item.id,
)
}
style={
item.draft
? { color: "var(--approaches-warn)" }
: undefined
}
>
<span>{item.label}</span>
<IconChevronRight
Expand All @@ -208,6 +214,11 @@ export default function DocsSidebar() {
to={item.to!}
className={`sidebar-link${active ? " is-active" : ""}`}
tabIndex={isOpen ? undefined : -1}
style={
item.draft
? { color: "var(--approaches-warn)" }
: undefined
}
>
{item.label}
</Link>
Expand Down Expand Up @@ -236,6 +247,11 @@ export default function DocsSidebar() {
tabIndex={
childrenTabbable ? undefined : -1
}
style={
child.draft
? { color: "var(--approaches-warn)" }
: undefined
}
>
{child.label}
</Link>
Expand Down
2 changes: 2 additions & 0 deletions packages/docs/app/components/docs-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[];
}
Expand Down Expand Up @@ -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,
};
Expand Down
19 changes: 15 additions & 4 deletions packages/docs/app/components/docsNavItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] };
Expand All @@ -19,6 +20,7 @@ type NavItemConfig = {
id: string;
labelKey: keyof typeof enUS.nav;
slug?: string;
draft?: boolean;
children?: NavItemConfig[];
};

Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nav filtering never reads draft frontmatter

toNavItem filters only the static NavItemConfig.draft field, but this tree is not populated from the DocEntry frontmatter parsed in docs-content.ts. As a result, an existing page marked draft: true remains visible in the sidebar with drafts disabled, then navigates to the new 404 route; derive or synchronize draft status from the document metadata.

Additional Info
Confirmed by independent code-review agent and source inspection: NAV_SECTION_CONFIG contains no draft entries or metadata bridge.

Fix in Builder

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,
};
}

Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions packages/docs/app/routes/docs.$locale.$slug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ const SLUG_REDIRECTS: Record<string, string> = {
"migration-workbench": "code-agents-ui",
};

function DraftBanner() {
return (
<div
className="mb-6 rounded-md border p-4 text-sm"
style={{
borderColor: "var(--approaches-warn)",
color: "var(--approaches-warn)",
}}
>
<strong>Draft</strong> — This page is a work in progress. Content may be
incomplete or subject to change before publication.
</div>
);
}

function requireLocale(value: unknown): DocsLocale {
if (isDocsLocale(value)) return value;
throw new Response("Not Found", { status: 404 });
Expand Down Expand Up @@ -61,6 +76,9 @@ export async function loader({ params, request, url }: LoaderFunctionArgs) {
if (!doc) {
throw new Response("Not Found", { status: 404 });
}
if (doc.draft && import.meta.env.VITE_SHOW_DRAFTS !== "true") {
throw new Response("Not Found", { status: 404 });
}
return doc;
}

Expand Down Expand Up @@ -106,6 +124,7 @@ export default function LocalizedDocPage() {
toc={toc}
markdownUrl={docsMarkdownPathForDoc(doc.slug, locale) ?? undefined}
>
{doc.draft && <DraftBanner />}
<DocContent markdown={doc.body} />
</DocsLayout>
);
Expand Down
19 changes: 19 additions & 0 deletions packages/docs/app/routes/docs.$slug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ const SLUG_REDIRECTS: Record<string, string> = {
"migration-workbench": "code-agents-ui",
};

function DraftBanner() {
return (
<div
className="mb-6 rounded-md border p-4 text-sm"
style={{
borderColor: "var(--approaches-warn)",
color: "var(--approaches-warn)",
}}
>
<strong>Draft</strong> — This page is a work in progress. Content may be
incomplete or subject to change before publication.
</div>
);
}

export async function loader({ params }: LoaderFunctionArgs) {
const slug = params.slug!;
if (isDocsLocale(slug)) {
Expand All @@ -43,6 +58,9 @@ export async function loader({ params }: LoaderFunctionArgs) {
if (!doc) {
throw new Response("Not Found", { status: 404 });
}
if (doc.draft && import.meta.env.VITE_SHOW_DRAFTS !== "true") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Draft content remains exposed through generated Markdown assets

This check protects only the React document route. The docs Vite agent-web build still enumerates all source documents, including drafts, and emits their Markdown twins plus sitemap/LLM discovery entries, so /docs/<draft>.md can expose the full draft even when /docs/<draft> returns 404. Apply the same draft filter to the agent-web page generation unless previews are explicitly enabled.

Additional Info
Confirmed by independent code-review agent and inspection of packages/docs/app/vite-sitemap-plugin.ts, which parses every docs source without checking draft metadata.

Fix in Builder

throw new Response("Not Found", { status: 404 });
}
return doc;
}

Expand Down Expand Up @@ -84,6 +102,7 @@ export default function DocPage() {
docsMarkdownPathForDoc(doc.slug, DEFAULT_DOCS_LOCALE) ?? undefined
}
>
{doc.draft && <DraftBanner />}
<DocContent markdown={doc.body} />
</DocsLayout>
);
Expand Down
Loading