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