Remix V2 Meta Sessions
/install remix-v2-meta-sessions
Remix v2 Meta, Sessions, Auth, and CSRF
Quick Reference
v2 meta returns an array of descriptor objects — NOT the v1 object shape.
A v1-style object literal still typechecks in stale codebases but renders no
tags at runtime.
// app/routes/posts.$slug.tsx
import type { MetaFunction } from "@remix-run/node";
export const meta: MetaFunction\x3Ctypeof 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 session storage with secure defaults and secret rotation:
// 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\x3CSessionData, 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] : []),
],
},
});
Document Head: meta and links
\x3CMeta /> and \x3CLinks /> must live inside \x3Chead> in root.tsx;
\x3CScrollRestoration />, \x3CScripts />, and \x3CLiveReload /> go at the end of
\x3Cbody>. Missing either of these aggregators produces "css doesn't load" or
"meta tags missing" with no compile error.
// app/root.tsx
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
export default function App() {
return (
\x3Chtml lang="en">
\x3Chead>
\x3Cmeta charSet="utf-8" />
\x3Cmeta name="viewport" content="width=device-width, initial-scale=1" />
\x3CMeta />
\x3CLinks />
\x3C/head>
\x3Cbody>
\x3COutlet />
\x3CScrollRestoration />
\x3CScripts />
\x3CLiveReload />
\x3C/body>
\x3C/html>
);
}
\x3CMeta /> and \x3CLinks /> aggregate differently. \x3CLinks /> walks the
entire route match chain and renders every matched route's links export
— a stylesheet declared in a leaf route is rendered automatically and
unloaded on navigation away. \x3CMeta /> does NOT aggregate; Remix picks
the last matching route's meta array only. To inherit from parents in
meta, flatMap matches explicitly:
import type { MetaFunction } from "@remix-run/node";
import type { loader as projectLoader } from "./project.$pid";
export const meta: MetaFunction\x3C
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}` },
];
};
The second generic on MetaFunction (keyed by route id) types
matches.find(...).data for parent routes. See
references/meta-v2.md.
Sessions
commitSession must be attached as a Set-Cookie header on every mutating
response. Remix does NOT auto-commit; calling session.set(...) and returning
plain json(data) silently drops the change.
return redirect("/dashboard", {
headers: { "Set-Cookie": await commitSession(session) },
});
session.flash(key, value) is read-once; the consuming loader must still call
commitSession after reading to clear the flash. See
references/sessions.md.
Auth: throw redirect from loaders
The canonical pattern is a requireUserId(request) helper that throws
redirect() for unauthenticated requests. The thrown response short-circuits
the loader; no top-level return is needed.
// app/auth.server.ts
import { redirect } from "@remix-run/node";
import { getSession } from "./session.server";
export async function requireUserId(request: Request): Promise\x3Cstring> {
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;
}
Never gate routes inside React components — the protected component still SSRs and ships HTML/loader data to unauthenticated users. See references/auth-csrf.md.
CSRF
Remix has no built-in CSRF protection. Same-origin \x3CForm> posts rely
entirely on whatever SameSite value you set on the session cookie.
SameSite=Lax blocks cookies on cross-site POST navigations in all current
browsers. (Chrome briefly had a 2-minute "Lax+POST" window in 2020 — removed
in 2021.) The real Lax-vs-Strict tradeoff is subdomain takeover: with
Lax, a compromised subdomain can initiate top-level GET nav with
credentials; with Strict, deep-link navigations from external sites lose
session. Apps that use SameSite=None for legitimate cross-site needs
(OAuth popups, iframe embeds) have no cookie-level CSRF protection at all.
Recommend remix-utils/csrf with a dedicated signed cookie — never
reuse the session cookie. Manual fetch("/api/x", { method: "POST" })
bypasses AuthenticityTokenInput, so any action that does not call
csrf.validate(request) is an attacker entry point.
Gates (decision sequencing)
Answer in order. Pass means the condition is true; pick the API on the same line and stop.
Where does this meta tag live?
- Is it site-wide (charset, viewport, default OG image)?
- Pass → Plain JSX inside
\x3Chead>inroot.tsx. Avoids the v2 no-merge surprise and prevents duplicate tags. Stop. - Fail → Step 2.
- Pass → Plain JSX inside
- Is it route-specific (title, description, canonical, JSON-LD)?
- Pass →
export const metain the leaf route file; if you need parent values,matches.flatMap((m) => m.meta ?? []). Stop.
- Pass →
Auth check: loader, action, or helper?
- Is this a GET (page render) that must be protected?
- Pass → Call
await requireUserId(request)at the top of theloader. Stop.
- Pass → Call
- Is this a POST/PUT/DELETE mutation that must be protected?
- Pass → Call
await requireUserId(request)at the top of theaction, AND callawait csrf.validate(request). Stop.
- Pass → Call
- Logout?
- Pass →
actiononly, neverloader. A\x3CLink to="/logout">pointing at a loader is CSRF-able via\x3Cimg src="/logout">. Use\x3CForm method="post" action="/logout">. Stop.
- Pass →
Where does the CSRF token live?
- Are you using
remix-utils/csrf?- Pass → A dedicated
createCookie("csrf", { ... })cookie, separate from the session cookie. The CSRF value is a signed string; the session value is a serialized object — reusing one cookie throws on validate. Stop. - Fail → Step 2.
- Pass → A dedicated
- No CSRF library?
- Pass → Document the threat model; require
sameSite: "strict"on the session cookie and verify theOriginheader in every action. Prefer addingremix-utils/csrfinstead.
- Pass → Document the threat model; require
Additional Documentation
- Meta v2: See references/meta-v2.md for
descriptor types, parent merging via
matches, JSON-LD, and v1→v2 migration pitfalls. - Links: See references/links.md for stylesheet,
preload, dns-prefetch, and the parent-aggregation behavior of
\x3CLinks />. - Sessions: See references/sessions.md for
createCookieSessionStorageconfig,commitSession/destroySessionpatterns, flash messages, and database-backed sessions. - Auth and CSRF: See references/auth-csrf.md
for
requireUserIdhelpers, login/logout actions,remix-auth, andremix-utils/csrfwiring.
v1 vs v2 Quick Comparison
| Concern | v1 | v2 |
|---|---|---|
meta return shape |
Object { title, description } |
Array [{ title }, { name, content }] |
| Parent meta merge | Auto-merged (last-write-wins per key) | No merge; last matching route only |
meta argument for parent data |
parentsData |
matches (flatMap manually) |
| OG tags | { "og:title": "..." } shorthand |
{ property: "og:title", content: "..." } |
| Migration flag | v2_meta: true future flag |
N/A (v2 default) |
- 确保已安装 OpenClaw(本地或 Docker 部署)
- 在对话框中输入安装命令:
/install remix-v2-meta-sessions - 安装完成后,直接呼叫该 Skill 的名称或使用
/remix-v2-meta-sessions触发 - 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
Remix V2 Meta Sessions 是什么?
Remix v2 meta/SEO, sessions, auth, and CSRF. Use when working with document head, cookie sessions, auth gates, or CSRF protection. Triggers on meta export (v... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 50 次。
如何安装 Remix V2 Meta Sessions?
在 OpenClaw 或 Claude Code 对话框中运行命令「/install remix-v2-meta-sessions」即可一键安装,无需额外配置。
Remix V2 Meta Sessions 是免费的吗?
是的,Remix V2 Meta Sessions 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。
Remix V2 Meta Sessions 支持哪些平台?
Remix V2 Meta Sessions 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。
谁开发了 Remix V2 Meta Sessions?
由 Kevin Anderson(@anderskev)开发并维护,当前版本 v1.0.0。