MkDocs Architecture - How MkDocs Works

mkdocs
architecture
python
documentation
Deep dive into MkDocs’ core architecture, build pipeline, plugin system, and rendering process
Author

Dario Airoldi

Published

December 16, 2025

Modified

December 29, 2025

MkDocs Architecture - How MkDocs Works

📋 Table of Contents


📖 Overview

MkDocs is a Python-based static site generator designed specifically for creating project documentation.
Understanding its architecture helps developers:

  • Customize themes and templates effectively
  • Extend functionality through plugins
  • Optimize build performance for large documentation sites
  • Troubleshoot issues in the build process

Key Architectural Principles:

Principle Description
Static Generation Generates pure HTML/CSS/JS—no server-side processing required
Markdown-First Content authored in Markdown, converted via Python-Markdown
Configuration-Driven Single YAML file controls all behavior
Event-Based Plugins Extensible through well-defined lifecycle events
Template-Based Theming Jinja2 templates power all HTML generation

🏗️ Core Architecture

High-Level Architecture

MkDocs follows a straightforward pipeline architecture:

┌─────────────────────────────────────────────────────────────────┐
│                        MkDocs Build Process                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│   ┌─────────┐    ┌─────────────┐    ┌─────────────┐             │
│   │mkdocs.yml│───▶│Configuration│───▶│  Validation │             │
│   │         │    │   Parser    │    │   Engine    │             │
│   └─────────┘    └─────────────┘    └──────┬──────┘             │
│                                            │                      │
│                                            ▼                      │
│   ┌─────────┐    ┌─────────────┐    ┌─────────────┐             │
│   │  docs/  │───▶│    File     │───▶│  Navigation │             │
│   │  *.md   │    │  Discovery  │    │   Builder   │             │
│   └─────────┘    └─────────────┘    └──────┬──────┘             │
│                                            │                      │
│                                            ▼                      │
│   ┌─────────┐    ┌─────────────┐    ┌─────────────┐             │
│   │ Plugins │───▶│  Markdown   │───▶│   Template  │             │
│   │         │    │  Rendering  │    │   Engine    │             │
│   └─────────┘    └─────────────┘    └──────┬──────┘             │
│                                            │                      │
│                                            ▼                      │
│                                     ┌─────────────┐             │
│                                     │   site/     │             │
│                                     │   Output    │             │
│                                     └─────────────┘             │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Core Components

MkDocs consists of several interconnected components:

1. Configuration System

The configuration system parses mkdocs.yml and validates all settings:

# Internal configuration structure (simplified)
class MkDocsConfig:
    site_name: str           # Required
    site_url: str | None     # Optional
    nav: list | None         # Navigation structure
    theme: ThemeConfig       # Theme configuration
    plugins: PluginCollection # Enabled plugins
    markdown_extensions: list # Markdown extensions
    docs_dir: str            # Source directory
    site_dir: str            # Output directory

2. File System

The Files collection manages all documentation files:

# File structure representation
class File:
    src_path: str        # Path relative to docs_dir
    dest_path: str       # Path relative to site_dir
    url: str             # URL for the file
    name: str            # Filename without extension
    abs_src_path: str    # Absolute source path
    abs_dest_path: str   # Absolute destination path

4. Template System

Jinja2 templates render the final HTML:

# Template context structure
class TemplateContext:
    config: MkDocsConfig      # Configuration object
    nav: Navigation           # Navigation structure
    page: Page | None         # Current page (if rendering a page)
    base_url: str             # Relative path to site root
    mkdocs_version: str       # MkDocs version
    build_date_utc: datetime  # Build timestamp

⚙️ Build Pipeline

Build Phases

The build process consists of several distinct phases:

graph TD
    A[Start Build] --> B[Load Configuration]
    B --> C[Initialize Plugins]
    C --> D[Discover Files]
    D --> E[Build Navigation]
    E --> F[Initialize Jinja Environment]
    F --> G[Render Templates]
    G --> H[Render Pages]
    H --> I[Copy Static Files]
    I --> J[Post-Build Cleanup]
    J --> K[Build Complete]
    
    C -->|on_config| C1[Plugin Hook]
    D -->|on_files| D1[Plugin Hook]
    E -->|on_nav| E1[Plugin Hook]
    F -->|on_env| F1[Plugin Hook]
    H -->|on_page_*| H1[Plugin Hooks]
    J -->|on_post_build| J1[Plugin Hook]

Phase 1: Configuration Loading

# Configuration loading process
def load_config(config_file):
    # 1. Read YAML file
    raw_config = yaml.load(config_file)
    
    # 2. Apply configuration inheritance (INHERIT key)
    if 'INHERIT' in raw_config:
        parent = load_config(raw_config['INHERIT'])
        raw_config = deep_merge(parent, raw_config)
    
    # 3. Validate all options
    config = MkDocsConfig(**raw_config)
    config.validate()
    
    # 4. Trigger on_config event for plugins
    config = plugins.run_event('on_config', config)
    
    return config

Phase 2: File Discovery

MkDocs discovers all files in the docs_dir:

# File discovery process
def get_files(config):
    files = Files([])
    
    for path in walk(config.docs_dir):
        # Skip excluded patterns
        if matches_exclude(path, config.exclude_docs):
            continue
            
        # Create File object
        file = File(
            path=path,
            docs_dir=config.docs_dir,
            site_dir=config.site_dir,
            use_directory_urls=config.use_directory_urls
        )
        files.append(file)
    
    # Trigger on_files event
    files = plugins.run_event('on_files', files, config=config)
    
    return files

Phase 3: Navigation Building

Navigation is constructed from configuration or auto-generated:

# Navigation building
def get_navigation(files, config):
    if config.nav:
        # Build from explicit configuration
        nav = build_nav_from_config(config.nav, files)
    else:
        # Auto-generate from file structure
        nav = build_nav_from_files(files)
    
    # Trigger on_nav event
    nav = plugins.run_event('on_nav', nav, config=config, files=files)
    
    return nav

Phase 4: Page Rendering

Each page goes through a multi-stage rendering process:

# Page rendering pipeline
def render_page(page, config, nav, files):
    # 1. Pre-page event
    page = plugins.run_event('on_pre_page', page, config=config, files=files)
    
    # 2. Read source content
    page.read_source(config)
    
    # 3. Get markdown content
    markdown = page.markdown
    markdown = plugins.run_event('on_page_markdown', markdown, 
                                  page=page, config=config, files=files)
    
    # 4. Convert to HTML
    html = markdown_to_html(markdown, config.markdown_extensions)
    html = plugins.run_event('on_page_content', html,
                              page=page, config=config, files=files)
    page.content = html
    
    # 5. Build template context
    context = get_context(page, config, nav)
    context = plugins.run_event('on_page_context', context,
                                 page=page, config=config, nav=nav)
    
    # 6. Render template
    output = theme.render('main.html', context)
    output = plugins.run_event('on_post_page', output,
                                page=page, config=config)
    
    # 7. Write to site_dir
    write_file(page.abs_dest_path, output)

🎨 Template System

Jinja2 Integration

MkDocs uses Jinja2 as its templating engine.
Themes are collections of Jinja2 templates.

Basic Template Structure

<!-- main.html - Main page template -->
<!DOCTYPE html>
<html lang="{{ config.theme.locale }}">
<head>
    <meta charset="utf-8">
    <title>{% if page.title %}{{ page.title }} - {% endif %}{{ config.site_name }}</title>
    
    {# Include extra CSS #}
    {% for css_file in config.extra_css %}
        <link href="{{ css_file | url }}" rel="stylesheet">
    {% endfor %}
</head>
<body>
    {# Navigation #}
    {% include "nav.html" %}
    
    {# Page content #}
    <main>
        {{ page.content }}
    </main>
    
    {# Table of contents #}
    {% if page.toc %}
        {% include "toc.html" %}
    {% endif %}
    
    {# Include extra JavaScript #}
    {% for script in config.extra_javascript %}
        {{ script | script_tag }}
    {% endfor %}
</body>
</html>

Template Variables

Templates have access to these global variables:

Variable Type Description
config MkDocsConfig Full configuration object
page Page Current page being rendered
nav Navigation Site navigation structure
base_url str Relative path to site root
mkdocs_version str MkDocs version string
build_date_utc datetime Build timestamp

Page Object Attributes

# Available on page object in templates
page.title          # Page title
page.content        # Rendered HTML content
page.toc            # Table of contents
page.meta           # YAML front matter metadata
page.url            # Page URL relative to site root
page.abs_url        # Absolute URL (includes site_url path)
page.canonical_url  # Full canonical URL
page.edit_url       # Link to edit source (if configured)
page.is_homepage    # True if this is the homepage
page.previous_page  # Previous page in navigation
page.next_page      # Next page in navigation

Custom Filters

MkDocs provides custom Jinja2 filters:

{# url filter - Makes URLs relative to current page #}
<a href="{{ 'path/to/page.md' | url }}">Link</a>

{# tojson filter - Safe JSON conversion #}
<script>
    var pageTitle = {{ page.title | tojson }};
</script>

{# script_tag filter - Generates proper script tags #}
{{ script | script_tag }}

🔌 Plugin Architecture

Plugin System Overview

MkDocs plugins extend functionality through an event-driven architecture:

┌─────────────────────────────────────────────────────────────────┐
│                      Plugin Event Flow                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────┐                                                │
│  │  One-Time    │  on_startup ──▶ on_shutdown                    │
│  │  Events      │  on_serve                                      │
│  └──────────────┘                                                │
│                                                                   │
│  ┌──────────────┐                                                │
│  │   Global     │  on_config ──▶ on_pre_build ──▶ on_files       │
│  │   Events     │  ──▶ on_nav ──▶ on_env ──▶ on_post_build       │
│  └──────────────┘                                                │
│                                                                   │
│  ┌──────────────┐                                                │
│  │  Template    │  on_pre_template ──▶ on_template_context       │
│  │   Events     │  ──▶ on_post_template                          │
│  └──────────────┘                                                │
│                                                                   │
│  ┌──────────────┐                                                │
│  │    Page      │  on_pre_page ──▶ on_page_markdown              │
│  │   Events     │  ──▶ on_page_content ──▶ on_page_context       │
│  └──────────────┘  ──▶ on_post_page                              │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Creating a Plugin

Plugins inherit from BasePlugin and implement event handlers:

from mkdocs.plugins import BasePlugin, event_priority
from mkdocs.config import config_options as c
from mkdocs.config.base import Config

class MyPluginConfig(Config):
    """Plugin configuration schema (MkDocs 1.4+ recommended approach)."""
    enabled = c.Type(bool, default=True)
    option_name = c.Type(str, default='default_value')

class MyPlugin(BasePlugin[MyPluginConfig]):
    """Example MkDocs plugin."""
    
    def on_config(self, config):
        """Called after config is loaded."""
        if not self.config.enabled:
            return config
        
        # Modify configuration
        config.site_name += ' (Modified)'
        return config
    
    def on_files(self, files, config):
        """Called after files are collected."""
        # Add, remove, or modify files
        return files
    
    def on_page_markdown(self, markdown, page, config, files):
        """Called for each page's markdown content."""
        # Transform markdown
        return markdown.replace('foo', 'bar')
    
    def on_page_content(self, html, page, config, files):
        """Called after markdown is converted to HTML."""
        # Transform HTML
        return html
    
    @event_priority(-50)  # Run late
    def on_post_build(self, config):
        """Called after build completes."""
        print(f"Site built to: {config.site_dir}")

Event Priority

Control execution order with @event_priority (MkDocs 1.4+):

from mkdocs.plugins import event_priority

class MyPlugin(BasePlugin):
    
    @event_priority(100)   # Run first
    def on_files(self, files, config):
        pass
    
    @event_priority(0)     # Default priority
    def on_nav(self, nav, config, files):
        pass
    
    @event_priority(-100)  # Run last
    def on_post_build(self, config):
        pass

Plugin Registration

Register plugins via entry points in setup.py:

setup(
    name='mkdocs-my-plugin',
    entry_points={
        'mkdocs.plugins': [
            'my-plugin = my_plugin:MyPlugin',
        ]
    }
)

🪝 Hooks (Lightweight Plugins)

New in MkDocs 1.4

For simple customizations without creating a full plugin package, MkDocs supports hooks—Python scripts that implement event handlers directly:

# mkdocs.yml
hooks:
  - my_hooks.py
# my_hooks.py - No plugin class needed!
def on_page_markdown(markdown, page, config, files):
    """Transform markdown before rendering."""
    return markdown.replace('TODO', '⚠️ TODO')

def on_config(config):
    """Modify configuration at build start."""
    print(f"Building: {config.site_name}")
    return config

Key Differences from Plugins:

Aspect Hooks Plugins
Installation Just a .py file Requires packaging & pip install
Configuration No config schema Full config validation
Distribution Copy file PyPI package
Use Case Project-specific tweaks Reusable functionality
Event Handlers Functions (no self) Methods on BasePlugin

Note: Hook files can import adjacent Python modules normally (MkDocs 1.6+). In older versions, you need to add the path to sys.path.


🔍 Search System

Search Architecture

MkDocs includes a built-in search plugin using Lunr.js:

┌─────────────────────────────────────────────────────────────────┐
│                      Search System                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Build Time:                                                     │
│  ┌──────────┐    ┌───────────────┐    ┌──────────────────┐      │
│  │  Pages   │───▶│ Index Builder │───▶│search_index.json│      │
│  └──────────┘    └───────────────┘    └──────────────────┘      │
│                                                                   │
│  Runtime:                                                        │
│  ┌──────────────────┐    ┌──────────┐    ┌───────────┐          │
│  │search_index.json│───▶│ Lunr.js  │───▶│ Results   │          │
│  └──────────────────┘    └──────────┘    └───────────┘          │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Search Index Structure

The generated search_index.json:

{
    "config": {
        "lang": ["en"],
        "separator": "[\\s\\-]+",
        "pipeline": ["stemmer"]
    },
    "docs": [
        {
            "location": "index.html",
            "title": "Home",
            "text": "Welcome to the documentation..."
        },
        {
            "location": "guide/installation.html",
            "title": "Installation",
            "text": "Install the package using pip..."
        }
    ],
    "index": {
        // Pre-built Lunr.js index (optional)
    }
}

Search Configuration

plugins:
  - search:
      lang: en
      separator: '[\s\-\.]+'
      min_search_length: 3
      prebuild_index: true  # Requires Node.js
      indexing: full        # 'full', 'sections', or 'titles'

📁 File Processing

Markdown Processing Pipeline

┌─────────────────────────────────────────────────────────────────┐
│                 Markdown Processing Pipeline                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────┐                                                │
│  │  Source.md   │                                                │
│  └──────┬───────┘                                                │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐    Extract YAML front matter                   │
│  │  Meta Parse  │───▶ Store in page.meta                         │
│  └──────┬───────┘                                                │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐    Plugin: on_page_markdown                    │
│  │  Pre-process │───▶ Modify raw markdown                        │
│  └──────┬───────┘                                                │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐    Python-Markdown + Extensions                │
│  │   Convert    │───▶ Generate HTML                              │
│  └──────┬───────┘                                                │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐    Plugin: on_page_content                     │
│  │ Post-process │───▶ Modify HTML output                         │
│  └──────┬───────┘                                                │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐                                                │
│  │  Output.html │                                                │
│  └──────────────┘                                                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

URL Generation

MkDocs supports two URL styles:

Directory URLs (Default)

use_directory_urls: true
Source File Generated File URL
index.md index.html /
about.md about/index.html /about/
guide/install.md guide/install/index.html /guide/install/

Flat URLs

use_directory_urls: false
Source File Generated File URL
index.md index.html /index.html
about.md about.html /about.html
guide/install.md guide/install.html /guide/install.html

🌐 Development Server

Live Reload Server

MkDocs includes a development server with live reloading:

mkdocs serve

Features:

  • Auto-rebuild: Rebuilds on file changes
  • Live reload: Browser automatically refreshes
  • URL simulation: Matches production URL structure
  • Error reporting: Clear build error messages

Server Architecture

# Simplified server architecture
class LiveReloadServer:
    def __init__(self, config):
        self.config = config
        self.watcher = FileWatcher(config.docs_dir)
        
    def serve(self):
        # Initial build
        self.build()
        
        # Start watching for changes
        self.watcher.on_change(self.rebuild)
        
        # Start HTTP server
        self.start_server()
    
    def rebuild(self, changed_files):
        # Incremental rebuild
        self.build()
        # Notify browser to reload
        self.notify_clients()

Watch Configuration

Control which directories trigger rebuilds:

watch:
  - custom_theme/
  - includes/

⚙️ Advanced Configuration Features

Configuration Inheritance (INHERIT)

Multiple MkDocs sites can share common configuration:

# base.yml - Shared configuration
theme:
  name: material
markdown_extensions:
  toc:
    permalink: true
# mkdocs.yml - Site-specific config
INHERIT: ../base.yml
site_name: My Project
site_url: https://example.com/

MkDocs deep-merges the configurations, allowing overrides while inheriting defaults.

Special YAML Tags

MkDocs supports dynamic configuration values:

# Environment variables with fallback
site_name: !ENV [SITE_NAME, 'Default Site']

plugins:
  - search
  - expensive-plugin:
      enabled: !ENV [CI, false]  # Only enable in CI

# Relative paths (useful for extensions)
markdown_extensions:
  - pymdownx.snippets:
      base_path: !relative  # Relative to current Markdown file
      # Or: !relative $docs_dir
      # Or: !relative $config_dir

Validation Configuration (MkDocs 1.5+)

Control strictness of link and navigation validation:

validation:
  omitted_files: warn
  absolute_links: warn  # Or 'relative_to_docs' (1.6+)
  unrecognized_links: warn
  anchors: warn  # New in 1.6

  nav:
    omitted_files: info
    not_found: warn
    absolute_links: ignore

  links:
    not_found: warn
    anchors: warn
    absolute_links: relative_to_docs  # Validate and convert

Draft and Excluded Documents (MkDocs 1.5+)

# Exclude files from build entirely
exclude_docs: |
  drafts/           # Directory anywhere
  *.py              # Python files
  /requirements.txt # Top-level only
  !.assets          # Except .assets

# Draft files: available in serve, excluded from build (1.6+)
draft_docs: |
  _draft.md         # Files ending in _draft.md
  wip/              # Work-in-progress directory

📊 Performance Considerations

Build Performance Factors

Factor Impact Optimization
Page Count High Use exclude_docs for unused files
Extensions Medium Disable unused extensions
Plugins Medium Audit plugin performance
Search Index Medium Use prebuild_index for large sites
Theme Complexity Low Simplify templates

Large Site Optimizations

# Optimizations for large documentation sites
plugins:
  - search:
      prebuild_index: true    # Pre-build search index
      indexing: sections      # Index less content

# Exclude non-documentation files
exclude_docs: |
  drafts/
  *.py
  *.sh

# Reduce validation overhead
validation:
  links:
    not_found: ignore
    anchors: ignore

Build Time Comparison

Site Size Cold Build Incremental
~50 pages 2-5 seconds < 1 second
~200 pages 10-20 seconds 2-3 seconds
~500 pages 30-60 seconds 5-10 seconds

📖 References

Official Documentation

Technical References