Skip to content

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:

  1. Images don't display - images referenced in notebooks may not display correctly through relative path
  2. No kernel available - notebooks specify python3 but JupyterLite uses python

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:

  1. JupyterLite intercepts the request
  2. JupyterLite tries to fetch via its contents API
  3. The API returns 404 because IMGs wasn't included in the virtual filesystem (it doesn't serve static files this way)
  4. Image shows briefly (browser tries direct path) then breaks (API takes over)
Browser Console Error:
GET /jupyterlite/api/contents/IMGs/all.json HTTP/1.1" code 404

Why Kernels Don't WorkΒΆ

Your notebooks specify:

"kernelspec": {
  "name": "python3"
}

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ΒΆ

  1. Replaces relative image paths with GitHub raw URLs
  2. Changes kernel name from python3 to python for Pyodide

ConfigurationΒΆ

Add this to mkdocs.yml:

hooks:
  - hooks/fix_notebook_img_issue.py

ImplementationΒΆ

Create hooks/fix_notebook_img_issue.py in the project root:

hooks/fix_notebook_img_issue.py
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:

βœ“ Updated X JupyterLite notebooks (images + kernel)

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:

Before: "name": "python3"
After:  "name": "python"

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:

  1. JupyterLite intercepts the request
  2. Tries to fetch via /jupyterlite/api/contents/IMGs/image.png
  3. Returns 404 because the virtual filesystem API doesn't serve static files this way
  4. 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):

// In jupyterlite_config.json
{
  "LiteBuildConfig": {
    "files": ["docs/IMGs/"]
  }
}

Usage in notebooks (would be):

from IPython.display import Image
Image("../files/IMGs/myplot.png")

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 ...

  1. Check deployment process: Ensure entire site/ directory is deployed

  2. Verify notebook modifications: Check that built notebooks contain GitHub raw URLs

  3. Verify hook ran: Check build output for "βœ“ Updated X JupyterLite notebooks" message

  4. Verify hook is registered: Check mkdocs.yml has hooks: - hooks/fix_notebook_img_issue.py

  5. Check GitHub URL accessibility: Ensure the raw GitHub URLs work in browser

  6. Inspect browser console: Open DevTools β†’ Console, look for 404 errors on images

  7. Verify source notebooks: Ensure notebooks use ../../IMGs/ not ../IMGs/ or other variants

  8. Clear browser cache - JupyterLite caches aggressively

  9. Verify GitHub URL is correct - Check the raw URL works in browser

  10. Check web server configuration: Ensure proper MIME types for images

  11. CORS issues: Check browser console for cross-origin errors

Kernel Still Not FoundΒΆ

  1. Verify hook ran - Check build output for update message

  2. Check notebook metadata - Should show "name": "python" not "python3"

  3. Wait for Pyodide - First load downloads ~15MB runtime; be patient

  4. Check browser console - Look for Pyodide loading errors

Hook Not RunningΒΆ

  1. Check mkdocs.yml - Ensure hooks: - hooks/fix_notebook_img_issue.py is present

  2. Check file path - Hook must be at hooks/fix_notebook_img_issue.py relative to project root

  3. Check for Python errors - Run python hooks/fix_notebook_img_issue.py to syntax check

  4. Hook function name - Must be exactly on_post_build

  5. File permissions: Ensure hook file is readable

  6. 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ΒΆ

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:

  1. GitHub raw URLs for images (bypasses virtual filesystem)
  2. Correct kernel name (python for 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:

  1. Initial Problem: Images don't display in JupyterLite notebooks
  2. First Attempt: Copy IMGs folder during build (seemed logical but failed)
  3. Root Cause Discovery: JupyterLite's virtual filesystem API blocks direct file access
  4. Working Solution: Replace relative paths with external GitHub raw URLs
  5. Additional Discovery: Kernel name mismatch (python3 vs python)

The journey demonstrates the importance of understanding platform-specific constraints and iterative problem-solving in complex integrations.