diff --git a/.changeset/v2-route-convention.md b/.changeset/v2-route-convention.md
new file mode 100644
index 00000000000..9ddfb8450b0
--- /dev/null
+++ b/.changeset/v2-route-convention.md
@@ -0,0 +1,8 @@
+---
+"@remix-run/dev": major
+"@remix-run/react": major
+"@remix-run/server-runtime": major
+"@remix-run/testing": major
+---
+
+Remove `v2_routeConvention` flag. The flat route file convention is now standard.
diff --git a/docs/file-conventions/route-files-v2.md b/docs/file-conventions/route-files-v2.md
index 3c4a380f324..18222849cfc 100644
--- a/docs/file-conventions/route-files-v2.md
+++ b/docs/file-conventions/route-files-v2.md
@@ -1,403 +1,11 @@
---
title: Route File Naming (v2)
-new: true
+toc: false
+hidden: true
---
# Route File Naming (v2)
-You can opt in to the new route file naming convention with a future flag in Remix config.
+[The v2 route file naming convention has now been stabilized.][moved]
-```js filename=remix.config.js
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- future: {
- v2_routeConvention: true,
- },
-};
-```
-
-While you can configure routes in [remix.config.js][remix-config], most routes are created with this file system convention. Add a file, get a route.
-
-Please note that you can use either `.js`, `.jsx`, `.ts` or `.tsx` file extensions. We'll stick with `.tsx` in the examples to avoid duplication.
-
-## Root Route
-
-
-```markdown lines=[3]
-app/
-├── routes/
-└── root.tsx
-```
-
-The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes, so you can export a [`loader`][loader], [`action`][action], etc.
-
-The root route typically looks something like this. It serves as the root layout of the entire app, all other routes will render inside the ``.
-
-```tsx
-import {
- Links,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
-} from "@remix-run/react";
-
-export default function Root() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-```
-
-## Basic Routes
-
-Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index-route] for the [root route][root-route].
-
-
-```markdown lines=[3-4]
-app/
-├── routes/
-│ ├── _index.tsx
-│ └── about.tsx
-└── root.tsx
-```
-
-| URL | Matched Routes |
-| -------- | -------------- |
-| `/` | `_index.tsx` |
-| `/about` | `about.tsx` |
-
-Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested-routing].
-
-## Dot Delimiters
-
-Adding a `.` to a route filename will create a `/` in the URL.
-
-
-```markdown lines=[5-7]
-app/
-├── routes/
-│ ├── _index.tsx
-│ ├── about.tsx
-│ ├── concerts.trending.tsx
-│ ├── concerts.salt-lake-city.tsx
-│ └── concerts.san-diego.tsx
-└── root.tsx
-```
-
-| URL | Matched Route |
-| -------------------------- | ----------------------------- |
-| `/concerts/trending` | `concerts.trending.tsx` |
-| `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` |
-| `/concerts/san-diego` | `concerts.san-diego.tsx` |
-
-The dot delimiter also creates nesting, see the [nesting section][nested-routes] for more information.
-
-## Dynamic Segments
-
-Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.
-
-
-```markdown lines=[5]
-app/
-├── routes/
-│ ├── _index.tsx
-│ ├── about.tsx
-│ ├── concerts.$city.tsx
-│ └── concerts.trending.tsx
-└── root.tsx
-```
-
-| URL | Matched Route |
-| -------------------------- | ----------------------- |
-| `/concerts/trending` | `concerts.trending.tsx` |
-| `/concerts/salt-lake-city` | `concerts.$city.tsx` |
-| `/concerts/san-diego` | `concerts.$city.tsx` |
-
-Remix will parse the value from the URL and pass it to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders][loader] and [actions][action].
-
-```tsx
-export function loader({ params }: LoaderArgs) {
- return fakeDb.getAllConcertsForCity(params.city);
-}
-```
-
-You'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`.
-
-Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name:
-
-```tsx
-export function loader({ params }: LoaderArgs) {
- return fake.db.getConcerts({
- date: params.date,
- city: params.city,
- });
-}
-```
-
-See the [routing guide][routing-guide] for more information.
-
-## Nested Routes
-
-Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested-routing].
-
-You create nested routes with [dot delimiters][dot-delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes:
-
-
-```markdown lines=[5-8]
-app/
-├── routes/
-│ ├── _index.tsx
-│ ├── about.tsx
-│ ├── concerts._index.tsx
-│ ├── concerts.$city.tsx
-│ ├── concerts.trending.tsx
-│ └── concerts.tsx
-└── root.tsx
-```
-
-All the routes that start with `concerts.` will be child routes of `concerts.tsx` and render inside the parent route's [outlet][outlet].
-
-| URL | Matched Route | Layout |
-| -------------------------- | ----------------------- | -------------- |
-| `/` | `_index.tsx` | `root.tsx` |
-| `/about` | `about.tsx` | `root.tsx` |
-| `/concerts` | `concerts._index.tsx` | `concerts.tsx` |
-| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` |
-| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` |
-
-Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
-
-## Nested URLs without Layout Nesting
-
-Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment:
-
-
-```markdown lines=[8]
-app/
-├── routes/
-│ ├── _index.tsx
-│ ├── about.tsx
-│ ├── concerts.$city.tsx
-│ ├── concerts.trending.tsx
-│ ├── concerts.tsx
-│ └── concerts_.mine.tsx
-└── root.tsx
-```
-
-| URL | Matched Route | Layout |
-| -------------------------- | ----------------------- | -------------- |
-| `/` | `_index.tsx` | `root.tsx` |
-| `/concerts/mine` | `concerts_.mine.tsx` | `root.tsx` |
-| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` |
-| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` |
-
-Note that `/concerts/mine` does not nest with `concerts.tsx` anymore, but `root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting.
-
-Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting.
-
-## Nested Layouts without Nested URLs
-
-We call these Pathless Routes
-
-Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore.
-
-
-```markdown lines=[3-5]
-app/
-├── routes/
-│ ├── _auth.login.tsx
-│ ├── _auth.register.tsx
-│ ├── _auth.tsx
-│ ├── _index.tsx
-│ ├── concerts.$city.tsx
-│ └── concerts.tsx
-└── root.tsx
-```
-
-| URL | Matched Route | Layout |
-| -------------------------- | -------------------- | -------------- |
-| `/` | `_index.tsx` | `root.tsx` |
-| `/login` | `_auth.login.tsx` | `_auth.tsx` |
-| `/register` | `_auth.register.tsx` | `_auth.tsx` |
-| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` |
-
-Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
-
-## Optional Segments
-
-Wrapping a route segment in parentheses will make the segment optional.
-
-
-```markdown lines=[3-5]
-app/
-├── routes/
-│ ├── ($lang)._index.tsx
-│ ├── ($lang).$productId.tsx
-│ └── ($lang).categories.tsx
-└── root.tsx
-```
-
-| URL | Matched Route |
-| -------------------------- | ------------------------ |
-| `/` | `($lang)._index.tsx` |
-| `/categories` | `($lang).categories.tsx` |
-| `/en/categories` | `($lang).categories.tsx` |
-| `/fr/categories` | `($lang).categories.tsx` |
-| `/american-flag-speedo` | `($lang)._index.tsx` |
-| `/en/american-flag-speedo` | `($lang).$productId.tsx` |
-| `/fr/american-flag-speedo` | `($lang).$productId.tsx` |
-
-You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.
-
-## Splat Routes
-
-While [dynamic segments][dynamic-segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.
-
-
-```markdown lines=[4,6]
-app/
-├── routes/
-│ ├── _index.tsx
-│ ├── $.tsx
-│ ├── about.tsx
-│ └── files.$.tsx
-└── root.tsx
-```
-
-| URL | Matched Route |
-| -------------------------------------------- | ------------- |
-| `/` | `_index.tsx` |
-| `/beef/and/cheese` | `$.tsx` |
-| `/files` | `files.$.tsx` |
-| `/files/talks/remix-conf_old.pdf` | `files.$.tsx` |
-| `/files/talks/remix-conf_final.pdf` | `files.$.tsx` |
-| `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `files.$.tsx` |
-
-Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key.
-
-```tsx filename=app/routes/files.$.tsx
-export function loader({ params }) {
- const filePath = params["*"];
- return fake.getFileInfo(filePath);
-}
-```
-
-## Escaping Special Characters
-
-If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters.
-
-| Filename | URL |
-| ------------------------------- | ------------------- |
-| `routes/sitemap[.]xml.tsx` | `/sitemap.xml` |
-| `routes/[sitemap.xml].tsx` | `/sitemap.xml` |
-| `routes/weird-url.[_index].tsx` | `/weird-url/_index` |
-| `routes/dolla-bills-[$].tsx` | `/dolla-bills-$` |
-| `routes/[[so-weird]].tsx` | `/[so-weird]` |
-
-## Folders for Organization
-
-Routes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
-
-The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name
-
-Consider these routes:
-
-```
-routes/
- _landing._index.tsx
- _landing.about.tsx
- _landing.tsx
- app._index.tsx
- app.projects.tsx
- app.tsx
- app_.projects.$id.roadmap.tsx
-```
-
-Some, or all of them can be folders holding their own `route` module inside.
-
-```
-routes/
- _landing._index/
- route.tsx
- scroll-experience.tsx
- _landing.about/
- employee-profile-card.tsx
- get-employee-data.server.tsx
- route.tsx
- team-photo.jpg
- _landing/
- header.tsx
- footer.tsx
- route.tsx
- app._index/
- route.tsx
- stats.tsx
- app.projects/
- get-projects.server.tsx
- project-card.tsx
- project-buttons.tsx
- route.tsx
- app/
- primary-nav.tsx
- route.tsx
- footer.tsx
- app_.projects.$id.roadmap/
- route.tsx
- chart.tsx
- update-timeline.server.tsx
- contact-us.tsx
-```
-
-Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example:
-
-```
-# these are the same route:
-routes/app.tsx
-routes/app/route.tsx
-
-# as are these
-routes/app._index.tsx
-routes/app._index/route.tsx
-```
-
-## Scaling
-
-Our general recommendation for scale is to make every route a folder and put the modules used exclusively by that route in the folder, then put the shared modules outside of routes folder elsewhere. This has a couple benefits:
-
-- Easy to identify shared modules, so tread lightly when changing them
-- Easy to organize and refactor the modules for a specific route without creating "file organization fatigue" and cluttering up other parts of the app
-
-## More Flexibility
-
-While we like this file convention, we recognize that at a certain scale many organizations won't like it. You can always define your routes programmatically in the [remix config][remix-config].
-
-There's also the [Flat Routes][flat-routes] third-party package with configurable options beyond the defaults in Remix.
-
-[loader]: ../route/loader
-[action]: ../route/action
-[outlet]: ../components/outlet
-[routing-guide]: ../guides/routing
-[root-route]: #root-route
-[resource-route]: ../guides/resource-routes
-[routeconvention-v2]: ./route-files-v2
-[flatroutes-rfc]: https://github.com/remix-run/remix/discussions/4482
-[root-route]: #root-route
-[index-route]: ../guides/routing#index-routes
-[nested-routing]: ../guides/routing#what-is-nested-routing
-[nested-routes]: #nested-routes
-[remix-config]: ./remix-config#routes
-[dot-delimiters]: #dot-delimiters
-[dynamic-segments]: #dynamic-segments
-[remix-config]: ./remix-config#routes
-[flat-routes]: https://github.com/kiliman/remix-flat-routes
+[moved]: ./route-files
diff --git a/docs/file-conventions/routes-files.md b/docs/file-conventions/routes-files.md
index 37616858b9e..3e929dde060 100644
--- a/docs/file-conventions/routes-files.md
+++ b/docs/file-conventions/routes-files.md
@@ -1,12 +1,11 @@
---
title: Route File Naming
+new: true
---
# Route File Naming
-The route file convention is changing in v2. You can prepare for this change at your convenience with the `v2_routeConvention` future flag. For instructions on making this change see the [v2 guide][v2guide].
-
-Setting up routes in Remix is as simple as creating files in your `app` directory. These are the conventions you should know to understand how routing in Remix works.
+While you can configure routes in [remix.config.js][remix-config], most routes are created with this file system convention. Add a file, get a route.
Please note that you can use either `.js`, `.jsx`, `.ts` or `.tsx` file extensions. We'll stick with `.tsx` in the examples to avoid duplication.
@@ -19,298 +18,375 @@ app/
└── root.tsx
```
-The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes:
-
-- You can export a [`loader`][loader], [`action`][action], [`meta`][meta], [`headers`][headers], or [`links`][links] function
-- You can export an [`ErrorBoundary`][error-boundary]
-- Your default export is the layout component that renders the rest of your app in an [``][outlet]
+The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes, so you can export a [`loader`][loader], [`action`][action], etc.
+
+The root route typically looks something like this. It serves as the root layout of the entire app, all other routes will render inside the ``.
+
+```tsx
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+
+export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
## Basic Routes
-Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `index.tsx` which maps to the root pathname.
+Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index-route] for the [root route][root-route].
```markdown lines=[3-4]
app/
├── routes/
-│ ├── about.tsx
-│ └── index.tsx
+│ ├── _index.tsx
+│ └── about.tsx
└── root.tsx
```
-| URL | Matched Route |
-| -------- | ---------------------- |
-| `/` | `app/routes/index.tsx` |
-| `/about` | `app/routes/about.tsx` |
+| URL | Matched Routes |
+| -------- | -------------- |
+| `/` | `_index.tsx` |
+| `/about` | `about.tsx` |
+
+Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested-routing].
-The default export in this file is the component that is rendered at that route and will render within the `` rendered by the root route.
+## Dot Delimiters
-## Dynamic Route Parameters
+Adding a `.` to a route filename will create a `/` in the URL.
-```markdown lines=[4]
+```markdown lines=[5-7]
app/
├── routes/
-│ ├── blog/
-│ │ ├── $postId.tsx
-│ │ ├── categories.tsx
-│ │ └── index.tsx
+│ ├── _index.tsx
│ ├── about.tsx
-│ └── index.tsx
+│ ├── concerts.trending.tsx
+│ ├── concerts.salt-lake-city.tsx
+│ └── concerts.san-diego.tsx
└── root.tsx
```
-
+| URL | Matched Route |
+| -------------------------- | ----------------------------- |
+| `/concerts/trending` | `concerts.trending.tsx` |
+| `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` |
+| `/concerts/san-diego` | `concerts.san-diego.tsx` |
-URL Route Matches
+The dot delimiter also creates nesting, see the [nesting section][nested-routes] for more information.
-| URL | Matched Route |
-| ------------------ | -------------------------------- |
-| `/blog` | `app/routes/blog/index.tsx` |
-| `/blog/categories` | `app/routes/blog/categories.tsx` |
-| `/blog/my-post` | `app/routes/blog/$postId.tsx` |
+## Dynamic Segments
-
+Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.
-Routes that begin with a `$` character indicate the name of a dynamic segment of the URL. It will be parsed and passed to your loader and action data as a value on the `param` object.
-
-For example: `app/routes/blog/$postId.tsx` will match the following URLs:
+
+```markdown lines=[5]
+app/
+├── routes/
+│ ├── _index.tsx
+│ ├── about.tsx
+│ ├── concerts.$city.tsx
+│ └── concerts.trending.tsx
+└── root.tsx
+```
-- `/blog/my-story`
-- `/blog/once-upon-a-time`
-- `/blog/how-to-ride-a-bike`
+| URL | Matched Route |
+| -------------------------- | ----------------------- |
+| `/concerts/trending` | `concerts.trending.tsx` |
+| `/concerts/salt-lake-city` | `concerts.$city.tsx` |
+| `/concerts/san-diego` | `concerts.$city.tsx` |
-On each of these pages, the dynamic segment of the URL path is the value of the parameter. There can be multiple parameters active at any time (as in `/dashboard/:client/invoices/:invoiceId` [view example app][view-example-app]) and all parameters can be accessed within components via [`useParams`][use-params] and within loaders/actions via the argument's [`params`][params] property:
+Remix will parse the value from the URL and pass it to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders][loader] and [actions][action].
-```tsx filename=app/routes/blog/$postId.tsx
-import type {
- ActionArgs,
- LoaderArgs,
-} from "@remix-run/node"; // or cloudflare/deno
-import { useParams } from "@remix-run/react";
+```tsx
+export function loader({ params }: LoaderArgs) {
+ return fakeDb.getAllConcertsForCity(params.city);
+}
+```
-export const loader = async ({ params }: LoaderArgs) => {
- console.log(params.postId);
-};
+You'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`.
-export const action = async ({ params }: ActionArgs) => {
- console.log(params.postId);
-};
+Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name:
-export default function PostRoute() {
- const params = useParams();
- console.log(params.postId);
+```tsx
+export function loader({ params }: LoaderArgs) {
+ return fake.db.getConcerts({
+ date: params.date,
+ city: params.city,
+ });
}
```
-Nested routes can also contain dynamic segments by using the `$` character in the parent's directory name. For example, `app/routes/blog/$postId/edit.tsx` might represent the editor page for blog entries.
-
See the [routing guide][routing-guide] for more information.
-## Optional Segments
+## Nested Routes
-Wrapping a route segment in parens will make the segment optional.
+Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested-routing].
+
+You create nested routes with [dot delimiters][dot-delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes:
-```markdown lines=[3]
+```markdown lines=[5-8]
app/
├── routes/
-│ ├── ($lang)/
-│ │ ├── $pid.tsx
-│ │ ├── categories.tsx
-│ └── index.tsx
+│ ├── _index.tsx
+│ ├── about.tsx
+│ ├── concerts._index.tsx
+│ ├── concerts.$city.tsx
+│ ├── concerts.trending.tsx
+│ └── concerts.tsx
└── root.tsx
```
-
+All the routes that start with `concerts.` will be child routes of `concerts.tsx` and render inside the parent route's [outlet][outlet].
-URL Route Matches
+| URL | Matched Route | Layout |
+| -------------------------- | ----------------------- | -------------- |
+| `/` | `_index.tsx` | `root.tsx` |
+| `/about` | `about.tsx` | `root.tsx` |
+| `/concerts` | `concerts._index.tsx` | `concerts.tsx` |
+| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` |
+| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` |
-| URL | Matched Route |
-| -------------------------- | ----------------------------------- |
-| `/categories` | `app/routes/($lang)/categories.tsx` |
-| `/en/categories` | `app/routes/($lang)/categories.tsx` |
-| `/fr/categories` | `app/routes/($lang)/categories.tsx` |
-| `/american-flag-speedo` | `app/routes/($lang)/$pid.tsx` |
-| `/en/american-flag-speedo` | `app/routes/($lang)/$pid.tsx` |
-| `/fr/american-flag-speedo` | `app/routes/($lang)/$pid.tsx` |
+Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
-
+## Nested URLs without Layout Nesting
-## Layout Routes
+Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment:
-```markdown lines=[3,8]
+```markdown lines=[8]
app/
├── routes/
-│ ├── blog/
-│ │ ├── $postId.tsx
-│ │ ├── categories.tsx
-│ │ └── index.tsx
+│ ├── _index.tsx
│ ├── about.tsx
-│ ├── blog.tsx
-│ └── index.tsx
+│ ├── concerts.$city.tsx
+│ ├── concerts.trending.tsx
+│ ├── concerts.tsx
+│ └── concerts_.mine.tsx
└── root.tsx
```
-
+| URL | Matched Route | Layout |
+| -------------------------- | ----------------------- | -------------- |
+| `/` | `_index.tsx` | `root.tsx` |
+| `/concerts/mine` | `concerts_.mine.tsx` | `root.tsx` |
+| `/concerts/trending` | `concerts.trending.tsx` | `concerts.tsx` |
+| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` |
-URL Route Matches
+Note that `/concerts/mine` does not nest with `concerts.tsx` anymore, but `root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting.
-| URL | Matched Route | Layout |
-| ------------------ | -------------------------------- | --------------------- |
-| `/` | `app/routes/index.tsx` | `app/root.tsx` |
-| `/about` | `app/routes/about.tsx` | `app/root.tsx` |
-| `/blog` | `app/routes/blog/index.tsx` | `app/routes/blog.tsx` |
-| `/blog/categories` | `app/routes/blog/categories.tsx` | `app/routes/blog.tsx` |
-| `/blog/my-post` | `app/routes/blog/$postId.tsx` | `app/routes/blog.tsx` |
+Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting.
-
+## Nested Layouts without Nested URLs
-In the example above, the `blog.tsx` is a "layout route" for everything within the `blog` directory (`blog/index.tsx` and `blog/categories.tsx`). When a route has the same name as its directory (`routes/blog.tsx` and `routes/blog/`), it becomes a layout route for all the routes inside that directory ("child routes"). Similar to your [root route][root-route], the parent route should render an `` where the child routes should appear. This is how you can create multiple levels of persistent layout nesting associated with URLs.
+We call these Pathless Routes
-## Pathless Layout Routes
+Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore.
-```markdown lines=[3,7,10-11]
+```markdown lines=[3-5]
app/
├── routes/
-│ ├── __app/
-│ │ ├── dashboard.tsx
-│ │ └── $userId/
-│ │ └── profile.tsx
-│ └── __marketing
-│ │ ├── index.tsx
-│ │ └── product.tsx
-│ ├── __app.tsx
-│ └── __marketing.tsx
+│ ├── _auth.login.tsx
+│ ├── _auth.register.tsx
+│ ├── _auth.tsx
+│ ├── _index.tsx
+│ ├── concerts.$city.tsx
+│ └── concerts.tsx
└── root.tsx
```
-
-
-URL Route Matches
-
-| URL | Matched Route | Layout |
-| ----------------- | -------------------------------------- | ---------------------------- |
-| `/` | `app/routes/__marketing/index.tsx` | `app/routes/__marketing.tsx` |
-| `/product` | `app/routes/__marketing/product.tsx` | `app/routes/__marketing.tsx` |
-| `/dashboard` | `app/routes/__app/dashboard.tsx` | `app/routes/__app.tsx` |
-| `/chance/profile` | `app/routes/__app/$userId/profile.tsx` | `app/routes/__app.tsx` |
+| URL | Matched Route | Layout |
+| -------------------------- | -------------------- | -------------- |
+| `/` | `_index.tsx` | `root.tsx` |
+| `/login` | `_auth.login.tsx` | `_auth.tsx` |
+| `/register` | `_auth.register.tsx` | `_auth.tsx` |
+| `/concerts/salt-lake-city` | `concerts.$city.tsx` | `concerts.tsx` |
-
+Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
-You can also create layout routes _without adding segments to the URL_ by prepending the directory and associated parent route file with double underscores: `__`.
-
-For example, all of your marketing pages could be in `app/routes/__marketing/*` and then share a layout by creating `app/routes/__marketing.tsx`. A route `app/routes/__marketing/product.tsx` would be accessible at the `/product` URL because `__marketing` won't add segments to the URL, just UI hierarchy.
-
-Be careful, pathless layout routes introduce the possibility of URL conflicts
+## Optional Segments
-## Dot Delimiters
+Wrapping a route segment in parentheses will make the segment optional.
-```markdown lines=[8]
+```markdown lines=[3-5]
app/
├── routes/
-│ ├── blog/
-│ │ ├── $postId.tsx
-│ │ ├── categories.tsx
-│ │ └── index.tsx
-│ ├── about.tsx
-│ ├── blog.authors.tsx
-│ ├── blog.tsx
-│ └── index.tsx
+│ ├── ($lang)._index.tsx
+│ ├── ($lang).$productId.tsx
+│ └── ($lang).categories.tsx
└── root.tsx
```
-
-
-URL Route Matches
+| URL | Matched Route |
+| -------------------------- | ------------------------ |
+| `/` | `($lang)._index.tsx` |
+| `/categories` | `($lang).categories.tsx` |
+| `/en/categories` | `($lang).categories.tsx` |
+| `/fr/categories` | `($lang).categories.tsx` |
+| `/american-flag-speedo` | `($lang)._index.tsx` |
+| `/en/american-flag-speedo` | `($lang).$productId.tsx` |
+| `/fr/american-flag-speedo` | `($lang).$productId.tsx` |
-| URL | Matched Route | Layout |
-| ------------------ | -------------------------------- | --------------------- |
-| `/blog` | `app/routes/blog/index.tsx` | `app/routes/blog.tsx` |
-| `/blog/categories` | `app/routes/blog/categories.tsx` | `app/routes/blog.tsx` |
-| `/blog/authors` | `app/routes/blog.authors.tsx` | `app/root.tsx` |
-
-
-
-By creating a file with `.` characters between segments, you can create a nested URL without nested layouts. For example, a file `app/routes/blog.authors.tsx` will route to the pathname `/blog/authors`, but it will not share a layout with routes in the `app/routes/blog/` directory.
+You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.
## Splat Routes
+While [dynamic segments][dynamic-segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.
+
-```markdown lines=[7]
+```markdown lines=[4,6]
app/
├── routes/
-│ ├── blog/
-│ │ ├── $postId.tsx
-│ │ ├── categories.tsx
-│ │ └── index.tsx
+│ ├── _index.tsx
│ ├── $.tsx
│ ├── about.tsx
-│ ├── blog.authors.tsx
-│ ├── blog.tsx
-│ └── index.tsx
+│ └── files.$.tsx
└── root.tsx
```
-
+| URL | Matched Route |
+| -------------------------------------------- | ------------- |
+| `/` | `_index.tsx` |
+| `/beef/and/cheese` | `$.tsx` |
+| `/files` | `files.$.tsx` |
+| `/files/talks/remix-conf_old.pdf` | `files.$.tsx` |
+| `/files/talks/remix-conf_final.pdf` | `files.$.tsx` |
+| `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `files.$.tsx` |
-URL Route Matches
+Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key.
-| URL | Matched Route | Layout |
-| ----------------- | --------------------------- | --------------------- |
-| `/` | `app/routes/index.tsx` | `app/root.tsx` |
-| `/blog` | `app/routes/blog/index.tsx` | `app/routes/blog.tsx` |
-| `/somewhere-else` | `app/routes/$.tsx` | `app/root.tsx` |
+```tsx filename=app/routes/files.$.tsx
+export function loader({ params }) {
+ const filePath = params["*"];
+ return fake.getFileInfo(filePath);
+}
+```
-
+## Escaping Special Characters
-Files that are named `$.tsx` are called "splat" (or "catch-all") routes. These routes will map to any URL not matched by other route files in the same directory.
+If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters.
-Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key.
+| Filename | URL |
+| ------------------------------- | ------------------- |
+| `routes/sitemap[.]xml.tsx` | `/sitemap.xml` |
+| `routes/[sitemap.xml].tsx` | `/sitemap.xml` |
+| `routes/weird-url.[_index].tsx` | `/weird-url/_index` |
+| `routes/dolla-bills-[$].tsx` | `/dolla-bills-$` |
+| `routes/[[so-weird]].tsx` | `/[so-weird]` |
-```tsx filename=app/routes/$.tsx
-import type {
- ActionArgs,
- LoaderArgs,
-} from "@remix-run/node"; // or cloudflare/deno
-import { useParams } from "@remix-run/react";
+## Folders for Organization
-export const loader = async ({ params }: LoaderArgs) => {
- console.log(params["*"]);
-};
+Routes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
-export const action = async ({ params }: ActionArgs) => {
- console.log(params["*"]);
-};
+The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name
-export default function PostRoute() {
- const params = useParams();
- console.log(params["*"]);
-}
+Consider these routes:
+
+```
+routes/
+ _landing._index.tsx
+ _landing.about.tsx
+ _landing.tsx
+ app._index.tsx
+ app.projects.tsx
+ app.tsx
+ app_.projects.$id.roadmap.tsx
```
-## Escaping special characters
+Some, or all of them can be folders holding their own `route` module inside.
-Because some characters have special meaning, you must use our escaping syntax if you want those characters to actually appear in the route. For example, if I wanted to make a [Resource Route][resource-route] for a `/sitemap.xml`, I could name the file `app/routes/[sitemap.xml].tsx`. So you simply wrap any part of the filename with brackets and that will escape any special characters.
+```
+routes/
+ _landing._index/
+ route.tsx
+ scroll-experience.tsx
+ _landing.about/
+ employee-profile-card.tsx
+ get-employee-data.server.tsx
+ route.tsx
+ team-photo.jpg
+ _landing/
+ header.tsx
+ footer.tsx
+ route.tsx
+ app._index/
+ route.tsx
+ stats.tsx
+ app.projects/
+ get-projects.server.tsx
+ project-card.tsx
+ project-buttons.tsx
+ route.tsx
+ app/
+ primary-nav.tsx
+ route.tsx
+ footer.tsx
+ app_.projects.$id.roadmap/
+ route.tsx
+ chart.tsx
+ update-timeline.server.tsx
+ contact-us.tsx
+```
+
+Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example:
+
+```
+# these are the same route:
+routes/app.tsx
+routes/app/route.tsx
+
+# as are these
+routes/app._index.tsx
+routes/app._index/route.tsx
+```
-
- Note, you could even do `app/routes/sitemap[.]xml.tsx` if you wanted to only wrap the part that needs to be escaped. It makes no difference. Choose the one you like best.
-
+## Scaling
+
+Our general recommendation for scale is to make every route a folder and put the modules used exclusively by that route in the folder, then put the shared modules outside of routes folder elsewhere. This has a couple benefits:
+
+- Easy to identify shared modules, so tread lightly when changing them
+- Easy to organize and refactor the modules for a specific route without creating "file organization fatigue" and cluttering up other parts of the app
+
+## More Flexibility
+
+While we like this file convention, we recognize that at a certain scale many organizations won't like it. You can always define your routes programmatically in the [remix config][remix-config].
+
+There's also the [Flat Routes][flat-routes] third-party package with configurable options beyond the defaults in Remix.
[loader]: ../route/loader
[action]: ../route/action
-[meta]: ../route/meta
-[headers]: ../route/headers
-[links]: ../route/links
-[error-boundary]: ../route/error-boundary
[outlet]: ../components/outlet
-[view-example-app]: https://github.com/remix-run/examples/tree/main/multiple-params
-[use-params]: https://reactrouter.com/hooks/use-params
-[params]: ../route/loader#params
[routing-guide]: ../guides/routing
[root-route]: #root-route
[resource-route]: ../guides/resource-routes
-[v2guide]: ../pages/v2#file-system-route-convention
+[routeconvention-v2]: ./route-files-v2
+[flatroutes-rfc]: https://github.com/remix-run/remix/discussions/4482
+[root-route]: #root-route
+[index-route]: ../guides/routing#index-routes
+[nested-routing]: ../guides/routing#what-is-nested-routing
+[nested-routes]: #nested-routes
+[remix-config]: ./remix-config#routes
+[dot-delimiters]: #dot-delimiters
+[dynamic-segments]: #dynamic-segments
+[remix-config]: ./remix-config#routes
+[flat-routes]: https://github.com/kiliman/remix-flat-routes
diff --git a/docs/guides/data-loading.md b/docs/guides/data-loading.md
index dadd8d5a349..3e70da6df72 100644
--- a/docs/guides/data-loading.md
+++ b/docs/guides/data-loading.md
@@ -146,7 +146,7 @@ export { db };
And then your routes can import it and make queries against it:
-```tsx filename=app/routes/products/$categoryId.tsx
+```tsx filename=app/routes/products.$categoryId.tsx
import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
@@ -176,7 +176,7 @@ export default function ProductCategory() {
If you are using TypeScript, you can use type inference to use Prisma Client generated types when calling `useLoaderData`. This allows better type safety and intellisense when writing code that uses the loaded data.
-```tsx filename=app/routes/products/$productId.tsx
+```tsx filename=app/routes/products.$productId.tsx
import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
@@ -324,7 +324,7 @@ Sometimes you need to read and change the search params from your component inst
Perhaps the most common way to set search params is letting the user control them with a form:
-```tsx filename=app/routes/products/shoes.tsx lines=[8,9,16,17]
+```tsx filename=app/routes/products.shoes.tsx lines=[8,9,16,17]
export default function ProductFilters() {
return (