Relevant source files
The following files were used as context for generating this wiki page:
- packages/create-app/src/bin.ts
- packages/create-app/src/index.ts
- packages/create-app/src/constants.ts
- packages/mdx/src/next/index.ts
- packages/cli/src/commands/export-epub.ts
- packages/create-app/package.json
- packages/create-app/scripts/sync.ts
- packages/mdx/src/vite/index.ts
- packages/create-app/src/plugins/orama-cloud.ts
- packages/create-app/src/plugins/ai.ts
- packages/create-app/src/transform/index.ts
- packages/cli/src/index.ts
- packages/base-ui/src/provider/tanstack.tsx
- packages/create-app-versions/package.json
- packages/typescript/src/lib/base.ts
- packages/create-app/src/plugins/next-use-takumi.ts
- packages/create-app/src/plugins/biome.ts
- packages/create-app/src/plugins/oxlint.ts
- packages/radix-ui/src/provider/tanstack.tsx
- packages/openapi/src/generate-file.ts
- packages/content/src/index.ts
- packages/create-app/src/plugins/eslint.ts
- packages/preview/src/pages/...slugs.tsx
- packages/create-app/src/transform/tanstack-start.ts
- packages/cli/src/commands/add.ts
- packages/create-app/src/plugins/biome.next.json
- packages/cli/src/config.ts
- packages/preview/src/cli/index.ts
- packages/create-app/tsdown.config.ts
- packages/core/src/framework/tanstack.tsx
Overview
The create-fumadocs-app package provides an automated CLI scaffolding tool to set up new documentation projects configured with Fumadocs, supporting frameworks including Next.js, Waku, React Router, and TanStack Start. It addresses the complexity of configuring build-time MDX compilation, router integration, layout providers, and peripheral developer tooling by orchestrating interactive prompt groups, template asset copying, workspace dependency version alignment, and AST-based code transformations. Design decisions center around modular template plugins and AST manipulation via ts-morph to programmatically inject search integrations, AI chat components, and linter configurations directly into generated project structures without manual boilerplate intervention.
Sources: packages/create-app/src/bin.ts:1-60, packages/create-app/src/index.ts:1-126, packages/create-app/src/constants.ts:34-80, packages/create-app/src/transform/index.ts:1-174
Interactive Prompts and Scaffolding Execution
CLI Configuration and Argument Parsing
The CLI entry point uses @commander-js/extra-typings to define positional arguments and option flags before initiating interactive prompts. The root command accepts an optional [name] argument for the target project and configures flags for build options, templates, package managers, and tool integrations.
const command = program
.argument('[name]', 'the project name')
.option('--src', '(Next.js only) enable `src/` directory')
.option('--install', 'install packages automatically')
.option('--no-git', 'disable auto Git repository initialization')
.addOption(
new Option(
'--linter <name>',
'configure a linter/formatter, ESLint is currently Next.js only.',
).choices(['eslint', 'oxlint', 'biome']),
)
.addOption(
new Option('--search <name>', 'configure a search solution').choices(['orama', 'orama-cloud']),
)
.addOption(
new Option('--og-image <name>', 'configure OG image generation').choices(['next-og', 'takumi']),
)
.addOption(
new Option('--ai-chat <name>', 'configure AI chat').choices([
'openrouter',
'llmgateway',
'inkeep',
]),
)
.addOption(
new Option('--template <name>', 'choose a template').choices(
templates.map((item) => item.value),
),
)
.addOption(
new Option('--pm <name>', 'choose a package manager')
.choices(managers)
.default(getPackageManager()),
);Sources: packages/create-app/src/bin.ts:21-54
The command line options govern project parameters such as framework features, linters, search providers, and package managers.
| Option Flag | Argument Type | Choices / Default | Purpose |
|---|---|---|---|
--src | boolean | undefined | Enable src/ directory (Next.js only) |
--install | boolean | undefined | Install packages automatically |
--no-git | boolean | undefined | Disable auto Git repository initialization |
--linter <name> | string | eslint, oxlint, biome | Configure a linter or formatter |
--search <name> | string | orama, orama-cloud | Configure a search solution |
--og-image <name> | string | next-og, takumi | Configure Open Graph image generation |
--ai-chat <name> | string | openrouter, llmgateway, inkeep | Configure AI chat integration |
--template <name> | string | Dynamic template list | Choose a starter template |
--pm <name> | string | Detected package manager | Choose a package manager |
Sources: packages/create-app/src/bin.ts:23-54
Note
When executing in a CI environment detected via isCI, interactive prompts are bypassed entirely, falling back to default values such as untitled for project name and +next+fuma-docs-mdx for the template.
Sources: packages/create-app/src/bin.ts:65-76
Interactive Prompt Groups
Using @clack/prompts, the CLI prompts the user through a structured group flow if flags are omitted from the command line execution. Each prompt checks for existing configuration values or CI flags before rendering interactive selectors or text inputs.
const options = await group(
{
name: async () => {
if (defaultName) return defaultName;
if (isCI) return 'untitled';
return text({
message: 'Project name',
placeholder: 'my-app',
defaultValue: 'my-app',
});
},
template: async () => {
if (config.template) return config.template;
if (isCI) return '+next+fuma-docs-mdx';
return select<Template>({
message: 'Choose a template',
initialValue: '+next+fuma-docs-mdx',
options: templates,
});
},
src: async ({ results }: { results: { template?: Template } }) => {
if (config.src !== undefined) return config.src;
if (isCI || !results.template?.startsWith('+next')) return false;
return confirm({
message: 'Use `/src` directory?',
initialValue: false,
});
},Sources: packages/create-app/src/bin.ts:62-92
The prompt sequence evaluates template selections conditionally. For instance, the src directory prompt is restricted to templates starting with +next, and the linter options differ between Next.js templates and alternative frameworks.
Warning
If a user cancels out of the Clack prompt group using the abort shortcut, the onCancel handler triggers cancel('Installation Stopped.') and immediately terminates the process with code 0.
Sources: packages/create-app/src/bin.ts:219-223
Project Directory Initialization and Template Execution
Once prompt inputs are gathered, the core create function from packages/create-app/src/index.ts initializes the destination directory, applies registered template plugins, mirrors template files, updates package.json configurations, and sets up dependencies or Git repositories.
export async function create(createOptions: Options): Promise<void> {
const {
outputDir,
plugins = [],
packageManager = 'npm',
initializeGit = false,
installDeps = false,
log = console.log,
} = createOptions;
let template = templates.find((item) => item.value === createOptions.template)!;
for (const plugin of plugins) {
template = (await plugin.template?.call({ dest: outputDir }, template)) ?? template;
}
const appDir = path.join(outputDir, template.appDir);
const projectName = path.basename(outputDir);
const pluginContext: TemplatePluginContext = {
template,
dest: outputDir,
log,
appDir,
};
await copy(path.join(sourceDir, 'template', template.value), outputDir, {
rename(file) {
file = file.replace('example.gitignore', '.gitignore');
return template.rename?.(file) ?? file;
},
});
const packageJsonPath = path.join(outputDir, 'package.json');
let packageJson = await initPackageJson(projectName, packageJsonPath);
for (const plugin of plugins) {
packageJson = (await plugin.packageJson?.call(pluginContext, packageJson)) ?? packageJson;
}
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
const readmePath = path.join(outputDir, 'README.md');
let readme = `# ${projectName}\n\n${await fs.readFile(readmePath)}`;
for (const plugin of plugins) {
readme = (await plugin.readme?.call(pluginContext, readme)) ?? readme;
}
await fs.writeFile(readmePath, readme);
for (const plugin of plugins) {
await plugin.afterWrite?.call(pluginContext);
}
if (installDeps) {
try {
await autoInstall(packageManager, outputDir);
log('Installed dependencies');
} catch (err) {
log(`Failed to install dependencies: ${err}`);
}
}
if (initializeGit && (await tryGitInit(outputDir))) {
log('Initialized Git repository');
}
}Sources: packages/create-app/src/index.ts:64-126
Tip
During template asset copying, template files named example.gitignore are automatically renamed to .gitignore, and any custom template rename hooks are executed to map source paths correctly.
Sources: packages/create-app/src/index.ts:88-94
Template Sync and Dependency Alignment
Template Asset Mirroring and Filtering
The template synchronization script (packages/create-app/scripts/sync.ts) mirrors example applications from the repository workspace into package-internal template directories (packages/create-app/template). When executing main, the script iterates across all defined template mappings, clearing existing non-README files via tinyglobby before copying fresh files from the source example directory.
const templates = [
'react-router',
'tanstack-start',
'tanstack-start-spa',
'react-router-spa',
'waku',
['next', '+next+fuma-docs-mdx'],
['next-static', '+next+fuma-docs-mdx+static'],
];
async function rm(files: string[]) {
await Promise.all(files.map((file) => fs.rm(file)));
}
async function main() {
await Promise.all(
templates.map(async (template) => {
template = Array.isArray(template) ? template : [template, template];
const inDir = path.join(examplesDir, template[0]);
const outDir = path.join(templateDir, template[1]);
await rm(
await glob(['**/*', '!README.md'], {
cwd: outDir,
absolute: true,
}),
);
Sources: packages/create-app/scripts/sync.ts:11-38
The synchronization process relies on filter functions and file-renaming rules during asset copying. Directories such as node_modules, dist, out, and build are excluded, along with hidden files starting with a dot. Specific build artifacts and environment files like next-env.d.ts, pages.gen.ts, routeTree.gen.ts, README.md, and tsconfig.tsbuildinfo are filtered out, whereas .gitignore files are explicitly renamed to example.gitignore so they safely check in without affecting package publishing.
| Source Filename / Pattern | Filter / Rename Action | Target Treatment in Template |
|---|---|---|
node_modules, dist, out, build | filterDir returns false | Excluded entirely from copy |
Hidden directories/files (.*) | filterDir / filter returns false | Excluded unless explicitly allowed |
.gitignore | rename maps to example.gitignore, filter returns true | Copied and renamed to prevent local git ignore collisions |
next-env.d.ts, pages.gen.ts, routeTree.gen.ts, README.md, tsconfig.tsbuildinfo | filter returns false | Excluded from generated templates |
Sources: packages/create-app/scripts/sync.ts:39-77
Note
Template definitions explicitly map source directories in ../../examples to their respective CLI template identifiers, supporting Next.js, React Router, Waku, and TanStack Start configurations.
Sources: packages/create-app/scripts/sync.ts:11-19
Workspace Package Version Replacement
When scaffolding a new project, the initPackageJson function in packages/create-app/src/index.ts reads the target package.json file, updates the project name, appends the postinstall script fumadocs-mdx, and replaces any workspace-protocol dependencies with explicit version numbers mapped from package constants.
async function initPackageJson(
projectName: string,
packageJsonPath: string,
): Promise<PackageJsonType> {
function replaceWorkspaceDeps(deps: Record<string, string> = {}) {
for (const k in deps) {
if (deps[k].startsWith('workspace:') && k in depVersions) {
deps[k] = depVersions[k as keyof typeof depVersions];
}
}
return deps;
}
const packageJson: PackageJsonType = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
return {
...packageJson,
name: projectName,
scripts: {
...packageJson.scripts,
postinstall: 'fumadocs-mdx',
},
dependencies: replaceWorkspaceDeps(packageJson.dependencies),
devDependencies: replaceWorkspaceDeps(packageJson.devDependencies),
};
}Sources: packages/create-app/src/index.ts:128-154
The workspace dependency replacement helper (replaceWorkspaceDeps) scans dependency and devDependency records. Any entry whose version string starts with workspace: and whose package name exists in depVersions has its value overwritten by the resolved version constant.
Warning
If a dependency specifies a workspace: protocol prefix but its package name is missing from depVersions, the version string remains unmodified, which can cause package manager resolution failures during installation.
Sources: packages/create-app/src/index.ts:132-140
Dependency Constants and Version Tracking
Dependency versions are aggregated inside packages/create-app/src/constants.ts by combining external dependencies declared in packages/create-app-versions/package.json with workspace package versions extracted directly from core workspace package manifests.
import { fileURLToPath } from 'node:url';
import versionPkg from '../../create-app-versions/package.json';
import * as corePkg from '../../core/package.json';
import * as mdxPkg from '../../mdx/package.json';
import * as radixPkg from '../../radix-ui/package.json';
import * as basePkg from '../../base-ui/package.json';
Sources: packages/create-app/src/constants.ts:1-7
The version aggregator iterates over workspace packages (corePkg, mdxPkg, radixPkg, basePkg) and injects their current versions into the depVersions record exported for scaffolding operations.
const workspaces = [corePkg, mdxPkg, radixPkg, basePkg];
export const depVersions = versionPkg.dependencies;
for (const workspace of workspaces) {
depVersions[workspace.name as keyof typeof depVersions] = workspace.version;Sources: packages/create-app/src/constants.ts:82-87
The baseline versions defined in packages/create-app-versions/package.json supply default dependency specifiers for external tooling and libraries.
"dependencies": {
"@biomejs/biome": "^2.4.16",
"@orama/core": "^1.2.19",
"@takumi-rs/image-response": "^1.8.1",
"fumadocs-core": "workspace:*",
"fumadocs-mdx": "workspace:*",
"fumadocs-ui": "workspace:*",
"oxlint": "^1.69.0",
"react": "^19.2.7"
}Sources: packages/create-app-versions/package.json:6-15
Linter and Tooling Integrations
ESLint, Biome, and Oxlint Plugins
Template plugins for quality and formatting tools automate both script injections and configuration file generation during project scaffolding. Each linter plugin hooks into the template lifecycle by mutating package manifests through packageJson and writing out configuration files in afterWrite.
| Plugin | Package / Config Target | Lint Script Command | Added Dev Dependencies |
|---|---|---|---|
eslint | eslint.config.mjs | eslint | eslint: ^9.39.4, eslint-config-next |
biome | biome.json | biome check | @biomejs/biome |
oxlint | .oxlintrc.json | oxlint | oxlint |
Sources: packages/create-app/src/plugins/eslint.ts:30-35, packages/create-app/src/plugins/biome.ts:15-20, packages/create-app/src/plugins/oxlint.ts:53-57
The ESLint plugin restricts execution strictly to Next.js templates by checking this.template.value.startsWith('+next'), writing an eslint.config.mjs configuration file that consumes eslint-config-next/core-web-vitals alongside global ignores for build directories.
Sources: packages/create-app/src/plugins/eslint.ts:5-17, packages/create-app/src/plugins/eslint.ts:24-40
Warning
The ESLint plugin returns early without modifying package manifests or writing files if the selected template does not start with the +next prefix.
Sources: packages/create-app/src/plugins/eslint.ts:24-40
The Biome plugin generates a biome.json configuration file by conditionally picking either next or base rulesets depending on the template prefix, and registers lint (biome check) and format (biome format --write) scripts.
Sources: packages/create-app/src/plugins/biome.ts:13-17, packages/create-app/src/plugins/biome.ts:25-27
The Oxlint plugin configures .oxlintrc.json with schema validation pointing to ./node_modules/oxlint/configuration_schema.json, enabling typescript, react, and import plugins by default, with nextjs added under Next.js template variants.
Sources: packages/create-app/src/plugins/oxlint.ts:10-44, packages/create-app/src/plugins/oxlint.ts:62-64
Next.js Takumi Asset Presets
For Next.js templates, the nextUseTakumi plugin swaps out standard image-generation bindings for the Takumi Rust-powered image response engine. When active, it injects @takumi-rs/image-response into project dependencies and applies three distinct file transformations during the afterWrite phase.
async afterWrite() {
if (this.template.value.startsWith('+next')) {
await replaceImports(this);
await replaceImagePath(this);
await nextConfigExternal(this);
}
},Sources: packages/create-app/src/plugins/next-use-takumi.ts:21-27
| Target File | Modification Method | Replacement Details |
|---|---|---|
app/og/docs/[...slug]/route.tsx | replaceAll / replace | Substitutes next/og with @takumi-rs/image-response, updates UI import to fumadocs-ui/og/takumi, and adds format: 'webp' next to image dimensions. |
lib/source.ts | replaceAll | Replaces occurrences of image.png with image.webp. |
next.config.mjs | replace | Inserts serverExternalPackages: ['@takumi-rs/image-response'] into the exported configuration object. |
Sources: packages/create-app/src/plugins/next-use-takumi.ts:31-61
Note
nextConfigExternal locates the const config = { anchor inside next.config.mjs and injects the server external packages array directly beneath it.
Sources: packages/create-app/src/plugins/next-use-takumi.ts:50-60
AST Route Manipulation for Frameworks
TanStack Start Route and Prerender Configuration
Fumadocs utilizes ts-morph to inspect and programmatically manipulate TypeScript and JavaScript configuration files across different frameworks. When configuring project options like Orama Cloud search integrations, plugins modify route files and Vite or React Router configuration files in place.
Sources: packages/create-app/src/transform/index.ts:1-12, packages/create-app/src/transform/tanstack-start.ts:1-3
For TanStack Start templates, the transformation pipeline updates vite.config.ts to register prerendered search paths by analyzing the AST structure of the configuration file.
maininvokesoramaCloudplugin execution during project generation. Sources: packages/create-app/src/plugins/orama-cloud.ts:37-49oramaClouddelegates totanstackStartRouteswith a callback modifying route structures. Sources: packages/create-app/src/plugins/orama-cloud.ts:38-49tanstackStartRoutescallsaddTanstackPrerenderwhenprerenderis set to true on a route option. Sources: packages/create-app/src/transform/index.ts:164-166addTanstackPrerenderevaluatestoItemto format path objects for the prerender configuration array. Sources: packages/create-app/src/transform/tanstack-start.ts:19-21getTanstackStartCalllocates thetanstackStartplugin call expression inside the default export ofvite.config.ts. Sources: packages/create-app/src/transform/tanstack-start.ts:54-69
Sources: packages/create-app/src/plugins/orama-cloud.ts:37-49, packages/create-app/src/transform/index.ts:151-174, packages/create-app/src/transform/tanstack-start.ts:9-49
Caution
If the pages property is missing from the options argument passed to the tanstackStart plugin call, addTanstackPrerender will dynamically add a new pages property containing the target routes.
Sources: packages/create-app/src/transform/tanstack-start.ts:46-48
React Router Route Filtering and Normalization
React Router route updates modify both routes.ts and react-router.config.ts. The reactRouterRoutes wrapper normalizes path arguments by splitting segments and filtering out empty entries before matching or removing elements.
Sources: packages/create-app/src/transform/index.ts:71-116
| Modification Utility | Target File | Action Performed |
|---|---|---|
addRoute | routes.ts | Inserts a new route configuration into the route manifest. |
removeRoute | routes.ts | Deletes the physical route entry file from disk and filters out matching entries. |
filterReactRouterPrerenderArray | react-router.config.ts | Removes paths matching normalized patterns from excluded and paths prerender arrays. |
Sources: packages/create-app/src/transform/index.ts:83-112
Important
When removing a route via removeRoute, path comparison relies on normalizePath which strips empty path segments to prevent mismatch errors caused by leading or trailing slashes.
Sources: packages/create-app/src/transform/index.ts:79-99
Search and Registry Add-On Configuration
Orama Cloud Integration and Sync Script Generation
The oramaCloud template plugin customizes scaffolded projects by injecting third-party search capabilities, adding @orama/core to package dependencies, and appending documentation notes to the project README.
Sources: packages/create-app/src/plugins/orama-cloud.ts:8-29
During the post-writing phase, oramaCloud copies template asset files to the destination and root provider directories, and injects search dialog components via rootProvider.
Sources: packages/create-app/src/plugins/orama-cloud.ts:30-35
It also handles framework-specific route generation for pre-rendered search indices by removing default API search endpoints and creating static JSON index routes across Next.js, TanStack Start, React Router, and Waku frameworks. Sources: packages/create-app/src/plugins/orama-cloud.ts:37-65
| Template Target | Pre-rendered Static JSON Path |
|---|---|
+next+fuma-docs-mdx | .next/server/app/static.json.body |
tanstack-start | .output/public/static.json |
react-router | build/client/static.json |
waku | dist/public/static.json |
Sources: packages/create-app/src/plugins/orama-cloud.ts:67-75
Furthermore, oramaCloud generates a content synchronization script at scripts/sync-content.ts that reads the pre-rendered index and synchronizes records with Orama Cloud.
Sources: packages/create-app/src/plugins/orama-cloud.ts:77-104
Note
The generated sync script automatically appends && bun scripts/sync-content.ts to the project's build script in package.json.
Sources: packages/create-app/src/plugins/orama-cloud.ts:13-16
AI Chat Component Installation and Environment Configuration
The ai plugin supports three distinct LLM providers: openrouter, llmgateway, and inkeep. It initializes component installations using the Fumadocs component installer connected to the remote registry.
Sources: packages/create-app/src/plugins/ai.ts:10-25
| Provider Key | Environment Variable Name |
|---|---|
openrouter | OPENROUTER_API_KEY |
llmgateway | LLM_GATEWAY_API_KEY |
inkeep | INKEEP_API_KEY |
Sources: packages/create-app/src/plugins/ai.ts:10-14
After installing required dependencies, the plugin writes an empty environment variable key to .env.local and invokes addAIChat to inject AI search components into documentation layout files.
Sources: packages/create-app/src/plugins/ai.ts:26-31
Using ts-morph, addAIChat locates the DocsLayout JSX element within layout files across different frameworks and prepends the <AISearch> component tree containing <AISearchPanel> and <AISearchTrigger>.
Sources: packages/create-app/src/plugins/ai.ts:39-87
It then inserts import declarations for AISearch, MessageCircleIcon, cn, and buttonVariants.
Sources: packages/create-app/src/plugins/ai.ts:89-106
Warning
If DocsLayout cannot be matched as an opening JSX element child within the target source file, the AST traversal skips child injection entirely.
Sources: packages/create-app/src/plugins/ai.ts:77-80
Related
On This Page
OverviewInteractive Prompts and Scaffolding ExecutionCLI Configuration and Argument ParsingInteractive Prompt GroupsProject Directory Initialization and Template ExecutionTemplate Sync and Dependency AlignmentTemplate Asset Mirroring and FilteringWorkspace Package Version ReplacementDependency Constants and Version TrackingLinter and Tooling IntegrationsESLint, Biome, and Oxlint PluginsNext.js Takumi Asset PresetsAST Route Manipulation for FrameworksTanStack Start Route and Prerender ConfigurationReact Router Route Filtering and NormalizationSearch and Registry Add-On ConfigurationOrama Cloud Integration and Sync Script GenerationAI Chat Component Installation and Environment ConfigurationRelated