MkDocs, JupyterLite & Image Resolution: Complete GuideΒΆ
Comprehensive Solution Guide
This tutorial combines the complete journey of solving JupyterLite image display issues with MkDocs, from failed attempts to working solution.
The ProblemΒΆ
When using the mkdocs-jupyterlite plugin to embed interactive Jupyter notebooks in your MkDocs site, you may encounter two issues:
- Images don't display - images referenced in notebooks may not display correctly through relative path
- No kernel available - notebooks specify
python3but JupyterLite usespython
File Structure VisualizationΒΆ
Before JupyterLite Plugin:
docs/
βββ IMGs/
β βββ image1.png
β βββ image2.png
βββ BC_Weeks/
βββ Week_1/
βββ notebook.ipynb β references ../../IMGs/image1.png β Works
After JupyterLite Plugin (WITHOUT fix):
site/jupyterlite/files/
βββ BC_Weeks/
βββ Week_1/
βββ notebook.ipynb β references ../../IMGs/image1.png β Broken (no IMGs folder!)
β ATTEMPTED SOLUTION (Copy IMGs Folder):
After JupyterLite Plugin (with folder copy attempt):
site/jupyterlite/files/
βββ IMGs/ β Hook copied this here
β βββ image1.png
β βββ image2.png
βββ BC_Weeks/
βββ Week_1/
βββ notebook.ipynb β references ../../IMGs/image1.png β Still broken!
After JupyterLite Plugin (WITH working fix):
site/jupyterlite/files/
βββ BC_Weeks/
βββ Week_1/
βββ notebook.ipynb β references GitHub raw URL β Works!
Why Images BreakΒΆ
JupyterLite uses a virtual filesystem served via an API, not regular static files. When a notebook references ../../IMGs/image.png:
- JupyterLite intercepts the request
- JupyterLite tries to fetch via its contents API
- The API returns 404 because IMGs wasn't included in the virtual filesystem (it doesn't serve static files this way)
- Image shows briefly (browser tries direct path) then breaks (API takes over)
Why Kernels Don't WorkΒΆ
Your notebooks specify:
But JupyterLite's Pyodide kernel is registered as python, not python3.
The Solution: MkDocs Post-Build HookΒΆ
For this solution, we use an on_post_build hook to:
- modify the built copies of notebooks, leaving source files unchanged
- replace relative image paths with GitHub raw URLs for JupyterLite compatibility
Understanding MkDocs HooksΒΆ
Hooks vs Git Hooks
Don't confuse MkDocs hooks (Python functions that run during documentation builds) with Git hooks (scripts that run during Git operations). See Git Hooks: Preventing Pipeline Failures for Git hooks.
MkDocs hooks are Python functions that run at specific points during the documentation build process. They allow you to customize build behavior without creating a full plugin.
Common Hook EventsΒΆ
| Hook Event | When It Runs | Common Use Cases |
|---|---|---|
on_startup |
Before anything else | Initialize resources, check dependencies |
on_config |
After config loads | Modify configuration dynamically |
on_pre_build |
Before build starts | Clean directories, prepare assets |
on_files |
When files are collected | Add/remove/modify file list |
on_post_build |
After build completes | Copy assets, post-process files β We use this! |
on_page_markdown |
Before markdown rendering | Transform markdown content |
on_page_content |
After markdown to HTML | Modify HTML content |
What The Solution Hook DoesΒΆ
- Replaces relative image paths with GitHub raw URLs
- Changes kernel name from
python3topythonfor Pyodide
ConfigurationΒΆ
Add this to mkdocs.yml:
ImplementationΒΆ
Create hooks/fix_notebook_img_issue.py in the project root:
from pathlib import Path
# GitHub raw URL base for images
GITHUB_RAW_BASE = "https://raw.githubusercontent.com/ProsperousHeart/Basics-Boot-Camp/main/docs/IMGs/" # noqa: E501
# Relative path pattern used in source notebooks
RELATIVE_IMG_PATH = "../../IMGs/"
def on_post_build(config, **kwargs):
"""
Update notebooks in built JupyterLite for compatibility.
Performs two transformations on built notebook copies:
1. Replaces relative image paths with GitHub raw URLs
2. Changes kernel name from 'python3' to 'python' for Pyodide compatibility
Source notebooks remain unchanged - only the built copies are modified.
Args:
config: MkDocs configuration dictionary containing:
- site_dir: Path to the built site directory
**kwargs: Additional hook arguments (unused)
"""
site_dir = Path(config["site_dir"])
jupyterlite_files = site_dir / "jupyterlite" / "files"
if not jupyterlite_files.exists():
print(
"β JupyterLite files directory not found, skipping notebook updates" # noqa: E501
)
return
notebooks_updated = 0
for notebook_path in jupyterlite_files.rglob("*.ipynb"):
content = notebook_path.read_text(encoding="utf-8")
modified = False
# Fix image paths
if RELATIVE_IMG_PATH in content:
content = content.replace(RELATIVE_IMG_PATH, GITHUB_RAW_BASE)
modified = True
# Fix kernel name for Pyodide (python3 -> python)
if '"name": "python3"' in content:
content = content.replace('"name": "python3"', '"name": "python"')
modified = True
if modified:
notebook_path.write_text(content, encoding="utf-8")
notebooks_updated += 1
if notebooks_updated > 0:
print(
f"β Updated {notebooks_updated} JupyterLite notebooks (images + kernel)" # noqa: E501
)
else:
print("βΉ No notebooks needed updates")
The hook also includes built-in verification that prints the number of notebooks updated. Look for this message in your build output:
How It WorksΒΆ
Build Process FlowΒΆ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MkDocs Build Process Timeline β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. on_startup β
β β β
β 2. on_config β
β β β
β 3. on_pre_build β
β β β
β 4. on_files β
β β |
β 1. MkDocs processes markdown files β HTML β
β β β
β 2. JupyterLite plugin copies notebooks to: β
β site/jupyterlite/files/BC_Weeks/Week_X/*.ipynb β
β β β
β 3. on_post_build hook runs β
β β Finds all .ipynb files in jupyterlite/files/ β
β β Replaces ../../IMGs/ with GitHub raw URL β
β β Replaces "python3" kernel with "python" β
β β β
β 4. Build complete! Site is ready to deploy. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Before and AfterΒΆ
Image paths:
Before: ../../IMGs/learn-to-code-online.png
After: https://raw.githubusercontent.com/ProsperousHeart/Basics-Boot-Camp/main/docs/IMGs/learn-to-code-online.png
Kernel specification:
Why This Approach WorksΒΆ
Alternatives ConsideredΒΆ
| Approach | Pros | Cons |
|---|---|---|
| Embed images as base64 | Always works | Huge file sizes (686KB image = 915K chars) |
| Copy IMGs folder to site | Simple - no changes to source files | JupyterLite API doesn't serve static files requires mkdocs hook massive file duplication - increases repo size |
| Modify source notebooks (Use Absolute URLs in source) | Permanent fix No extra hooks needed |
Breaks local Jupyter usage Requires internet requires updating all notebooks breaks if URL changes |
| GitHub raw URLs via hook | Works everywhere, source unchanged | Requires internet breaks if URL changes |
| Preload images into virtual FS | Offline access, clean paths | Untested, increases bundle size |
Why GitHub Raw URLs WorkΒΆ
- External URLs bypass JupyterLite's virtual filesystem
- Images are fetched directly from GitHub's CDN
- No API routing involved - just standard HTTP GET
Alternative Solutions ConsideredΒΆ
β Option 1: Use Absolute URLsΒΆ
Approach: Change image paths to https://yoursite.com/IMGs/image.png
Pros:
- Works everywhere (local notebooks, JupyterLite, GitHub)
Cons:
- Couples notebooks to deployment URL
- Doesn't work offline
- Requires updating all notebooks
- Breaks if site URL changes
β Option 2: Use MkDocs Hook (Copy IMGs Folder - Attempted)ΒΆ
Approach: Copy IMGs folder during build process
Pros:
- No notebook changes required (notebooks can use
./IMGs/image.png) - Massive file duplication
- Maintains DRY principle (single image source)
- Works with existing relative paths (in theory)
- Increases repository size significantly
Cons:
- β Doesn't work with JupyterLite's virtual filesystem
- Images still don't display despite being copied
Why it fails: Even after copying the IMGs folder to site/jupyterlite/files/, images still don't display because JupyterLite serves files through a virtual filesystem API, not direct HTTP requests.
When a notebook references ../../IMGs/image.png:
- JupyterLite intercepts the request
- Tries to fetch via
/jupyterlite/api/contents/IMGs/image.png - Returns 404 because the virtual filesystem API doesn't serve static files this way
- Browser shows broken image despite folder being present
β Option 3: Use MkDocs Hook (URL Replacement - Working Solution)ΒΆ
Approach: Replace relative image paths with GitHub raw URLs during build
Pros:
- β Actually works with JupyterLite
- No notebook source changes required
- Maintains DRY principle (single image source)
- Images served from reliable GitHub CDN
- Minimal overhead
Cons:
- Requires internet connection for images
- Couples to GitHub repository URL
β Option 4: Preload Images into Virtual File System (Untested)ΒΆ
Approach: Configure JupyterLite to pre-bundle images into its virtual filesystem
Pros:
- Images would be available offline
- No external dependencies
- Clean relative path access in notebooks
- Potentially simpler than build hooks
Cons:
- β Untested - May not work with MkDocs integration
- Increases bundle size
- Requires JupyterLite configuration changes
Implementation (Untested):
Usage in notebooks (would be):
JupyterLite Files Configuration
The files option in LiteBuildConfig allows pre-bundling additional files into the JupyterLite virtual filesystem. See JupyterLite Configuration Reference for details.
Untested Alternative
This approach was suggested by AI after our working solution was implemented. It may or may not work with the MkDocs JupyterLite plugin integration. Test thoroughly before adopting.
TestingΒΆ
Local TestingΒΆ
# Build the site
mkdocs build --clean
# Check the hook ran
# Look for: "β Updated X JupyterLite notebooks (images + kernel)"
# Serve locally
mkdocs serve
Verify Changes Were AppliedΒΆ
Check a built notebook:
# Windows
type site\jupyterlite\files\BC_Weeks\Week_1\Python_Basics_04_-_Operators.ipynb | findstr "github"
# Linux/Mac
grep "github" site/jupyterlite/files/BC_Weeks/Week_1/Python_Basics_04_-_Operators.ipynb
Should show GitHub raw URLs instead of relative paths.
TroubleshootingΒΆ
Images Still Not ShowingΒΆ
If your images still appear locally but not in deployment ...
-
Check deployment process: Ensure entire
site/directory is deployed -
Verify notebook modifications: Check that built notebooks contain GitHub raw URLs
-
Verify hook ran: Check build output for "β Updated X JupyterLite notebooks" message
-
Verify hook is registered: Check
mkdocs.ymlhashooks: - hooks/fix_notebook_img_issue.py -
Check GitHub URL accessibility: Ensure the raw GitHub URLs work in browser
-
Inspect browser console: Open DevTools β Console, look for 404 errors on images
-
Verify source notebooks: Ensure notebooks use
../../IMGs/not../IMGs/or other variants -
Clear browser cache - JupyterLite caches aggressively
-
Verify GitHub URL is correct - Check the raw URL works in browser
-
Check web server configuration: Ensure proper MIME types for images
-
CORS issues: Check browser console for cross-origin errors
Kernel Still Not FoundΒΆ
-
Verify hook ran - Check build output for update message
-
Check notebook metadata - Should show
"name": "python"not"python3" -
Wait for Pyodide - First load downloads ~15MB runtime; be patient
-
Check browser console - Look for Pyodide loading errors
Hook Not RunningΒΆ
-
Check mkdocs.yml - Ensure
hooks: - hooks/fix_notebook_img_issue.pyis present -
Check file path - Hook must be at
hooks/fix_notebook_img_issue.pyrelative to project root -
Check for Python errors - Run
python hooks/fix_notebook_img_issue.pyto syntax check -
Hook function name - Must be exactly
on_post_build -
File permissions: Ensure hook file is readable
-
Clear cache: Try
mkdocs build --clean
CustomizationΒΆ
Warning
All examples in this section are based on the structure of the repo. Actual customization may differ based on yours.
Using a Different Image LocationΒΆ
Edit the constants at the top of the hook:
# Change this to your repo's raw URL
GITHUB_RAW_BASE = (
"https://raw.githubusercontent.com/YOUR_USER/YOUR_REPO/main/docs/IMGs/"
)
# Change this to match your notebook's relative paths
RELATIVE_IMG_PATH = "../../IMGs/"
Using a Different BranchΒΆ
If your default branch isn't main:
GITHUB_RAW_BASE = (
"https://raw.githubusercontent.com/ProsperousHeart/Basics-Boot-Camp/master/docs/IMGs/"
)
Additional ResourcesΒΆ
MkDocs DocumentationΒΆ
JupyterLite ResourcesΒΆ
GitHub Raw URLsΒΆ
- Format:
https://raw.githubusercontent.com/{user}/{repo}/{branch}/{path} - GitHub Raw Content
Python shutil ModuleΒΆ
- shutil Documentation
shutil.copytree()- Copy entire directory treesshutil.rmtree()- Remove directory trees
Related TopicsΒΆ
- Understanding Relative vs Absolute Paths
- Python Pathlib Guide
- MkDocs Plugin Development
- Git Hooks: Preventing Pipeline Failures
SummaryΒΆ
JupyterLite's virtual filesystem doesn't support relative paths to directories outside its content scope.
MkDocs hooks provide a powerful way to customize the build process without creating full plugins. By using the on_post_build hook, we ensure that images referenced in Jupyter notebooks remain accessible when notebooks are served through JupyterLite, maintaining the relative path structure that works in the original documentation.
By using an MkDocs post-build hook, we transform the built notebook copies to use:
- GitHub raw URLs for images (bypasses virtual filesystem)
- Correct kernel name (
pythonfor Pyodide compatibility)
This approach is:
- β Maintainable: Single source of truth for images
- β Scalable: Works for any number of notebooks/images
- β Clean: No notebook modifications required
- β Efficient: Minimal build overhead & no manual intervention (runs automatically on build)
The Problem-Solving JourneyΒΆ
This solution evolved through multiple iterations:
- Initial Problem: Images don't display in JupyterLite notebooks
- First Attempt: Copy IMGs folder during build (seemed logical but failed)
- Root Cause Discovery: JupyterLite's virtual filesystem API blocks direct file access
- Working Solution: Replace relative paths with external GitHub raw URLs
- Additional Discovery: Kernel name mismatch (
python3vspython)
The journey demonstrates the importance of understanding platform-specific constraints and iterative problem-solving in complex integrations.