The syncPageIndex function automatically maintains index pages by extracting metadata from documentation pages and updating parent directory indexes. It's designed to work with Next.js file-based routing, keeping navigation indexes in sync as pages are added or modified.
Note
This function is typically called by the
transformMarkdownMetadataplugin during the build process. Most users won't need to call it directly.
When you have documentation pages organized in directories:
app/components/
├── page.mdx ← Index page (auto-updated)
├── button/
│ └── page.mdx ← Button docs
├── checkbox/
│ └── page.mdx ← Checkbox docs
└── dialog/
└── page.mdx ← Dialog docs
The syncPageIndex function:
page.mdx with links and descriptionsimport { syncPageIndex } from '@mui/internal-docs-infra/pipeline/syncPageIndex';
await syncPageIndex({
pagePath: './app/components/button/page.mdx',
metadata: {
slug: 'button',
path: './button/page.mdx',
title: 'Button',
description: 'A clickable button component.',
},
});
This updates ./app/components/page.mdx with an entry for the Button page.
Update multiple pages in a single operation (more efficient with file locking):
await syncPageIndex({
pagePath: './app/components/page.mdx', // The index file itself
metadataList: [
{ slug: 'button', path: './button/page.mdx', title: 'Button', description: '...' },
{ slug: 'checkbox', path: './checkbox/page.mdx', title: 'Checkbox', description: '...' },
{ slug: 'dialog', path: './dialog/page.mdx', title: 'Dialog', description: '...' },
],
});
Update all parent indexes up to a base directory:
await syncPageIndex({
pagePath: './app/components/forms/text-field/page.mdx',
metadata: { ... },
updateParents: true,
baseDir: './app',
});
This updates:
./app/components/forms/page.mdx./app/components/page.mdx./app/page.mdxControl which directories receive index updates:
await syncPageIndex({
pagePath: './app/docs/getting-started/page.mdx',
metadata: { ... },
baseDir: './app',
include: ['docs'], // Only update indexes under 'docs'
exclude: ['docs/internal'], // But skip 'docs/internal'
});
Check if indexes are up-to-date without modifying files:
await syncPageIndex({
pagePath: './app/components/button/page.mdx',
metadata: { ... },
errorIfOutOfDate: true, // Throws if index needs updating
onlyUpdateIndexes: true, // Don't create new indexes
});
The function generates markdown with a specific structure:
# Components
[//]: # 'This file is autogenerated, but the following list can be modified. Automatically sorted alphabetically.'
<PagesIndex>
- [Button](#button) - [Full Docs](./button/page.mdx)
- [Checkbox](#checkbox) - [Full Docs](./checkbox/page.mdx)
- [Dialog](#dialog) - [Full Docs](./dialog/page.mdx)
[//]: # 'This file is autogenerated, DO NOT EDIT AFTER THIS LINE, run: pnpm docs-infra validate'
## Button
A clickable button component.
<details>
<summary>Outline</summary>
- Sections:
- Installation
- Usage
- Props
- Examples
</details>
[Read more](./button/page.mdx)
<!-- More entries... -->
</PagesIndex>
function syncPageIndex(options: SyncPageIndexOptions): Promise<void>;
Updates a parent directory's index file with page metadata.
| Option | Type | Default | Description |
|---|---|---|---|
pagePath | string | Required | Path to the page file or index file (for batch) |
metadata | PageMetadata | — | Metadata for single page update |
metadataList | PageMetadata[] | — | Metadata array for batch updates |
indexTitle | string | From dirname | Title for the index file |
indexFileName | string | 'page.mdx' | Name of index files |
baseDir | string | — | Base directory to stop recursion |
updateParents | boolean | false | Update parent indexes recursively |
include | string[] | — | Only update indexes matching these patterns |
exclude | string[] | — | Skip indexes matching these patterns |
onlyUpdateIndexes | boolean | false | Don't create new index files |
markerDir | string | false | false | Directory for update marker files |
errorIfOutOfDate | boolean | false | Throw if index needs updating |
indexWrapperComponent | string | — | Component to wrap autogenerated content |
lockOptions | LockOptions | {} | Options for proper-lockfile |
interface PageMetadata {
slug: string; // URL slug (e.g., 'button')
path: string; // Relative path to MDX file
title: string; // Page title
description?: string; // Short description
keywords?: string[]; // Search keywords
tags?: string[]; // Display tags (e.g., 'New', 'Beta')
sections?: HeadingHierarchy; // Section outline
skipDetailSection?: boolean; // Skip generating detail section
parts?: Record<string, PartMetadata>; // Multi-part component API
exports?: Record<string, ExportMetadata>; // Component exports API
}
interface PartMetadata {
props?: string[];
dataAttributes?: string[];
cssVariables?: string[];
}
type ExportMetadata = PartMetadata;
Add status indicators that appear in the index list:
await syncPageIndex({
pagePath: './app/components/new-component/page.mdx',
metadata: {
slug: 'new-component',
path: './new-component/page.mdx',
title: 'New Component',
description: 'A brand new component.',
tags: ['New', 'Beta'], // Displays as [New] [Beta] in the index
},
});
For components with API documentation, include parts or exports to make them searchable:
await syncPageIndex({
pagePath: './app/components/dialog/page.mdx',
metadata: {
slug: 'dialog',
path: './dialog/page.mdx',
title: 'Dialog',
description: 'A modal dialog component.',
// For multi-part components
parts: {
Root: { props: ['open', 'onOpenChange'], dataAttributes: ['data-state'] },
Trigger: { props: ['asChild'] },
Content: { props: ['side', 'align'], cssVariables: ['--dialog-width'] },
},
// Or for single exports
exports: {
Dialog: { props: ['open', 'onOpenChange', 'modal'] },
},
},
});
This metadata is used by useSearch to enable searching for specific props, data attributes, and CSS variables.
For links to external resources, use skipDetailSection:
await syncPageIndex({
pagePath: './app/resources/page.mdx',
metadata: {
slug: 'github',
path: 'https://github.com/mui/base-ui',
title: 'GitHub',
tags: ['External'],
skipDetailSection: true, // Don't generate a detail section
},
});
Parses a markdown index file and extracts page metadata:
import { markdownToMetadata } from '@mui/internal-docs-infra/pipeline/syncPageIndex';
const result = await markdownToMetadata(markdownContent);
// result.title - Index title
// result.pages - Array of PageMetadata
// result.description - Optional description
Merges new page metadata into existing markdown:
import { mergeMetadataMarkdown } from '@mui/internal-docs-infra/pipeline/syncPageIndex';
const updatedMarkdown = await mergeMetadataMarkdown(existingMarkdown, newMetadata, options);
The function uses proper-lockfile to prevent concurrent writes:
metadataList) are more efficient as they require only one lockNext.js route groups (directories in parentheses) are handled specially:
(public), (content), etc. are skipped when finding parent directoriesapp/(public)/(content)/components/button/page.mdx
↓
Updates: app/(public)/(content)/components/page.mdx
Title derived from: 'components' → 'Components'
transformMarkdownMetadata - Remark plugin that calls syncPageIndexdocs-infra validate - CLI command that validates indexesloadServerSitemap - Uses page indexes to build sitemapsloadServerPageIndex - Loads page metadata at runtime