Guides

In-depth guides for configuring, writing, and deploying your documentation.


Configuration

mdsvr can be configured via a settings.json file in the _mdsvr/ directory in your docs root. All settings are optional — the server works out of the box with sensible defaults.

Creating settings.json

Use the CLI to create a starter configuration:

npx mdsvr . --init

Or create it manually:

{
  "$schema": "https://mdsvr.js.org/schema/v2.json",
  "site": {
    "title": "My Docs",
    "description": "Project documentation"
  }
}

Site Settings

{
  "site": {
    "title": "My Documentation",
    "description": "A great documentation site",
    "baseUrl": "https://docs.example.com",
    "language": "en",
    "logo": {
      "src": "./assets/logo.svg",
      "alt": "My Project",
      "href": "/"
    },
    "favicon": "./assets/favicon.ico",
    "author": {
      "name": "Acme Docs Team",
      "url": "https://acme.example"
    }
  }
}
  • author — optional site-wide default author (string or { name, url } object); emitted as <meta name="author"> and JSON-LD author on document pages unless page frontmatter overrides it
  • baseUrl — required for absolute canonical URLs, og:url, exported sitemap.xml, and the robots.txt Sitemap: line; without it those URL-dependent tags/files are omitted rather than emitted as relative URLs

Appearance

Control the look and feel of your documentation:

{
  "appearance": {
    "defaultTheme": "system",
    "allowThemeToggle": true,
    "accentColor": "#0969da",
    "codeTheme": {
      "light": "github",
      "dark": "github-dark"
    }
  }
}

Theme Options:

  • defaultTheme: "light", "dark", or "system" (follows OS preference)
  • allowThemeToggle: Show/hide the theme toggle button in the header
  • accentColor: Primary color for links, buttons, and accents

Configure the sidebar, breadcrumbs, and table of contents:

{
  "navigation": {
    "sidebar": {
      "enabled": true,
      "autoGenerate": true,
      "collapsible": true,
      "defaultOpen": true
    },
    "breadcrumbs": true,
    "prevNextLinks": true,
    "tocEnabled": true,
    "tocMaxDepth": 3
  }
}

Enable and configure the built-in search:

{
  "search": {
    "enabled": true,
    "placeholder": "Search docs...",
    "maxResults": 10
  }
}

Press ⌘+K (or Ctrl+K) to open the search modal.

SEO

Configure search engine optimization features:

{
  "seo": {
    "titleTemplate": "%s | My Docs",
    "defaultImage": "./assets/og-image.png",
    "twitterCard": "summary_large_image",
    "twitterSite": "@myhandle",
    "generateSitemap": true,
    "generateRobotsTxt": true,
    "generateRssFeed": true,
    "generateLlmsTxt": true,
    "structuredData": true,
    "noIndex": false,
    "noIndexDirectoryPages": false,
    "verification": {
      "google": "google-site-verification-token",
      "bing": "msvalidate-token"
    },
    "og": {
      "enabled": true,
      "imageFormat": "jpg",
      "fontFamily": "Inter",
      "colors": {
        "background": "#0a0a0f",
        "text": "#ffffff"
      }
    },
    "rss": {
      "title": "My Docs Updates",
      "feedUrl": "/feed.xml",
      "siteUrl": "https://docs.example.com"
    }
  }
}

When enabled, these endpoints are automatically generated:

  • /sitemap.xml — XML sitemap for search engines (requires site.baseUrl for static export; uses request origin in serve mode)
  • /robots.txt — crawler rules; a custom robots.txt in the docs root takes precedence
  • /feed.xml — RSS feed for blog posts (files with date in frontmatter)
  • /llms.txt + /llms-full.txt — LLM-facing page index and full Markdown corpus; custom files in the docs root take precedence (see SEO)
  • titleTemplate — optional; defaults to %s | <site title> when unset, %s alone when site.title is empty
  • structuredData — emit JSON-LD Article/WebSite/BreadcrumbList per page (default true)
  • noIndexDirectoryPages — emit noindex, nofollow on auto-generated directory listing pages and exclude them from the sitemap (default false)
  • verification — emit site-verification meta tags; keys: google, bing, yandex, pinterest, naver. (For Google you can also drop the googleXXXX.html file into the docs root — it is served/exported as-is.)
  • Page frontmatter: seoTitle, noindex/noIndex, datePublished, dateModified — see SEO

Analytics

Emit analytics snippets in <head> (and the GTM <noscript> right after <body>) on every page, in both serve and export modes:

{
  "analytics": {
    "googleAnalytics": "G-XXXXXXXXXX",
    "googleTagManager": "GTM-XXXXXXX",
    "clarity": "clarity-project-id",
    "plausible": {
      "domain": "docs.example.com",
      "scriptSrc": "https://plausible.io/js/script.js"
    },
    "umami": {
      "websiteId": "xxxx-xxxx",
      "scriptSrc": "https://cloud.umami.is/script.js"
    },
    "customHead": ["<script>/* any provider snippet */</script>"]
  }
}

All fields are optional — nothing is emitted when absent. Malformed GA/GTM IDs log a warning at startup but never fail. customHead entries are appended verbatim to the end of <head>; they come from your own settings.json, so only add snippets you trust. Note that local preview also fires analytics hits — filter the preview origin on the provider side if needed.

MDX

Enable MDX support and configure available components:

{
  "mdx": {
    "enabled": true,
    "components": {
      "Callout": true,
      "CodeGroup": true,
      "Steps": true,
      "Card": true,
      "CardGroup": true,
      "Tabs": true,
      "Accordion": true,
      "Badge": true,
      "Mermaid": true,
      "Quiz": true
    }
  }
}

Files

Control which files are served, blocked, or hidden:

{
  "files": {
    "extensions": {
      "serve": [".md", ".mdx", ".png", ".jpg", ".svg", ".css", ".js", ".html"],
      "block": [".env", ".key", ".pem"],
      "hidden": ["settings.json", ".git", "node_modules"]
    },
    "indexFiles": ["README.md", "index.md", "index.html"],
    "ignorePatterns": ["**/node_modules/**", "**/.git/**"],
    "staticFolders": ["assets", "public", "images"]
  }
}
  • Files and folders starting with _ or . are automatically hidden
  • Only web-servable extensions in extensions.serve are copied during static export
  • robots.txt is generated or copied when seo.generateRobotsTxt is enabled
  • llms.txt/llms-full.txt are generated or copied when seo.generateLlmsTxt is enabled
  • Blocked extensions return 403 Forbidden
  • Hidden files return 404 Not Found (as if they don’t exist)
  • staticFolders: Array of folder names to serve as static assets (all files in these folders are served directly)

Customize the site footer:

{
  "footer": {
    "text": "Built with mdsvr",
    "links": [
      { "label": "GitHub", "href": "https://github.com/..." },
      { "label": "Twitter", "href": "https://twitter.com/..." }
    ]
  }
}

Hot Reload

Settings are automatically reloaded when _mdsvr/settings.json changes (except when using --no-watch flag). You’ll see a message in the console:

[mdsvr] Settings reloaded

Validation

Validate your settings.json:

npx mdsvr . --validate

This will check your configuration and report any errors.

Generate Settings

Configure static export generation options:

{
  "generate": {
    "basePath": "/docs",
    "outputDir": "dist"
  }
}

Options:

  • basePath: Base path for generated URLs (default: ""). Useful when deploying to a subdirectory (e.g., /docs)
  • outputDir: Default output directory for static exports (default: "dist")

These settings are used during static export with the --export flag.

Complete Example

Here’s a complete _mdsvr/settings.json with all available options:

{
  "$schema": "https://mdsvr.js.org/schema/v2.json",
  "site": {
    "title": "My Docs",
    "description": "Project documentation",
    "baseUrl": "https://docs.example.com",
    "language": "en",
    "logo": {
      "src": "./assets/logo.svg",
      "alt": "My Project",
      "href": "/"
    },
    "favicon": "./assets/favicon.ico"
  },
  "appearance": {
    "defaultTheme": "system",
    "allowThemeToggle": true,
    "accentColor": "#0969da"
  },
  "navigation": {
    "sidebar": {
      "enabled": true,
      "autoGenerate": true,
      "collapsible": true,
      "defaultOpen": true
    },
    "breadcrumbs": true,
    "prevNextLinks": true,
    "tocEnabled": true,
    "tocMaxDepth": 3
  },
  "search": {
    "enabled": true,
    "placeholder": "Search docs...",
    "maxResults": 10
  },
  "seo": {
    "titleTemplate": "%s | My Docs",
    "defaultImage": "./assets/og-default.png",
    "twitterCard": "summary_large_image",
    "generateSitemap": true,
    "generateRobotsTxt": true,
    "generateRssFeed": false,
    "og": {
      "enabled": true,
      "imageFormat": "jpg"
    }
  },
  "files": {
    "extensions": {
      "serve": [
        ".md",
        ".mdx",
        ".pdf",
        ".png",
        ".jpg",
        ".svg",
        ".css",
        ".js",
        ".html"
      ],
      "block": [".env", ".key", ".pem", ".p12"],
      "hidden": ["_mdsvr", "settings.json", ".git", "node_modules", ".DS_Store"]
    },
    "indexFiles": ["README.md", "index.md", "INDEX.md"]
  },
  "mdx": {
    "enabled": true,
    "components": {
      "Callout": true,
      "CodeGroup": true,
      "Steps": true,
      "Card": true,
      "CardGroup": true,
      "Tabs": true,
      "Accordion": true,
      "Badge": true,
      "Mermaid": true,
      "Quiz": true
    }
  },
  "footer": {
    "text": "Built with mdsvr",
    "links": [{ "label": "GitHub", "href": "https://github.com/..." }]
  }
}

Writing Content

Tips and best practices for creating great documentation.

File Organization

docs/
├── README.md              # Homepage
├── 01-getting-started/    # Getting started guides
├── 02-settings/           # Settings & configuration
├── 03-features/           # Feature documentation
└── 04-reference/          # API/reference docs

Frontmatter

Every page should have frontmatter:

---
title: Page Title
description: Brief description for SEO
---

Optional fields:

---
title: Page Title
description: Description
date: 2025-01-15 # For RSS feed / article datePublished
author: John Doe # Attribution (or { name, url })
tags: [setup, guide] # For organization
seoTitle: Title Override # Wins over `title` for SEO output
datePublished: 2025-01-15 # Article published date
dateModified: 2025-01-20 # Article last update (also sitemap lastmod)
noindex: true # Exclude from search + sitemap (noIndex also works)
---

Markdown Tips

Use Descriptive Headings

<!-- Good -->

## Installing via npm

<!-- Less clear -->

## Installation

Use relative paths:

See [configuration guide](./)

Code Blocks

Always specify language:

```javascript
const x = 1;
```

MDX Components

Use components for interactive content:

<Callout type="warning">Important information here</Callout>

<Steps>
  ### Step 1
  First step content

### Step 2

Second step content

</Steps>

See MDX Components for all available components.

Images & Assets

Place assets in docs/assets/:

![Alt text](../assets/screenshot.png)

Note: The assets folder is optional - create it if you need to include images in your documentation.

Best Practices

Follow these guidelines to ensure your documentation passes validation and works correctly in both server mode and static export.

File Naming

  • Avoid dots in filenames — Don’t use dots in the middle of filenames (except for the extension)
    • ❌ 0.1.quickstart.md or config.v2.md
    • ✅ 01-quickstart.md or config-v2.md
  • Use descriptive names — quickstart.md not page1.md
  • Use kebab-case — Separate words with hyphens for clean URLs
  • Numbered prefixes — Use 01-, 02- for ordering in sidebar
  • Use relative paths — Never use absolute paths starting with /
    • ❌ [Guide](/docs/guide)
    • ✅ [Guide](./guide.md) or [Guide](../guide.md)
  • Include file extensions — .md/.mdx links resolve on GitHub and in IDE previews; mdsvr converts them to clean URLs when serving (with generate.cleanUrls) and in static export
    • ❌ [Setup](./setup)
    • ✅ [Setup](./setup.md)
  • Link to index files explicitly — Point at README.md so links work on GitHub; directory links are converted to clean directory URLs
    • ❌ [Getting Started](./01-getting-started)
    • ✅ [Getting Started](./01-getting-started/)
  • Validate anchors — Ensure #section links match actual headings

Heading Structure

  • Always include an H1 — Every page must have a top-level heading (# Title)
  • Maintain hierarchy — Don’t skip heading levels
    • ❌ H1 → H3 (skips H2)
    • ✅ H1 → H2 → H3
  • Use descriptive headings — Make them searchable and meaningful

Assets & Images

  • Use relative paths for assets — Same rules as links
    • ❌ ![Screenshot](/assets/screenshot.png)
    • ✅ ![Screenshot](../assets/screenshot.png)
  • Ensure files exist — Validator checks for missing images and assets
  • Use supported formats — PNG, JPG, SVG, WebP, GIF, ICO

General Guidelines

  1. Start with a README.md — Required for homepage
  2. Keep pages focused — One topic per page
  3. Link liberally — Help users navigate between related topics
  4. Use MDX components — Callouts for warnings, Steps for procedures
  5. Add frontmatter — Include title and description for SEO
  6. Test with validator — Run npx mdsvr . --validate to check your docs

Validation

Run the validator to check your documentation:

npx mdsvr ./docs --validate

The validator checks for:

  • 🔗 Broken internal links
  • ⚠️ Absolute paths (should use relative)
  • ⚠️ Links with .md/.mdx extensions
  • ⚠️ Links to index files (use directory paths)
  • 🔗 Broken anchor links
  • 🖼️ Missing assets
  • 📁 Filenames with dots in the middle
  • 📌 Missing H1 headings
  • 📝 Heading hierarchy violations

Use --autofix to automatically fix some issues:

npx mdsvr ./docs --validate --autofix

Deployment

Deploy your documentation to any static hosting platform.

Export Static Site

Generate static HTML files:

npx mdsvr ./docs --export

This creates _html/public/ with your static site.

Custom Output

npx mdsvr ./docs --export ./dist

Export Caching

mdsvr uses intelligent caching to speed up exports:

  • File-level caching: Only re-renders pages whose source files changed
  • OG image caching: Only regenerates OG images when content changes
  • Settings tracking: Detects when _mdsvr/settings.json changes and forces full re-export

The export state is stored in _mdsvr/export-state.json.

Force Full Export

To bypass cache and force a complete re-export:

# Delete export state only
rm _mdsvr/export-state.json

# Then export
npx mdsvr ./docs --export

Note: Only delete export-state.json, not the entire _mdsvr directory, to preserve your settings.

Firebase Hosting

# Export
npx mdsvr ./docs --export

# Deploy
cd _html
firebase init
firebase deploy

Netlify

Via CLI

npx mdsvr ./docs --export
netlify deploy --dir=_html/public --prod

Via Git

  1. Push docs to GitHub
  2. Connect repo to Netlify
  3. Set build command: npx mdsvr . --export _html/public
  4. Publish directory: _html/public

Vercel

# vercel.json
{
  "buildCommand": "npx mdsvr . --export _html/public",
  "outputDirectory": "_html/public"
}

GitHub Pages

# Export to docs folder
npx mdsvr ./src --export ./docs

# Or use GitHub Actions

GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npx mdsr . --export
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./_html/public

Docker

Using Pre-built Image

docker pull ghcr.io/satoshiman/mdsvr:latest
docker run -d \
  -p 1800:1800 \
  -v /path/to/docs:/app/docs:ro \
  ghcr.io/satoshiman/mdsvr:latest

Note: Mount your docs read-only (:ro) where possible. mdsvr renders raw HTML in Markdown and executes MDX in the server process — it is designed for trusted content, not as a sandbox for user-supplied documents.

Build Your Own

FROM ghcr.io/satoshiman/mdsvr:latest
COPY ./docs /app/docs
docker build -t my-docs .
docker run -d -p 1800:1800 my-docs

VPS / Self-Hosted

Export and serve with any web server:

# Export
npx mdsvr ./docs --export /var/www/docs

# Nginx config
server {
    listen 80;
    root /var/www/docs;
    try_files $uri $uri/ $uri.html =404;
}

Clean URLs

Exported sites use clean URLs:

  • /guide/setup/ instead of /guide/setup.html
  • Works on all modern static hosts