DEV Community

Cover image for Build a Docusaurus-like Site with FastAPI: Step 6 - Sidebar Generation
Leapcell
Leapcell

Posted on

Build a Docusaurus-like Site with FastAPI: Step 6 - Sidebar Generation

Cover

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
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

ImageP1

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.

Leapcell


Follow us on X: @LeapcellHQ


Read other articles in this series

Related Posts:

Top comments (0)