MUI Docs Infra

Sync Page Index

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 transformMarkdownMetadata plugin during the build process. Most users won't need to call it directly.

Overview

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:

  1. Receives metadata (title, description, sections) for a page
  2. Updates the parent page.mdx with links and descriptions
  3. Optionally propagates up the directory tree

Basic Usage

import { 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.


Common Patterns

Batch Updates

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: '...' },
  ],
});

Recursive Parent Updates

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.mdx

Include/Exclude Patterns

Control 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'
});

CI Validation Mode

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

Generated Index Format

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>

API Reference

syncPageIndex

function syncPageIndex(options: SyncPageIndexOptions): Promise<void>;

Updates a parent directory's index file with page metadata.

SyncPageIndexOptions

OptionTypeDefaultDescription
pagePathstringRequiredPath to the page file or index file (for batch)
metadataPageMetadataMetadata for single page update
metadataListPageMetadata[]Metadata array for batch updates
indexTitlestringFrom dirnameTitle for the index file
indexFileNamestring'page.mdx'Name of index files
baseDirstringBase directory to stop recursion
updateParentsbooleanfalseUpdate parent indexes recursively
includestring[]Only update indexes matching these patterns
excludestring[]Skip indexes matching these patterns
onlyUpdateIndexesbooleanfalseDon't create new index files
markerDirstring | falsefalseDirectory for update marker files
errorIfOutOfDatebooleanfalseThrow if index needs updating
indexWrapperComponentstringComponent to wrap autogenerated content
lockOptionsLockOptions{}Options for proper-lockfile

PageMetadata

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;

Extra Metadata

Tags

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

Parts and Exports

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.

External Links

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

Helper Functions

markdownToMetadata

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

mergeMetadataMarkdown

Merges new page metadata into existing markdown:

import { mergeMetadataMarkdown } from '@mui/internal-docs-infra/pipeline/syncPageIndex';

const updatedMarkdown = await mergeMetadataMarkdown(existingMarkdown, newMetadata, options);

File Locking

The function uses proper-lockfile to prevent concurrent writes:

  • Locks are acquired before reading/writing
  • Multiple processes can safely update the same index
  • Batch updates (metadataList) are more efficient as they require only one lock

Route Group Handling

Next.js route groups (directories in parentheses) are handled specially:

  • (public), (content), etc. are skipped when finding parent directories
  • URL prefixes exclude route group segments
  • Title generation ignores route groups
app/(public)/(content)/components/button/page.mdx
                       ↓
Updates: app/(public)/(content)/components/page.mdx
Title derived from: 'components' → 'Components'

Related