CLI and TypeScript

This content was generated with the assistance of AI and should be verified against the official documentation of each tool before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against Docusaurus 3.10.2 (the last release of the 3.x line) with TypeScript 6.0.3, Node.js v24.21.0 and npm 11.19.0, and checked on 2026-10-26. It describes the docusaurus command line, one command at a time, and the way a Docusaurus site is written in TypeScript. The running example is the documentation site of Pocket Units, a tiny units-conversion library with a C# and a Python implementation. A site generated with create-docusaurus@3.10.2 classic --typescript was used as the test bed: every command below was run in it, except where a sentence says otherwise, and the outputs shown are the ones it printed.

The docusaurus executable and the npm scripts

The @docusaurus/core package installs one executable, docusaurus. A generated site wraps each command in an npm script of the same name, so npm run build and npx docusaurus build do the same thing. docusaurus --version prints the installed version:

npx docusaurus --version
3.10.2

Running docusaurus --help lists these commands, and docusaurus <command> --help lists the options of one of them:

Command Purpose

start [siteDir]

Starts the development server with hot reload.

build [siteDir]

Compiles the site into static files, by default in build.

serve [siteDir]

Serves a previously built folder with a small static web server.

deploy [siteDir]

Builds the site and pushes the result to a GitHub Pages branch.

swizzle [themeName] [componentName] [siteDir]

Copies or wraps a theme component into src/theme.

clear [siteDir]

Removes build artifacts and caches.

write-translations [siteDir]

Extracts the translatable strings of the site into JSON files.

write-heading-ids [siteDir] [files...]

Adds explicit heading ids to Markdown files.

docs:version <version>

Freezes the current docs as a numbered version.

Three conventions apply to all of them. The optional last positional argument siteDir is the folder that holds the site (the one containing the configuration file); it defaults to the current directory. The --config <path> option points to another configuration file. And the help text of --config still says that the default is [siteDir]/docusaurus.config.js, but the loader looks for several file names in order: docusaurus.config.ts, .mts, .cts, .js, .mjs and .cjs, and uses the first one that exists. A missing file given with --config fails with a clear message:

npx docusaurus build --config nonexist.ts
[ERROR] Error: Config file at ".../cli-and-typescript/nonexist.ts" not found.

The next figure shows what each command reads and writes. Only the generated folders and the files that the commands create for you are drawn.

flowchart LR SRC["docs, blog, src, static, config"] --> START["start"] SRC --> BUILD["build"] BUILD --> OUT["build/ and .docusaurus/"] OUT --> SERVE["serve"] BUILD --> DEPLOY["deploy"] DEPLOY --> GHP["gh-pages branch"] SRC --> SWZ["swizzle"] SWZ --> THEME["src/theme/..."] SRC --> WT["write-translations"] WT --> I18N["i18n/locale/*.json"] SRC --> VER["docs:version"] VER --> VDOCS["versioned_docs, versioned_sidebars, versions.json"] SRC --> WH["write-heading-ids"] WH --> SRC CLEAR["clear"] -.-> OUT

start

docusaurus start builds the site in development mode and serves it with hot reloading on port 3000. In the 3.10 test site, which enables future.v4 (and therefore the faster bundler), the server reports Rspack as its bundler:

npx docusaurus start --no-open --port 3124
[INFO] Starting the development server...
[SUCCESS] Docusaurus website is running at: http://localhost:3124/
client (Rspack 1.7.12) compiled successfully

A request for /docs/pocket-units on that server answered with HTTP 200. The options, taken from --help and the official reference, are:

Option Default Meaning

-p, --port <port>

3000

Port of the development server.

-h, --host <host>

localhost

Host to bind. Use 0.0.0.0 to reach the server from another machine, for example from a container or a cloud workspace (which also needs port forwarding).

-l, --locale <locale>

the default locale

Starts the site in one locale.

--hot-only

false

Does not fall back to a full page refresh when hot reload fails.

--no-open

false

Does not open a browser tab on start.

--poll [interval]

false

Polls for changes instead of using file watchers, optionally every given number of milliseconds. It is meant for environments where watching does not work.

--no-minify

false

Does not minify the JavaScript bundles.

--config <config>

see above

Path to the configuration file.

The official reference adds two caveats: some features, such as anchor links, behave differently in development than in a production build, so always check the output of build before publishing, and HTTPS in development is enabled with the environment variables HTTPS=true, SSL_CRT_FILE and SSL_KEY_FILE (the guide suggests creating a local certificate with mkcert). The HTTPS setup was not executed here.

build

docusaurus build produces the production site: the Markdown and React sources are compiled, every route is rendered to static HTML, and the result is written to a folder. It reads the future flags of the configuration, so the same command runs Webpack or Rspack depending on the site (see Docusaurus Faster and v4 readiness).

npx docusaurus build
[INFO] [en] Creating an optimized production build...
[SUCCESS] Generated static files in "build".
[INFO] Use `npm run serve` command to test your build locally.

The options are:

Option Meaning

--out-dir <dir>

Output folder, relative to the workspace. The default is build. --out-dir dist-test printed Generated static files in "dist-test".

-l, --locale <locale...>

One or more locales to build. Without it every locale of i18n.locales is built.

--no-minify

Does not minimize the JavaScript bundles.

--dev

Builds in development mode, which keeps the full React error messages. In the test site a normal build produced a folder of 2148 KB and --no-minify --dev one of 9156 KB, measured with du -sk.

--bundle-analyzer

Visualizes the size of the bundler output as an interactive tree map. The option starts an analyzer page, so it was not executed here.

--config <config>

Path to the configuration file.

Two environment variables tune the build, according to the official reference (they were not exercised here): USE_SIMPLE_CSS_MINIFIER=true selects the default cssnano preset instead of the more aggressive one, which helps when the optimized CSS is broken, and SKIP_HTML_MINIFICATION=true skips the minification of the generated HTML.

Building locales

On a site with two locales, build builds each in turn, and writes the non-default locale in a sub-folder named after it. This is the output with locales: ['en', 'fr'] and build/fr as the second target:

[INFO] [en] Creating an optimized production build...
[SUCCESS] Generated static files in "build".
[INFO] [fr] Creating an optimized production build...
[SUCCESS] Generated static files in "build/fr".
[INFO] Use `npm run serve` command to test your build locally.

With npx docusaurus build -l en only the first block is printed and build/fr is not produced. The effect of locales on URLs and folders is covered in Internationalization.

When a build fails

A build is the place where invalid content stops the pipeline. Two failures seen while testing this page:

  • An unknown field in the configuration is rejected before any compilation:

    [ERROR] Error: These field(s) ("tagLine",) are not recognized in docusaurus.config.ts.
    If you still want these fields to be in your configuration, put them in the "customFields" field.
    See https://docusaurus.io/docs/api/docusaurus-config/#customfields
  • A page that MDX cannot compile fails the client bundle with Client bundle compiled with errors therefore further build is impossible. followed by a cause such as Could not parse expression with acorn. This is what an old {#id} heading id produced in the test site, as described under write-heading-ids below.

serve

docusaurus serve serves an existing build folder with a static server so that you can test the production output before publishing it. It does not rebuild unless asked:

npx docusaurus serve --no-open --port 3123
[SUCCESS] Serving "build" directory at: http://localhost:3123/

A request for /docs/pocket-units answered 200 and a request for /nope answered 404. The options are -p, --port (default 3000), -h, --host (default localhost), --dir <dir> (the folder to serve, default build), --build (builds first), --no-open (the default is false locally and true in CI) and --config. Running serve --build --no-open on the two-locale site built both locales and then served them, and /fr/docs/pocket-units answered 200.

clear

docusaurus clear removes the generated artifacts. The official reference recommends it after upgrading Docusaurus and before reporting a bug, and it is the first thing to try when the site behaves strangely after a dependency change. The command printed:

npx docusaurus clear
[SUCCESS] Removed the build output folder at "build".
[SUCCESS] Removed the generated folder at ".docusaurus".
[SUCCESS] Removed the bundler persistent cache folder at "node_modules/.cache".

It takes only siteDir. It removes the default output folder, so a build written with --out-dir dist-test was still there afterwards and has to be deleted by hand.

swizzle

Swizzling copies or wraps a component of a theme into src/theme, where your version takes precedence. The concepts, the safe and unsafe components and the styling side are explained in Styling, theming and swizzling; this section covers the command itself. Its form is docusaurus swizzle [themeName] [componentName] [siteDir]. Without arguments it asks questions interactively. The options are:

Option Meaning

-w, --wrap

Creates a wrapper that renders the original component, so you can add elements before or after it.

-e, --eject

Copies the full source of the component, so you can replace its UI and logic.

-l, --list

Lists the available components of a theme without prompting for anything else.

-t, --typescript

Copies the TypeScript files when the theme provides them.

-j, --javascript

Copies the JavaScript files.

--danger

Allows swizzling components that the theme marks as unsafe without asking for confirmation.

--config <config>

Path to the configuration file.

The official reference documents --list, --eject, --wrap, --danger, --typescript and --config; the short forms and --javascript come from --help.

Listing components and safety levels

Listing the components of @docusaurus/theme-classic prints a table with a Wrap column and an Eject column. In the installed version each cell says Safe, Unsafe or Forbidden. A few rows of the output, reduced to the component name and the two cells:

npx docusaurus swizzle @docusaurus/theme-classic --list --typescript
DocSidebar          Safe        Unsafe
Footer              Safe        Safe
ColorModeToggle     Safe        Safe
AnnouncementBar     Unsafe      Unsafe
MDXComponents       Forbidden   Safe
Blog                Forbidden   Forbidden

Without a language option, --list first asks whether to list the JavaScript or the TypeScript files, which blocks scripts and CI, so pass --typescript or --javascript together with it.

Wrapping and ejecting

Wrapping the footer in TypeScript and ejecting a color-mode toggle produced these results:

npx docusaurus swizzle @docusaurus/theme-classic Footer --wrap --typescript
npx docusaurus swizzle @docusaurus/theme-classic ColorModeToggle --eject --typescript
[SUCCESS]
Created wrapper of Footer from @docusaurus/theme-classic in
- ".../src/theme/Footer/index.tsx"

[SUCCESS]
Ejected ColorModeToggle from @docusaurus/theme-classic to
- ".../src/theme/ColorModeToggle/index.tsx"
- ".../src/theme/ColorModeToggle/styles.module.css"

The generated wrapper imports the original component through the @theme-original alias and types its props with WrapperProps, and tsc accepts it without changes:

src/theme/Footer/index.tsx
import React, {type ReactNode} from 'react';
import Footer from '@theme-original/Footer';
import type FooterType from '@theme/Footer';
import type {WrapperProps} from '@docusaurus/types';

type Props = WrapperProps<typeof FooterType>;

export default function FooterWrapper(props: Props): ReactNode {
  return (
    <>
      <Footer {...props} />
    </>
  );
}

Unsafe components

Asking for an eject of DocSidebar (Unsafe for eject) without --danger prints a warning that the action is more likely to be affected by future breaking changes, and then asks for confirmation with a default of "NO: cancel and stay safe". Adding --danger skips the question, and swizzle @docusaurus/theme-classic DocSidebar --wrap --typescript --danger created src/theme/DocSidebar/index.tsx directly. Because the prompt needs a terminal, run swizzle with all the arguments and flags in scripts.

One npm detail: flags for the command must reach it. With npm run swizzle <theme> <component> -- --wrap --typescript the double dash passes the flags through, and that worked here with npm 11.19.0 (it created a wrapper for Footer/Copyright). Without the double dash npm may consume the flags itself (this was seen on npm 11.19.0), so prefer npx docusaurus swizzle ... when in doubt.

deploy

docusaurus deploy publishes to GitHub Pages in one command: it builds the site (unless --skip-build), clones the deployment repository, copies the output into it, commits and pushes. Its options are:

Option Meaning

-l, --locale <locale>

Deploys only the given locale. Without it every locale is deployed.

--out-dir <dir>

Output folder of the build, default build.

--skip-build

Deploys the existing output without building, which is useful with a custom pipeline.

--target-dir <dir>

Directory in the deployment branch to copy into, default ..

--config <config>

Path to the configuration file.

The official deployment guide documents the configuration that the command depends on: organizationName, projectName and deploymentBranch (it defaults to gh-pages for ordinary repositories and must be set explicitly when the project name ends in .github.io), plus a recommendation to set trailingSlash explicitly. The environment variables ORGANIZATION_NAME, PROJECT_NAME and DEPLOYMENT_BRANCH override them. The guide lists these variables for the command:

Variable Meaning

USE_SSH

true to push over SSH. It is inferred as true when the repository URL is an SSH URL.

GIT_USER

A GitHub account with push access to the deployment repository. Required without SSH.

GIT_PASS

A personal access token of that account, for non-interactive use.

CURRENT_BRANCH

The source branch; by default the branch where the command runs.

GIT_USER_NAME, GIT_USER_EMAIL

The user.name and user.email used for the deployment commit.

GITHUB_HOST, GITHUB_PORT

Host and port of a GitHub Enterprise server.

A typical manual deployment with HTTPS and placeholders for the account looks like this. It was not executed with a real repository, because it pushes to a remote:

GIT_USER=<YOUR_GITHUB_USER> npx docusaurus deploy

What was run is the failure outside a Git repository. After a warning that recommends an explicit trailingSlash setting, the command stops at its first Git call:

[INFO] Deploy command invoked...
[INFO] `git --version` code: 0
[ERROR] Error: Error while executing command `git remote get-url origin`
  fatal: not a git repository (or any of the parent directories): .git

Most CI setups use the Actions artifact flow instead of this command; both are compared in Deployment.

write-translations

docusaurus write-translations extracts every string that needs translating, from the theme, the configuration (navbar and footer labels) and the plugins, and writes them as JSON files below i18n/<locale>. The default locale is used unless --locale is given. Run in the test site, it printed one line per plugin and created these files:

npx docusaurus write-translations
[INFO] 5 translations will be written at "i18n/en/docusaurus-theme-classic/navbar.json".
[INFO] 10 translations will be written at "i18n/en/docusaurus-theme-classic/footer.json".
[INFO] 3 translations will be written at "i18n/en/docusaurus-plugin-content-blog/options.json".
[INFO] 4 translations will be written at "i18n/en/docusaurus-plugin-content-docs/current.json".

Together with i18n/en/code.json, which holds the strings of the theme and of translate() calls in React code, that is five files. Every entry has a message and a description, for example the entry named theme.colorToggle.ariaLabel.mode.system with the message system mode. The options are:

Option Meaning

-l, --locale <locale>

Locale folder to write to; --locale fr writes to i18n/fr.

--override

By default only missing messages are appended to existing files. This option overwrites existing messages. Commit or back up the translations first.

--messagePrefix <prefix>

Prefix added to newly written messages, so that untranslated strings stand out in the UI.

--config <config>

Path to the configuration file.

After adding fr to the locales, write-translations --locale fr with --messagePrefix set to the text TR: and a space created the same five files in i18n/fr. The theme already ships a French translation for some of its strings. The entry for the light color mode became TR: mode clair, and a string without a bundled translation became TR: system mode, so the prefix is added to every message that is written. Without --override, existing messages are kept and only missing ones are appended. The content translations of Markdown files are not produced by this command; the layout of i18n and the workflow around it are described in Internationalization.

write-heading-ids

Docusaurus derives an anchor from every heading text. docusaurus write-heading-ids writes explicit ids into the Markdown source, so that a link such as /docs/pocket-units#converting-a-length survives a rewording of the heading. Its form is docusaurus write-heading-ids [siteDir] [files...]: the first positional argument is the site directory, not a file, so a file list must be preceded by it (a dot for the current directory). Without files, all Markdown files used by the plugins are processed. With a page of the Pocket Units site that has three headings:

npx docusaurus write-heading-ids . docs/pocket-units.md
[SUCCESS] Heading ids added to Markdown files (1/1 files):
- docs/pocket-units.md

Only second-level headings and below were changed; the page title (the first-level heading) kept no id:

# Pocket Units
## Converting a length {#converting-a-length}
## Converting a temperature {#converting-a-temperature}

The options are --syntax <classic|mdx-comment>, --migrate, --overwrite and --maintain-case:

Option Meaning

--syntax classic

The default. Writes {#id}.

--syntax mdx-comment

Writes {/* #id */}, which is valid MDX.

--migrate

Converts ids that already exist in the other syntax to the target syntax.

--overwrite

Regenerates existing ids from the heading text.

--maintain-case

Keeps the casing of the heading instead of lowercasing it.

The default syntax has a consequence on a site that follows the v4 settings, such as the generated TypeScript site with future.v4: true: building the page with {#converting-a-length} failed with Could not parse expression with acorn, because MDX 3 reads braces as JavaScript expressions. Running the command with --syntax mdx-comment --migrate rewrote the same headings and the build passed again:

npx docusaurus write-heading-ids . docs/pocket-units.md --syntax mdx-comment --migrate
## Converting a length {/* #converting-a-length */}
## Converting a temperature {/* #converting-a-temperature */}

Adding --overwrite --maintain-case regenerated the ids as Converting-a-length: the case is kept and the words are joined with hyphens. The official migration guide to v3 keeps {#id} working through markdown.mdx1Compat.headingIds and shows an escaped form for files that use it; the v4 flag mdx1CompatDisabledByDefault turns those compatibility options off by default, which is why the comment form is the safer one for new content. Whether to use .md or .mdx for the files is a separate topic, covered in MDX and React components.

docs:version

docusaurus docs:version <version> freezes a copy of the current docs folder as a numbered version. It takes the version name as its only argument. Run once in the test site with 1.0.0:

npx docusaurus docs:version 1.0.0
[SUCCESS] [docs]: version 1.0.0 created!

It created versioned_docs/version-1.0.0, versioned_sidebars/version-1.0.0-sidebars.json and a versions.json file that contains the single entry 1.0.0. With the two-locale site, it also printed a warning that i18n/fr has no docs to snapshot, and skipped that locale. Repeating the command with the same name fails:

[ERROR] Error: [docs]: this version already exists! Use a version tag that does not already exist.

The versions model (current and latest, lastVersion, onlyIncludeVersions, the dropdown, and why to keep few versions) is the subject of Versioning. For a site that uses several docs plugin instances the plugin id is part of the command syntax; the CLI reference page does not describe it.

TypeScript support

Docusaurus itself is written in TypeScript, and supports sites whose configuration, sidebars, pages, components and swizzled theme files are TypeScript. The official guide gives TypeScript 5.1 as the minimum version. The 3.10 release moved new TypeScript sites to TypeScript 6 and to strict mode; the generated site depends on typescript ~6.0.2 (6.0.3 was installed), @docusaurus/tsconfig, @docusaurus/types and @docusaurus/module-type-aliases at 3.10.2.

The same release moved the publishing of Docusaurus’s own npm packages to npm Trusted Publishing: CI authenticates to npm with OIDC instead of a long-lived token, and the packages carry provenance statements that npm audit signatures can verify. It changes nothing in a site’s configuration.

Creating and migrating a site

A new site is created in TypeScript with the --typescript flag, as shown in Installation and project structure. An existing JavaScript site is migrated by installing the four packages as development dependencies and adding a tsconfig.json:

npm install --save-dev typescript @docusaurus/module-type-aliases @docusaurus/tsconfig @docusaurus/types
tsconfig.json
{
  "extends": "@docusaurus/tsconfig",
  "compilerOptions": {
    "baseUrl": ".",
    "ignoreDeprecations": "6.0",
    "strict": true
  },
  "exclude": [".docusaurus", "build"]
}

This is the file of the generated 3.10.2 site; the official guide shows the shorter form with only extends and baseUrl. The base configuration @docusaurus/tsconfig sets, among others, jsx to preserve, target and lib to ES2022 (with DOM), moduleResolution to bundler, noEmit, skipLibCheck, and a path alias @site/* for files at the site root. With TypeScript 6 the baseUrl option is deprecated, and removing ignoreDeprecations from the generated file produced this error from tsc:

tsconfig.json(7,5): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0.
Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.

The type-check is separate from the build

The guide states that tsconfig.json is for editor support only: Docusaurus does not use it to compile the project. The configuration files are loaded on the fly (the jiti loader is used for them in the installed version), so types are stripped, not checked, and type errors do not stop docusaurus build. The generated package.json adds a script for the check, "typecheck": "tsc", and the guide suggests running tsc locally or in CI. To see the difference, the property tagline was misspelled as tagLine in the configuration:

npx tsc
docusaurus.config.ts(9,3): error TS2561: Object literal may only specify known properties, but 'tagLine' does not exist in type 'Config'. Did you mean to write 'tagline'?

tsc exited with status 2 on that file. The build did not use the type information, but it did fail, because the configuration validator rejects unknown fields (the message shown under "When a build fails" above). With the typo reverted both commands passed. A reasonable CI order is tsc first, so that the error points at the line, then docusaurus build.

Typing the configuration

The configuration is typed with Config from @docusaurus/types, and preset and theme options are checked with satisfies against the types of the preset. This is the shape of the generated docusaurus.config.ts, reduced to a Pocket Units site:

docusaurus.config.ts
import {themes as prismThemes} from 'prism-react-renderer';
import type {Config} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';

const config: Config = {
  title: 'Pocket Units',
  tagline: 'Unit conversions for C# and Python',
  favicon: 'img/favicon.ico',
  future: {
    v4: true,
  },
  url: 'https://your-docusaurus-site.example.com',
  baseUrl: '/',
  onBrokenLinks: 'throw',
  i18n: {
    defaultLocale: 'en',
    locales: ['en'],
  },
  presets: [
    [
      'classic',
      {
        docs: {
          sidebarPath: './sidebars.ts',
        },
        theme: {
          customCss: './src/css/custom.css',
        },
      } satisfies Preset.Options,
    ],
  ],
  themeConfig: {
    prism: {
      theme: prismThemes.github,
      darkTheme: prismThemes.dracula,
    },
  } satisfies Preset.ThemeConfig,
};

export default config;

The sidebars file is typed the same way, with SidebarsConfig from @docusaurus/plugin-content-docs:

sidebars.ts
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';

const sidebars: SidebarsConfig = {
  tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
};

export default sidebars;

Both files were part of the generated site and were type-checked and built with it. A site that stays in JavaScript can get the same checking by adding // @ts-check at the top of docusaurus.config.js and annotating the object with a JSDoc @type of import('@docusaurus/types').Config; the guide shows this form, and it was not executed here.

TypeScript in the theme and in pages

Files under src (pages, components, swizzled theme components) are compiled by the bundler, which strips the types as well. All the official themes (theme-classic, theme-live-codeblock and theme-search-algolia) ship TypeScript variants, which is what swizzle --typescript copies, as shown above. The @docusaurus/module-type-aliases package declares the @theme/*, @theme-original/*, @docusaurus/* and @site/* module names, so imports in the editor resolve. A theme author who wants TypeScript support for a theme adds a getTypeScriptThemePath entry to the theme plugin, as the lifecycle documentation of Docusaurus describes.

Pitfalls and version notes

  • --help says the default configuration file is docusaurus.config.js, yet a docusaurus.config.ts is found first.

  • swizzle --list prompts for a language unless --typescript or --javascript is given, which hangs a non-interactive script. The same applies to swizzling an unsafe component without --danger.

  • write-heading-ids takes the site directory as its first positional argument, so a file list must be preceded by . or the site path.

  • The classic {#id} heading ids fail a build in a site that follows the v4 settings; use --syntax mdx-comment for new content or migrate the old ones with --migrate.

  • clear only deletes the default output folder, the generated .docusaurus folder and the bundler cache.

  • docusaurus deploy pushes with your credentials. Keep GIT_PASS in a CI secret, never in the repository.

  • A type error does not fail docusaurus build; run tsc as a separate step.

  • In 3.10 TypeScript 6 deprecates baseUrl, so generated sites carry ignoreDeprecations.