In the previous article, we solved the issue of loading static resources (like images) within Markdown.
Up to this point, our documentation pages can display content, code highlighting, and images nicely. However, readers still face difficulties navigating the documentation. The pages are isolated islands: without manually typing the URL, you cannot jump from one article to another.
Documentation sites like Docusaurus typically use a "Left Sidebar + Right Content" layout.
In this article, we will implement this feature: we will write a function to automatically scan all Markdown files in the docs/ directory, extract their titles, and dynamically generate a sidebar navigation menu.
Step 1: Create Sidebar Layout Styles
First, we need to change the page layout from the original "single-column vertical structure" to a "two-column horizontal structure."
We need a new CSS file to define this layout. Create a layout.css file in the static/css/ directory.
Updated File Structure:
static/
└── css/
├── highlight.css
└── layout.css <-- New
Edit static/css/layout.css:
/* Global reset and basic styles */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #333;
}
/* Main container: Use Flexbox to implement side-by-side layout */
.main-container {
display: flex;
min-height: 100vh;
}
/* Left Sidebar Styles */
.sidebar {
width: 250px;
background-color: #f4f4f4;
border-right: 1px solid #ddd;
padding: 20px;
flex-shrink: 0; /* Prevent sidebar from being compressed */
}
.sidebar h3 {
margin-top: 0;
font-size: 1.1rem;
color: #555;
}
.sidebar ul {
list-style: none;
padding: 0;
}
.sidebar li {
margin-bottom: 10px;
}
.sidebar a {
text-decoration: none;
color: #333;
font-size: 0.95rem;
}
.sidebar a:hover {
color: #007bff;
}
/* Right Content Area Styles */
.content {
flex-grow: 1;
padding: 20px 40px;
max-width: 800px; /* Limit content max-width for better reading experience */
}
Step 2: Modify the HTML Template
Next, modify templates/doc.html to include the new CSS file and adjust the HTML structure to accommodate the sidebar.
We will introduce a new variable, sidebar_items, in the template. This will be an array containing the list of documents, which will be passed in from the Python code later.
Modify templates/doc.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ page_title }} - My Docs Site</title>
<link rel="stylesheet" href="{{ url_for('static', path='css/highlight.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', path='css/layout.css') }}" />
</head>
<body>
<div class="main-container">
<aside class="sidebar">
<h3>Contents</h3>
<ul>
{% for item in sidebar_items %}
<li>
<a href="{{ item.url }}">{{ item.title }}</a>
</li>
{% endfor %}
</ul>
</aside>
<main class="content">
<h1>{{ page_title }}</h1>
<hr />
<div class="doc-content">{{ content | safe }}</div>
</main>
</div>
</body>
</html>
Step 3: Implement Directory Scanning Logic
Now we need to write some Python code to scan the docs/ folder, find all .md files, and parse their Frontmatter to get the titles.
Open main.py. We need to import the pathlib library (you can also use os, but pathlib is more modern) and write a helper function.
Modify main.py:
# main.py
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import markdown
from fastapi.staticfiles import StaticFiles
import frontmatter
from pathlib import Path # 1. Import Path
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/docs/assets", StaticFiles(directory="docs/assets"), name="doc_assets")
templates = Jinja2Templates(directory="templates")
# --- Helper Function: Generate Sidebar Data ---
def get_sidebar_items():
items = []
docs_path = Path("docs")
# Iterate over all .md files in the docs directory
for file_path in docs_path.glob("*.md"):
# Parse Frontmatter to get the title
try:
post = frontmatter.load(file_path)
# If no title exists, use the filename
title = post.metadata.get("title", file_path.stem)
except:
title = file_path.stem
# Generate URL, assuming the rule is /docs/{filename}
# file_path.stem gets the filename without extension (e.g., "hello")
url = f"/docs/{file_path.stem}"
items.append({"title": title, "url": url})
# Sort by title (or you can add an 'order' field to sort, here we simply sort by title)
items.sort(key=lambda x: x["title"])
return items
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
# Homepage remains unchanged for now, we can add sidebar later
context = {
"request": request,
"page_title": "Hello, Jinja2!"
}
return templates.TemplateResponse("index.html", context)
@app.get("/docs/hello", response_class=HTMLResponse)
async def get_hello_doc(request: Request):
md_file_path = "docs/hello.md"
try:
post = frontmatter.load(md_file_path)
except FileNotFoundError:
return HTMLResponse(content="<h1>404 - Document Not Found</h1>", status_code=404)
except Exception as e:
return HTMLResponse(content=f"<h1>500 - Parse Error: {e}</h1>", status_code=500)
metadata = post.metadata
md_content = post.content
extensions = ['fenced_code', 'codehilite']
html_content = markdown.markdown(md_content, extensions=extensions)
page_title = metadata.get('title', 'Untitled Document')
# 2. Get sidebar data
sidebar_items = get_sidebar_items()
context = {
"request": request,
"page_title": page_title,
"content": html_content,
"sidebar_items": sidebar_items # 3. Pass to template
}
return templates.TemplateResponse("doc.html", context)
Step 4: Add a Second Document for Testing
To verify that the sidebar is actually working, we need a second Markdown file.
Create setup.md in the docs/ directory:
---
title: Environment Setup Guide
author: Leapcell
date: 2025-11-10
---
# Project Environment Setup
This is our second document.
1. Install Python
2. Install FastAPI
3. Run Code
Step 5: Run and Test
Execute uvicorn main:app --reload to start the server.
Visit http://127.0.0.1:8000/docs/hello.
You will notice the following changes:
- The page layout has changed to a left sidebar navigation and right content area.
- The left sidebar automatically lists two links: "Hello, Frontmatter!" and "Environment Setup Guide".
However, there is a small problem:
If you click on "Environment Setup Guide" in the sidebar, the browser will jump to /docs/setup. At this point, you will see an error saying the content is not found.
This is because our main.py currently only has the hardcoded /docs/hello route and does not yet handle /docs/setup.
Summary
By scanning the file system, we have given our documentation site the ability to automatically generate a table of contents. No matter how many .md files you add to or remove from the docs/ folder, the sidebar will update automatically.
Although the sidebar is smart, the routing is "clumsy"—because the route addresses are hardcoded. If we have 100 documents, surely we can't write 100 @app.get("/docs/xxx") functions in main.py? That is obviously not viable.
We need a generic way to capture requests for /docs/{any_filename} and automatically find the corresponding Markdown file.
In the next article, we will implement File-path based Dynamic Routing, completely solving the 404 issue and ensuring that every link in the sidebar actually navigates to the corresponding article.
Other
After building your site, you might want to deploy it online for others to see. But most cloud platforms are expensive, and it's not worth paying a high price for a practice project like this.
Is there a more economical way to deploy? You can try Leapcell. It supports deploying multiple languages like Python, Node.js, Go, and Rust, and offers a generous free tier every month, allowing you to deploy up to 20 projects without spending a dime.
Follow us on X: @LeapcellHQ
Read other articles in this series
Related Posts:



Top comments (0)