FastAPIでDocusaurus風サイトを構築:ステップ6 - サイドバー生成
Daniel Hayes
Full-Stack Engineer · Leapcell

前回の記事では、Markdown内での静的リソース(画像など)の読み込み問題を解決しました。
これまでのところ、ドキュメントページはコンテンツ、コードハイライト、画像をきれいに表示できます。しかし、読者はドキュメントのナビゲーションで依然として困難に直面しています。ページは孤立した島のようなものです:手動でURLを入力しない限り、ある記事から別の記事へジャンプすることはできません。
Docusaurusのようなドキュメントサイトは、通常、**「左サイドバー+右コンテンツ」**レイアウトを使用します。
この記事では、この機能を実装します:docs/ ディレクトリ内のすべてのMarkdownファイルを自動的にスキャンし、そのタイトルを抽出し、サイドバーナビゲーションメニューを動的に生成する関数を作成します。
ステップ1:サイドバーレイアウトスタイルの作成
まず、ページレイアウトを元の「単一カラムの垂直構造」から「2カラムの水平構造」に変更する必要があります。
このレイアウトを定義するためには、新しいCSSファイルが必要です。static/css/ ディレクトリに layout.css ファイルを作成します。
更新されたファイル構造:
static/
└── css/
├── highlight.css
└── layout.css <-- 新規
static/css/layout.css の編集:
/* グローバルリセットと基本スタイル */ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; color: #333; } /* メインコンテナ:Flexboxを使用してサイドバイサイドレイアウトを実装 */ .main-container { display: flex; min-height: 100vh; } /* 左サイドバーのスタイル */ .sidebar { width: 250px; background-color: #f4f4f4; border-right: 1px solid #ddd; padding: 20px; flex-shrink: 0; /* サイドバーが圧縮されるのを防ぐ */ } .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; } /* 右コンテンツエリアのスタイル */ .content { flex-grow: 1; padding: 20px 40px; max-width: 800px; /* 読みやすいようにコンテンツの最大幅を制限 */ }
ステップ2:HTMLテンプレートの変更
次に、templates/doc.html を変更して新しいCSSファイルを含め、サイドバーを収容するためにHTML構造を調整します。
テンプレートに新しい変数 sidebar_items を導入します。これはドキュメントのリストを含む配列であり、後でPythonコードから渡されます。
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>
ステップ3:ディレクトリスキャンロジックの実装
ここで、docs/ フォルダをスキャンし、すべての .md ファイルを見つけて、それらのFrontmatterを解析してタイトルを取得するPythonコードを作成する必要があります。
main.py を開きます。pathlib ライブラリをインポート(os も使用できますが、pathlib の方がモダンです)し、ヘルパー関数を記述する必要があります。
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. 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") # --- ヘルパー関数:サイドバーデータの生成 --- def get_sidebar_items(): items = [] docs_path = Path("docs") # docsディレクトリ内のすべての.mdファイルを反復処理 for file_path in docs_path.glob("*.md"): # Frontmatterを解析してタイトルを取得 try: post = frontmatter.load(file_path) # タイトルが存在しない場合は、ファイル名を使用 title = post.metadata.get("title", file_path.stem) except: title = file_path.stem # URLを生成、ルールは/docs/{filename}と仮定 # file_path.stemは拡張子なしのファイル名を取得します(例:「hello」) url = f"/docs/{file_path.stem}" items.append({"title": title, "url": url}) # タイトルでソート(またはソートのために「order」フィールドを追加できます。ここでは単純にタイトルでソートします) items.sort(key=lambda x: x["title"]) return items @app.get("/", response_class=HTMLResponse) async def root(request: Request): # ホームページは今のところ変更なし、後でサイドバーを追加できます 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. サイドバーデータを取得 sidebar_items = get_sidebar_items() context = { "request": request, "page_title": page_title, "content": html_content, "sidebar_items": sidebar_items # 3. テンプレートに渡す } return templates.TemplateResponse("doc.html", context)
ステップ4:テスト用の2番目のドキュメントを追加
サイドバーが実際に機能していることを確認するために、2番目のMarkdownファイルが必要です。
docs/ ディレクトリに setup.md を作成します。
--- 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
ステップ5:実行とテスト
サーバーを起動するには uvicorn main:app --reload を実行します。
http://127.0.0.1:8000/docs/hello にアクセスします。
以下の変更に気づくでしょう:
- ページレイアウトが左サイドバーナビゲーションと右コンテンツエリアに変更されました。
- 左サイドバーには、「Hello, Frontmatter! 」と「Environment Setup Guide」の2つのリンクが自動的にリストされています。

しかし、小さな問題があります:
サイドバーの「Environment Setup Guide」をクリックすると、ブラウザは /docs/setup にジャンプします。この時点で、「コンテンツが見つかりません」というエラーが表示されるでしょう。
これは、現在の main.py には ハードコーディングされた /docs/hello ルートしかないため、/docs/setup を処理しないからです。
要約
ファイルシステムをスキャンすることで、ドキュメントサイトに目次を自動生成する機能を持たせました。docs/ フォルダに追加または削除した.mdファイルがいくつあっても、サイドバーは自動的に更新されます。
サイドバーはスマートですが、ルーティングは「ぎこちない」です。ルートアドレスはハードコーディングされているためです。もし100個のドキュメントがあったら、main.py に100個の @app.get("/docs/xxx") 関数を書くわけにはいかないでしょう?それは明らかに実行可能ではありません。
/docs/{any_filename} へのリクエストをキャプチャし、対応するMarkdownファイルを自動的に見つけるための汎用的な方法が必要です。
次の記事では、ファイルパスベースの動的ルーティングを実装し、404エラー問題を完全に解決し、サイドバーのすべてのリンクが実際に対応する記事にナビゲートすることを保証します。
その他
サイトを構築した後、オンラインで公開して他の人に見てもらいたいと思うかもしれません。しかし、ほとんどのクラウドプラットフォームは高価であり、このよう練習プロジェクトにお金を払うのは割に合いません。
デプロイにもっと経済的な方法はありませんか?Leapcellを試してみてください。Python、Node.js、Go、Rustのような複数の言語のデプロイをサポートしており、毎月十分な無料枠を提供しており、1円もかけずに最大20個のプロジェクトをデプロイできます。

