zudo-doc
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

末尾スラッシュポリシー

作成 2026年4月28日更新 2026年6月20日Takeshi Takatsudo

zfbがビルド時とランタイムで末尾スラッシュの適用をどのように分担するかについての解説。

概要

zudo-docは「常に末尾スラッシュあり」のURLポリシーを採用しています。すべてのページは末尾が / で終わるパスに配置されます。例えば /docs/getting-started ではなく /docs/getting-started/ です。

zfbは静的サイトビルダーのみであり、サーバーサイドのミドルウェアレイヤーはありません。そのため、適用は異なる担当者を持つ2つの関心事に明確に分割されます。

Astroについての背景

zudo-docはもともとAstroで構築されており、2箇所で末尾スラッシュを強制していました。ビルド時には trailingSlash: "always" の設定により dist/docs/getting-started/index.html が出力され、src/middleware-handler.ts がパスに末尾スラッシュのないリクエストに対して301リダイレクトを発行していました。 zfbでは、この2つの関心事は以下で説明するのと同じ分割(ビルド時とデプロイホスト)に対応しますが、それぞれの強制メカニズムは異なります。

ビルド時 — zfb / url-normalizerの担当

静的ビルド中に出力されるすべてのURLは / で終わる必要があります。これは @takazudo/zudo-doc/url-normalizerbuildUrl() によって強制されます:

import { buildUrl } from "@takazudo/zudo-doc/url-normalizer";

// paths()の実装やインテグレーションで:
const url = buildUrl("docs", slug);          // "/docs/my-slug/"
const root = buildUrl();                      // "/"
const localized = buildUrl("ja", "docs", slug); // "/ja/docs/my-slug/"

buildUrl() は主要なヘルパーです。パスセグメントを結合し、冗長なスラッシュを除去し、結果を /<joined>/ でラップします。以下の箇所で使用されています:

  • pages/sitemap.xml.tsx<loc> エントリの生成。

  • コレクションスラッグをルートパスにマップするzfbの paths() エクスポート。

  • インテグレーションレベルのURL出力(llms.txt、claude-resourcesなど)。

外部データからパス名が届き、末尾スラッシュがあるかどうか不明な場合のために、補助ヘルパー normalizePathname() も用意されています:

import { normalizePathname } from "@takazudo/zudo-doc/url-normalizer";

const canonical = normalizePathname(rawPathname); // 末尾スラッシュがない場合は追加

スキップルール

normalizePathname() とその内部の shouldSkipNormalization() は、以下のスキップルールを適用します:

条件動作
すでに / で終わっている何もしない
最後のセグメントに文字で始まる拡張子がある(.js.css.png など)何もしない — 静的アセット
/_astro/ または /_image で始まる何もしない — クロスフレームワーク間のアセットパス互換性のため保持
/docs/v2.0 のようなバージョン文字列(ドットの後に数字)正規化される — 拡張子とは扱われない

ランタイム — デプロイホストの担当

訪問者が末尾スラッシュなしで https://example.com/docs/guide と入力した場合、リクエストをインターセプトするフレームワークのコードはありません。静的ファイルはすでにデプロイされています。デプロイホストがリダイレクトを発行する責任を持ちます。

Cloudflare Pages

public/_redirects に以下のルールを追加します。ページ固有のリダイレクトルールより前に記述する必要があります(Cloudflare Pagesはルールを上から順に評価し、最初にマッチしたルールで停止します):

# 末尾スラッシュなしのURLを正規形にリダイレクトする。
/:splat  /:splat/  301

ホストがランタイムを担当する理由

ランタイムリダイレクトをフレームワークコードではなくホストに委ねることで、_redirects(または同等の)ファイルを変更するだけでデプロイターゲットを切り替えられます。Cloudflare Pages、Netlify、Vercel、plain nginxのいずれでも対応可能です。フレームワークはプラットフォームに依存しない状態を保てます。

Cloudflare Workers Static Assets

zudo-docは現在、Cloudflare PagesではなくCloudflare Workersにデプロイされ(wrangler.toml)、ビルド済みの dist/ をWorkersの静的アセットレイヤーを通じて配信します。静的ビルドはすでに末尾スラッシュのディレクトリ構造(dist/docs/getting-started/index.html)を出力しているため、Workersのアセットレイヤーは各正規 /<route>/ URLをビルド済みの index.html から直接配信します。

そのため、Pagesセクションで説明している手動の public/_redirects ルールはWorkersでは配信されません。これはPages → Workersへの移行(zudolab/zudo-doc#1691)で廃止されました。関連する wrangler.toml の設定は以下のとおりです:

[assets]
directory = "./dist"
not_found_handling = "404-page"   # マッチしないGETに対して dist/404.html を配信
run_worker_first = false          # アセットレイヤーがワーカーより先に参照される

Note

上記のPagesセクションは参考のために残してあります。このサイトをCloudflare Pagesや他のホストにデプロイする場合は、引き続き適用されます。Workersの静的アセットでは、プラットフォーム組み込みのアセット処理が末尾スラッシュのケースをカバーするため、_redirects ファイルはデプロイ成果物に含まれません。

その他のホスト

ホストメカニズム
Netlifypublic/_redirects — 同じ /:splat /:splat/ 301 構文
Vercelvercel.json"trailingSlash": true
nginxtry_files + return 301 $uri/
AWS CloudFrontviewer-requestイベント用のLambda@EdgeまたはCloudFront Function

まとめ

レイヤー担当メカニズム
ビルド時のURL構築zfb / url-normalizerbuildUrl() — 常に /<path>/ を出力
ビルド時のパス名補正zfb / url-normalizernormalizePathname()
ランタイムリダイレクト(スラッシュなし→あり)デプロイホスト_redirects またはホスト設定

この分割により、各関心事が適切な場所に置かれ、問題が発生した際にどのレイヤーが担当しているかを容易に把握できます。

Revision History

Takeshi Takatsudo作成: 2026-04-28T20:41:06+09:00更新: 2026-06-20T07:20:58Z

AI Assistant

Ask a question about the documentation.