Migrating from Docusaurus

Last updated: 04/16/2026Edit this page

Trellis Docs includes a migration script that automates the bulk of moving from Docusaurus. It copies your content, converts your sidebar, transforms frontmatter, and flags anything that needs manual attention.

Prerequisites

Before running the migration:

  1. Scaffold a Trellis Docs project — follow Getting Started to create a new project
  2. Have your Docusaurus project accessible — the script reads from its docs/ directory
  3. Back up your Docusaurus project — the script only reads from it, but backups are always a good idea

Running the migration

node scripts/migrate-docusaurus.js <path-to-docusaurus-project>

Options

FlagDescription
--dry-runPreview what would change without writing any files
--forceOverwrite existing files in content/docs/
--no-type-checkSkip the post-migration TypeScript check on copied components

Always run with --dry-run first to review the changes:

node scripts/migrate-docusaurus.js ../my-docusaurus-site --dry-run

Then run without the flag to perform the migration:

node scripts/migrate-docusaurus.js ../my-docusaurus-site

What the script does

The migration runs in five phases.

Phase 0: Custom component scan

Before touching any files, the script walks the Docusaurus project's src/components/ directory to build a list of custom React components. During content processing it checks each migrated MDX file for references to those components and reports them so you know exactly what needs to be ported.

src/theme/ (Docusaurus swizzle overrides) is intentionally excluded — Trellis has its own built-in equivalents for Navbar, Footer, and other swizzleable components, so those overrides don't need to be ported.

The script distinguishes between TypeScript (.ts/.tsx) and JavaScript (.js/.jsx) source files because they require different porting steps — see Custom components below.

Phase 0.5: Runtime dependency scan

Once components are discovered, the script extracts every bare-package import specifier from their source files (e.g., @mui/material, react-bootstrap, clsx) and diffs against the target project's package.json. It also reads each found package's own peerDependencies from the Docusaurus node_modules/ to surface hidden requirements — for example, @mui/material declares @emotion/react and @emotion/styled as peer deps that your code never imports directly but that the build will fail without.

The report lists three buckets:

  • Already installed — packages Trellis already has (e.g., clsx, react)
  • Missing — direct imports — packages your migrated components reference by name
  • Missing — peer dependencies — packages pulled in transitively through a direct import

At the end of Phase 0.5, the script prints a copy-paste npm install command listing every missing package, so you can install them in one shot before npm run build.

Phase 1: Content files

The script copies every .md and .mdx file from the Docusaurus docs/ directory into content/docs/, applying these transforms:

File paths

TransformExample
Numbered prefixes stripped01-getting-started.mdgetting-started.mdx
.md renamed to .mdxguide.mdguide.mdx
README.md renamed to index.mdxapi/README.mdapi/index.mdx

Frontmatter

Docusaurus-only frontmatter fields are removed automatically:

  • sidebar_position, sidebar_label, displayed_sidebar
  • id, slug, pagination_label, pagination_next, pagination_prev
  • custom_edit_url, parse_number_prefixes, hide_title

The tags field is converted to keywords (the Trellis equivalent).

Docusaurus id and slug fields

Docusaurus is unique among docs-as-code platforms in supporting separate id and slug frontmatter fields:

  • id — An internal identifier used to reference the doc in sidebars.js. When omitted, Docusaurus derives it from the file path. For example, a file at guides/setup.md with id: quickstart would be referenced in the sidebar as guides/quickstart.
  • slug — Controls the URL path independently of the id and file path. A doc at guides/setup.md with slug: /get-started would be served at /get-started.

Trellis Docs does not use either field. Instead, the file path is the single source of truth for both sidebar references and URL routing — the same approach used by MkDocs, VitePress, Starlight, and most other docs frameworks.

The migration script strips both fields and maps sidebar entries to file paths. If your Docusaurus project relies on either:

  • Custom id values — No action needed. The generated config/sidebar.ts already references docs by file path, which is the Trellis Docs convention.
  • Custom slug values — URLs will change. The migration report warns about each affected file. Add entries to redirects.json so old links continue to work (see Add redirects below).
Tip

If your Docusaurus sidebars.js references docs by custom id values and the mapping isn't obvious, run the migration with --dry-run first and cross-reference the generated sidebar against your original to verify every doc is accounted for.

Content body

TransformDetails
@theme imports removedimport Tabs from '@theme/Tabs' — Trellis provides these automatically
@site imports removedWarns you to verify no custom components are missing
HTML comments converted<!-- comment -->{/* comment */} (MDX syntax)
require() image paths convertedrequire('@site/static/img/foo.png').default'/img/foo.png'
MDX partial imports convertedimport Foo from './_foo.mdx'; <Foo />@include ./_foo.mdx
Excessive blank lines cleanedThree or more blank lines reduced to two

The MDX partial conversion handles self-closing tags with or without props (<Foo />, <Foo prop="x" />), and block forms including multiline content (<Foo>...</Foo>). Any usage pattern that can't be auto-converted is flagged as a warning.

require() paths under @site/static/ and relative /static/ paths are converted to root-relative URLs. Any require() call that can't be resolved automatically is flagged because next-mdx-remote blocks all require() calls for security.

The script warns about <CodeBlock> components and non-Docusaurus imports that need manual review.

Assets

Non-markdown files (images, PDFs, etc.) in the docs/ directory are copied alongside the content with the same path transforms.

Static assets

Docusaurus serves files from static/ at the site root — most projects store images in static/img/. The script recursively copies the entire static/ directory into public/ (the Next.js equivalent), preserving the directory structure. Image references like /img/screenshot.png continue to work without changes.

Phase 2: Sidebar conversion

The script reads your Docusaurus sidebar configuration and generates a Trellis Docs config/sidebar.ts file.

Docusaurus sourceHow it's handled
sidebars.js / .cjs / .jsonLoaded and converted directly
sidebars.tsParsed as plain data (TypeScript types stripped)
type: 'autogenerated'Generated from the filesystem after content is copied
type: 'link' (external)Skipped with a warning
_category_.json labelsUsed for category names in the generated sidebar
No sidebar file foundGenerated entirely from the filesystem

If a config/sidebar.ts already exists, the script backs it up to config/sidebar.backup.ts before writing.

Phase 3: Custom component copy

For every custom component detected in Phase 0 that is actually used in your migrated content, the script:

  1. Checks for Trellis built-in equivalentsTabs, TabItem, Admonition, CodeBlock, Details, TOCInline, and DocCardList are skipped since Trellis provides them natively
  2. Resolves transitive dependencies — components imported by a used component (but not referenced in MDX themselves) are pulled in as [transitive] so the import graph closes. Only MDX-used components are registered globally; transitive deps stay internal.
  3. Copies the source files to components/custom/migrated/ — component directories are copied recursively, single files are copied directly
  4. Renames JS to TS.jsx.tsx and .js.ts (Trellis enforces strict TypeScript)
  5. Rewrites Docusaurus imports in the copied source:
    • @theme/* imports are commented out (use Trellis built-ins instead)
    • @site/src/components/* imports are rewritten to relative paths
    • @site/src/data/*.json imports are rewritten to @/data/migrated/*.json, and the referenced JSON files are copied to data/migrated/
    • useColorMode from @docusaurus/theme-common is automatically remapped to useTheme from next-themes; destructuring patterns are aliased ({ resolvedTheme: colorMode, setTheme: setColorMode }) so colorMode === 'dark' style checks keep working without code edits
    • Other @site/* / @docusaurus/* imports are commented out with a TODO marker
  6. Auto-types JS components for strict TypeScript — adds prop interfaces from destructuring patterns, types useState(null)-style inference gaps, types event-handler params, adds 'use client' when hooks/browser APIs are detected, and annotates untyped arrow-function params as : any
  7. Registers components in the MDX component map — reads each copied file's exports to determine named-vs-default import syntax, then adds the right line to components/docs/mdx/index.tsx
Note

CSS modules (.module.css) are copied as-is — Next.js supports CSS modules natively with the same import pattern as Docusaurus.

Phase 3b: TypeScript check

Immediately after components are copied, the script runs tsc --noEmit against the Trellis Docs project and filters the output down to errors inside the just-migrated files. Because the JS → TS auto-typing in Phase 3 relies on heuristics (prop-name inference, default-value inference, any fallbacks), some files may still need manual tightening before they satisfy strict mode.

The type check surfaces each error inline in the migration report, grouped by file, with its line, column, and TSxxxx code — giving you an actionable worklist instead of discovering errors later at npm run build time.

Pass --no-type-check to skip this phase (useful on CI runs where you've already validated types, or when tsc isn't available on the machine running the migration).

Note

If npx or tsc cannot be resolved, the phase is skipped with a visible reason rather than failing the migration. Errors that exist elsewhere in the Trellis Docs project are filtered out — only errors in files written by this migration run are reported.

Phase 4: Variable suggestions

After migration, the script scans your content for repeated strings — version numbers, product names, and URLs that appear three or more times. These are candidates for content variables in config/variables.ts.

Migration report

After running, the script prints a summary:

========================================
       MIGRATION REPORT
========================================

Files migrated:  42
Files skipped:   0
Assets copied:   8
Errors:          0
Warnings:        3

Frontmatter fields stripped:
  sidebar_position: 38 file(s)
  sidebar_label: 12 file(s)
  id: 5 file(s)

HTML comments converted to MDX: 7
Files renamed (.md -> .mdx): 15
Numbered prefixes stripped: 22

Sidebar: Generated config/sidebar.ts

Custom components copied to components/custom/migrated/:
  BrowserWindow → components/custom/migrated/browser-window.tsx
  InfoBox → components/custom/migrated/info-box.tsx ⚠ needs TypeScript type annotations (strict mode)
  Registered in components/docs/mdx/index.tsx

Phase 3b: Type-checking 3 migrated file(s)...
  ✗ 2 type error(s) in 1 migrated file(s) — see report

Components with Trellis built-in equivalents (not copied):
  Tabs: Built-in Trellis <Tabs>/<TabItem> — works as-is
  CodeBlock: Use fenced code blocks (```) instead

TypeScript errors in migrated components (2 in 1 file(s)):
(These files compile as-is with auto-typing but may need manual tightening.)

  components/custom/migrated/info-box.tsx
    14:22  error TS7006  Parameter 'variant' implicitly has an 'any' type.
    27:5   error TS2322  Type 'string | undefined' is not assignable to type 'string'.

Suggested variables for config/variables.ts:
  acme_platform: 'Acme Platform' (found 28 times)
  v2_1_0: 'v2.1.0' (found 8 times)

Warnings:
  - guides/api.mdx: Had custom slug "/api-reference" — URL will change
  - guides/advanced.mdx: Stripped 2 @site import(s) — verify no custom components are missing
  - External sidebar link skipped: Community -> https://discord.gg/acme

Next steps:
  1. Review migrated files in content/docs/
  2. Review generated config/sidebar.ts
  3. Run "npm run build" to verify the build succeeds
  4. Check any warnings above and fix manually if needed
  5. Review copied components in components/custom/migrated/
     - Check that @theme/@site imports were rewritten correctly
     - Verify component props and APIs work with Trellis
     - Add TypeScript type annotations to renamed .js/.jsx files
       (Trellis tsconfig enforces strict mode)
  6. Consider adding suggested variables to config/variables.ts

After migration

1. Review the sidebar

Open config/sidebar.ts and verify the structure matches your expectations. You may want to:

  • Reorder items
  • Adjust collapsed values
  • Add link properties to categories that have index pages

See Configuring the sidebar for the full sidebar API.

2. Review copied components

The migration script automatically copies custom components from src/components/ to components/custom/migrated/ and registers them in the MDX component map. Components with Trellis built-in equivalents (Tabs, CodeBlock, Admonition, etc.) are skipped automatically.

After migration, review the copied components:

  • Check rewritten imports@theme/* imports are commented out and @site/src/components/* imports are rewritten to relative paths. Verify these changes are correct.
  • Fix TypeScript errors — JavaScript components are renamed to .tsx/.ts but still need type annotations. At minimum, add a props interface and return type:
components/custom/migrated/info-box.tsx
interface InfoBoxProps {
  children: React.ReactNode
  type?: 'info' | 'warning'
}

export function InfoBox({ children, type = 'info' }: InfoBoxProps) {
  // ... original component body
}
  • Verify MDX registration — the script adds imports and entries to components/docs/mdx/index.tsx. Check that the component names match what your MDX files use.
Tip

Many Docusaurus custom components can be replaced entirely with Trellis built-ins: <BrowserWindow><Callout>, <CodeBlock> → fenced code blocks, info boxes → :::note admonitions. Check what the component actually does before keeping it.

3. Run the build

npm run build

Fix any MDX compilation errors. Common issues:

ErrorFix
JSX expression expectedHTML comments missed by the converter — change <!-- --> to {/* */}
Security: require() calls are not allowedA require() call the script couldn't auto-convert — rewrite as a static image path (/img/foo.png) or Next.js <Image> import
Expected component X to be definedA custom component whose import was stripped but the JSX tag remains — port the component or remove the tag
Unknown componentA Docusaurus component (<CodeBlock>, <BrowserWindow>) that needs to be removed or replaced
Type error: comparison has no overlapThe SidebarItem type in config/sidebar.ts is missing the link or api variants — add them (see Sidebar type)
Expression expectedBare < or > characters in text — wrap in backticks or escape as &lt; / &gt;

4. Run the fixer scripts

Trellis Docs ships a set of targeted fixers for patterns the main migration script can't automate cleanly — things that depend on the target project state, or that are better handled post-migration because the errors only surface at build time. Each one defaults to dry-run mode; pass --write to apply changes.

ScriptWhat it fixesWhen to run
convert-img-tags.jsRewrites <img src={require('./foo.png').default}> JSX tags to markdown ![alt](./foo.png) syntaxDocusaurus MDX commonly imports images via require(), which MDX in Trellis rejects
fix-use-client.jsAdds missing 'use client' directives to components that use hooks or browser APIs; moves misplaced ones to the top of the fileErrors like useState is not defined at runtime, or The 'use client' directive must be placed before other expressions at build
fix-jsx-extension.jsRenames .ts files that contain JSX to .tsxParsing ecmascript source code failed / Expected '>', got 'ident' errors
fix-rest-props.jsReplaces invalid ...rest: Record<string, any> in generated interfaces with an index signatureExpected ident TypeScript parse errors in generated prop interfaces
fix-css-modules.jsWraps bare-element selectors in :global() so .module.css files pass Next.js's pure-selector ruleSelector "h3" is not pure. Pure selectors must contain at least one local class or id
fix-css-imports.jsComments out @import statements pointing at Docusaurus token / custom / variables stylesheetsCan't resolve '../../css/tokens.css'
fix-docs-imports.jsRewrites relative imports pointing at the old Docusaurus docs/ tree (e.g., '../../docs/images/foo.svg') to Trellis @/content/docs/ alias pathsModule not found for paths that traverse into a removed docs/ sibling
fix-data-imports.jsRestores @site/src/data/* JSON imports the migration commented out, and copies the data files to data/migrated/ReferenceError: infrastructureData is not defined at runtime
fix-theme-hooks.jsConverts remaining Docusaurus useColorMode() calls to next-themes useTheme() (usually handled by the main migration, but covers edge cases it missed)useColorMode is not defined at runtime
fix-mdx-imports.jsReads each migrated component's actual export style and rewrites components/docs/mdx/index.tsx to use named or default import accordinglyExport X doesn't exist in target module. Did you mean to import default?
fix-infima.jsConverts Docusaurus Infima CSS classes (container-fluid, row, col col--6, card__header, margin-top--sm, text--muted, etc.) to Tailwind / shadcn equivalentsYour content uses Infima markup for grids, cards, or spacing and the layout breaks
fix-untyped-params.jsAdds : any to untyped arrow-function parameters (useCallback, useMemo, array methods, helpers)Parameter 'x' implicitly has an 'any' type during npm run build type checking

Each script supports the same flags:

FlagDescription
--dry-runPreview changes (default when --write is omitted)
--writeApply changes in place

Run order matters a little: apply build-blocking fixes first (fix-jsx-extension, fix-use-client, fix-rest-props), then content/layout fixes (fix-css-modules, fix-css-imports, fix-docs-imports, fix-data-imports, fix-mdx-imports, fix-infima), then strict-mode fixes (fix-untyped-params, fix-theme-hooks). The convert-img-tags script runs against MDX in content/docs/ and can be run at any point.

convert-img-tags

node scripts/convert-img-tags.js content/docs/my-page.mdx --write

Handles multi-line <img> tags (self-closing or paired), src={require('...').default} expressions, and width/height attributes (mapped to the Trellis title syntax, e.g. ![alt](src "width=200px")). Drops unsupported JSX attributes (id, className, style). Tags the script can't parse are left untouched with a warning.

Note

Shell globbing ("content/**/*.mdx") may not expand on Windows — pass explicit file paths or wrap the call in a shell loop when converting many files at once.

fix-use-client

node scripts/fix-use-client.js --write

Scans components/custom/migrated/ for .tsx/.ts files. Adds 'use client' at the top (after any leading JSDoc) to files that call hooks (useState, useEffect, useCallback, useMediaQuery, or any useXxx(), or that access window./document./navigator./localStorage./sessionStorage.. Moves misplaced directives (ones the old migration inserted after imports) to the correct position. Files that don't use client features are left alone.

fix-jsx-extension

node scripts/fix-jsx-extension.js --write

Walks components/custom/migrated/ and renames any .ts file whose contents actually contain JSX to .tsx. Uses a robust detector that recognizes closing tags (</div>), PascalCase component usage (<Button>), and arrow-body JSX (=> <). No import updates needed — Next.js resolves extensionless imports against both .ts and .tsx via tsconfig.

fix-rest-props

node scripts/fix-rest-props.js --write

Replaces invalid ...props: Record<string, any>; lines inside generated TypeScript interfaces with [key: string]: any; (the correct TS syntax for arbitrary extra keys).

fix-css-modules

node scripts/fix-css-modules.js --write

Processes .module.css files. Wraps top-level bare-element selectors (h3, h3, h4, ul li, etc.) in :global(...) so they pass Next.js's purity rule. Leaves alone: selectors that already contain ., #, :global, or &, and rules inside @keyframes blocks. Walks into @media and @supports blocks correctly.

Note

:global() preserves the Docusaurus behavior of styles leaking into the global stylesheet. For long-term correctness, refactor each wrapped rule to nest under a containing class — the fixer prints a reminder after every run.

fix-css-imports

node scripts/fix-css-imports.js --write

Comments out @import statements targeting Docusaurus token files (tokens.css, custom.css, variables.css) or any @site//~/ alias path. Trellis loads design tokens globally via app/tokens.css, so per-component imports are unnecessary. Leaves a migration note so you can port any var(--ifm-*) references manually.

fix-docs-imports

node scripts/fix-docs-imports.js --write

Finds imports like import Circle1 from '../../../docs/contributor-guide/images/1-circle.svg' and rewrites them to '@/content/docs/contributor-guide/images/1-circle.svg' after verifying the target file exists. Also detects @site/docs/* alias imports. Flags SVG-as-component usage (<Circle1 />) since Next.js's default config returns a URL string — you'll need SVGR or a refactor to <img src={Circle1} /> for those to render correctly.

fix-data-imports

node scripts/fix-data-imports.js ../path/to/docusaurus-project --write

Takes the Docusaurus project path as its first argument. Handles both commented-out (migration TODO) and live @site/src/data/*.json imports. Copies each referenced JSON file to data/migrated/ in the Trellis project and rewrites the import path to @/data/migrated/<name>.json.

fix-theme-hooks

node scripts/fix-theme-hooks.js --write

Remaps Docusaurus's useColorMode() (from @docusaurus/theme-common) to next-themes' useTheme(). Four destructuring shapes are handled automatically with alias preservation:

BeforeAfter
{ colorMode } = useColorMode(){ resolvedTheme: colorMode } = useTheme()
{ setColorMode } = useColorMode(){ setTheme: setColorMode } = useTheme()
{ colorMode, setColorMode } = useColorMode(){ resolvedTheme: colorMode, setTheme: setColorMode } = useTheme()

Adds the import { useTheme } from 'next-themes' line if not already present. next-themes is already a Trellis Docs runtime dependency — no install needed.

fix-mdx-imports

node scripts/fix-mdx-imports.js --write

Reads components/docs/mdx/index.tsx and, for each import { X } from '@/components/custom/migrated/...' line, looks at how the target file actually exports X. If the target uses export default and no named export matches, the import is rewritten to import X from .... Files that use named exports (or both) are left alone.

fix-infima

node scripts/fix-infima.js --write                 # MDX in content/docs/
node scripts/fix-infima.js --write --also-jsx      # also scan migrated JSX components

Rewrites Docusaurus Infima CSS classes to Tailwind equivalents across ~50 tokens:

CategoryInfimaTailwind
Gridcontainer-fluid / row / col col--6w-full px-4 / grid grid-cols-12 gap-4 / col-span-12 md:col-span-6
Cardscard / card__header / card__bodyshadcn-style rounded-border divs
Spacingmargin-top--sm / padding-horiz--lgmt-2 / px-6
Texttext--center / text--muted / text--dangertext-center / text-muted-foreground / text-red-600 dark:text-red-400
Buttonsbutton--primary / button--lgshadcn-flavored utility strings
Badgesbadge--warningRounded-pill Tailwind

Unknown tokens (project-specific classes like .before, .no-pointer) are preserved as-is — you'll still need to write CSS for them, but layout won't break. The fixer reports any unmapped Infima-looking tokens at the end so you can audit them.

fix-untyped-params

node scripts/fix-untyped-params.js --write

Adds : any to every untyped arrow-function parameter in migrated components. Handles defaults (x = 0x: any = 0), rest params (...args...args: any[]), and destructured params ({ a, b }{ a, b }: any). Already-typed params are left alone. Safe unblocker for strict mode — once the code compiles, tighten types manually where it matters.

5. Update site configuration

Edit config/site.ts with your branding, logo, and URLs. See Site Configuration for all options.

6. Add variables

If the migration report suggested variables, add them to config/variables.ts and replace hardcoded values in your content with {vars.key}:

config/variables.ts
export const docVariables: Record<string, string> = {
  productName: 'Acme Platform',
  version: '2.1.0',
}

7. Set up navigation

Edit config/navigation.ts to configure your top navbar and footer. See Navigation.

8. Add redirects

If your Docusaurus site used custom slugs or numbered prefixes, URLs will have changed. Add entries to redirects.json so old links still work:

[
  { "from": "/api-reference/", "to": "/guides/api/" },
  { "from": "/01-intro/", "to": "/intro/" }
]

See the Redirects guide for details.

What the script does not migrate

ItemWhyWhat to do
Blog postsDifferent directory structureCopy from blog/ to content/blog/ manually
Custom pagesDocusaurus uses React pages in src/pages/Recreate in app/ as Next.js pages
Custom CSSDocusaurus uses src/css/custom.cssMap to design tokens
Theme overridessrc/theme/ swizzle overridesTrellis has built-in equivalents — no porting needed
PluginsDocusaurus plugins have no Trellis equivalentUse built-in features or build custom components
Versioned docsDifferent snapshot formatRe-snapshot with npm run version:snapshot after migration
i18n translationsDifferent directory structureCopy translated content into content/i18n/<locale>/docs/

Syntax compatibility

Most Docusaurus markdown syntax works in Trellis Docs without changes:

FeatureDocusaurusTrellis DocsStatus
Admonitions:::tip:::tipWorks as-is
Custom admonition titles:::tip Custom Title:::tip Custom TitleWorks as-is
Tabs<Tabs> / <TabItem><Tabs> / <TabItem>Works — no import needed
Code blocks``````Works as-is
FrontmatterYAMLYAMLWorks — Docusaurus fields stripped
MDX comments{/* comment */}{/* comment */}Works as-is
HTML comments<!-- comment -->Not supportedAuto-converted by script
require() image pathsrequire('@site/static/img/foo.png').default'/img/foo.png'Auto-converted by script
Partials (import)import Foo from './_foo.mdx'; <Foo />@include ./_foo.mdxAuto-converted by script (handles props and block form)
<DocCardList /><DocCardList /><DocCardList category="..." />Same name — pass category prop
Image widthNot supported![alt](/img/pic.png "50%")New — set width via title attribute
VariablesNot supported{vars.key}New feature

Was this page helpful?