Flat-routes v2 filename rules (all files live in app/routes/):
text
_index.tsx → /
concerts.tsx → /concerts (acts as layout when dotted children exist; otherwise leaf for /concerts)
concerts._index.tsx → /concerts (renders under layout)
concerts.$city.tsx → /concerts/:city params.city
concerts.trending.tsx → /concerts/trending
_auth.tsx + _auth.login.tsx → /login (pathless layout, no URL segment)
files.$.tsx → /files/* params["*"]
($lang)._index.tsx → / and /en (or /fr etc.) — optional segment
sitemap[.]xml.tsx → /sitemap.xml (escape literal)
concerts_.mine.tsx → /concerts/mine (opts out of layout)
dashboard/route.tsx → /dashboard (folder + route.tsx)
reports.$id[.pdf].tsx → /reports/:id.pdf (no default export = resource)
Imports — always use @remix-run/react, never react-router-dom:
tsx
import { Outlet, Link, useLoaderData, useParams } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node"; // or /cloudflare, /deno
Dots in filenames create URL slashes and parent/child nesting. Underscore prefix marks pathless segments ( _auth.tsx) and index routes ( _index.tsx). Trailing underscore ( concerts_.mine.tsx) opts out of layout nesting while keeping the URL nested. Brackets escape literal characters: sitemap[.]xml.tsx. Splat is the single dollar sign: $.tsx exposes the rest of the path under params["*"]. Optional segments are wrapped in parens: ($lang).
See references/conventions.md for the full table and edge cases.
A parent module ( concerts.tsx) renders <Outlet />; child routes ( concerts.$city.tsx, concerts._index.tsx) mount inside it automatically based on the dot-delimited filename.
tsx
// app/routes/concerts.tsx
import { Outlet } from "@remix-run/react";
export default function ConcertsLayout() {
return (
<section>
<nav>{/* concerts subnav */}</nav>
<Outlet />
</section>
);
}
tsx
// app/routes/concerts._index.tsx (renders at exactly /concerts)
export default function ConcertsIndex() {
return <h1>Browse concerts</h1>;
}
For a layout with no URL contribution, prefix with a single underscore:
tsx
// app/routes/_auth.tsx → wraps /login, /signup; no URL segment
// app/routes/_auth.login.tsx → /login (inherits _auth layout)
// app/routes/_auth.signup.tsx → /signup
$name captures a single segment; $.tsx captures the rest of the path. Loader receives values via params:
tsx
// app/routes/concerts.$city.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
if (!params.city) throw new Response("Not found", { status: 404 });
return json({ city: params.city });
}
export default function City() {
const { city } = useLoaderData<typeof loader>();
return <h1>{city}</h1>;
}
Splat values live under "*" — there is no params.splat:
tsx
// app/routes/files.$.tsx
export async function loader({ params }: LoaderFunctionArgs) {
const rest = params["*"]; // bracket access only
return new Response(await readBlob(rest), { headers: { "Content-Type": "application/octet-stream" } });
}
app/root.tsx is the only required route. It owns the document shell and must render <Meta />, <Links />, <Outlet />, <ScrollRestoration />, <Scripts />, and (during dev) <LiveReload />. See references/root.md.
A route module without a default export is a resource route — it returns raw Response objects (PDF, JSON, RSS, webhooks). Parent loaders do not run, and <Link> must use reloadDocument (or be replaced with <a>) to trigger a real document request. See references/resource-routes.md.
Answer in order. Pass means the condition is true; pick the answer on the same line and stop.
Is there shared chrome at all (nav, breadcrumbs, sidebar) at this level?
about.tsx, pricing.tsx); no layout module needed. Stop.Should this URL share UI (nav, breadcrumbs, sidebar) with a parent path?
concerts.$city.tsx under concerts.tsx). Stop.Does the URL just happen to be nested but should render standalone?
concerts_.mine.tsx). Stop.Need a wrapper layout but no parent URL segment?
_auth.tsx + _auth.login.tsx). Stop.Does this URL ever render HTML to a user?
default component. UI route. Stop.Returns JSON, PDF, RSS, sitemap, webhook, or other raw Response?
default export — module becomes a resource route. Use reloadDocument on any <Link> pointing to it. Stop._index.tsx vs index.tsx#On Remix v2 with flat-routes?
_index.tsx (leading underscore). Stop.Need to keep a v1 nested-folder tree alive?
@remix-run/v1-route-convention and wire it in remix.config.js. See references/v1-migration.md. Stop._index, _layout, $param, splat, optional, escape, folder + route.tsx, trailing underscore).root.tsx scaffold and the required document elements.default-export rule, parent-loader skipping, and reloadDocument.__double underscore folders, index.tsx, @remix-run/v1-route-convention, ignoredRouteFiles).Concernv1 (nested folders)v2 (flat routes)Index routeindex.tsx``_index.tsxPathless layout__auth/ (double underscore)_auth.tsx (single)Nested URLfolder hierarchydot delimiter in filenameDynamic segment$param.tsx``$param.tsx (unchanged)Splat$.tsx``$.tsx (unchanged)Escape literaln/a[.], [] bracketsOpt-out of layoutmove out of foldertrailing _ ( foo_.bar.tsx)Co-locationadjacent files in folderfeature/route.tsx + siblingsFallback adaptern/a@remix-run/v1-route-convention