mcpField
R
remix-v2-meta-sessions

remix-v2-meta-sessions

github.com/existential-birds開発8540 回インストール11 週間前に更新
リポジトリ

remix-v2-meta-sessions について

Remix v2 のメタデータ、セッション、認証、CSRF 処理をレビューするためのリファレンススキルです。バージョン固有の API と、ルートメタデータや保護対象リクエストの判断手順を説明します。

主な機能

  1. 01Remix v2 meta 記述子の指針
  2. 02親ルートのメタデータ統合パターン
  3. 03セッション確定とフラッシュ処理
  4. 04loader と action の認証パターン
  5. 05CSRF の脅威とトークンの指針

活用シーン

  1. 01ルートメタデータの挙動確認
  2. 02セッション保存とフラッシュメッセージの検査
  3. 03保護対象 loader と action の監査
  4. 04変更リクエストの CSRF 対応評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/existential-birds/beagle --skill remix-v2-meta-sessions

インストール後、Claude Code で/remix-v2-meta-sessionsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
auth-csrf.md
links.md
meta-v2.md
sessions.md

詳細

Remix v2 の Meta、セッション、認証、CSRF

クイックリファレンス

v2 の meta は記述子オブジェクトの配列を返します — v1 のオブジェクト形式ではありません。 古いコードベースでは v1 形式のオブジェクトリテラルでも型チェックを通りますが、実行時にはタグが一切レンダリングされません。

// app/routes/posts.$slug.tsx
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  if (!data?.post) return [{ title: "Not Found" }];
  return [
    { title: `${data.post.title} | My Blog` },
    { name: "description", content: data.post.excerpt },
    { property: "og:title", content: data.post.title },
    { tagName: "link", rel: "canonical", href: data.post.url },
  ];
};

安全なデフォルト設定とシークレットローテーションを備えた Cookie セッションストレージ:

// app/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";

type SessionData = { userId: string };
type SessionFlashData = { error: string };

const SESSION_SECRET = process.env.SESSION_SECRET;
if (!SESSION_SECRET) throw new Error("SESSION_SECRET is required");

export const { getSession, commitSession, destroySession } =
  createCookieSessionStorage<SessionData, SessionFlashData>({
    cookie: {
      name: "__session",
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
      maxAge: 60 * 60 * 24 * 30,
      secrets: [
        SESSION_SECRET,
        ...(process.env.SESSION_SECRET_OLD ? [process.env.SESSION_SECRET_OLD] : []),
      ],
    },
  });

ドキュメントのヘッド:meta と links

<Meta /> と <Links /> は root.tsx の <head> 内に配置する必要があります。 <ScrollRestoration />、<Scripts />、<LiveReload /> は <body> の末尾に配置します。どちらかの集約コンポーネントが欠けていると、コンパイルエラーが発生しないまま「css doesn't load」や「meta tags missing」が発生します。

// app/root.tsx
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

<Meta /> と <Links /> の集約方法は異なります。 <Links /> はルートマッチのチェーン全体をたどり、マッチしたすべてのルートの links エクスポートをレンダリングします。つまり、リーフルートで宣言したスタイルシートは自動的にレンダリングされ、画面遷移でそのルートを離れるとアンロードされます。一方、<Meta /> は集約しません。Remix は最後にマッチしたルートの meta 配列のみを選択します。meta で親ルートから継承するには、matches を明示的に flatMap します。

import type { MetaFunction } from "@remix-run/node";
import type { loader as projectLoader } from "./project.$pid";

export const meta: MetaFunction<
  typeof loader,
  { "routes/project.$pid": typeof projectLoader }
> = ({ data, matches }) => {
  const parentMeta = matches.flatMap((m) => m.meta ?? []);
  const project = matches.find((m) => m.id === "routes/project.$pid")?.data;
  return [
    ...parentMeta,
    { title: `${data?.task.name} | ${project?.name}` },
  ];
};

MetaFunction の2つ目のジェネリック型(ルート ID をキーとする)は、親ルートの matches.find(...).data に型を付けます。詳しくは references/meta-v2.md を参照してください。

セッション

変更を伴うすべてのレスポンスでは、commitSession を Set-Cookie ヘッダーとして付与する必要があります。Remix は自動でコミットしません。session.set(...) を呼び出して通常の json(data) を返すと、変更は何も通知されずに破棄されます。

return redirect("/dashboard", {
  headers: { "Set-Cookie": await commitSession(session) },
});

session.flash(key, value) は一度だけ読み取られます。値を読み取るローダーでも、フラッシュをクリアするために commitSession を呼び出す必要があります。詳しくは references/sessions.md を参照してください。

認証:ローダーから redirect をスローする

標準的なパターンは、未認証のリクエストに対して redirect() をスローする requireUserId(request) ヘルパーを使うことです。スローされたレスポンスによってローダーの処理は中断されるため、最上位で return する必要はありません。

// app/auth.server.ts
import { redirect } from "@remix-run/node";
import { getSession } from "./session.server";

export async function requireUserId(request: Request): Promise<string> {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");
  if (!userId) {
    const url = new URL(request.url);
    const redirectTo = `${url.pathname}${url.search}`;
    throw redirect(`/login?redirectTo=${encodeURIComponent(redirectTo)}`);
  }
  return userId;
}

React コンポーネント内でルートへのアクセスを制御してはいけません。保護対象のコンポーネントも SSR され、認証されていないユーザーに HTML やローダーデータが送信されます。詳しくは references/auth-csrf.md を参照してください。

CSRF

Remix には組み込みの CSRF 保護がありません。 同一オリジンの <Form> による POST は、セッション Cookie に設定した SameSite の値だけに全面的に依存します。 SameSite=Lax は、現在のすべてのブラウザーでクロスサイト POST ナビゲーション時の Cookie をブロックします。(Chrome では2020年に「Lax+POST」の2分間の猶予期間が一時的に設けられましたが、2021年に廃止されました。)実際の Lax と Strict のトレードオフは、サブドメインの乗っ取りです。Lax では、侵害されたサブドメインから認証情報を伴うトップレベルの GET ナビゲーションを開始できます。一方、Strict では、外部サイトからのディープリンクによるナビゲーションでセッションが失われます。 OAuth ポップアップや iframe の埋め込みなど、正当なクロスサイト利用のために SameSite=None を使うアプリには、Cookie レベルの CSRF 保護が一切ありません。 remix-utils/csrf と専用の署名付き Cookie の利用を推奨します。セッション Cookie を再利用してはいけません。 手動の fetch("/api/x", { method: "POST" }) は AuthenticityTokenInput を迂回するため、csrf.validate(request) を呼び出さないアクションは攻撃者の侵入口になります。

ゲート(判断の順序)

順番に回答してください。条件が真ならパスです。同じ行に示された API を選び、そこで終了してください。

この meta タグはどこに配置しますか?
  1. サイト全体に適用されますか(charset、viewport、デフォルトの OG 画像など)?
    • パス → root.tsx の <head> 内にプレーンな JSX を記述します。v2 のマージされない仕様による予想外の動作を避け、タグの重複も防げます。終了。
    • 不合格 → ステップ2。
  2. 特定のルートに適用されますか(title、description、canonical、JSON-LD など)?
    • パス → リーフルートのファイルで export const meta を宣言します。親ルートの値が必要な場合は、matches.flatMap((m) => m.meta ?? []) を使います。終了。
認証チェック:loader、action、それともヘルパー?
  1. 保護が必要な GET(ページのレンダリング)ですか?
    • はい → loader の先頭で await requireUserId(request) を呼び出します。終了。
  2. 保護が必要な POST/PUT/DELETE の変更処理ですか?
    • はい → action の先頭で await requireUserId(request) を呼び出し、さらに await csrf.validate(request) を呼び出します。終了。
  3. ログアウトですか?
    • はい → action のみを使用し、loader は使用しません。loader を指す <Link to="/logout"> は、<img src="/logout"> 経由で CSRF 攻撃が可能です。<Form method="post" action="/logout"> を使用します。終了。
CSRF トークンはどこに保存されますか?
  1. remix-utils/csrf を使用していますか?
    • はい → セッション Cookie とは別に、専用の createCookie("csrf", { ... }) Cookie を使用します。CSRF 値は署名付き文字列ですが、セッション値はシリアライズされたオブジェクトです。同じ Cookie を再利用すると、検証時にエラーになります。終了。
    • いいえ → ステップ 2 へ。
  2. CSRF ライブラリを使用していませんか?
    • はい → 脅威モデルを文書化し、セッション Cookie に sameSite: "strict" を必須としたうえで、すべての action で Origin ヘッダーを検証します。代わりに remix-utils/csrf を追加することをおすすめします。

追加ドキュメント

  • Meta v2:記述子の型、matches による親からのマージ、JSON-LD、v1 から v2 への移行時の落とし穴については、references/meta-v2.md を参照してください。
  • リンク:スタイルシート、プリロード、DNS プリフェッチ、<Links /> の親からの集約動作については、references/links.md を参照してください。
  • セッション:createCookieSessionStorage の設定、commitSession/destroySession のパターン、フラッシュメッセージ、データベースを使用したセッションについては、references/sessions.md を参照してください。
  • 認証と CSRF:requireUserId ヘルパー、ログイン/ログアウトの action、remix-auth、remix-utils/csrf の設定については、references/auth-csrf.md を参照してください。

v1 と v2 の簡易比較

項目v1v2
meta の戻り値の形式オブジェクト { title, description }配列 [{ title }, { name, content }]
親の meta のマージ自動的にマージ(キーごとに後勝ち)マージなし。一致する最後のルートのみ
親データ用の meta 引数parentsDatamatches(手動で flatMap)
OG タグ{ "og:title": "..." } の省略形{ property: "og:title", content: "..." }
移行フラグ将来のフラグ v2_meta: true該当なし(v2 のデフォルト)

コメント