MUI Docs Infra

Warning

This is an internal project, and is not intended for public use. No support or stability guarantees are provided.

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>

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'

Types

syncPageIndex

Updates the parent directory’s index file with metadata from a page.

This function:

  1. Acquires a lock on the index file
  2. Reads the existing index markdown (if it exists)
  3. Merges the new page metadata with existing metadata
  4. Writes the updated markdown back to the index file
  5. Releases the lock
  6. Optionally updates parent indexes recursively
ParameterTypeDescription
options{ pagePath: string; metadata?: PageMetadata; metadataList?: PageMetadata[]; indexTitle?: string; indexFileName?: string; lockOptions?: lockfile.LockOptions; baseDir?: string; updateParents?: boolean; include?: string[]; exclude?: string[]; onlyUpdateIndexes?: boolean; markerDir?: string | false; errorIfOutOfDate?: boolean; indexWrapperComponent?: string; preserveExistingTitleAndSlug?: boolean }
Return Type
Promise<void>
SyncPageIndexOptions
type SyncPageIndexOptions = {
  pagePath: string;
  metadata?: PageMetadata;
  metadataList?: PageMetadata[];
  indexTitle?: string;
  indexFileName?: string;
  lockOptions?: lockfile.LockOptions;
  baseDir?: string;
  updateParents?: boolean;
  include?: string[];
  exclude?: string[];
  onlyUpdateIndexes?: boolean;
  markerDir?: string | false;
  errorIfOutOfDate?: boolean;
  indexWrapperComponent?: string;
  preserveExistingTitleAndSlug?: boolean;
};

Related