web-meta-framework-remix-9d721a — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-meta-framework-remix-9d721a (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: A route exports aloaderfor reads and anactionfor writes, both server-only, so a component never fetches its own data. Forms submit without JavaScript and nested routes load in parallel. The version fact that changes every example below: React Router v7 deprecatesjson()anddefer()— return raw objects and raw Promises, and usedata()only when you need a custom status or header.
Detailed Resources:
intent fielduseFetcherSuspense and Awaitroutes.ts, generated types, clientAction, Single Fetch@remix-run/* imports,json(), defer() and useLoaderData<typeof loader>().
Read examples/react-router-v7.md first and translate as you go; the mapping table is below.
loader, a default export, and an ErrorBoundary; follow<migration_notice>
Remix has merged into React Router v7. What was planned as Remix v3 is React Router v7's "framework mode".
| Remix v2 | React Router v7 |
|---|---|
json(data) | Return the raw object |
json(data, { status, headers }) | data(data, { status, headers }) |
defer({ key: promise }) | Return { key: promise } — Single Fetch streams it |
@remix-run/node imports | react-router / @react-router/node |
LoaderFunctionArgs | Route.LoaderArgs (generated) |
ActionFunctionArgs | Route.ActionArgs (generated) |
useLoaderData<typeof loader>() | loaderData from Route.ComponentProps |
RemixServer | ServerRouter |
RemixBrowser | HydratedRouter (from react-router/dom) |
| File-based routing by default | routes.ts, with @react-router/fs-routes optional |
Migration guide: Upgrading from Remix
</migration_notice>
<critical_requirements>
Export `loader` and `action` from route modules only. The build wires them up by route, so the same export in a helper file is dead code that silently never runs.
Throw a Response for an expected failure — 404, 403 — and let the `ErrorBoundary` render it. Returning null instead pushes a null check onto every consumer and loses the status code.
Await the data the page cannot render without, and return the rest as Promises. Anything awaited delays the first byte; anything returned as a Promise streams in behind it.
Name HTTP status codes as constants. HTTP_NOT_FOUND says what the branch is for, where 404 has to be recognised.
</critical_requirements>
Auto-detection: loader, action, clientLoader, clientAction, useLoaderData, useActionData, useFetcher, useNavigation, useRouteError, isRouteErrorResponse, ErrorBoundary, HydrateFallback, shouldRevalidate, defer, Await, Outlet, NavLink, meta function, links function, @remix-run/node, @remix-run/react, react-router, ServerRouter, HydratedRouter, Route.LoaderArgs, Route.ComponentProps, routes.ts, Single Fetch, app/routes/
Applies to:
Suspense and AwaituseFetchermeta, and stylesheets and preloads through linksHandled elsewhere:
settled here
FormData; which library defines the schema is a separatechoice
links returns stylesheet descriptors, and what is in them is someone else's concern<philosophy>
Remix collapses full-stack development to one mental model: a route exports a loader for reads and an action for writes, and both run only on the server. That is what lets a route query a database directly without a secret reaching the browser, and it is why there is no client-side fetching library in the picture.
Four things follow:
parallel with each other rather than in sequence down the tree
<Form> is a real form; JavaScript makes thesubmission smoother rather than making it possible
Cache-Control, errors are statuscodes, and requests and responses are the platform's own objects
where filter and pagination state lives
URL change -> loaders run in parallel -> component renders -> user submits
|
action runs -> loaders revalidate</philosophy>
<patterns>
Files in app/routes/ become URLs; the naming characters control nesting and dynamic segments.
| File | URL | What the name does |
|---|---|---|
_index.tsx | / | Index route |
about.tsx | /about | Static segment |
blog.$slug.tsx | /blog/:slug | $ marks a dynamic parameter |
blog_.tsx | /blog | Trailing _ escapes the parent layout |
_auth.tsx | none | Leading _ makes a pathless layout |
_auth.login.tsx | /login | Nested inside that layout |
$.tsx | /* | Splat / catch-all |
// app/routes/blog.$slug.tsx
const HTTP_NOT_FOUND = 404;
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPostBySlug(params.slug);
if (!post) throw new Response("Not Found", { status: HTTP_NOT_FOUND });
return { post };
}Full code: examples/core.md and examples/nested-routes.md
A loader runs on the server for the initial render and over fetch on every client navigation afterwards.
export async function loader({ params, request }: LoaderFunctionArgs) {
const user = await getUser(params.userId);
if (!user) {
throw json({ message: "User not found" }, { status: HTTP_NOT_FOUND });
}
return json({ user });
}Parent loaders re-run when a child route changes — shouldRevalidate is the opt-out.
Full code: examples/loaders.md
An action handles every non-GET method, runs before the loaders, and the loaders revalidate after it.
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
switch (formData.get("intent")) {
case "update":
return json({ success: true });
case "delete":
return redirect("/items");
default:
throw new Error(`Unknown intent`);
}
}A hidden intent field is how one route serves several forms. Redirect after a successful mutation so a refresh does not resubmit.
Full code: examples/actions.md and examples/forms.md
Await what the page needs; hand back the rest as Promises.
// Remix v2
return defer({ user, analytics: getAnalytics() });
// React Router v7 — Single Fetch streams a raw Promise
return { user, analytics: getAnalytics() };<Suspense fallback={<Skeleton />}>
<Await resolve={analytics} errorElement={<p>Failed to load</p>}>
{(data) => <Chart data={data} />}
</Await>
</Suspense>Stream analytics, comments and recommendations. Await auth state, the page title and anything a crawler reads.
Full code: examples/deferred.md
A fetcher submits or loads without navigating, which is what inline interactions need.
const fetcher = useFetcher();
// Optimistic UI: the in-flight submission is already in fetcher.formData
const optimisticIsLiked = fetcher.formData
? fetcher.formData.get("liked") === "true"
: isLiked;<Form> for anything that should change the URL; useFetcher for likes, toggles, inline edits and autocomplete.
Full code: examples/optimistic.md
An exported ErrorBoundary catches everything below it in the route tree, and the first branch tells a thrown Response from an unexpected exception.
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div role="alert">
<h1>{error.status}</h1>
</div>
);
}
return (
<div role="alert">
<h1>Unexpected Error</h1>
</div>
);
}The boundary is route-scoped, so the rest of the page keeps working.
Full code: examples/error-handling.md
meta builds the head tags from the loader's data; links declares stylesheets and preloads.
export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data) return [{ title: "Not Found" }];
return [
{ title: `${data.post.title} | ${SITE_NAME}` },
{ property: "og:title", content: data.post.title },
{ tagName: "link", rel: "canonical", href: url },
];
};meta receives undefined data when the loader threw, and links cannot see loader data at all — a dynamic <link> goes through meta with tagName: "link".
Full code: examples/meta.md
A route module with no default export renders nothing and returns whatever its loader or action does.
// app/routes/api.health.ts
export async function loader() {
return json({ status: "healthy", timestamp: new Date().toISOString() });
}Full code: examples/resource-routes.md
Nested routes share the parent's layout and load alongside it rather than after it, so an auth check in a parent layout protects every child.
| File | Role |
|---|---|
admin.tsx | Layout — renders <Outlet /> |
admin._index.tsx | What renders at /admin itself |
admin.users.tsx | A child route |
admin_.settings.tsx | /admin/settings without the admin layout |
_auth.tsx | A layout that contributes no URL segment |
Full code: examples/nested-routes.md
</patterns>
<red_flags>
Breaks at runtime:
loader or action exported from a file that is not a route module — nothing calls it<Form> with no method="post" — it submits as a GET, so the action never runsdefer() without a <Suspense> and <Await> around the consumer?index — the parent's action receives itmeta reading data without a null branch — data is undefined when the loader threwloaders and actions
Surprising behaviour:
than expected — shouldRevalidate is the control
links cannot access loader data, so a dynamic stylesheet URL has to go through metaclientAction takes priority when both exist, and the server action isskipped unless the clientAction calls it
null from a loader compiles and type-checks, and pushes the failure to whatever rendersit
useFetcher with no optimistic read of fetcher.formData shows nothing until the round tripfinishes
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.