Skip to content
Development
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 (v2 array shape), links export, createCookieSessionStorage, commitSession, destroySession, requireUserId, remix-utils/csrf,

From plugin
beagle
82139 skills2 commands
Install
$ npx -y skills add existential-birds/beagle --skill remix-v2-meta-sessions --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/remix-v2-meta-sessions

Context preview

The summary Claude sees to decide when to auto-load this skill.

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 (v2 array shape), links export, createCookieSessionStorage, commitSession, destroySession, requireUserId, remix-utils/csrf,

SKILL.md

remix-v2-meta-sessions.SKILL.md
name: remix-v2-meta-sessions
description: 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 (v2 array shape), links export, createCookieSessionStorage, commitSession, destroySession, requireUserId, remix-utils/csrf, remix-auth.

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<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 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<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] : []),
      ],
    },
  });

Document Head: `meta` and `links`

`<Meta />` and `<Links />` must live inside `<head>` in `root.tsx`; `<ScrollRestoration />`, `<Scripts />`, and `<LiveReload />` go at the end of `<body>`. 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 (
    <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 />` and `<Links />` aggregate differently.** `<Links />` 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. `<Meta />` 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<
  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](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](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<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;
}

Never gate routes inside React components — the protected component still SSRs and ships HTML/loader data to unauthenticated users. See [references/auth-csrf.md](references/auth-csrf.md).

CSRF

**Remix has no built-in CSRF protection.** Same-origin `<Form>` 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 `Au

Read more
Ships withbeagle

Image: NASA, Public Domain. Source Beagle is an Agent Skills marketplace: framework-aware code review, documentation, testing, architectural analysis, and git workflows for any compatible coding agent.

Get the whole plugin

Other skills on beagle.