Azure Static Web Apps × 認証付きドキュメント基盤の裏側 
~第2弾:Pandoc + GitHub Actions 実装編~

2026年3月16日掲載

キービジュアル

前回の記事では、プロダクトに関する技術仕様書をGoogle Docsでの管理から脱却し、Azure Static Web Apps(以下、SWA)とAzure AD B2Cを組み合わせて、複数テナントに対応した認証付きドキュメント公開基盤を構築する背景とインフラ構成について解説しました。

第2弾となる本記事では、より実装に踏み込んだ内容をお届けします。具体的には、GitHubにプッシュされたMarkdownがどのようにして「見やすく、管理しやすいHTML」へと変換され、デプロイされるのか。その裏側にあるPythonスクリプトによる自動採番ロジックや、Azure Blob Storageを中間層に置いたGitHub Actionsのワークフローについて詳しくご紹介します。

※注意

第1弾でも触れましたが、2025年5月1日以降、Azure AD B2C は新規顧客向けの購入ができなくなっています(既存テナントでは利用可能です)。

目次

この記事では
  • PandocとPythonを用いたドキュメントビルドの具体的な実装方法について説明しています。
  • 正規表現を活用した見出しや図表の自動採番ロジックを紹介しています。
  • Azure Blob Storageをデータストアとして活用し、複数バージョンのドキュメントを並行管理するGitHub Actionsの運用戦略について解説しています。

はじめに

私たちは、生成AIパッケージの開発に関連する技術仕様書を、社内関係者向けに操作手順や設定方法などを記述したドキュメントとして整備・管理しています。

こうした技術仕様書は、常に最新のシステム仕様を反映している必要があります。しかし、更新のたびに手動でHTMLを書き換えたり、複雑なリンク構造をメンテナンスしたりするのは、エンジニアにとって大きな負担となっていました。

「エンジニアはMarkdownを書くだけ」という体験を実現するために、以下の3点を自動化の柱として据えました。

  1. 構成管理の自動化: 複数のMarkdownファイルを一つのドキュメントとして統合
  2. 品質の自動化:見出し・図表番号の自動採番、目次生成

  3. 公開の自動化:ブランチ・タグに応じたプレビュー環境と本番環境の出し分け

本記事では、これらの仕組みを支えるコードとパイプラインの設計を順に順を追って解説します。

Pandoc × Python によるビルドプロセスの設計

ドキュメントの核となるHTML変換には、強力なドキュメント変換ツールである Pandoc を採用しました。これをPythonから制御することで、複数ファイルの統合、採番、目次生成などの前処理をまとめて自動化しています。

全体の処理フローは以下のようになります。

1. 構成定義(YAML)に基づいたファイルの結合

YAML形式の定義ファイルで指定された順序に従って Markdown ファイルを読み込みます。この際、単に結合するだけではありません。フォルダ単位で指定された箇所については、読み込んだYAML上のキー(フォルダ名)を「H1 見出し」として挿入し、その配下のファイルを順次結合していく処理を行っています。これにより、ファイルやフォルダ構成を変更するだけでドキュメントの章立てを柔軟に入れ替えることが可能です。

2. 動的コンテンツ生成のための前処理

結合されたMarkdownテキストに対し、Python スクリプトを用いて、以下の処理を順に適用します。

  • 見出し(H1〜H4)の自動採番:見出しレベルに応じた採番(例: 1.1, 1.1.2)と、リンク用のアンカーIDを付与
  • 目次(TOC)の自動生成:生成された番号付き見出しをもとに、目次用Markdownを作成

  • 図表キャプションの自動採番とHTML化:「図 :」「表 :」といった独自の記法を検出し、章番号と連動した番号(例:図2.1)を付与してHTMLタグへ変換

このように、Pandocに渡す前段で「ドキュメントとして読みやすい状態」に加工してからHTML変換に進む構成です。  なお、"表紙"コンポーネントのみは採番や目次の対象外とするため、これらの処理が完了した後に、最終的なMarkdownの先頭へ結合しています。

3. Pandoc による HTML 変換と Luaフィルタの適用

前処理済みのMarkdownを Pandoc に渡し、最終的なHTMLを1ファイルとして出力します。

ここで重要な役割を果たすのが Luaフィルタです。

Luaフィルタを使うことで、標準の Markdown 記法を拡張し、以下のようなスタイル適用を自動化しています。

  • 注釈の変換:> [!NOTE] や > [!WARNING] といったエンジニアに馴染み深い GitHub Markdown のアラート記法を検知し、HTML 出力時に対応する CSS クラスを持つ div タグへと構造変換

  • アイコンの自動挿入:フィルタ内で SVG アイコンのパスを指定し、注釈の種類に応じたアイコンを自動的に付与

自動採番ロジックの実装

手動で「1.1」「1.2」と番号を振るのは労力も大きく、ズレの原因にもなります。そこで、正規表現を用いて見出しを検出し、階層に応じた番号を動的に付与する処理を実装しました。

以下は、見出しレベル(H1〜H4)ごとのカウンタを更新し、番号とアンカーを付与する実装の抜粋です。

def add_heading_numbers(self, md_text: str) -> str:
    lines = md_text.splitlines()
    result = []
    self.heading_counters = [0] * 4
    in_code_block = False

    for line in lines:
        if re.match(r"^```", line.strip()):
            in_code_block = not in_code_block
            result.append(line)
            continue

        if not in_code_block:
            heading_match = re.match(r"(#{1,4})\s+(.*)", line) 
            if heading_match:
                level = len(heading_match.group(1))
                title = heading_match.group(2)

                self.heading_counters[level - 1] += 1
                for i in range(level, 4):
                    self.heading_counters[i] = 0
                numbering = ".".join(str(self.heading_counters[i]) for i in range(level) if self.heading_counters[i] > 0)

                full_title = f"{numbering} {title}"
                anchor = slugify(full_title)
                result.append(f"{'#' * level} {full_title} {{#{anchor}}}")
                continue

        result.append(line)

    return "\n".join(result)

この処理のポイントは、トリプルバッククォート(```)のコードフェンスを検知してトグル管理し、コードブロック内の # を見出しとして誤検知しないようにしている点です。これにより、サンプルコード内のコメントが誤って採番されるのを防いでいます。

また、採番された見出しには見出し文字列から生成したアンカー(slug)が付与されるため、後述の目次生成でリンク先として利用できます。

図表番号と目次の自動生成

技術ドキュメントにおいて、「図1.1」や「表2.1」といった参照番号は、情報の正確性を担保するために欠かせません。これらを全て手動で管理すると、途中に図を一つ挿入しただけで以降の番号を全て書き直す必要が出てしまいます。

本システムでは、見出し採番後のMarkdownをもとに、章番号(H1の番号)と連動した図表番号を動的に生成・付与します。

章番号と連動したカウントロジック

単なる通し番号ではなく、「どの章の何番目の図か」を明確にするため、スクリプト内では以下のフローで処理を行っています 。

1.章番号の保持

採番済みの#(H1)を検出するたびに、その番号(例:2章なら 2)を current_section として保持し、図表のカウンタをリセットします。

2.キャプションの検出

正規表現を用い、行頭が「図 : 」または「表 : 」で始まる行を抽出します。

3.HTML変換と採番

 <div class="figure-caption"> / <div class="table-caption"> に変換し、「図{章}.{連番}」や「表{章}.{連番}」といった形式で番号を付与します。 

実装コードは以下の通りです。

def add_figure_and_table_numbers(self, md_text: str) -> str:
    lines = md_text.splitlines()
    current_section = "0"
    fig_count = 0
    table_count = 0
    result = []

    for line in lines:
        heading_match = re.match(r"#\s+([\d\.]+)\s+", line)
        if heading_match:
            current_section = heading_match.group(1)
            fig_count = 0
            table_count = 0
            result.append(line)
            continue

        caption_match = re.match(r"\s*図\s*[::]\s*(.+)", line)
        if caption_match:
            fig_count += 1
            caption_text = caption_match.group(1).strip()
            new_caption = f'<div class="figure-caption">図{current_section}.{fig_count} {caption_text}</div>'
            result.append(new_caption)
            continue

        table_caption_match = re.match(r"\s*表\s*[::]\s*(.+)", line)
        if table_caption_match:
            table_count += 1
            caption_text = table_caption_match.group(1).strip()
            new_caption = f'<div class="table-caption">表{current_section}.{table_count} {caption_text}</div>'
            result.append(new_caption)
            continue

        result.append(line)

    return "\n".join(result)

Markdown上では、以下のようにシンプルに記述するだけです。

![](images/network.png) 
図 : ネットワーク構成図

これがビルドプロセスを通ることで、以下のようなHTMLへと自動変換されます 。

<div class="figure-caption">図2.1 ネットワーク構成図</div>

このロジックにより、ドキュメントの構成を変更しても、ビルドを実行するだけで図表番号が再計算され、整合性を保った状態にできます。

ユーザビリティを高める目次(TOC)の生成

ドキュメントが長大になると、目的の情報へ素早くアクセスするための目次が不可欠です。本システムでは、採番済みの見出し(H1〜H4)を再スキャンし、各項目のレベルに応じたインデントを持つ目次を自動生成しています。

目次の各項目には、見出しに付与されたアンカーへのリンクが設定されます。これにより、単一の巨大なHTMLファイルであっても、ページ上部の目次から各セクションへ即座にジャンプできます。

実装コードは以下の通りです。

def generate_toc(self, md_text: str) -> str:
    toc = []
    for line in md_text.splitlines():
        match = re.match(r"(#{1,4})\s+([\d\.]+\s+.+?)\s+\{#([^\}]+)\}", line)
        if match:
            level = len(match.group(1))
            title = match.group(2)
            anchor = match.group(3)
            indent = "  " * (level - 1)
            toc.append(f"{indent}- [{title}](#{anchor})")
    return "\n".join(toc)

GitHub Actions と Blob Storage によるデプロイ戦略

ビルドされた成果物をSWAにデプロイする際、単にstatic-web-apps-deployアクションを実行するだけでなく、Azure Blob Storageを中間層として活用する設計を採用しました。

なぜ直接 SWA に上げないのか?

通常のSWAデプロイでは、デプロイのたびにコンテンツが入れ替わってしまいます。しかし、技術仕様書の運用においては「過去の特定バージョン」を保存し続けたり、「複数の開発中ブランチ」のプレビューを同時に提供したりする必要があります。これらを一つのカスタムドメイン配下で共存させるため、Blob Storageを利用しています。

GitHub Actions ワークフローの詳細

実際の運用を支えるワークフローは、以下のステップで構成されています。

条件に応じたビルドと配布 

プッシュされたのが「ブランチ」か「タグ」かを判定し、Blob Storage内の適切なディレクトリへ成果物を振り分けます。

  1. Build:Pandoc を使用して Markdown を HTML へ変換

  2. Versioning: 既存の versions.html を Blob からダウンロードし、新規タグの場合はリンクを追記して更新

  3. Upload to Blob:生成された HTML や画像を一式アップロード

    1. ブランチ push → docs/branches/<branch名>/ へアップロード

    2. タグ push → docs/tags/<tag名>/ へアップロードおよび versions.htmlの更新

コンテンツの同期 (Sync)

Blob Storage内のdocsコンテナ配下の全コンテンツ(作成済みの branches, tagsフォルダを含む)を dist/ フォルダへ一括ダウンロードします。これにより、SWAへデプロイするフォルダには「最新のビルド結果」だけでなく、「過去のバージョン」や「他のブランチのプレビュー」も全て含まれた状態になります。

SWAへのデプロイ

最終的に整った dist/ フォルダの内容をAzure/static-web-apps-deployアクションを使って SWA へアップロードします。

この構成により、新しくデプロイしても過去のアーカイブが消去されることなく、常にすべてのバージョンを包含した状態で仕様書を公開することが可能になりました。

マージ後のクリーンアップ

Pull Requestがmasterにマージされると、最新のドキュメントをルート(本番環境)に反映すると同時に、不要になったブランチ用ディレクトリを削除してストレージの肥大化を防ぎます。

- name: ☁️ Delete Branch Blob Artifacts
  run: |
    BRANCH_NAME="${{ github.head_ref }}"
    az storage blob delete-batch \
      --account-name "$AZURE_BLOB_ACCOUNT_NAME" \
      --account-key "$AZURE_BLOB_ACCESS_KEY" \
      --source "docs" \
      --pattern "branches/${BRANCH_NAME}/*"

最後にswa deployコマンドを用いて、最新のdist内容をプロダクション環境へ反映させ、一連のサイクルが完了します。

開発を支える運用機能

Pull Requestによるプレビュー通知

GitHub Actions のワークフローでは、Pull Requestがオープンされた際、そのブランチ専用のプレビュー URL をPull Requestのコメントに自動投稿する仕組みを構築しています。

- name: 💬 Post comments to PR
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    BRANCH="${{ github.head_ref }}"
    URL="${BASE_URL}/branches/${BRANCH}/index.html"
    echo "Preview: ${URL}" > ./comments
    gh pr comment ${{ github.event.pull_request.number }} -F ./comments

これにより開発者は、Markdown のソースコード差分を追うだけでなく、実際の HTML としてのレンダリング結果(図表の自動採番やスタイル適用状況)を即座に確認できます。なお、前述の通り Pull Requestがmasterにマージされると、Blob 内のブランチ用ディレクトリは az storage blob delete-batch によって自動的にクリーンアップされ、ストレージの肥大化を防ぎます。

タグ付けによる「過去バージョン」の自動管理

リリースに合わせて「その時点の仕様書」をアーカイブ保存するため、Git タグ(例: v1.0)をプッシュするだけで過去バージョン一覧が更新される仕組みを実装しました。

1.タグのプッシュ

git tag v1.0 などを実行してプッシュします。

2.HTML の一元管理と自動更新

バージョン一覧を管理する専用のHTMLファイルを Blob Storage の所定のパスで一元管理しています。ワークフロー内ではこれを一度ダウンロードし、Python スクリプトで新しいタグへのリンクをリストの先頭に追記した後、再度アップロードしています。

def update_versions_html(tag: str, file_path: Path):
    with open(file_path, encoding="utf-8") as f:
        soup = BeautifulSoup(f, "html.parser")

    ul_tag = soup.find("ul")
    if not ul_tag:
        ul_tag = soup.new_tag("ul")
        soup.append(ul_tag)

    for li in ul_tag.find_all("li"):
        a = li.find("a", href=True)
        if a and a["href"] == f"{tag}/index.html":
            print(f"Tag '{tag}' already exists in the HTML.")
            return

    new_li = soup.new_tag("li")
    new_a = soup.new_tag("a", href=f"{tag}/index.html")
    new_a.string = tag
    new_li.append(new_a)
    ul_tag.insert(0, new_li)

    with open(file_path, "w", encoding="utf-8") as f:
        f.write(str(soup))

3.ポータルへの反映

更新された一覧用HTMLファイルと、タグ付けされた時点のビルド成果物が Blob と SWA にデプロイされます。最新ドキュメントにある「リンク一覧はこちら」をクリックすると過去バージョンのリンク一覧ページに遷移し、ユーザーはいつでも任意の時点の仕様書へアクセス可能になります。

この自動化により、手動でのリンク更新ミスを排除し、常に正確なバージョン履歴を維持できる運用を実現しました。

おわりに

本記事では、Azure Static Web Appsを基盤としたドキュメント管理システムの「実装編」として、Pandocによるビルドプロセスから、GitHub Actionsによって自動化されたデプロイ運用までをご紹介しました。

Google DocsからMarkdownへの移行は、単なるツールの変更にとどまりません。エンジニアにとっては「コードと同じワークフローでドキュメントを管理できる」という大きなメリットがあります。Gitによるバージョン管理、Pull Requestを通じたレビュー、そしてGitHub Actionsによるビルドの自動化が組み込まれたことで、仕様書の品質は以前よりも格段に向上しました。

今回の取り組みが、社内ドキュメントの運用に課題を感じている方の参考になれば幸いです。

最後までお読みいただき、ありがとうございました。

関連サービス

セキュアなAzure OpenAI Service環境をパッケージとして提供するサービスです。よりスムーズに生成AIの導入を実現することができます。

MSP(Managed Service Provider)サービスは、お客さまのパブリッククラウドの導入から運用までをトータルでご提供するマネージドサービスです。

おすすめの記事

条件に該当するページがございません