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
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ exports[`sidebar site with undefined sidebar 1`] = `
`;

exports[`sidebar site with wrong sidebar content 1`] = `
"Invalid sidebar file at "packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/wrong-sidebars.json".
"Invalid sidebar file at "../docusaurus/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/wrong-sidebars.json".
These sidebar document ids do not exist:
- nonExistent

Expand All @@ -46,6 +46,7 @@ Available document ids are:
- slugs/relativeSlug
- slugs/resolvedSlug
- slugs/tryToEscapeSlug
- unlisted-category/index
- unlisted-category/unlisted-category-doc
- unlisted-category/unlisted-category-index
"
Expand Down Expand Up @@ -1041,6 +1042,7 @@ exports[`simple website content: data 1`] = `
"version": "current",
},
"site-docs-unlisted-category-index-md-efa.json": {
"autoGeneratedId": "unlisted-category/index",
"description": "This is an unlisted category index",
"draft": false,
"editUrl": undefined,
Expand Down Expand Up @@ -2334,6 +2336,7 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha

exports[`site with full autogenerated sidebar docs in fully generated sidebar have correct metadata 3`] = `
{
"autoGeneratedId": "Guides/z-guide1",
"description": "Guide 1 text",
"draft": false,
"editUrl": undefined,
Expand Down Expand Up @@ -2465,6 +2468,7 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha

exports[`site with full autogenerated sidebar docs in fully generated sidebar have correct metadata 7`] = `
{
"autoGeneratedId": "Guides/a-guide4",
"description": "Guide 4 text",
"draft": false,
"editUrl": undefined,
Expand Down Expand Up @@ -2497,6 +2501,7 @@ exports[`site with full autogenerated sidebar docs in fully generated sidebar ha

exports[`site with full autogenerated sidebar docs in fully generated sidebar have correct metadata 8`] = `
{
"autoGeneratedId": "Guides/b-guide5",
"description": "Guide 5 text",
"draft": false,
"editUrl": undefined,
Expand Down
117 changes: 117 additions & 0 deletions packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
readVersionDocs,
readDocFile,
addDocNavigation,
createDocsByIdIndex,
isCategoryIndex,
type DocEnv,
} from '../docs';
Expand Down Expand Up @@ -1419,6 +1420,122 @@ describe('versioned site', () => {
});
});

describe('autoGeneratedId', () => {
it('doc with explicit frontmatter id different from filename gets autoGeneratedId', async () => {
const siteDir = path.join(fixtureDir, 'simple-site');
const context = await loadContext({siteDir});
const options = {
id: DEFAULT_PLUGIN_ID,
...DEFAULT_OPTIONS,
};
const versionsMetadata = await readVersionsMetadata({context, options});
const currentVersion = versionsMetadata[0]!;
const testUtils = await createTestUtils({
siteDir,
context,
options,
versionMetadata: currentVersion,
});

// A doc in subdirectory 'myDir' with frontMatter id 'customId'
// and file name 'myFile.md'. Since 'customId' !== 'myFile',
// autoGeneratedId should be 'myDir/myFile'.
const result = await testUtils.processDocFile(
createFakeDocFile({
source: 'myDir/myFile.md',
frontMatter: {id: 'customId'},
markdown: '# Custom Doc',
}),
);
expect(result.id).toBe('myDir/customId');
expect(result.autoGeneratedId).toBe('myDir/myFile');
});

it('doc with explicit frontmatter id matching filename has no autoGeneratedId', async () => {
const siteDir = path.join(fixtureDir, 'simple-site');
const context = await loadContext({siteDir});
const options = {
id: DEFAULT_PLUGIN_ID,
...DEFAULT_OPTIONS,
};
const versionsMetadata = await readVersionsMetadata({context, options});
const currentVersion = versionsMetadata[0]!;
const testUtils = await createTestUtils({
siteDir,
context,
options,
versionMetadata: currentVersion,
});

// frontMatter id matches the file name, so no alias needed.
const result = await testUtils.processDocFile(
createFakeDocFile({
source: 'myDir/bar.md',
frontMatter: {id: 'bar'},
markdown: '# Bar',
}),
);
expect(result.id).toBe('myDir/bar');
expect(result.autoGeneratedId).toBeUndefined();
});

it('doc without frontmatter id has no autoGeneratedId', async () => {
const siteDir = path.join(fixtureDir, 'simple-site');
const context = await loadContext({siteDir});
const options = {
id: DEFAULT_PLUGIN_ID,
...DEFAULT_OPTIONS,
};
const versionsMetadata = await readVersionsMetadata({context, options});
const currentVersion = versionsMetadata[0]!;
const testUtils = await createTestUtils({
siteDir,
context,
options,
versionMetadata: currentVersion,
});

const result = await testUtils.processDocFile(
createFakeDocFile({
source: 'myDir/someDoc.md',
frontMatter: {},
markdown: '# Some Doc',
}),
);
expect(result.id).toBe('myDir/someDoc');
expect(result.autoGeneratedId).toBeUndefined();
});
});

describe('createDocsByIdIndex', () => {
it('indexes docs by explicit id', () => {
const docs = [
{id: 'foo', autoGeneratedId: undefined},
{id: 'bar', autoGeneratedId: undefined},
];
const index = createDocsByIdIndex(docs);
expect(index.foo).toBe(docs[0]);
expect(index.bar).toBe(docs[1]);
});

it('indexes docs by autoGeneratedId as fallback alias', () => {
const doc = {id: 'guides/intro', autoGeneratedId: 'guides/getting-started'};
const index = createDocsByIdIndex([doc]);
// Both the explicit id and the autogenerated id resolve to the same doc
expect(index['guides/intro']).toBe(doc);
expect(index['guides/getting-started']).toBe(doc);
});

it('does not let autoGeneratedId override an existing explicit id', () => {
const docA = {id: 'guides/intro', autoGeneratedId: 'guides/conflict'};
const docB = {id: 'guides/conflict', autoGeneratedId: undefined};
const index = createDocsByIdIndex([docA, docB]);
// The explicit id 'guides/conflict' belongs to docB, not docA's alias
expect(index['guides/conflict']).toBe(docB);
expect(index['guides/intro']).toBe(docA);
});
});

describe('isConventionalDocIndex', () => {
it('supports readme', () => {
expect(
Expand Down
31 changes: 26 additions & 5 deletions packages/docusaurus-plugin-content-docs/src/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ async function doProcessDocMetadata({
throw new Error(`Document id "${baseID}" cannot include slash.`);
}

// Compute the autogenerated ID (file-path-based) when the front matter
// provides an explicit id that differs from the file name. This allows
// the file-path-based id to serve as a fallback alias.
const autoGeneratedBaseID =
frontMatter.id !== undefined && frontMatter.id !== unprefixedFileName
? unprefixedFileName
: undefined;

// For autogenerated sidebars, sidebar position can come from filename number
// prefix or front matter
const sidebarPosition: number | undefined =
Expand All @@ -166,6 +174,10 @@ async function doProcessDocMetadata({

const id = [computeDirNameIdPrefix(), baseID].filter(Boolean).join('/');

const autoGeneratedId = autoGeneratedBaseID
? [computeDirNameIdPrefix(), autoGeneratedBaseID].filter(Boolean).join('/')
: undefined;

const docSlug = getSlug({
baseID,
source,
Expand Down Expand Up @@ -227,6 +239,7 @@ async function doProcessDocMetadata({
// class transitions.
return {
id,
...(autoGeneratedId !== undefined && {autoGeneratedId}),
title,
description,
source: aliasedSitePath(filePath, siteDir),
Expand Down Expand Up @@ -394,9 +407,17 @@ export function toCategoryIndexMatcherParam({
};
}

// Docs are indexed by their id
export function createDocsByIdIndex<Doc extends {id: string}>(
docs: Doc[],
): {[docId: string]: Doc} {
return _.keyBy(docs, (d) => d.id);
// Docs are indexed by their id, with autogenerated IDs as fallback aliases
export function createDocsByIdIndex<
Doc extends {id: string; autoGeneratedId?: string},
>(docs: Doc[]): {[docId: string]: Doc} {
const index = _.keyBy(docs, (d) => d.id);
// Register autogenerated IDs as fallback aliases.
// These never override an entry already keyed by an explicit id.
for (const doc of docs) {
if (doc.autoGeneratedId && !(doc.autoGeneratedId in index)) {
index[doc.autoGeneratedId] = doc;
}
}
return index;
}
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,13 @@ declare module '@docusaurus/plugin-content-docs' {
* Multiple documents can have the same id, when in different versions.
*/
id: string;
/**
* The autogenerated document id based on the file path, before any
* front matter `id` override. Only set when `id` differs from what
* the file path would produce. Acts as a fallback alias so that
* references using the file-path-based id still resolve.
*/
autoGeneratedId?: string;
/** The name of the version this doc belongs to. */
version: string;
/**
Expand Down