MkDocs Architecture - How MkDocs Works
MkDocs Architecture - How MkDocs Works
📋 Table of Contents
- 📖 Overview
- 🏗️ Core Architecture
- ⚙️ Build Pipeline
- 🎨 Template System
- 🔌 Plugin Architecture
- 🪝 Hooks (Lightweight Plugins)
- 🔍 Search System
- 📁 File Processing
- 🌐 Development Server
- ⚙️ Advanced Configuration Features
- 📊 Performance Considerations
- 📖 References
📖 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 directory2. 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 path4. 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 configPhase 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 filesPhase 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 navigationCustom 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):
passPlugin 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 configKey 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 serveFeatures:
- 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.
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 convertDraft 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: ignoreBuild 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
- MkDocs Documentation - Official MkDocs documentation
- MkDocs Plugin Development - Guide to creating plugins
- MkDocs Theme Development - Guide to creating themes
- MkDocs Configuration - Complete configuration reference
- MkDocs Release Notes - Version history and changelog
- MkDocs Translation Guide - Theme localization/i18n
Technical References
- Python-Markdown - Markdown processing library (v3.10)
- Jinja2 Documentation - Template engine documentation (v3.1)
- Lunr.js - Client-side search library