ファイルベースルーティングの仕様と使い方。

基本概念

ファイルの配置がそのままURLパスになる。ルーター設定は不要。

lib/app/page.ex               /
lib/app/about/page.ex         /about
lib/app/users/page.ex         /users
lib/app/users/[id]/page.ex    /users/:id

静的ルーティング

基本的な対応

ファイルパスURL
app/page.ex/
app/about/page.ex/about
app/docs/guide/page.ex/docs/guide

インデックスページ

ディレクトリ配下の page.ex が該当パスを担当する。

app/users/page.ex     /usersユーザー一覧
app/users/[id]/page.ex  /users/123ユーザー詳細

動的ルーティング

ブラケット記法

[パラメータ名] でURLパラメータを受け取る。

app/users/[id]/page.ex     /users/123, /users/abc など
app/posts/[slug]/page.ex   /posts/hello-world など

paramsへのアクセス

mount/2 の第1引数で受け取る。パスパラメータとクエリパラメータが両方含まれる:

defmodule MyApp.Users.Id do
  use Dialup.Page

  # /users/123?tab=posts にアクセス
  def mount(params, assigns) do
    id  = params["id"]    # "123"(パスパラメータ)
    tab = params["tab"]   # "posts"(クエリパラメータ)
    user = Users.get!(id)
    {:ok, %{user: user, tab: tab || "profile"}}
  end
end

テンプレートからは @params でもアクセスできる:

<p>現在のタブ: {@params["tab"]}</p>

クエリパラメータ

クエリ文字列(?key=value)は自動的に params に含まれる。複数パラメータにも対応:

/search?q=elixir&page=2&sort=date
 params = %{"q" => "elixir", "page" => "2", "sort" => "date"}

パスパラメータとクエリパラメータで同名のキーがある場合、パスパラメータが優先される。

パターンマッチ

異なるパラメータ値で分岐できる:

# /users/new → 新規作成画面
def mount(%{"id" => "new"}, assigns) do
  {:ok, assigns |> overwrite(%{mode: :new, user: nil})}
end

# /users/:id → 編集画面
def mount(%{"id" => id}, assigns) do
  user = Users.get!(id)
  {:ok, assigns |> overwrite(%{mode: :edit, user: user})}
end

レイアウトの継承

レイアウトファイルの配置

app/layout.ex              # 全ページの共通レイアウト
app/users/layout.ex        # /users 以下のレイアウト
app/users/[id]/layout.ex   # /users/:id 以下のレイアウト(あれば)

レイアウトの適用ルール

URL /users/123/profile の場合:

  1. app/layout.ex(最上位)
  2. app/users/layout.ex(中間)
  3. app/users/[id]/page.ex または app/users/profile/page.ex(該当ページ)

レイアウトは上から順にネストされる。

ルーティングの優先順位

  1. 静的ルートを先に探索(完全一致)
  2. 動的ルートにフォールバック(パターンマッチ)
  3. マッチしない場合は404
app/users/new/page.ex       静的: /users/new
app/users/[id]/page.ex      動的: /users/:id

/users/new       静的ルートが優先
te/users/123      動的ルートがマッチ

コロケーション CSS

page.ex / layout.ex と同じディレクトリに同名の .css ファイルを置くと、コンパイル時に自動検出・スコーピングされる。

app/
 layout.ex
 layout.css       全ページ共通スタイル
 page.ex
 page.css         / のみに適用
 docs/
     layout.css   /docs 配下全ページに適用
     page.css     /docs トップのみに適用

スコーピングの仕組み

モジュール名の MD5 先頭 7 文字から d-xxxxxxx というスコープクラスを生成し、CSS をネイティブ CSS nesting でラップする:

/* page.css に書いたもの → コンパイル後 */
.d-a1b2c3 {
  h1 { color: blue; }
  .card { padding: 1rem; }
}

レンダリング時にそのページの HTML が <div class="d-a1b2c3">...</div> でラップされる。CSS は <style data-dialup-css> としてその直前に注入される。

layout.css のカスケード

layout.css のスコープ div は全子ページの HTML を包むため、layout.css に書いたスタイルは配下の全ページに自然にカスケードする:

<div class="d-layout">           <!-- app/layout.css のスコープ -->
  <header>...</header>
  <div class="d-docs-layout">    <!-- docs/layout.css のスコープ -->
    <div class="d-docs-page">    <!-- docs/page.css のスコープ -->
      <h1>Docs</h1>
    </div>
  </div>
</div>

注意事項

  • CSS が存在しないページでは <style> タグも wrapper div も出力されない(オーバーヘッドゼロ)
  • .css 変更時は @external_resource 経由で対応する .ex が自動再コンパイルされ、ホットリロードが動作する
  • CSS nesting はモダンブラウザ 96%+ でサポート(Chrome 120+, Firefox 117+, Safari 17.2+)

レイアウトのオプトアウト

特定のページでレイアウト継承を無効にし、全画面表示にしたい場合は @layout false を指定する。

defmodule Dialup.App.Login.Page do
  use Dialup.Page

  @layout false

  def render(assigns) do
    ~H"""
    <div class="fullscreen-login">
      <h1>ログイン</h1>
      <form ws-submit="login">
        <input name="email" placeholder="Email" />
        <input name="password" type="password" />
        <button type="submit">ログイン</button>
      </form>
    </div>
    """
  end
end

@layout false を指定すると、親ディレクトリの layout.ex は適用されず、ページの HTML が直接 Shell に挿入される。コロケーション CSS(page.css)は @layout false でも適用される。

エラーページ

カスタムエラーページ

app/ ディレクトリに error.ex を置くと、404 や 500 エラー時にユーザー定義のエラーページが表示される。layout.ex と同様にディレクトリ階層で継承される。

app/
 layout.ex           # 全ページ共通レイアウト
 error.ex            # 全ページ共通エラーページ
 page.ex
 admin/
     layout.ex       # /admin 配下の共通レイアウト
     error.ex        # /admin 配下のエラーページ(より具体的)
     page.ex

error.ex の書き方

use Dialup.Error を使い、render/2 でステータスコードごとに表示を分岐する。

defmodule Dialup.App.Error do
  use Dialup.Error

  def render(404, assigns) do
    ~H"""
    <div class="error-page">
      <h1>404</h1>
      <p>ページが見つかりませんでした</p>
      <a ws-href="/">ホームへ戻る</a>
    </div>
    """
  end

  def render(500, assigns) do
    ~H"""
    <div class="error-page">
      <h1>500</h1>
      <p>サーバーエラーが発生しました</p>
    </div>
    """
  end

  def render(_status, assigns) do
    ~H"""
    <div class="error-page">
      <h1>{@status}</h1>
      <p>エラーが発生しました</p>
    </div>
    """
  end
end

エラーページの継承

リクエストパスに最も近い error.ex が選択される。

  • /admin/users/123 で 404 → app/admin/error.ex があればそれ、なければ app/error.ex
  • error.ex が一つも存在しなければフレームワークのデフォルト表示が使われる

レイアウトとの関係

エラーページは通常のページと同様に layout に囲われて出力される。全画面のエラーページにしたい場合は @layout false を指定する。

defmodule Dialup.App.Error do
  use Dialup.Error

  @layout false  # layout なしの全画面エラーページ

  def render(404, assigns) do
    ~H"""
    <div class="fullscreen-error">
      <h1>404 Not Found</h1>
    </div>
    """
  end
end

assigns に渡される情報

%{
  status: 404,            # HTTP ステータスコード
  message: "Not Found"    # ステータスメッセージ
}

500 エラーの場合、開発環境(Mix.env() == :dev)ではさらに exceptionstacktrace が含まれる。

コロケーション CSS

error.csserror.ex と同じディレクトリに配置すると、ページや layout と同様に自動スコーピングが適用される。

制限事項

  • 正規表現やカスタムパターンマッチは不可
  • [id] はURL全体のセグメントにマッチ(/ を含まない)
  • オプショナルパラメータは不可(/users/users/:id は別ファイルが必要)