remix-v2-meta-sessions について
Remix v2 のメタデータ、セッション、認証、CSRF 処理をレビューするためのリファレンススキルです。バージョン固有の API と、ルートメタデータや保護対象リクエストの判断手順を説明します。
主な機能
- 01Remix v2 meta 記述子の指針
- 02親ルートのメタデータ統合パターン
- 03セッション確定とフラッシュ処理
- 04loader と action の認証パターン
- 05CSRF の脅威とトークンの指針
活用シーン
- 01ルートメタデータの挙動確認
- 02セッション保存とフラッシュメッセージの検査
- 03保護対象 loader と action の監査
- 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.mdlinks.mdmeta-v2.mdsessions.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 タグはどこに配置しますか?
- サイト全体に適用されますか(charset、viewport、デフォルトの OG 画像など)?
- パス →
root.tsxの<head>内にプレーンな JSX を記述します。v2 のマージされない仕様による予想外の動作を避け、タグの重複も防げます。終了。 - 不合格 → ステップ2。
- パス →
- 特定のルートに適用されますか(title、description、canonical、JSON-LD など)?
- パス → リーフルートのファイルで
export const metaを宣言します。親ルートの値が必要な場合は、matches.flatMap((m) => m.meta ?? [])を使います。終了。
- パス → リーフルートのファイルで
認証チェック:loader、action、それともヘルパー?
- 保護が必要な GET(ページのレンダリング)ですか?
- はい →
loaderの先頭でawait requireUserId(request)を呼び出します。終了。
- はい →
- 保護が必要な POST/PUT/DELETE の変更処理ですか?
- はい →
actionの先頭でawait requireUserId(request)を呼び出し、さらにawait csrf.validate(request)を呼び出します。終了。
- はい →
- ログアウトですか?
- はい →
actionのみを使用し、loaderは使用しません。loaderを指す<Link to="/logout">は、<img src="/logout">経由で CSRF 攻撃が可能です。<Form method="post" action="/logout">を使用します。終了。
- はい →
CSRF トークンはどこに保存されますか?
remix-utils/csrfを使用していますか?- はい → セッション Cookie とは別に、専用の
createCookie("csrf", { ... })Cookie を使用します。CSRF 値は署名付き文字列ですが、セッション値はシリアライズされたオブジェクトです。同じ Cookie を再利用すると、検証時にエラーになります。終了。 - いいえ → ステップ 2 へ。
- はい → セッション Cookie とは別に、専用の
- CSRF ライブラリを使用していませんか?
- はい → 脅威モデルを文書化し、セッション Cookie に
sameSite: "strict"を必須としたうえで、すべての action でOriginヘッダーを検証します。代わりにremix-utils/csrfを追加することをおすすめします。
- はい → 脅威モデルを文書化し、セッション Cookie に
追加ドキュメント
- 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 の簡易比較
| 項目 | v1 | v2 |
|---|---|---|
meta の戻り値の形式 | オブジェクト { title, description } | 配列 [{ title }, { name, content }] |
| 親の meta のマージ | 自動的にマージ(キーごとに後勝ち) | マージなし。一致する最後のルートのみ |
親データ用の meta 引数 | parentsData | matches(手動で flatMap) |
| OG タグ | { "og:title": "..." } の省略形 | { property: "og:title", content: "..." } |
| 移行フラグ | 将来のフラグ v2_meta: true | 該当なし(v2 のデフォルト) |