Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
existential-birds avatar

Remix V2 Meta Sessions

  • 27 installs
  • 74 repo stars
  • Updated July 21, 2026
  • existential-birds/beagle

Helps with ai & agent building tasks.

About

remix-v2-meta-sessions is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.

  • remix-v2-meta-sessions
  • AI & Agent Building
  • AI-coding skill

Remix V2 Meta Sessions by the numbers

  • 27 all-time installs (skills.sh)
  • Ranked #9,601 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill remix-v2-meta-sessions

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs27
repo stars74
Last updatedJuly 21, 2026
Repositoryexistential-birds/beagle

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

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.

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<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.

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 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?

1. Is it site-wide (charset, viewport, default OG image)?

  • Pass → Plain JSX inside <head> in root.tsx. Avoids the v2

no-merge surprise and prevents duplicate tags. Stop.

  • Fail → Step 2.

2. Is it route-specific (title, description, canonical, JSON-LD)?

  • Pass → export const meta in the leaf route file; if you need

parent values, matches.flatMap((m) => m.meta ?? []). Stop.

Auth check: loader, action, or helper?

1. Is this a GET (page render) that must be protected?

  • Pass → Call await requireUserId(request) at the top of the

loader. Stop. 2. Is this a POST/PUT/DELETE mutation that must be protected?

  • Pass → Call await requireUserId(request) at the top of the

action, AND call await csrf.validate(request). Stop. 3. Logout?

  • Pass → action only, never loader. A <Link to="/logout">

pointing at a loader is CSRF-able via <img src="/logout">. Use <Form method="post" action="/logout">. Stop.

Where does the CSRF token live?

1. 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.

2. No CSRF library?

  • Pass → Document the threat model; require sameSite: "strict" on

the session cookie and verify the Origin header in every action. Prefer adding remix-utils/csrf instead.

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 <Links />.

  • Sessions: See references/sessions.md for

createCookieSessionStorage config, commitSession/destroySession patterns, flash messages, and database-backed sessions.

  • Auth and CSRF: See references/auth-csrf.md

for requireUserId helpers, login/logout actions, remix-auth, and remix-utils/csrf wiring.

v1 vs v2 Quick Comparison

Concernv1v2
meta return shapeObject { title, description }Array [{ title }, { name, content }]
Parent meta mergeAuto-merged (last-write-wins per key)No merge; last matching route only
meta argument for parent dataparentsDatamatches (flatMap manually)
OG tags{ "og:title": "..." } shorthand{ property: "og:title", content: "..." }
Migration flagv2_meta: true future flagN/A (v2 default)

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.