Hide internal docs from docs site (#4357)

This commit is contained in:
Dragon authored and GitHub committed 2026-06-01 15:55:14 +08:00
1 parent 707f1bdb5f
commit 59c283670e
9 files changed
+195 -8

No files matched your search

+4 -2
View File
@@ -17,13 +17,15 @@ npm install
### Setup Content
Link the documentation content from the parent `docs` directory:
Prepare the public documentation content from the parent `docs` directory:
```bash
npm run link
```
This creates a symbolic link from `../docs` to `content` in the project.
This creates a `content` directory with copies of the public docs sections.
Internal planning, design, and E2E notes remain outside the docs site content
tree.
### Development
+5 -2
View File
@@ -7,10 +7,10 @@
"type": "module",
"main": "index.js",
"scripts": {
"link": "ln -s ../docs content",
"link": "node scripts/link-public-docs.mjs",
"clean": "rm -rf .next",
"dev": "npm run clean && next --turbopack",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest run --config vitest.config.js"
},
"dependencies": {
"next": "^16.0.8",
@@ -18,5 +18,8 @@
"nextra-theme-docs": "^4.6.1",
"react": "^19.2.1",
"react-dom": "^19.2.1"
},
"devDependencies": {
"vitest": "^3.2.4"
}
}
+26
View File
@@ -0,0 +1,26 @@
import { cp, mkdir, rm, symlink } from 'node:fs/promises';
import { join } from 'node:path';
import { PUBLIC_DOC_ROOTS } from '../src/app/public-docs.js';
const contentDir = 'content';
async function linkPublicDocs() {
try {
await rm(contentDir, { force: true, recursive: true });
await mkdir(contentDir);
await cp('../docs/index.md', join(contentDir, 'index.md'));
await cp('../docs/_meta.ts', join(contentDir, '_meta.ts'));
for (const root of PUBLIC_DOC_ROOTS) {
await symlink(join('..', '..', 'docs', root), join(contentDir, root));
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to link public docs into ${contentDir}: ${message}`,
);
}
}
await linkPublicDocs();
+18 -1
View File
@@ -1,10 +1,23 @@
import { generateStaticParamsFor, importPage } from 'nextra/pages';
import { notFound } from 'next/navigation';
import { useMDXComponents as getMDXComponents } from '../../../mdx-components';
import { filterPublicStaticParams, isPublicDocsPath } from '../public-docs';
export const generateStaticParams = generateStaticParamsFor('mdxPath');
const generateAllStaticParams = generateStaticParamsFor('mdxPath');
export const dynamicParams = false;
export async function generateStaticParams(...args) {
const staticParams = await generateAllStaticParams(...args);
return filterPublicStaticParams(staticParams);
}
export async function generateMetadata(props) {
const params = await props.params;
if (!isPublicDocsPath(params.mdxPath)) {
notFound();
}
const { metadata } = await importPage(params.mdxPath);
return metadata;
}
@@ -13,6 +26,10 @@ const Wrapper = getMDXComponents().wrapper;
export default async function Page(props) {
const params = await props.params;
if (!isPublicDocsPath(params.mdxPath)) {
notFound();
}
const {
default: MDXContent,
toc,
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => {
const generateAllStaticParams = vi.fn();
return {
generateAllStaticParams,
generateStaticParamsFor: vi.fn(() => generateAllStaticParams),
};
});
vi.mock('nextra/pages', () => ({
generateStaticParamsFor: mocks.generateStaticParamsFor,
importPage: vi.fn(),
}));
vi.mock('next/navigation', () => ({
notFound: vi.fn(),
}));
vi.mock('../../../mdx-components', () => ({
useMDXComponents: () => ({
wrapper: ({ children }) => children,
}),
}));
describe('generateStaticParams', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('filters internal docs from Nextra static params', async () => {
mocks.generateAllStaticParams.mockResolvedValue([
{ mdxPath: [] },
{ mdxPath: ['users', 'foo'] },
{ mdxPath: ['en', 'users'] },
{ mdxPath: ['design', 'bar'] },
{ mdxPath: ['plans'] },
]);
const { generateStaticParams } = await import('./page.jsx');
await expect(generateStaticParams()).resolves.toEqual([
{ mdxPath: [] },
{ mdxPath: ['users', 'foo'] },
{ mdxPath: ['en', 'users'] },
]);
});
it('fails closed if Nextra changes the static params shape', async () => {
mocks.generateAllStaticParams.mockResolvedValue([{ slug: ['users'] }]);
const { generateStaticParams } = await import('./page.jsx');
await expect(generateStaticParams()).rejects.toThrow(
'Expected generateStaticParamsFor("mdxPath") to return objects with an mdxPath array.',
);
});
});
+33
View File
@@ -0,0 +1,33 @@
const LOCALE_SEGMENTS = new Set(['en', 'zh', 'de', 'fr', 'ja', 'ru', 'pt-BR']);
// Keep this in sync with the public top-level page entries in docs/_meta.ts.
// docs-site/scripts/link-public-docs.mjs consumes the same allowlist.
export const PUBLIC_DOC_ROOTS = ['users', 'developers'];
const PUBLIC_DOC_ROOT_SET = new Set(PUBLIC_DOC_ROOTS);
function publicRootFromSegments(segments = []) {
if (segments.length === 0 || (segments.length === 1 && segments[0] === '')) {
return undefined;
}
const rootIndex = LOCALE_SEGMENTS.has(segments[0]) ? 1 : 0;
return segments[rootIndex];
}
export function isPublicDocsPath(mdxPath = []) {
const root = publicRootFromSegments(mdxPath);
return root === undefined || PUBLIC_DOC_ROOT_SET.has(root);
}
export function filterPublicStaticParams(staticParams = []) {
return staticParams.filter((staticParam) => {
if (!Array.isArray(staticParam?.mdxPath)) {
throw new TypeError(
'Expected generateStaticParamsFor("mdxPath") to return objects with an mdxPath array.',
);
}
return isPublicDocsPath(staticParam.mdxPath);
});
}
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { filterPublicStaticParams, isPublicDocsPath } from './public-docs.js';
describe('isPublicDocsPath', () => {
it.each([
[[], true],
[[''], true],
[['users', 'foo'], true],
[['design', 'bar'], false],
[['en', 'users'], true],
[['plans'], false],
[['en'], true],
])('returns %s for %j', (mdxPath, expected) => {
expect(isPublicDocsPath(mdxPath)).toBe(expected);
});
});
describe('filterPublicStaticParams', () => {
it('keeps public paths and rejects internal docs paths', () => {
expect(
filterPublicStaticParams([
{ mdxPath: [] },
{ mdxPath: [''] },
{ mdxPath: ['users', 'foo'] },
{ mdxPath: ['en', 'developers'] },
{ mdxPath: ['design', 'bar'] },
{ mdxPath: ['plans'] },
]),
).toEqual([
{ mdxPath: [] },
{ mdxPath: [''] },
{ mdxPath: ['users', 'foo'] },
{ mdxPath: ['en', 'developers'] },
]);
});
it('fails closed if Nextra changes the static params shape', () => {
expect(() => filterPublicStaticParams([{ slug: ['users'] }])).toThrow(
'Expected generateStaticParamsFor("mdxPath") to return objects with an mdxPath array.',
);
});
});
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['src/**/*.test.{js,jsx}'],
},
});
-3
View File
@@ -1,9 +1,6 @@
export default {
settings: 'Settings',
auth: 'Authentication',
memory: {
display: 'hidden',
},
'qwen-ignore': 'Ignoring Files',
'trusted-folders': 'Trusted Folders',
themes: 'Themes',