Configuration

mkdnsite is configured via a mkdnsite.config.ts file at your project root. All options can also be set via CLI flags — the two are equivalent.

Config file

Create mkdnsite.config.ts in your project root:

import type { MkdnSiteConfig } from 'mkdnsite'

const config: Partial<MkdnSiteConfig> = {
  site: {
    title: 'My Docs',
    description: 'Documentation for my project.',
    url: 'https://docs.example.com'
  },
  theme: {
    colorScheme: 'system',
    colors: {
      accent: '#7c3aed'
    }
  }
}

export default config

When you run mkdnsite, it auto-detects mkdnsite.config.ts in the current working directory. Use --config <path> to specify a different file.

Top-level options

OptionTypeDefaultDescription
contentDirstring'./content'Path to the directory containing .md files
staticDirstring—Directory for static assets (images, CSS, etc.) served at /
githubGitHubSourceConfig—Serve a GitHub repository instead of a local directory
preset'docs' | 'blog'—Apply sensible defaults for a docs or blog use case
renderer'portable' | 'bun-native''portable'Markdown renderer engine

contentDir

contentDir: './docs'  // serve from ./docs instead of ./content

Also settable with: mkdnsite ./docs (positional argument)

staticDir

staticDir: './static'  // serve ./static/logo.png at /logo.png

Also settable with: --static <dir>

staticHandler (programmatic API only)

A callback for serving static assets in non-filesystem deployments (Cloudflare Workers + R2, Vercel Edge, Deno Deploy). When set, it is called instead of the built-in filesystem serveStatic for requests with static file extensions.

import { createHandler, resolveConfig } from 'mkdnsite'

const handler = createHandler({
  source,
  renderer,
  config,
  // Read static files from Cloudflare R2
  staticHandler: async (pathname) => {
    const key = pathname.replace(/^\//, '')
    const obj = await env.STATIC_BUCKET.get(key)
    if (obj == null) return null  // fall through to built-in serveStatic or 404
    return new Response(await obj.text(), {
      headers: { 'Content-Type': 'image/png' }
    })
  }
})

Behaviour: return a Response to serve it directly, or null to fall through to the built-in serveStatic (if staticDir is set) or ultimately a 404.

This option is programmatic-only — it accepts a function and cannot be set from a config file or CLI flag. Use staticDir for local/filesystem deployments.

If you are using the CloudflareAdapter, call adapter.createStaticHandler() to get a pre-built R2-backed handler:

const handler = createHandler({
  source: adapter.createContentSource(config),
  renderer: await adapter.createRenderer(config),
  config,
  staticHandler: adapter.createStaticHandler()  // reads from STATIC_BUCKET R2 binding
})

include

Glob patterns that limit which .md files are served. Only files whose path (relative to contentDir) matches at least one pattern will be served. Mutually exclusive with exclude.

// Only serve files under docs/ and guides/
include: ['docs/**', 'guides/**']

Also settable with: --include <pattern> (repeatable)

mkdnsite --include 'docs/**' --include 'guides/**'

exclude

Glob patterns for files to omit. Matching files are not served. Mutually exclusive with include.

// Hide draft files and the private/ directory
exclude: ['**/*.draft.md', 'private/**']

Also settable with: --exclude <pattern> (repeatable)

mkdnsite --exclude '**/*.draft.md' --exclude 'private/**'

Note: include and exclude are mutually exclusive — define one or the other, not both. Patterns use picomatch glob syntax.

Both options work with FilesystemSource (local dev) and GitHubSource (hosted). Patterns are matched against the path relative to contentDir or github.path.

github

Serve content from a public GitHub repository instead of a local directory. When set, contentDir is ignored.

github: {
  owner: 'mkdnsite',   // GitHub user or org
  repo: 'mkdnsite',    // repository name
  ref: 'main',         // branch, tag, or commit SHA (default: 'main')
  path: 'content',     // subdirectory within the repo (optional)
  token: process.env.GITHUB_TOKEN  // for private repos or higher rate limits
}
OptionTypeDefaultDescription
ownerstring—GitHub user or organization name
repostring—Repository name
refstring'main'Branch, tag, or commit SHA to serve
pathstring—Subdirectory within the repo to treat as content root
tokenstring—GitHub personal access token (increases API rate limit from 60 to 5,000 req/hr). Also reads GITHUB_TOKEN or MKDNSITE_GITHUB_TOKEN env var when not set explicitly.

Caching: File contents and the repository tree are cached for 5 minutes. Call source.refresh() to invalidate (done automatically in watch mode). On first request, all .md files are fetched in parallel and cached together.

Rate limits: The GitHub Git Trees API (file listing) counts toward your rate limit — one call per 5-minute window. File content is fetched from raw.githubusercontent.com which has generous limits.

CLI flags: --github <owner/repo>, --github-ref, --github-path, --github-token


preset

Apply a preset to get sensible defaults for a common use case without per-field configuration. User values always win — the preset is applied first and any explicit setting overrides it.

Merge order: DEFAULT_CONFIG → preset → user config

PresetEffect
'docs'Enables showNav, showToc, prevNext. Disables pageTitle, pageDate, readingTime. Best for documentation sites.
'blog'Enables pageTitle, pageDate, readingTime, prevNext. Disables showNav, showToc. Best for blog posts.
preset: 'blog'  // then override individual options as needed

CLI flag: --preset <name>


renderer

  • 'portable' — uses react-markdown + remark/rehype plugins. Works on Bun, Node, and Deno. Full GFM support.
  • 'bun-native' — uses Bun.markdown.react(). Faster, but Bun only. Does not fully support GFM — task list checkboxes are stripped, and other GFM features may behave differently. Use this when you need raw speed and don't rely on full GFM rendering.

Also settable with: --renderer <engine>


site

Site metadata used in <title>, <meta>, and /llms.txt.

site: {
  title: 'My Docs',
  description: 'Documentation for my project.',
  url: 'https://docs.example.com',
  lang: 'en'
}
OptionTypeDefaultDescription
titlestring'mkdnsite'Site name, appears in <title> as Page — Site
descriptionstring—Meta description. Also used as the fallback description in /llms.txt when llmsTxt.description is not set.
urlstring—Base URL for absolute links and sitemaps
langstring'en'HTML lang attribute
ogOgConfig—OpenGraph / social sharing configuration (see below)
favicon{ src: string }—Favicon path or URL (.ico, .png, .svg). src is the URL path or absolute URL.

CLI flags: --title, --url, --favicon <path>

site.og — OpenGraph / social meta tags

Controls the og:* and twitter:* meta tags that generate rich preview cards in Slack, Discord, iMessage, and social platforms.

site: {
  title: 'My Docs',
  url: 'https://docs.example.com',
  og: {
    image: 'https://docs.example.com/og-image.png',
    type: 'website',
    twitterCard: 'summary_large_image',
    twitterSite: '@myproject'
  }
}
OptionTypeDefaultDescription
og.imagestring—Default OG image URL. Override per-page via og_image frontmatter.
og.typestring'website' for /, 'article' for other pagesDefault og:type value
og.twitterCard'summary' | 'summary_large_image''summary'Twitter card display style
og.twitterSitestring—Twitter @handle for the site

Tags always emitted: og:title, og:type, og:site_name, twitter:card, twitter:title

Tags emitted when data is available: og:description, og:url (requires site.url), og:image, twitter:description, twitter:image, twitter:site

CLI flags: --og-image, --og-type, --twitter-card, --twitter-site


server

Local development server settings. Not used in edge/serverless deployments.

server: {
  port: 3000,
  hostname: '0.0.0.0'
}
OptionTypeDefaultDescription
portnumber3000Port to listen on
hostnamestring'0.0.0.0'Hostname to bind to

CLI flags: -p, --port


theme

Controls visual presentation. All theme options work with the built-in prose mode.

theme: {
  mode: 'prose',
  colorScheme: 'system',
  colors: {
    accent: '#7c3aed'
  },
  colorsDark: {
    accent: '#a78bfa'
  },
  logo: {
    src: '/logo.svg',
    alt: 'My Project'
  },
  logoText: 'My Project'
}

mode

ValueDescription
'prose'Built-in shadcn/Radix-inspired typography (default)
'components'Full React component overrides — you style everything

CLI flag: --theme-mode

colorScheme

ValueDescription
'system'Follow OS preference (default)
'light'Always light mode
'dark'Always dark mode

CLI flag: --color-scheme

colors and colorsDark

Override CSS variables for light and dark modes. See Theming for full details.

colors: {
  accent: '#7c3aed',     // --mkdn-accent
  text: '#1c1917',       // --mkdn-text
  textMuted: '#78716c',  // --mkdn-text-muted
  bg: '#ffffff',         // --mkdn-bg
  bgAlt: '#fafaf9',      // --mkdn-bg-alt
  border: '#e7e5e4',     // --mkdn-border
  link: '#7c3aed',       // --mkdn-link
  linkHover: '#6d28d9',  // --mkdn-link-hover
  codeBg: 'rgba(124,58,237,0.08)', // --mkdn-code-bg
  preBg: '#fafaf9'       // --mkdn-pre-bg
}

CLI flag: --accent (shortcut for colors.accent)

fonts

fonts: {
  body: '"Inter", sans-serif',           // --mkdn-font
  mono: '"JetBrains Mono", monospace',   // --mkdn-mono
  heading: '"Playfair Display", serif'   // --mkdn-font-heading
}

CLI flags: --font-body, --font-mono, --font-heading

logo and logoText

Displayed in the nav sidebar header.

logo: {
  src: '/logo.png',   // URL path (served from staticDir)
  alt: 'My Project',
  width: 32,          // pixels, default 32
  height: 32          // pixels, default 32
},
logoText: 'My Project'

CLI flags: --logo <url-path>, --logo-text <text>

customCss and customCssUrl

// Inline CSS appended after built-in styles
customCss: '.mkdn-main { max-width: 1000px; }',

// External stylesheet loaded via <link rel="stylesheet">
customCssUrl: '/my-theme.css'

CLI flags: --custom-css <css>, --custom-css-url <url>

builtinCss

Set to false to strip all built-in styles and start from scratch. Useful when you want to provide a completely custom stylesheet.

builtinCss: false,
customCssUrl: '/my-complete-theme.css'

CLI flag: --no-builtin-css

Other theme options

OptionTypeDefaultCLI flag(s)Description
showNavbooleantrue--no-navShow the navigation sidebar
showTocbooleantrue--no-tocShow table of contents sidebar
showFooterbooleantrue--no-footerShow "Powered by mkdnsite" footer
pageTitlebooleanfalse--page-title / --no-page-titleRender frontmatter title as <h1> above content
pageDatebooleanfalse--page-date / --no-page-dateRender date/updated from frontmatter below page title
readingTimebooleanfalse--reading-time / --no-reading-timeShow estimated reading time (238 wpm)
prevNextbooleanfalse--prev-next / --no-prev-nextShow prev/next page navigation links at bottom
editUrlstring——Edit URL template, e.g. https://github.com/org/repo/edit/main/{path} (Planned — not yet implemented)
syntaxThemestring'github-light'—Shiki light mode syntax theme
syntaxThemeDarkstring'github-dark'—Shiki dark mode syntax theme

negotiation

Controls HTTP content negotiation behavior.

negotiation: {
  enabled: true,
  includeTokenCount: true,
  contentSignals: {
    aiTrain: 'yes',
    search: 'yes',
    aiInput: 'yes'
  }
}
OptionTypeDefaultDescription
enabledbooleantrueEnable serving raw Markdown via Accept: text/markdown
includeTokenCountbooleantrueInclude x-markdown-tokens header
contentSignals.aiTrain'yes' | 'no''yes'Allow AI training on this content
contentSignals.search'yes' | 'no''yes'Allow search indexing
contentSignals.aiInput'yes' | 'no''yes'Allow use as AI context

CLI flag: --no-negotiate

See Content Negotiation for details.


llmsTxt

Auto-generates /llms.txt — a standard index for AI agents to discover your content.

llmsTxt: {
  enabled: true,
  description: 'Documentation for my project.',
  sections: {
    // Keys are the first path segment of each page's URL (i.e. the top-level
    // directory name). Values are the section heading to use in /llms.txt.
    // Without this override, section headings are derived from the directory
    // name via title-case (e.g. 'docs' → 'Docs').
    'docs': 'API Reference',   // /docs/* pages → "## API Reference"
    'guides': 'Tutorials'      // /guides/* pages → "## Tutorials"
  }
}
OptionTypeDefaultDescription
enabledbooleantrueGenerate /llms.txt
descriptionstring—Description line in the file. Falls back to site.description when not set.
sectionsRecord<string, string>—Map of top-level directory names (URL path segments) to custom section titles in /llms.txt. Keys must match the first segment of page URLs (e.g. 'docs' for /docs/*). Values are the ## heading label used in the output.

CLI flag: --no-llms-txt


client

Client-side JavaScript enhancements. Disable any combination as needed.

client: {
  enabled: true,      // master switch
  themeToggle: true,  // light/dark toggle button
  math: true,         // KaTeX math rendering
  mermaid: true,      // Mermaid diagram rendering
  copyButton: true,   // copy-to-clipboard on code blocks
  search: true        // ⌘K search modal + /api/search endpoint
}
OptionDefaultCLI flag
enabledtrue--no-client-js
themeToggletrue--no-theme-toggle
mathtrue--no-math
mermaidtrue—
copyButtontrue—
searchtrue--no-search
chartstrue--no-charts
syntaxHighlight'client'--syntax-highlight <mode> / --no-syntax-highlight

client.search

When true (default), enables:

  • ⌘K modal — press ⌘K (Mac) or Ctrl+K (Windows/Linux) to open a search modal. Also accessible via the magnifying glass button in the top-right corner.
  • /api/search endpoint — GET /api/search?q=<query>&limit=<n> returns a JSON array of SearchResult objects.
  • Result highlighting — navigating to a page via a search result appends ?q=<query> to the URL. On load, matched terms are highlighted in the page body and the page scrolls to the first match. Highlights fade after 8 seconds.

The search index is built server-side using TF-IDF with 3× boost for title, 2× for description and tags. See the search guide for details.

client.syntaxHighlight

Controls the syntax highlighting engine for code blocks.

ValueDescription
'client'Load Prism.js from CDN at runtime (default, works everywhere)
'server'Use Shiki for SSR highlighting (WASM — not supported in CF Workers)
falseDisable syntax highlighting entirely
client: {
  syntaxHighlight: 'server'  // SSR highlighting via Shiki
}

CLI flags: --syntax-highlight <mode>, --no-syntax-highlight

Note: --no-client-js disables everything. Individual --no-* flags disable specific features while leaving others enabled.


mcp

Built-in MCP (Model Context Protocol) server for AI agent access to your documentation.

mcp: {
  enabled: true,
  endpoint: '/mcp'   // MCP server URL path
}
OptionTypeDefaultDescription
enabledbooleantrueEnable the built-in MCP server
endpointstring'/mcp'URL path for the MCP endpoint

When enabled, AI clients (Claude Desktop, Cursor, etc.) can connect at http://localhost:3000/mcp and use the built-in tools: search_docs, get_page, list_pages, and get_nav.

CLI flags: --no-mcp, --mcp-endpoint <path>

See the MCP guide for full details.


analytics

Analytics configuration for tracking traffic and usage.

analytics: {
  googleAnalytics: {
    measurementId: 'G-XXXXXXXXXX'  // GA4 measurement ID
  },
  traffic: {
    enabled: false,   // enable server-side traffic logging
    console: false    // log events as JSON lines to stdout
  }
}

analytics.googleAnalytics

OptionTypeDescription
measurementIdstringGoogle Analytics 4 measurement ID (e.g. G-XXXXXXXXXX)

When set, the GA4 tracking script is injected into HTML pages automatically.

analytics.traffic

Server-side traffic analytics for classifying and logging requests.

OptionTypeDefaultDescription
enabledbooleanfalseEnable server-side traffic analytics
consolebooleanfalseLog each request as a JSON line to stdout

When enabled, each request is classified as human, ai_agent, bot, or mcp based on the User-Agent and request characteristics.

CLI flags: --ga-measurement-id <id>, --traffic-analytics, --traffic-console


csp

Content Security Policy configuration. When enabled, mkdnsite adds a Content-Security-Policy header to all HTML responses.

csp: {
  enabled: true,          // default: true
  extraScriptSrc: [],     // additional script-src sources
  extraStyleSrc: [],      // additional style-src sources
  extraImgSrc: [],        // additional img-src sources
  extraConnectSrc: [],    // additional connect-src sources
  extraFontSrc: [],       // additional font-src sources
  reportUri: undefined    // CSP violation report URI
}
OptionTypeDefaultDescription
enabledbooleantrueEnable the CSP header on HTML responses
extraScriptSrcstring[][]Additional script-src sources (e.g. analytics domains)
extraStyleSrcstring[][]Additional style-src sources (e.g. Google Fonts)
extraImgSrcstring[][]Additional img-src sources
extraConnectSrcstring[][]Additional connect-src sources
extraFontSrcstring[][]Additional font-src sources
reportUristring—URI for CSP violation reports

The default policy allows inline styles (required for Shiki syntax highlighting), same-origin resources, and CDN domains used by client-side features (Mermaid, KaTeX, Chart.js). Use the extra* arrays to whitelist additional domains.

CLI flag: --no-csp


cache

Response caching and CDN header configuration. Opt-in — disabled by default.

cache: {
  enabled: false,              // opt-in
  maxAge: 300,                 // Cache-Control max-age for HTML (seconds)
  maxAgeMarkdown: 300,         // Cache-Control max-age for markdown
  staleWhileRevalidate: 0,     // stale-while-revalidate (0 = omitted)
  versionTag: undefined        // version tag for ETag / cache busting
}
OptionTypeDefaultDescription
enabledbooleanfalseEnable response caching
maxAgenumber300Cache-Control max-age in seconds for HTML responses
maxAgeMarkdownnumber300Cache-Control max-age in seconds for markdown responses
staleWhileRevalidatenumber0stale-while-revalidate in seconds (0 means omitted)
versionTagstring—Version tag for ETag header and CDN cache busting (e.g. 'v1.2.3' or a git SHA)

When enabled locally, uses an in-memory cache (MemoryResponseCache). The CloudflareAdapter uses a KV-backed cache (KVResponseCache) when a CACHE_KV binding is present.

CLI flags: --cache, --no-cache, --cache-max-age <seconds>, --cache-max-age-markdown <seconds>, --cache-swr <seconds>, --cache-version <tag>


Complete example

import type { MkdnSiteConfig } from 'mkdnsite'

const config: Partial<MkdnSiteConfig> = {
  contentDir: './content',
  staticDir: './static',

  site: {
    title: 'My Project Docs',
    description: 'Comprehensive documentation for My Project.',
    url: 'https://docs.myproject.com',
    lang: 'en'
  },

  server: {
    port: 4000,
    hostname: 'localhost'
  },

  preset: 'docs',

  theme: {
    mode: 'prose',
    colorScheme: 'system',
    logo: {
      src: '/logo.svg',
      alt: 'My Project',
      width: 32,
      height: 32
    },
    logoText: 'My Project',
    colors: {
      accent: '#7c3aed',
      link: '#7c3aed'
    },
    colorsDark: {
      accent: '#a78bfa',
      link: '#a78bfa'
    },
    syntaxTheme: 'github-light',
    syntaxThemeDark: 'github-dark',
    showNav: true,
    showToc: true,
    prevNext: true
    // editUrl: 'https://github.com/myorg/myproject/edit/main/content/{path}'
    // ↑ Planned — not yet implemented
  },

  negotiation: {
    enabled: true,
    includeTokenCount: true,
    contentSignals: {
      aiTrain: 'yes',
      search: 'yes',
      aiInput: 'yes'
    }
  },

  llmsTxt: {
    enabled: true,
    description: 'Comprehensive documentation for My Project.'
  },

  client: {
    enabled: true,
    themeToggle: true,
    math: true,
    mermaid: true,
    copyButton: true,
    search: true
  },

  renderer: 'portable',

  analytics: {
    googleAnalytics: {
      measurementId: 'G-XXXXXXXXXX'
    }
  },

  csp: {
    enabled: true,
    extraScriptSrc: ['https://www.googletagmanager.com']
  },

  cache: {
    enabled: false
  }
}

export default config