Migrating from Docusaurus
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:
- Scaffold a Trellis Docs project — follow Getting Started to create a new project
- Have your Docusaurus project accessible — the script reads from its
docs/directory - 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
| Flag | Description |
|---|---|
--dry-run | Preview what would change without writing any files |
--force | Overwrite existing files in content/docs/ |
--no-type-check | Skip 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-runThen run without the flag to perform the migration:
node scripts/migrate-docusaurus.js ../my-docusaurus-siteWhat 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
| Transform | Example |
|---|---|
| Numbered prefixes stripped | 01-getting-started.md → getting-started.mdx |
.md renamed to .mdx | guide.md → guide.mdx |
README.md renamed to index.mdx | api/README.md → api/index.mdx |
Frontmatter
Docusaurus-only frontmatter fields are removed automatically:
sidebar_position,sidebar_label,displayed_sidebarid,slug,pagination_label,pagination_next,pagination_prevcustom_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 insidebars.js. When omitted, Docusaurus derives it from the file path. For example, a file atguides/setup.mdwithid: quickstartwould be referenced in the sidebar asguides/quickstart.slug— Controls the URL path independently of theidand file path. A doc atguides/setup.mdwithslug: /get-startedwould 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
idvalues — No action needed. The generatedconfig/sidebar.tsalready references docs by file path, which is the Trellis Docs convention. - Custom
slugvalues — URLs will change. The migration report warns about each affected file. Add entries toredirects.jsonso old links continue to work (see Add redirects below).
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
| Transform | Details |
|---|---|
@theme imports removed | import Tabs from '@theme/Tabs' — Trellis provides these automatically |
@site imports removed | Warns you to verify no custom components are missing |
| HTML comments converted | <!-- comment --> → {/* comment */} (MDX syntax) |
require() image paths converted | require('@site/static/img/foo.png').default → '/img/foo.png' |
| MDX partial imports converted | import Foo from './_foo.mdx'; <Foo /> → @include ./_foo.mdx |
| Excessive blank lines cleaned | Three 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 source | How it's handled |
|---|---|
sidebars.js / .cjs / .json | Loaded and converted directly |
sidebars.ts | Parsed 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 labels | Used for category names in the generated sidebar |
| No sidebar file found | Generated 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:
- Checks for Trellis built-in equivalents —
Tabs,TabItem,Admonition,CodeBlock,Details,TOCInline, andDocCardListare skipped since Trellis provides them natively - 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. - Copies the source files to
components/custom/migrated/— component directories are copied recursively, single files are copied directly - Renames JS to TS —
.jsx→.tsxand.js→.ts(Trellis enforces strict TypeScript) - 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/*.jsonimports are rewritten to@/data/migrated/*.json, and the referenced JSON files are copied todata/migrated/useColorModefrom@docusaurus/theme-commonis automatically remapped touseThemefromnext-themes; destructuring patterns are aliased ({ resolvedTheme: colorMode, setTheme: setColorMode }) socolorMode === 'dark'style checks keep working without code edits- Other
@site/*/@docusaurus/*imports are commented out with a TODO marker
- 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 - 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
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).
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.tsAfter migration
1. Review the sidebar
Open config/sidebar.ts and verify the structure matches your expectations. You may want to:
- Reorder items
- Adjust
collapsedvalues - Add
linkproperties 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/.tsbut still need type annotations. At minimum, add a props interface and return type:
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.
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 buildFix any MDX compilation errors. Common issues:
| Error | Fix |
|---|---|
| JSX expression expected | HTML comments missed by the converter — change <!-- --> to {/* */} |
Security: require() calls are not allowed | A 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 defined | A custom component whose import was stripped but the JSX tag remains — port the component or remove the tag |
| Unknown component | A Docusaurus component (<CodeBlock>, <BrowserWindow>) that needs to be removed or replaced |
| Type error: comparison has no overlap | The SidebarItem type in config/sidebar.ts is missing the link or api variants — add them (see Sidebar type) |
| Expression expected | Bare < or > characters in text — wrap in backticks or escape as < / > |
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.
| Script | What it fixes | When to run |
|---|---|---|
convert-img-tags.js | Rewrites <img src={require('./foo.png').default}> JSX tags to markdown  syntax | Docusaurus MDX commonly imports images via require(), which MDX in Trellis rejects |
fix-use-client.js | Adds missing 'use client' directives to components that use hooks or browser APIs; moves misplaced ones to the top of the file | Errors like useState is not defined at runtime, or The 'use client' directive must be placed before other expressions at build |
fix-jsx-extension.js | Renames .ts files that contain JSX to .tsx | Parsing ecmascript source code failed / Expected '>', got 'ident' errors |
fix-rest-props.js | Replaces invalid ...rest: Record<string, any> in generated interfaces with an index signature | Expected ident TypeScript parse errors in generated prop interfaces |
fix-css-modules.js | Wraps bare-element selectors in :global() so .module.css files pass Next.js's pure-selector rule | Selector "h3" is not pure. Pure selectors must contain at least one local class or id |
fix-css-imports.js | Comments out @import statements pointing at Docusaurus token / custom / variables stylesheets | Can't resolve '../../css/tokens.css' |
fix-docs-imports.js | Rewrites relative imports pointing at the old Docusaurus docs/ tree (e.g., '../../docs/images/foo.svg') to Trellis @/content/docs/ alias paths | Module not found for paths that traverse into a removed docs/ sibling |
fix-data-imports.js | Restores @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.js | Converts 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.js | Reads each migrated component's actual export style and rewrites components/docs/mdx/index.tsx to use named or default import accordingly | Export X doesn't exist in target module. Did you mean to import default? |
fix-infima.js | Converts Docusaurus Infima CSS classes (container-fluid, row, col col--6, card__header, margin-top--sm, text--muted, etc.) to Tailwind / shadcn equivalents | Your content uses Infima markup for grids, cards, or spacing and the layout breaks |
fix-untyped-params.js | Adds : 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:
| Flag | Description |
|---|---|
--dry-run | Preview changes (default when --write is omitted) |
--write | Apply 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 --writeHandles multi-line <img> tags (self-closing or paired), src={require('...').default} expressions, and width/height attributes (mapped to the Trellis title syntax, e.g. ). Drops unsupported JSX attributes (id, className, style). Tags the script can't parse are left untouched with a warning.
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 --writeScans 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 --writeWalks 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 --writeReplaces 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 --writeProcesses .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.
: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 --writeComments 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 --writeFinds 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 --writeTakes 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 --writeRemaps Docusaurus's useColorMode() (from @docusaurus/theme-common) to next-themes' useTheme(). Four destructuring shapes are handled automatically with alias preservation:
| Before | After |
|---|---|
{ 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 --writeReads 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 componentsRewrites Docusaurus Infima CSS classes to Tailwind equivalents across ~50 tokens:
| Category | Infima | Tailwind |
|---|---|---|
| Grid | container-fluid / row / col col--6 | w-full px-4 / grid grid-cols-12 gap-4 / col-span-12 md:col-span-6 |
| Cards | card / card__header / card__body | shadcn-style rounded-border divs |
| Spacing | margin-top--sm / padding-horiz--lg | mt-2 / px-6 |
| Text | text--center / text--muted / text--danger | text-center / text-muted-foreground / text-red-600 dark:text-red-400 |
| Buttons | button--primary / button--lg | shadcn-flavored utility strings |
| Badges | badge--warning | Rounded-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 --writeAdds : any to every untyped arrow-function parameter in migrated components. Handles defaults (x = 0 → x: 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}:
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
| Item | Why | What to do |
|---|---|---|
| Blog posts | Different directory structure | Copy from blog/ to content/blog/ manually |
| Custom pages | Docusaurus uses React pages in src/pages/ | Recreate in app/ as Next.js pages |
| Custom CSS | Docusaurus uses src/css/custom.css | Map to design tokens |
| Theme overrides | src/theme/ swizzle overrides | Trellis has built-in equivalents — no porting needed |
| Plugins | Docusaurus plugins have no Trellis equivalent | Use built-in features or build custom components |
| Versioned docs | Different snapshot format | Re-snapshot with npm run version:snapshot after migration |
| i18n translations | Different directory structure | Copy translated content into content/i18n/<locale>/docs/ |
Syntax compatibility
Most Docusaurus markdown syntax works in Trellis Docs without changes:
| Feature | Docusaurus | Trellis Docs | Status |
|---|---|---|---|
| Admonitions | :::tip | :::tip | Works as-is |
| Custom admonition titles | :::tip Custom Title | :::tip Custom Title | Works as-is |
| Tabs | <Tabs> / <TabItem> | <Tabs> / <TabItem> | Works — no import needed |
| Code blocks | ``` | ``` | Works as-is |
| Frontmatter | YAML | YAML | Works — Docusaurus fields stripped |
| MDX comments | {/* comment */} | {/* comment */} | Works as-is |
| HTML comments | <!-- comment --> | Not supported | Auto-converted by script |
require() image paths | require('@site/static/img/foo.png').default | '/img/foo.png' | Auto-converted by script |
| Partials (import) | import Foo from './_foo.mdx'; <Foo /> | @include ./_foo.mdx | Auto-converted by script (handles props and block form) |
<DocCardList /> | <DocCardList /> | <DocCardList category="..." /> | Same name — pass category prop |
| Image width | Not supported |  | New — set width via title attribute |
| Variables | Not supported | {vars.key} | New feature |