Repository: remix-run/remix
Pull request #6969 (2023-07-27 04:18:28)

feat!: remove v1 route convention
Replay of https://github.com/remix-run/remix/pull/5649 against current `dev`, plus updates to templates and files added/changed since the `v2` branch.

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: fa036be05ef12d307f6c26fd128642195b775805

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 18 packages</summary>

  | Name                          | Type  |
| ----------------------------- | ----- |
| @remix-run/dev                | Major |
| @remix-run/react              | Major |
| @remix-run/server-runtime     | Major |
| @remix-run/testing            | Major |
| @remix-run/cloudflare         | Major |
| @remix-run/deno               | Major |
| @remix-run/node               | Major |
| @remix-run/cloudflare-pages   | Major |
| @remix-run/cloudflare-workers | Major |
| @remix-run/architect          | Major |
| @remix-run/express            | Major |
| @remix-run/netlify            | Major |
| @remix-run/serve              | Major |
| @remix-run/vercel             | Major |
| create-remix                  | Major |
| remix                         | Major |
| @remix-run/css-bundle         | Major |
| @remix-run/eslint-config      | Major |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/remix-run/remix/new/markdalgleish/remove-v1-route-convention?filename=.changeset/four-cats-explode.md&value=---%0A%0A---%0A%0Afeat!%3A%20remove%20v1%20route%20convention%0A)


ryanflorence: nice

Patch:
diff --git a/.changeset/v2-route-convention.md b/.changeset/v2-route-convention.md
new file mode 100644
index 000000000..9ddfb8450
--- /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 3c4a380f3..18222849c 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
-
-<!-- prettier-ignore -->
-```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 `<Outlet />`.
-
-```tsx
-import {
-  Links,
-  Meta,
-  Outlet,
-  Scripts,
-  ScrollRestoration,
-} from "@remix-run/react";
-
-export default function Root() {
-  return (
-    <html lang="en">
-      <head>
-        <Links />
-        <Meta />
-      </head>
-      <body>
-        <Outlet />
-        <ScrollRestoration />
-        <Scripts />
-      </body>
-    </html>
-  );
-}
-```
-
-## 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].
-
-<!-- prettier-ignore -->
-```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.
-
-<!-- prettier-ignore -->
-```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.
-
-<!-- prettier-ignore -->
-```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:
-
-<!-- prettier-ignore -->
-```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:
-
-<!-- prettier-ignore -->
-```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 <a name="pathless-routes"><b>Pathless Routes</b></a>
-
-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.
-
-<!-- prettier-ignore -->
-```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.
-
-<!-- prettier-ignore -->
-```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.
-
-<!-- prettier-ignore -->
-```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.
-
-<docs-info>The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name</docs-info>
-
-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 37616858b..3e929dde0 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
 
-<docs-warning>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].</docs-warning>
-
-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 />`][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 `<Outlet />`.
+
+```tsx
+import {
+  Links,
+  Meta,
+  Outlet,
+  Scripts,
+  ScrollRestoration,
+} from "@remix-run/react";
+
+export default function Root() {
+  return (
+    <html lang="en">
+      <head>
+        <Links />
+        <Meta />
+      </head>
+      <body>
+        <Outlet />
+        <ScrollRestoration />
+        <Scripts />
+      </body>
+    </html>
+  );
+}
+```
 
 ## 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].
 
 <!-- prettier-ignore -->
 ```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 `<Outlet />` rendered by the root route.
+## Dot Delimiters
 
-## Dynamic Route Parameters
+Adding a `.` to a route filename will create a `/` in the URL.
 
 <!-- prettier-ignore -->
-```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
 ```
 
-<details>
+| URL                        | Matched Route                 |
+| -------------------------- | ----------------------------- |
+| `/concerts/trending`       | `concerts.trending.tsx`       |
+| `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` |
+| `/concerts/san-diego`      | `concerts.san-diego.tsx`      |
 
-<summary>URL Route Matches</summary>
+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
 
-</details>
+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:
+<!-- prettier-ignore -->
+```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:
 
 <!-- prettier-ignore -->
-```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
 ```
 
-<details>
+All the routes that start with `concerts.` will be child routes of `concerts.tsx` and render inside the parent route's [outlet][outlet].
 
-<summary>URL Route Matches</summary>
+| 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.
 
-</details>
+## 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:
 
 <!-- prettier-ignore -->
-```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
 ```
 
-<details>
+| 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` |
 
-<summary>URL Route Matches</summary>
+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.
 
-</details>
+## 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 `<Outlet />` where the child routes should appear. This is how you can create multiple levels of persistent layout nesting associated with URLs.
+We call these <a name="pathless-routes"><b>Pathless Routes</b></a>
 
-## 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.
 
 <!-- prettier-ignore -->
-```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
 ```
 
-<details>
-
-<summary>URL Route Matches</summary>
-
-| 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` |
 
-</details>
+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.
-
-<docs-warning>Be careful, pathless layout routes introduce the possibility of URL conflicts</docs-warning>
+## Optional Segments
 
-## Dot Delimiters
+Wrapping a route segment in parentheses will make the segment optional.
 
 <!-- prettier-ignore -->
-```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
 ```
 
-<details>
-
-<summary>URL Route Matches</summary>
+| 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`        |
-
-</details>
-
-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.
+
 <!-- prettier-ignore -->
-```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
 ```
 
-<details>
+| 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` |
 
-<summary>URL Route Matches</summary>
+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);
+}
+```
 
-</details>
+## 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["*"]);
-};
+<docs-info>The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name</docs-info>
 
-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
+```
 
-<docs-info>
-  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.
-</docs-info>
+## 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 dadd8d5a3..3e70da6df 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 (
     <Form method="get">
diff --git a/docs/guides/data-writes.md b/docs/guides/data-writes.md
index 1187ab9e1..70e9fc6c4 100644
--- a/docs/guides/data-writes.md
+++ b/docs/guides/data-writes.md
@@ -143,7 +143,7 @@ Whether you use `<form>` or `<Form>` though, you write the very same code. You c
 
 Let's start with our project form from earlier but make it usable:
 
-Let's say you've got the route `app/routes/projects/new.js` with this form in it:
+Let's say you've got the route `app/routes/projects.new.js` with this form in it:
 
 ```tsx
 export default function NewProject() {
diff --git a/docs/guides/mdx.md b/docs/guides/mdx.md
index 0e7621c89..e66c22a75 100644
--- a/docs/guides/mdx.md
+++ b/docs/guides/mdx.md
@@ -51,7 +51,7 @@ import SomeComponent from "~/components/some-component";
 
 ### Example
 
-By creating a `app/routes/posts/first-post.mdx` we can start writing a blog post:
+By creating a `app/routes/posts.first-post.mdx` we can start writing a blog post:
 
 ```mdx
 ---
@@ -126,7 +126,7 @@ import Component, {
 
 The following example demonstrates how you might build a simple blog with MDX, including individual pages for the posts themselves and an index page that shows all posts.
 
-```tsx filename=app/routes/index.tsx
+```tsx filename=app/routes/_index.tsx
 import { json } from "@remix-run/node"; // or cloudflare/deno
 import { Link, useLoaderData } from "@remix-run/react";
 
diff --git a/docs/guides/not-found.md b/docs/guides/not-found.md
index 34106cf48..fbb85869b 100644
--- a/docs/guides/not-found.md
+++ b/docs/guides/not-found.md
@@ -17,7 +17,7 @@ The first case is already handled by Remix, you don't have to throw a response y
 
 As soon as you know you don't have what the user is looking for you should _throw a response_.
 
-```tsx filename=routes/page/$slug.js
+```tsx filename=app/routes/page.$slug.js
 export async function loader({ params }: LoaderArgs) {
   const page = await db.page.findOne({
     where: { slug: params.slug },
diff --git a/docs/guides/optimistic-ui.md b/docs/guides/optimistic-ui.md
index 14dc3d705..e24f81048 100644
--- a/docs/guides/optimistic-ui.md
+++ b/docs/guides/optimistic-ui.md
@@ -22,7 +22,7 @@ Remix can help you build optimistic UI with [`useNavigation`][use-navigation] an
 
 Consider the workflow for viewing and creating a new project. The project route loads the project and renders it.
 
-```tsx filename=app/routes/project/$id.tsx
+```tsx filename=app/routes/project.$id.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";
@@ -59,7 +59,7 @@ export function ProjectView({ project }) {
 
 Now we can get to the fun part. Here's what a "new project" route might look like:
 
-```tsx filename=app/routes/projects/new.tsx
+```tsx filename=app/routes/projects.new.tsx
 import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno
 import { redirect } from "@remix-run/node"; // or cloudflare/deno
 import { Form } from "@remix-run/react";
@@ -92,7 +92,7 @@ export default function NewProject() {
 
 At this point, typically you'd render a busy spinner on the page while the user waits for the project to be sent to the server, added to the database, and sent back to the browser and then redirected to the project. Remix makes that pretty easy:
 
-```tsx filename=app/routes/projects/new.tsx lines=[3,15,27,29-31]
+```tsx filename=app/routes/projects.new.tsx lines=[3,15,27,29-31]
 import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno
 import { redirect } from "@remix-run/node"; // or cloudflare/deno
 import { Form, useNavigation } from "@remix-run/react";
@@ -133,7 +133,7 @@ export default function NewProject() {
 
 Since we know that almost every time this form is submitted it's going to succeed, we can just skip the busy spinners and show the UI as we know it's going to be: the `<ProjectView>`.
 
-```tsx filename=app/routes/projects/new.tsx lines=[5,17-23,31-32]
+```tsx filename=app/routes/projects.new.tsx lines=[5,17-23,31-32]
 import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno
 import { redirect } from "@remix-run/node"; // or cloudflare/deno
 import { Form, useNavigation } from "@remix-run/react";
@@ -178,7 +178,7 @@ One of the hardest parts about implementing optimistic UI is how to handle failu
 
 If you want to have more control over the UI when an error occurs and put the user right back where they were without losing any state, you can catch your own error and send it down through action data.
 
-```tsx filename=app/routes/projects/new.tsx lines=[5,15-23,27,47]
+```tsx filename=app/routes/projects.new.tsx lines=[5,15-23,27,47]
 import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno
 import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
 import {
@@ -235,7 +235,7 @@ Now in the rare case of an error on the server, the UI reverts back to the form,
 
 For this to work best, you'll want a bit of client-side validation so that form-validation issues on the server don't cause the app to flash between optimistic UI and validation messages. Fortunately, [HTML usually has everything you need][html-input] built-in. The browser will validate the fields before the form is even submitted to the server to avoid sending bad data and getting flashes of optimistic UI.
 
-```tsx filename=app/routes/projects/new.tsx lines=[43,45]
+```tsx filename=app/routes/projects.new.tsx lines=[43,45]
 import type { ActionArgs } from "@remix-run/node"; // or cloudflare/deno
 import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
 import {
diff --git a/docs/guides/resource-routes.md b/docs/guides/resource-routes.md
index 012e7a474..07ddca5a9 100644
--- a/docs/guides/resource-routes.md
+++ b/docs/guides/resource-routes.md
@@ -20,7 +20,7 @@ If a route doesn't export a default component, it can be used as a Resource Rout
 
 For example, consider a UI Route that renders a report, note the link:
 
-```tsx filename=app/routes/reports/$id.js lines=[10-12]
+```tsx filename=app/routes/reports.$id.js lines=[10-12]
 export async function loader({ params }: LoaderArgs) {
   return json(await getReport(params.id));
 }
@@ -41,7 +41,7 @@ export default function Report() {
 
 It's linking to a PDF version of the page. To make this work we can create a Resource Route below it. Notice that it has no component: that makes it a Resource Route.
 
-```tsx filename=app/routes/reports/$id/pdf.tsx
+```tsx filename=app/routes/reports.$id.pdf.tsx
 export async function loader({ params }: LoaderArgs) {
   const report = await getReport(params.id);
   const pdf = await generateReportPDF(report);
@@ -71,14 +71,14 @@ To add a `.` to a route's path, use the `[]` escape characters. Our PDF route fi
 ```sh
 # original
 # /reports/123/pdf
-app/routes/reports/$id/pdf.ts
+app/routes/reports.$id.pdf.ts
 
 # with a file extension
 # /reports/123.pdf
-app/routes/reports/$id[.pdf].ts
+app/routes/reports.$id[.pdf].ts
 
 # or like this, the resulting URL is the same
-app/routes/reports/$id[.]pdf.ts
+app/routes/reports.$id[.]pdf.ts
 ```
 
 ## Handling different request methods
diff --git a/docs/guides/routing.md b/docs/guides/routing.md
index 471446aee..812372167 100644
--- a/docs/guides/routing.md
+++ b/docs/guides/routing.md
@@ -74,32 +74,6 @@ app
     └── sales.tsx
 ```
 
-<details>
-
-<summary>Or if using the v1 routing convention:</summary>
-
-```
-app
-├── root.tsx
-└── routes
-    ├── accounts.tsx
-    ├── dashboard.tsx
-    ├── expenses.tsx
-    ├── index.tsx
-    ├── reports.tsx
-    ├── sales
-    │   ├── customers.tsx
-    │   ├── deposits.tsx
-    │   ├── index.tsx
-    │   ├── invoices
-    │   │   ├── $invoiceId.tsx
-    │   │   └── index.tsx
-    │   ├── invoices.tsx
-    │   └── subscriptions.tsx
-    └── sales.tsx
-```
-
-</details>
 
 - `root.tsx` is the "root route" that serves as the layout for the entire application. Every route will render inside its `<Outlet/>`.
 - Note that there are files with `.` delimiters. The `.` creates a `/` in the URL for that route, as well as layout nesting with another route that matches the segments before the `.`. For example, `sales.tsx` is the **parent route** for all the **child routes** that look like `sales.[the nested path].tsx`. The `<Outlet />` in `sales.tsx` will render the matching child route.
diff --git a/docs/guides/styling.md b/docs/guides/styling.md
index cf1dd70dd..3cefa8b30 100644
--- a/docs/guides/styling.md
+++ b/docs/guides/styling.md
@@ -97,7 +97,7 @@ export function links() {
 }
 ```
 
-```tsx filename=app/routes/dashboard/accounts.tsx
+```tsx filename=app/routes/dashboard.accounts.tsx
 import styles from "~/styles/accounts.css";
 
 export function links() {
@@ -105,7 +105,7 @@ export function links() {
 }
 ```
 
-```tsx filename=app/routes/dashboard/sales.tsx
+```tsx filename=app/routes/dashboard.sales.tsx
 import styles from "~/styles/sales.css";
 
 export function links() {
@@ -230,9 +230,9 @@ Note that the primary button's `links` include the base button's links. This way
 
 Because these buttons are not routes, and therefore not associated with a URL segment, Remix doesn't know when to prefetch, load, or unload the styles. We need to "surface" the links up to the routes that use the components.
 
-Consider that `routes/index.js` uses the primary button component:
+Consider that `routes/_index.js` uses the primary button component:
 
-```tsx filename=app/routes/index.js lines=[1-4,9]
+```tsx filename=app/routes/_index.js lines=[1-4,9]
 import {
   PrimaryButton,
   links as primaryButtonLinks,
diff --git a/docs/hooks/use-matches.md b/docs/hooks/use-matches.md
index 4b96e7c00..dff434d42 100644
--- a/docs/hooks/use-matches.md
+++ b/docs/hooks/use-matches.md
@@ -48,7 +48,7 @@ You can put whatever you want on a route `handle`. Here we'll use `breadcrumb`.
 
 2. We can do the same for a child route
 
-   ```tsx filename=app/routes/parent/child.tsx
+   ```tsx filename=app/routes/parent.child.tsx
    export const handle = {
      breadcrumb: () => (
        <Link to="/parent/child">Child Route</Link>
diff --git a/docs/pages/api-development-strategy.md b/docs/pages/api-development-strategy.md
index 60ed13b57..09463fb13 100644
--- a/docs/pages/api-development-strategy.md
+++ b/docs/pages/api-development-strategy.md
@@ -55,7 +55,6 @@ The lifecycle is thus either:
 | `v2_dev`             | Enable the new development server (including HMR/HDR support)   |
 | `v2_headers`         | Leverage ancestor `headers` if children do not export `headers` |
 | `v2_meta`            | Enable the new API for your `meta` functions                    |
-| `v2_routeConvention` | Enable the flat routes style of file-based routing              |
 
 [future-flags-blog-post]: https://remix.run/blog/future-flags
 [feature-flowchart]: /docs-images/feature-flowchart.png
diff --git a/docs/pages/faq.md b/docs/pages/faq.md
index cabe0a519..c6e3bbd0c 100644
--- a/docs/pages/faq.md
+++ b/docs/pages/faq.md
@@ -81,7 +81,7 @@ We find option (1) to be the simplest because you don't have to mess around with
 
 HTML buttons can send a value, so it's the easiest way to implement this:
 
-```tsx filename=app/routes/projects/$id.tsx lines=[3-4,33,39]
+```tsx filename=app/routes/projects.$id.tsx lines=[3-4,33,39]
 export async function action({ request }: ActionArgs) {
   const formData = await request.formData();
   const intent = formData.get("intent");
diff --git a/docs/pages/gotchas.md b/docs/pages/gotchas.md
index 4b10260de..afc7a1069 100644
--- a/docs/pages/gotchas.md
+++ b/docs/pages/gotchas.md
@@ -18,7 +18,7 @@ TypeError: Cannot read properties of undefined (reading 'root')
 
 For example, you can't import "fs-extra" directly into a route module:
 
-```tsx bad filename=app/routes/index.tsx lines=[2] nocopy
+```tsx bad filename=app/routes/_index.tsx lines=[2] nocopy
 import { json } from "@remix-run/node"; // or cloudflare/deno
 import fs from "fs-extra";
 
@@ -39,7 +39,7 @@ export { default } from "fs-extra";
 
 And then change our import in the route to the new "wrapper" module:
 
-```tsx filename=app/routes/index.tsx lines=[3]
+```tsx filename=app/routes/_index.tsx lines=[3]
 import { json } from "@remix-run/node"; // or cloudflare/deno
 
 import fs from "~/utils/fs-extra.server";
diff --git a/docs/route/loader.md b/docs/route/loader.md
index 639968614..77fc56719 100644
--- a/docs/route/loader.md
+++ b/docs/route/loader.md
@@ -74,7 +74,7 @@ export default function SomeRoute() {
 
 Route params are defined by route file names. If a segment begins with `$` like `$invoiceId`, the value from the URL for that segment will be passed to your loader.
 
-```tsx filename=app/routes/invoices/$invoiceId.tsx nocopy
+```tsx filename=app/routes/invoices.$invoiceId.tsx nocopy
 // if the user visits /invoices/123
 export async function loader({ params }: LoaderArgs) {
   params.invoiceId; // "123"
@@ -83,7 +83,7 @@ export async function loader({ params }: LoaderArgs) {
 
 Params are mostly useful for looking up records by ID:
 
-```tsx filename=app/routes/invoices/$invoiceId.tsx
+```tsx filename=app/routes/invoices.$invoiceId.tsx
 // if the user visits /invoices/123
 export async function loader({ params }: LoaderArgs) {
   const invoice = await fakeDb.getInvoice(params.invoiceId);
@@ -233,7 +233,7 @@ export async function requireUserSession(request) {
 }
 ```
 
-```tsx filename=app/routes/invoice/$invoiceId.tsx
+```tsx filename=app/routes/invoice.$invoiceId.tsx
 import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno
 import { json } from "@remix-run/node"; // or cloudflare/deno
 import {
diff --git a/docs/route/meta-v2.md b/docs/route/meta-v2.md
index 1fd24470f..96fcb0249 100644
--- a/docs/route/meta-v2.md
+++ b/docs/route/meta-v2.md
@@ -172,7 +172,7 @@ export const meta: V2_MetaFunction = () => {
 };
 ```
 
-```tsx bad filename=app/routes/projects/$id.tsx
+```tsx bad filename=app/routes/projects.$id.tsx
 export const meta: V2_MetaFunction<typeof loader> = ({
   data,
 }) => {
diff --git a/docs/route/should-revalidate.md b/docs/route/should-revalidate.md
index d66e2bcc0..4bef41285 100644
--- a/docs/route/should-revalidate.md
+++ b/docs/route/should-revalidate.md
@@ -92,7 +92,7 @@ For instance, consider an event slug with the id and an human-friendly title:
 - `/events/blink-182-united-center-saint-paul--ae3f9`
 - `/events/blink-182-little-caesars-arena-detroit--e87ad`
 
-```tsx filename=app/routes/events/$slug.tsx
+```tsx filename=app/routes/events.$slug.tsx
 export async function loader({ params }: LoaderArgs) {
   const id = params.slug.split("--")[1];
   return loadEvent(id);
diff --git a/docs/utils/cookies.md b/docs/utils/cookies.md
index 5a087e80d..3f2b95bd2 100644
--- a/docs/utils/cookies.md
+++ b/docs/utils/cookies.md
@@ -30,7 +30,7 @@ Then, you can `import` the cookie and use it in your `loader` and/or `action`. T
 
 **Note:** We recommend (for now) that you create all the cookies your app needs in a `*.server.ts` file and `import` them into your route modules. This allows the Remix compiler to correctly prune these imports out of the browser build where they are not needed. We hope to eventually remove this caveat.
 
-```tsx filename=app/routes/index.tsx lines=[8,12-13,19-20,24]
+```tsx filename=app/routes/_index.tsx lines=[8,12-13,19-20,24]
 import type {
   ActionArgs,
   LoaderArgs,
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index 609ce3169..9507412e3 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -9,7 +9,6 @@ import type { NodePolyfillsOptions as EsbuildPluginsNodeModulesPolyfillOptions }
 
 import type { RouteManifest, DefineRoutesFunction } from "./config/routes";
 import { defineRoutes } from "./config/routes";
-import { defineConventionalRoutes } from "./config/routesConvention";
 import { ServerMode, isValidServerMode } from "./config/serverModes";
 import { serverBuildVirtualModule } from "./compiler/server/virtualModules";
 import { flatRoutes } from "./config/flat-routes";
@@ -40,7 +39,6 @@ interface FutureConfig {
   v2_dev: boolean | Dev;
   v2_headers: boolean;
   v2_meta: boolean;
-  v2_routeConvention: boolean;
 }
 
 type ServerNodeBuiltinsPolyfillOptions = Pick<
@@ -600,21 +598,9 @@ export async function readConfig(
     root: { path: "", id: "root", file: rootRouteFile },
   };
 
-  let routesConvention: typeof flatRoutes;
-
-  if (appConfig.future?.v2_routeConvention) {
-    routesConvention = flatRoutes;
-  } else {
-    flatRoutesWarning();
-    routesConvention = defineConventionalRoutes;
-  }
-
   if (fse.existsSync(path.resolve(appDirectory, "routes"))) {
-    let conventionalRoutes = routesConvention(
-      appDirectory,
-      appConfig.ignoredRouteFiles
-    );
-    for (let route of Object.values(conventionalRoutes)) {
+    let fileRoutes = flatRoutes(appDirectory, appConfig.ignoredRouteFiles);
+    for (let route of Object.values(fileRoutes)) {
       routes[route.id] = { ...route, parentId: route.parentId || "root" };
     }
   }
@@ -655,7 +641,6 @@ export async function readConfig(
     v2_dev: appConfig.future?.v2_dev ?? false,
     v2_headers: appConfig.future?.v2_headers === true,
     v2_meta: appConfig.future?.v2_meta === true,
-    v2_routeConvention: appConfig.future?.v2_routeConvention === true,
   };
 
   return {
@@ -782,12 +767,6 @@ let futureFlagWarning =
     });
   };
 
-let flatRoutesWarning = futureFlagWarning({
-  message: "The route file convention is changing in v2",
-  flag: "v2_routeConvention",
-  link: "https://remix.run/docs/en/v1.15.0/pages/v2#file-system-route-convention",
-});
-
 let metaWarning = futureFlagWarning({
   message: "The route `meta` API is changing in v2",
   flag: "v2_meta",
diff --git a/packages/remix-dev/config/flat-routes.ts b/packages/remix-dev/config/flat-routes.ts
index 6f1d8ad28..c46d4a8df 100644
--- a/packages/remix-dev/config/flat-routes.ts
+++ b/packages/remix-dev/config/flat-routes.ts
@@ -5,15 +5,15 @@ import { makeRe } from "minimatch";
 import type { ConfigRoute, RouteManifest } from "./routes";
 import { normalizeSlashes } from "./routes";
 import { findConfig } from "../config";
-import {
-  escapeEnd,
-  escapeStart,
-  isSegmentSeparator,
-  optionalEnd,
-  optionalStart,
-  paramPrefixChar,
-  routeModuleExts,
-} from "./routesConvention";
+
+export const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"];
+
+export let paramPrefixChar = "$" as const;
+export let escapeStart = "[" as const;
+export let escapeEnd = "]" as const;
+
+export let optionalStart = "(" as const;
+export let optionalEnd = ")" as const;
 
 const PrefixLookupTrieEndSymbol = Symbol("PrefixLookupTrieEndSymbol");
 type PrefixLookupNode = {
@@ -543,3 +543,8 @@ export function getRouteIdConflictErrorMessage(
     "\n"
   );
 }
+
+export function isSegmentSeparator(checkChar: string | undefined) {
+  if (!checkChar) return false;
+  return ["/", ".", path.win32.sep].includes(checkChar);
+}
diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts
deleted file mode 100644
index 35713e756..000000000
--- a/packages/remix-dev/config/routesConvention.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-import * as fs from "fs";
-import * as path from "path";
-import { Minimatch } from "minimatch";
-
-import type { RouteManifest, DefineRouteFunction } from "./routes";
-import { defineRoutes, createRouteId } from "./routes";
-
-export const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"];
-
-export function isRouteModuleFile(filename: string): boolean {
-  return routeModuleExts.includes(path.extname(filename));
-}
-
-/**
- * Defines routes using the filesystem convention in `app/routes`. The rules are:
- *
- * - Route paths are derived from the file path. A `.` in the filename indicates
- *   a `/` in the URL (a "nested" URL, but no route nesting). A `$` in the
- *   filename indicates a dynamic URL segment.
- * - Subdirectories are used for nested routes.
- *
- * For example, a file named `app/routes/gists/$username.tsx` creates a route
- * with a path of `gists/:username`.
- */
-export function defineConventionalRoutes(
-  appDir: string,
-  ignoredFilePatterns?: string[]
-): RouteManifest {
-  let files: { [routeId: string]: string } = {};
-
-  // First, find all route modules in app/routes
-  visitFiles(path.join(appDir, "routes"), (file) => {
-    if (
-      ignoredFilePatterns &&
-      ignoredFilePatterns.some((pattern) => {
-        let minimatch = new Minimatch(pattern);
-        return minimatch.match(file);
-      })
-    ) {
-      return;
-    }
-
-    if (isRouteModuleFile(file)) {
-      let routeId = createRouteId(path.join("routes", file));
-      files[routeId] = path.join("routes", file);
-      return;
-    }
-
-    throw new Error(
-      `Invalid route module file: ${path.join(appDir, "routes", file)}`
-    );
-  });
-
-  let routeIds = Object.keys(files).sort(byLongestFirst);
-  let parentRouteIds = getParentRouteIds(routeIds);
-
-  let uniqueRoutes = new Map<string, string>();
-
-  // Then, recurse through all routes using the public defineRoutes() API
-  function defineNestedRoutes(
-    defineRoute: DefineRouteFunction,
-    parentId?: string
-  ): void {
-    let childRouteIds = routeIds.filter(
-      (id) => parentRouteIds[id] === parentId
-    );
-
-    for (let routeId of childRouteIds) {
-      let routePath: string | undefined = createRoutePath(
-        routeId.slice((parentId || "routes").length + 1)
-      );
-
-      let isIndexRoute = routeId.endsWith("/index");
-      let fullPath = createRoutePath(routeId.slice("routes".length + 1));
-      let uniqueRouteId = (fullPath || "") + (isIndexRoute ? "?index" : "");
-      let isPathlessLayoutRoute =
-        routeId.split("/").pop()?.startsWith("__") === true;
-
-      /**
-       * We do not try to detect path collisions for pathless layout route
-       * files because, by definition, they create the potential for route
-       * collisions _at that level in the tree_.
-       *
-       * Consider example where a user may want multiple pathless layout routes
-       * for different subfolders
-       *
-       *   routes/
-       *     account.tsx
-       *     account/
-       *       __public/
-       *         login.tsx
-       *         perks.tsx
-       *       __private/
-       *         orders.tsx
-       *         profile.tsx
-       *       __public.tsx
-       *       __private.tsx
-       *
-       * In order to support both a public and private layout for `/account/*`
-       * URLs, we are creating a mutually exclusive set of URLs beneath 2
-       * separate pathless layout routes.  In this case, the route paths for
-       * both account/__public.tsx and account/__private.tsx is the same
-       * (/account), but we're again not expecting to match at that level.
-       *
-       * By only ignoring this check when the final portion of the filename is
-       * pathless, we will still detect path collisions such as:
-       *
-       *   routes/parent/__pathless/foo.tsx
-       *   routes/parent/__pathless2/foo.tsx
-       *
-       * and
-       *
-       *   routes/parent/__pathless/index.tsx
-       *   routes/parent/__pathless2/index.tsx
-       */
-      if (uniqueRouteId && !isPathlessLayoutRoute) {
-        if (uniqueRoutes.has(uniqueRouteId)) {
-          throw new Error(
-            `Path ${JSON.stringify(
-              fullPath || "/"
-            )} defined by route ${JSON.stringify(
-              routeId
-            )} conflicts with route ${JSON.stringify(
-              uniqueRoutes.get(uniqueRouteId)
-            )}`
-          );
-        } else {
-          uniqueRoutes.set(uniqueRouteId, routeId);
-        }
-      }
-
-      if (isIndexRoute) {
-        let invalidChildRoutes = routeIds.filter(
-          (id) => parentRouteIds[id] === routeId
-        );
-
-        if (invalidChildRoutes.length > 0) {
-          throw new Error(
-            `Child routes are not allowed in index routes. Please remove child routes of ${routeId}`
-          );
-        }
-
-        defineRoute(routePath, files[routeId], {
-          index: true,
-        });
-      } else {
-        defineRoute(routePath, files[routeId], () => {
-          defineNestedRoutes(defineRoute, routeId);
-        });
-      }
-    }
-  }
-
-  return defineRoutes(defineNestedRoutes);
-}
-
-export let paramPrefixChar = "$" as const;
-export let escapeStart = "[" as const;
-export let escapeEnd = "]" as const;
-
-export let optionalStart = "(" as const;
-export let optionalEnd = ")" as const;
-
-// TODO: Cleanup and write some tests for this function
-export function createRoutePath(partialRouteId: string): string | undefined {
-  let result = "";
-  let rawSegmentBuffer = "";
-
-  let inEscapeSequence = 0;
-  let inOptionalSegment = 0;
-  let optionalSegmentIndex = null;
-  let skipSegment = false;
-  for (let i = 0; i < partialRouteId.length; i++) {
-    let char = partialRouteId.charAt(i);
-    let prevChar = i > 0 ? partialRouteId.charAt(i - 1) : undefined;
-    let nextChar =
-      i < partialRouteId.length - 1 ? partialRouteId.charAt(i + 1) : undefined;
-
-    function isNewEscapeSequence() {
-      return (
-        !inEscapeSequence && char === escapeStart && prevChar !== escapeStart
-      );
-    }
-
-    function isCloseEscapeSequence() {
-      return inEscapeSequence && char === escapeEnd && nextChar !== escapeEnd;
-    }
-
-    function isStartOfLayoutSegment() {
-      return char === "_" && nextChar === "_" && !rawSegmentBuffer;
-    }
-
-    function isNewOptionalSegment() {
-      return (
-        char === optionalStart &&
-        prevChar !== optionalStart &&
-        (isSegmentSeparator(prevChar) || prevChar === undefined) &&
-        !inOptionalSegment &&
-        !inEscapeSequence
-      );
-    }
-
-    function isCloseOptionalSegment() {
-      return (
-        char === optionalEnd &&
-        nextChar !== optionalEnd &&
-        (isSegmentSeparator(nextChar) || nextChar === undefined) &&
-        inOptionalSegment &&
-        !inEscapeSequence
-      );
-    }
-
-    if (skipSegment) {
-      if (isSegmentSeparator(char)) {
-        skipSegment = false;
-      }
-      continue;
-    }
-
-    if (isNewEscapeSequence()) {
-      inEscapeSequence++;
-      continue;
-    }
-
-    if (isCloseEscapeSequence()) {
-      inEscapeSequence--;
-      continue;
-    }
-
-    if (isNewOptionalSegment()) {
-      inOptionalSegment++;
-      optionalSegmentIndex = result.length;
-      result += optionalStart;
-      continue;
-    }
-
-    if (isCloseOptionalSegment()) {
-      if (optionalSegmentIndex !== null) {
-        result =
-          result.slice(0, optionalSegmentIndex) +
-          result.slice(optionalSegmentIndex + 1);
-      }
-      optionalSegmentIndex = null;
-      inOptionalSegment--;
-      result += "?";
-      continue;
-    }
-
-    if (inEscapeSequence) {
-      result += char;
-      continue;
-    }
-
-    if (isSegmentSeparator(char)) {
-      if (rawSegmentBuffer === "index" && result.endsWith("index")) {
-        result = result.replace(/\/?index$/, "");
-      } else {
-        result += "/";
-      }
-
-      rawSegmentBuffer = "";
-      inOptionalSegment = 0;
-      optionalSegmentIndex = null;
-      continue;
-    }
-
-    if (isStartOfLayoutSegment()) {
-      skipSegment = true;
-      continue;
-    }
-
-    rawSegmentBuffer += char;
-
-    if (char === paramPrefixChar) {
-      if (nextChar === optionalEnd) {
-        throw new Error(
-          `Invalid route path: ${partialRouteId}. Splat route $ is already optional`
-        );
-      }
-      result += typeof nextChar === "undefined" ? "*" : ":";
-      continue;
-    }
-
-    result += char;
-  }
-
-  if (rawSegmentBuffer === "index" && result.endsWith("index")) {
-    result = result.replace(/\/?index$/, "");
-  } else {
-    result = result.replace(/\/$/, "");
-  }
-
-  if (rawSegmentBuffer === "index" && result.endsWith("index?")) {
-    throw new Error(
-      `Invalid route path: ${partialRouteId}. Make index route optional by using (index)`
-    );
-  }
-
-  return result || undefined;
-}
-
-export function isSegmentSeparator(checkChar: string | undefined) {
-  if (!checkChar) return false;
-  return ["/", ".", path.win32.sep].includes(checkChar);
-}
-
-function getParentRouteIds(
-  routeIds: string[]
-): Record<string, string | undefined> {
-  return routeIds.reduce<Record<string, string | undefined>>(
-    (parentRouteIds, childRouteId) => ({
-      ...parentRouteIds,
-      [childRouteId]: routeIds.find((id) => childRouteId.startsWith(`${id}/`)),
-    }),
-    {}
-  );
-}
-
-function byLongestFirst(a: string, b: string): number {
-  return b.length - a.length;
-}
-
-function visitFiles(
-  dir: string,
-  visitor: (file: string) => void,
-  baseDir = dir
-): void {
-  for (let filename of fs.readdirSync(dir)) {
-    let file = path.resolve(dir, filename);
-    let stat = fs.lstatSync(file);
-
-    if (stat.isDirectory()) {
-      visitFiles(file, visitor, baseDir);
-    } else if (stat.isFile()) {
-      visitor(path.relative(baseDir, file));
-    }
-  }
-}
-
-/*
-eslint
-  no-loop-func: "off",
-*/
diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts
index d1513a7f6..db1e7fbb0 100644
--- a/packages/remix-react/entry.ts
+++ b/packages/remix-react/entry.ts
@@ -36,7 +36,6 @@ export interface FutureConfig {
   v2_dev: boolean | Dev;
   v2_headers: boolean;
   v2_meta: boolean;
-  v2_routeConvention: boolean;
 }
 
 export interface AssetsManifest {
diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts
index eba9031e0..394ef8407 100644
--- a/packages/remix-server-runtime/entry.ts
+++ b/packages/remix-server-runtime/entry.ts
@@ -24,7 +24,6 @@ export interface FutureConfig {
   v2_dev: boolean | Dev;
   v2_headers: boolean;
   v2_meta: boolean;
-  v2_routeConvention: boolean;
 }
 
 export interface AssetsManifest {
diff --git a/templates/arc/remix.config.js b/templates/arc/remix.config.js
index 310cfa560..62d5ba14e 100644
--- a/templates/arc/remix.config.js
+++ b/templates/arc/remix.config.js
@@ -12,6 +12,5 @@ export default {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/cloudflare-pages/remix.config.js b/templates/cloudflare-pages/remix.config.js
index c5005a25e..ac60a1e27 100644
--- a/templates/cloudflare-pages/remix.config.js
+++ b/templates/cloudflare-pages/remix.config.js
@@ -18,6 +18,5 @@ export default {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/cloudflare-workers/remix.config.js b/templates/cloudflare-workers/remix.config.js
index b5866e0a1..216ed73d0 100644
--- a/templates/cloudflare-workers/remix.config.js
+++ b/templates/cloudflare-workers/remix.config.js
@@ -20,6 +20,5 @@ export default {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/deno/remix.config.js b/templates/deno/remix.config.js
index deee56e18..5730c0ad8 100644
--- a/templates/deno/remix.config.js
+++ b/templates/deno/remix.config.js
@@ -21,6 +21,5 @@ module.exports = {
   future: {
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/express/remix.config.js b/templates/express/remix.config.js
index ccaabb773..3238eac42 100644
--- a/templates/express/remix.config.js
+++ b/templates/express/remix.config.js
@@ -11,6 +11,5 @@ export default {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/fly/remix.config.js b/templates/fly/remix.config.js
index 0bd3fd95c..d32a12921 100644
--- a/templates/fly/remix.config.js
+++ b/templates/fly/remix.config.js
@@ -11,6 +11,5 @@ module.exports = {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/netlify/remix.config.js b/templates/netlify/remix.config.js
index abf7871a9..5cb9a5227 100644
--- a/templates/netlify/remix.config.js
+++ b/templates/netlify/remix.config.js
@@ -15,6 +15,5 @@ module.exports = {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/remix-javascript/remix.config.js b/templates/remix-javascript/remix.config.js
index 7a9bff97f..e2f9e20ac 100644
--- a/templates/remix-javascript/remix.config.js
+++ b/templates/remix-javascript/remix.config.js
@@ -8,6 +8,5 @@ module.exports = {
   serverModuleFormat: "cjs",
   future: {
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/remix/remix.config.js b/templates/remix/remix.config.js
index 0bd3fd95c..d32a12921 100644
--- a/templates/remix/remix.config.js
+++ b/templates/remix/remix.config.js
@@ -11,6 +11,5 @@ module.exports = {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };
diff --git a/templates/vercel/remix.config.js b/templates/vercel/remix.config.js
index 564f8cee5..b6aa5ad23 100644
--- a/templates/vercel/remix.config.js
+++ b/templates/vercel/remix.config.js
@@ -15,6 +15,5 @@ module.exports = {
 
     v2_headers: true,
     v2_meta: true,
-    v2_routeConvention: true,
   },
 };


Test patch:
diff --git a/integration/abort-signal-test.ts b/integration/abort-signal-test.ts
index df4b5c38b..0888e3e61 100644
--- a/integration/abort-signal-test.ts
+++ b/integration/abort-signal-test.ts
@@ -9,9 +9,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/routes/_index.jsx": js`
         import { json } from "@remix-run/node";
diff --git a/integration/action-test.ts b/integration/action-test.ts
index 84f4edee4..bcaf27263 100644
--- a/integration/action-test.ts
+++ b/integration/action-test.ts
@@ -17,11 +17,6 @@ test.describe("actions", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/routes/urlencoded.jsx": js`
           import { Form, useActionData } from "@remix-run/react";
diff --git a/integration/browser-entry-test.ts b/integration/browser-entry-test.ts
index 7a85b0ee2..3644a958a 100644
--- a/integration/browser-entry-test.ts
+++ b/integration/browser-entry-test.ts
@@ -10,7 +10,7 @@ let appFixture: AppFixture;
 test.beforeAll(async () => {
   fixture = await createFixture({
     files: {
-      "app/routes/index.jsx": js`
+      "app/routes/_index.jsx": js`
         import { Link } from "@remix-run/react";
 
         export default function Index() {
diff --git a/integration/bug-report-test.ts b/integration/bug-report-test.ts
index 0d6fe2c89..3c596d60a 100644
--- a/integration/bug-report-test.ts
+++ b/integration/bug-report-test.ts
@@ -49,9 +49,6 @@ test.beforeEach(async ({ context }) => {
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     ////////////////////////////////////////////////////////////////////////////
     // 💿 Next, add files to this object, just like files in a real app,
     // `createFixture` will make an app and run your tests against it.
diff --git a/integration/catch-boundary-data-test.ts b/integration/catch-boundary-data-test.ts
index 966552755..ba94ec1cd 100644
--- a/integration/catch-boundary-data-test.ts
+++ b/integration/catch-boundary-data-test.ts
@@ -34,11 +34,6 @@ test.beforeEach(async ({ context }) => {
 test.describe("ErrorBoundary (thrown responses)", () => {
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/root.jsx": js`
           import { json } from "@remix-run/node";
diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts
index 00527d2c4..9bc4b6da2 100644
--- a/integration/catch-boundary-test.ts
+++ b/integration/catch-boundary-test.ts
@@ -24,11 +24,6 @@ test.describe("ErrorBoundary (thrown responses)", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/root.jsx": js`
             import { json } from "@remix-run/node";
diff --git a/integration/cf-compiler-test.ts b/integration/cf-compiler-test.ts
index 1077ed434..e10172dfe 100644
--- a/integration/cf-compiler-test.ts
+++ b/integration/cf-compiler-test.ts
@@ -28,9 +28,6 @@ test.describe("cloudflare compiler", () => {
 
   test.beforeAll(async () => {
     projectDir = await createFixtureProject({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       setup: "cloudflare",
       template: "cf-template",
       files: {
diff --git a/integration/compiler-mjs-output-test.ts b/integration/compiler-mjs-output-test.ts
index 4f6753444..3df4aba40 100644
--- a/integration/compiler-mjs-output-test.ts
+++ b/integration/compiler-mjs-output-test.ts
@@ -14,7 +14,6 @@ test.beforeAll(async () => {
         export default {
           serverModuleFormat: "esm",
           serverBuildPath: "build/index.mjs",
-          future: { v2_routeConvention: true },
         };
       `,
       "package.json": js`
diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts
index 4cc7e8617..92e0962f9 100644
--- a/integration/compiler-test.ts
+++ b/integration/compiler-test.ts
@@ -27,9 +27,6 @@ test.describe("compiler", () => {
         "remix.config.js": js`
           let { getDependenciesToBundle } = require("@remix-run/dev");
           module.exports = {
-            future: {
-              v2_routeConvention: true,
-            },
             serverDependenciesToBundle: [
               "esm-only-pkg",
               "esm-only-single-export",
@@ -331,9 +328,6 @@ test.describe("compiler", () => {
 
       await expect(() =>
         createFixtureProject({
-          config: {
-            future: { v2_routeConvention: true },
-          },
           buildStdio,
           files: {
             "app/routes/_index.jsx": js`
diff --git a/integration/conventional-routes-test.ts b/integration/conventional-routes-test.ts
deleted file mode 100644
index 665ad3516..000000000
--- a/integration/conventional-routes-test.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { test, expect } from "@playwright/test";
-
-import { PlaywrightFixture } from "./helpers/playwright-fixture";
-import type { Fixture, AppFixture } from "./helpers/create-fixture";
-import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
-
-let fixture: Fixture;
-let appFixture: AppFixture;
-
-test.beforeAll(async () => {
-  fixture = await createFixture({
-    files: {
-      "app/root.tsx": js`
-        import { Link, Outlet, Scripts, useMatches } from "@remix-run/react";
-
-        export default function App() {
-          let matches = 'Number of matches: ' + useMatches().length;
-          return (
-            <html lang="en">
-              <body>
-                <nav>
-                  <Link to="/nested">/nested</Link>
-                  <br />
-                  <Link to="/nested/foo">/nested/foo</Link>
-                  <br />
-                </nav>
-                <p>{matches}</p>
-                <Outlet />
-                <Scripts />
-              </body>
-            </html>
-          );
-        }
-      `,
-      "app/routes/nested/index.jsx": js`
-        export default function Index() {
-          return <h1>Index</h1>;
-        }
-      `,
-      "app/routes/nested/__pathless.jsx": js`
-        import { Outlet } from "@remix-run/react";
-
-        export default function Layout() {
-          return (
-            <>
-              <div>Pathless Layout</div>
-              <Outlet />
-            </>
-          );
-        }
-      `,
-      "app/routes/nested/__pathless/foo.jsx": js`
-        export default function Foo() {
-          return <h1>Foo</h1>;
-        }
-      `,
-      "app/routes/nested/__pathless2.jsx": js`
-        import { Outlet } from "@remix-run/react";
-
-        export default function Layout() {
-          return (
-            <>
-              <div>Pathless 2 Layout</div>
-              <Outlet />
-            </>
-          );
-        }
-      `,
-      "app/routes/nested/__pathless2/bar.jsx": js`
-        export default function Bar() {
-          return <h1>Bar</h1>;
-        }
-      `,
-    },
-  });
-
-  appFixture = await createAppFixture(fixture);
-});
-
-test.afterAll(async () => appFixture.close());
-
-test.describe("with JavaScript", () => {
-  runTests();
-});
-
-test.describe("without JavaScript", () => {
-  test.use({ javaScriptEnabled: false });
-  runTests();
-});
-
-/**
- * Routes for this test look like this, for reference for the matches assertions:
- *
- * <Routes>
- *   <Route file="root.jsx">
- *     <Route path="nested" file="routes/nested/__pathless.jsx">
- *       <Route path="foo" file="routes/nested/__pathless/foo.jsx" />
- *     </Route>
- *     <Route path="nested" index file="routes/nested/index.jsx" />
- *     <Route index file="routes/index.jsx" />
- *   </Route>
- * </Routes>
- */
-
-function runTests() {
-  test("displays index page and not pathless layout page", async ({ page }) => {
-    let app = new PlaywrightFixture(appFixture, page);
-    await app.goto("/nested");
-    expect(await app.getHtml()).toMatch("Index");
-    expect(await app.getHtml()).not.toMatch("Pathless Layout");
-    expect(await app.getHtml()).toMatch("Number of matches: 2");
-  });
-
-  test("displays page inside of pathless layout", async ({ page }) => {
-    let app = new PlaywrightFixture(appFixture, page);
-    await app.goto("/nested/foo");
-    expect(await app.getHtml()).not.toMatch("Index");
-    expect(await app.getHtml()).toMatch("Pathless Layout");
-    expect(await app.getHtml()).toMatch("Foo");
-    expect(await app.getHtml()).toMatch("Number of matches: 3");
-  });
-
-  // This also asserts that we support multiple sibling pathless route layouts
-  test("displays page inside of second pathless layout", async ({ page }) => {
-    let app = new PlaywrightFixture(appFixture, page);
-    await app.goto("/nested/bar");
-    expect(await app.getHtml()).not.toMatch("Index");
-    expect(await app.getHtml()).toMatch("Pathless 2 Layout");
-    expect(await app.getHtml()).toMatch("Bar");
-    expect(await app.getHtml()).toMatch("Number of matches: 3");
-  });
-}
diff --git a/integration/css-modules-test.ts b/integration/css-modules-test.ts
index d2869cb35..1ec95916b 100644
--- a/integration/css-modules-test.ts
+++ b/integration/css-modules-test.ts
@@ -19,11 +19,6 @@ test.describe("CSS Modules", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Outlet } from "@remix-run/react";
diff --git a/integration/css-side-effect-imports-test.ts b/integration/css-side-effect-imports-test.ts
index 2dc88893e..814d491f0 100644
--- a/integration/css-side-effect-imports-test.ts
+++ b/integration/css-side-effect-imports-test.ts
@@ -20,9 +20,6 @@ test.describe("CSS side-effect imports", () => {
     fixture = await createFixture({
       config: {
         serverDependenciesToBundle: [/@test-package/],
-        future: {
-          v2_routeConvention: true,
-        },
       },
       files: {
         "app/root.jsx": js`
diff --git a/integration/custom-entry-server-test.ts b/integration/custom-entry-server-test.ts
index 86115324c..c7f4f64b1 100644
--- a/integration/custom-entry-server-test.ts
+++ b/integration/custom-entry-server-test.ts
@@ -9,9 +9,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/entry.server.jsx": js`
         import * as React from "react";
diff --git a/integration/defer-loader-test.ts b/integration/defer-loader-test.ts
index 543bcc79b..d61bbe646 100644
--- a/integration/defer-loader-test.ts
+++ b/integration/defer-loader-test.ts
@@ -9,9 +9,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/routes/_index.jsx": js`
         import { useLoaderData, Link } from "@remix-run/react";
diff --git a/integration/defer-test.ts b/integration/defer-test.ts
index 91b42b07f..bdd76a990 100644
--- a/integration/defer-test.ts
+++ b/integration/defer-test.ts
@@ -42,11 +42,6 @@ test.describe("non-aborted", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/components/counter.tsx": js`
           import { useState } from "react";
@@ -980,9 +975,6 @@ test.describe("aborted", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       ////////////////////////////////////////////////////////////////////////////
       // 💿 Next, add files to this object, just like files in a real app,
       // `createFixture` will make an app and run your tests against it.
diff --git a/integration/deno-compiler-test.ts b/integration/deno-compiler-test.ts
index 3c74e03f8..e9013a453 100644
--- a/integration/deno-compiler-test.ts
+++ b/integration/deno-compiler-test.ts
@@ -34,9 +34,6 @@ const searchFiles = async (pattern: string | RegExp, files: string[]) => {
 
 test.beforeAll(async () => {
   projectDir = await createFixtureProject({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     template: "deno-template",
     files: {
       "package.json": json({
diff --git a/integration/deterministic-build-output-test.ts b/integration/deterministic-build-output-test.ts
index c8c107d06..3d81d20d2 100644
--- a/integration/deterministic-build-output-test.ts
+++ b/integration/deterministic-build-output-test.ts
@@ -26,11 +26,6 @@ test("builds deterministically under different paths", async () => {
   //  * serverRouteModulesPlugin (implicitly tested by build)
   //  * vanillaExtractPlugin (via app/routes/foo.tsx' .css.ts file import)
   let init: FixtureInit = {
-    config: {
-      future: {
-        v2_routeConvention: true,
-      },
-    },
     files: {
       "postcss.config.js": js`
         module.exports = {
diff --git a/integration/error-boundary-test.ts b/integration/error-boundary-test.ts
index 1680e24c8..cd29cbc6d 100644
--- a/integration/error-boundary-test.ts
+++ b/integration/error-boundary-test.ts
@@ -42,11 +42,6 @@ test.describe("ErrorBoundary", () => {
     console.error = () => {};
     fixture = await createFixture(
       {
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/root.jsx": js`
               import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
@@ -499,11 +494,6 @@ test.describe("ErrorBoundary", () => {
 
     test.beforeAll(async () => {
       fixture = await createFixture({
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/root.jsx": js`
               import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
@@ -669,11 +659,6 @@ test.describe("loaderData in ErrorBoundary", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/root.jsx": js`
             import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
@@ -1022,11 +1007,6 @@ test.describe("Default ErrorBoundary", () => {
     test.beforeAll(async () => {
       fixture = await createFixture(
         {
-          config: {
-            future: {
-              v2_routeConvention: true,
-            },
-          },
           files: getFiles({ includeRootErrorBoundary: false }),
         },
         ServerMode.Development
@@ -1098,11 +1078,6 @@ test.describe("Default ErrorBoundary", () => {
     test.beforeAll(async () => {
       fixture = await createFixture(
         {
-          config: {
-            future: {
-              v2_routeConvention: true,
-            },
-          },
           files: getFiles({ includeRootErrorBoundary: true }),
         },
         ServerMode.Development
@@ -1168,11 +1143,6 @@ test.describe("Default ErrorBoundary", () => {
   test.describe("When the root route has a boundary but it also throws 😦", () => {
     test.beforeAll(async () => {
       fixture = await createFixture({
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: getFiles({
           includeRootErrorBoundary: true,
           rootErrorBoundaryThrows: true,
@@ -1257,11 +1227,6 @@ test("Allows back-button out of an error boundary after a hard reload", async ({
   console.error = () => {};
 
   let fixture = await createFixture({
-    config: {
-      future: {
-        v2_routeConvention: true,
-      },
-    },
     files: {
       "app/root.jsx": js`
           import { Links, Meta, Outlet, Scripts, useRouteError } from "@remix-run/react";
diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts
index 8d76adc5a..b43b55e3b 100644
--- a/integration/error-boundary-v2-test.ts
+++ b/integration/error-boundary-v2-test.ts
@@ -13,11 +13,6 @@ test.describe("ErrorBoundary", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
diff --git a/integration/error-data-request-test.ts b/integration/error-data-request-test.ts
index f852d4786..d62ccb169 100644
--- a/integration/error-data-request-test.ts
+++ b/integration/error-data-request-test.ts
@@ -16,9 +16,6 @@ test.describe("ErrorBoundary", () => {
     };
 
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
diff --git a/integration/error-sanitization-test.ts b/integration/error-sanitization-test.ts
index 8f356e59e..85cc942ee 100644
--- a/integration/error-sanitization-test.ts
+++ b/integration/error-sanitization-test.ts
@@ -27,7 +27,7 @@ const routeFiles = {
     }
   `,
 
-  "app/routes/index.jsx": js`
+  "app/routes/_index.jsx": js`
     import { useLoaderData, useLocation, useRouteError } from "@remix-run/react";
 
     export function loader({ request }) {
@@ -177,7 +177,7 @@ test.describe("Error Sanitization", () => {
       expect(html).not.toMatch("LOADER");
       expect(html).toMatch("MESSAGE:Unexpected Server Error");
       expect(html).toMatch(
-        '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}'
+        '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}'
       );
       expect(html).not.toMatch(/stack/i);
       expect(errorLogs.length).toBe(1);
@@ -191,7 +191,7 @@ test.describe("Error Sanitization", () => {
       expect(html).toMatch("Index Error");
       expect(html).toMatch("MESSAGE:Unexpected Server Error");
       expect(html).toMatch(
-        '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}'
+        '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}'
       );
       expect(html).not.toMatch(/stack/i);
       expect(errorLogs.length).toBe(1);
@@ -225,7 +225,7 @@ test.describe("Error Sanitization", () => {
     });
 
     test("returns data without errors", async () => {
-      let response = await fixture.requestData("/", "routes/index");
+      let response = await fixture.requestData("/", "routes/_index");
       let text = await response.text();
       expect(text).toMatch("LOADER");
       expect(text).not.toMatch("MESSAGE:");
@@ -233,7 +233,7 @@ test.describe("Error Sanitization", () => {
     });
 
     test("sanitizes loader errors in data requests", async () => {
-      let response = await fixture.requestData("/?loader", "routes/index");
+      let response = await fixture.requestData("/?loader", "routes/_index");
       let text = await response.text();
       expect(text).toBe('{"message":"Unexpected Server Error"}');
       expect(errorLogs.length).toBe(1);
@@ -338,7 +338,7 @@ test.describe("Error Sanitization", () => {
       expect(html).toMatch("<p>MESSAGE:Loader Error");
       expect(html).toMatch("<p>STACK:Error: Loader Error");
       expect(html).toMatch(
-        'errors":{"routes/index":{"message":"Loader Error","stack":"Error: Loader Error\\n'
+        'errors":{"routes/_index":{"message":"Loader Error","stack":"Error: Loader Error\\n'
       );
       expect(errorLogs.length).toBe(1);
       expect(errorLogs[0][0].message).toMatch("Loader Error");
@@ -352,7 +352,7 @@ test.describe("Error Sanitization", () => {
       expect(html).toMatch("<p>MESSAGE:Render Error");
       expect(html).toMatch("<p>STACK:Error: Render Error");
       expect(html).toMatch(
-        'errors":{"routes/index":{"message":"Render Error","stack":"Error: Render Error\\n'
+        'errors":{"routes/_index":{"message":"Render Error","stack":"Error: Render Error\\n'
       );
       expect(errorLogs.length).toBe(1);
       expect(errorLogs[0][0].message).toMatch("Render Error");
@@ -382,7 +382,7 @@ test.describe("Error Sanitization", () => {
     });
 
     test("returns data without errors", async () => {
-      let response = await fixture.requestData("/", "routes/index");
+      let response = await fixture.requestData("/", "routes/_index");
       let text = await response.text();
       expect(text).toMatch("LOADER");
       expect(text).not.toMatch("MESSAGE:");
@@ -390,7 +390,7 @@ test.describe("Error Sanitization", () => {
     });
 
     test("does not sanitize loader errors in data requests", async () => {
-      let response = await fixture.requestData("/?loader", "routes/index");
+      let response = await fixture.requestData("/?loader", "routes/_index");
       let text = await response.text();
       expect(text).toMatch(
         '{"message":"Loader Error","stack":"Error: Loader Error'
@@ -536,7 +536,7 @@ test.describe("Error Sanitization", () => {
       expect(html).not.toMatch("LOADER");
       expect(html).toMatch("MESSAGE:Unexpected Server Error");
       expect(html).toMatch(
-        '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}'
+        '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}'
       );
       expect(html).not.toMatch(/stack/i);
       expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
@@ -552,7 +552,7 @@ test.describe("Error Sanitization", () => {
       expect(html).toMatch("Index Error");
       expect(html).toMatch("MESSAGE:Unexpected Server Error");
       expect(html).toMatch(
-        '{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}'
+        '{"routes/_index":{"message":"Unexpected Server Error","__type":"Error"}}'
       );
       expect(html).not.toMatch(/stack/i);
       expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
@@ -588,7 +588,7 @@ test.describe("Error Sanitization", () => {
     });
 
     test("returns data without errors", async () => {
-      let response = await fixture.requestData("/", "routes/index");
+      let response = await fixture.requestData("/", "routes/_index");
       let text = await response.text();
       expect(text).toMatch("LOADER");
       expect(text).not.toMatch("MESSAGE:");
@@ -596,12 +596,12 @@ test.describe("Error Sanitization", () => {
     });
 
     test("sanitizes loader errors in data requests", async () => {
-      let response = await fixture.requestData("/?loader", "routes/index");
+      let response = await fixture.requestData("/?loader", "routes/_index");
       let text = await response.text();
       expect(text).toBe('{"message":"Unexpected Server Error"}');
       expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
       expect(errorLogs[1][0]).toEqual(
-        "  Request: GET test://test/?loader=&_data=routes%2Findex"
+        "  Request: GET test://test/?loader=&_data=routes%2F_index"
       );
       expect(errorLogs[2][0]).toEqual("  Error: Loader Error");
       expect(errorLogs[3][0]).toMatch(" at ");
diff --git a/integration/fetcher-layout-test.ts b/integration/fetcher-layout-test.ts
index 1a8d8c44b..c7929ce43 100644
--- a/integration/fetcher-layout-test.ts
+++ b/integration/fetcher-layout-test.ts
@@ -9,9 +9,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/routes/layout-action.jsx": js`
         import { json } from "@remix-run/node";
diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts
index 2bca2fcbf..2f8489784 100644
--- a/integration/fetcher-test.ts
+++ b/integration/fetcher-test.ts
@@ -17,9 +17,6 @@ test.describe("useFetcher", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/resource-route-action-only.ts": js`
           import { json } from "@remix-run/node";
@@ -425,11 +422,6 @@ test.describe("fetcher aborts and adjacent forms", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/routes/_index.jsx": js`
           import * as React from "react";
diff --git a/integration/flat-routes-test.ts b/integration/flat-routes-test.ts
index 8af9c2003..646083c3c 100644
--- a/integration/flat-routes-test.ts
+++ b/integration/flat-routes-test.ts
@@ -15,9 +15,6 @@ test.describe("flat routes", () => {
     fixture = await createFixture({
       config: {
         ignoredRouteFiles: [IGNORED_ROUTE],
-        future: {
-          v2_routeConvention: true,
-        },
       },
       files: {
         "app/root.jsx": js`
@@ -165,56 +162,6 @@ test.describe("flat routes", () => {
   });
 });
 
-test.describe("warns when v1 routesConvention is used", () => {
-  let buildStdio = new PassThrough();
-  let buildOutput: string;
-
-  let originalConsoleLog = console.log;
-  let originalConsoleWarn = console.warn;
-  let originalConsoleError = console.error;
-
-  test.beforeAll(async () => {
-    console.log = () => {};
-    console.warn = () => {};
-    console.error = () => {};
-    await createFixtureProject({
-      buildStdio,
-      config: {
-        future: { v2_routeConvention: false },
-      },
-      files: {
-        "routes/index.tsx": js`
-          export default function () {
-            return <p>routes/index</p>;
-          }
-        `,
-      },
-    });
-
-    let chunks: Buffer[] = [];
-    buildOutput = await new Promise<string>((resolve, reject) => {
-      buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
-      buildStdio.on("error", (err) => reject(err));
-      buildStdio.on("end", () =>
-        resolve(Buffer.concat(chunks).toString("utf8"))
-      );
-    });
-  });
-
-  test.afterAll(() => {
-    console.log = originalConsoleLog;
-    console.warn = originalConsoleWarn;
-    console.error = originalConsoleError;
-  });
-
-  test("v2_routeConvention is not enabled", () => {
-    console.log(buildOutput);
-    expect(buildOutput).toContain(
-      "The route file convention is changing in v2"
-    );
-  });
-});
-
 test.describe("emits warnings for route conflicts", async () => {
   let buildStdio = new PassThrough();
   let buildOutput: string;
@@ -229,9 +176,6 @@ test.describe("emits warnings for route conflicts", async () => {
     console.error = () => {};
     await createFixtureProject({
       buildStdio,
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "routes/_dashboard._index.tsx": js`
           export default function () {
@@ -287,9 +231,6 @@ test.describe("", () => {
     console.error = () => {};
     await createFixtureProject({
       buildStdio,
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/_index/route.jsx": js``,
         "app/routes/_index/utils.js": js``,
@@ -320,9 +261,6 @@ test.describe("", () => {
 test.describe("pathless routes and route collisions", () => {
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.tsx": js`
           import { Link, Outlet, Scripts, useMatches } from "@remix-run/react";
@@ -407,11 +345,11 @@ test.describe("pathless routes and route collisions", () => {
    *
    * <Routes>
    *   <Route file="root.jsx">
-   *     <Route path="nested" file="routes/nested/__pathless.jsx">
-   *       <Route path="foo" file="routes/nested/__pathless/foo.jsx" />
+   *     <Route path="nested" file="routes/nested._pathless.jsx">
+   *       <Route path="foo" file="routes/nested._pathless/foo.jsx" />
    *     </Route>
-   *     <Route path="nested" index file="routes/nested/index.jsx" />
-   *     <Route index file="routes/index.jsx" />
+   *     <Route path="nested" index file="routes/nested._index.jsx" />
+   *     <Route index file="routes/_index.jsx" />
    *   </Route>
    * </Routes>
    */
diff --git a/integration/form-data-test.ts b/integration/form-data-test.ts
index 2697c512e..431beb3cf 100644
--- a/integration/form-data-test.ts
+++ b/integration/form-data-test.ts
@@ -7,9 +7,6 @@ let fixture: Fixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/routes/_index.jsx": js`
         import { json } from "@remix-run/node";
diff --git a/integration/form-test.ts b/integration/form-test.ts
index 658a3c8d9..e3f1d2ef7 100644
--- a/integration/form-test.ts
+++ b/integration/form-test.ts
@@ -56,9 +56,6 @@ test.describe("Forms", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/get-submission.jsx": js`
           import { useLoaderData, Form } from "@remix-run/react";
diff --git a/integration/headers-test.ts b/integration/headers-test.ts
index 179553778..5413ba191 100644
--- a/integration/headers-test.ts
+++ b/integration/headers-test.ts
@@ -17,8 +17,6 @@ test.describe("headers export", () => {
       {
         config: {
           future: {
-            v2_routeConvention: true,
-
             v2_headers: true,
           },
         },
@@ -226,9 +224,6 @@ test.describe("headers export", () => {
 
     let fixture = await createFixture(
       {
-        config: {
-          future: { v2_routeConvention: true },
-        },
         files: {
           "app/root.jsx": js`
             import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
@@ -438,8 +433,6 @@ test.describe("v1 behavior (future.v2_headers=false)", () => {
       {
         config: {
           future: {
-            v2_routeConvention: true,
-
             v2_headers: false,
           },
         },
diff --git a/integration/hmr-log-test.ts b/integration/hmr-log-test.ts
index 1691476cf..283895e34 100644
--- a/integration/hmr-log-test.ts
+++ b/integration/hmr-log-test.ts
@@ -17,8 +17,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({
       v2_dev: {
         port: options.devPort,
       },
-      v2_routeConvention: true,
-
       v2_meta: true,
       v2_headers: true,
     },
diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts
index 8df8a7473..e8c2d3d54 100644
--- a/integration/hmr-test.ts
+++ b/integration/hmr-test.ts
@@ -17,8 +17,6 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({
       v2_dev: {
         port: options.devPort,
       },
-      v2_routeConvention: true,
-
       v2_meta: true,
       v2_headers: true,
     },
diff --git a/integration/hook-useSubmit-test.ts b/integration/hook-useSubmit-test.ts
index 083fb0734..8a19b04a8 100644
--- a/integration/hook-useSubmit-test.ts
+++ b/integration/hook-useSubmit-test.ts
@@ -10,9 +10,6 @@ test.describe("`useSubmit()` returned function", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/_index.jsx": js`
           import { useLoaderData, useSubmit } from "@remix-run/react";
diff --git a/integration/js-routes-test.ts b/integration/js-routes-test.ts
index fa0d08c05..47ef5a33b 100644
--- a/integration/js-routes-test.ts
+++ b/integration/js-routes-test.ts
@@ -10,11 +10,6 @@ test.describe(".js route files", () => {
   test.beforeAll(async () => {
     appFixture = await createAppFixture(
       await createFixture({
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/routes/js.js": js`
             export default () => <div data-testid="route-js">Rendered with .js ext</div>;
diff --git a/integration/layout-route-test.ts b/integration/layout-route-test.ts
index 749c75918..9f3ac291c 100644
--- a/integration/layout-route-test.ts
+++ b/integration/layout-route-test.ts
@@ -10,11 +10,6 @@ test.describe("pathless layout routes", () => {
   test.beforeAll(async () => {
     appFixture = await createAppFixture(
       await createFixture({
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/routes/_layout.jsx": js`
             import { Outlet } from "@remix-run/react";
diff --git a/integration/link-test.ts b/integration/link-test.ts
index b301a0fb4..e6168e539 100644
--- a/integration/link-test.ts
+++ b/integration/link-test.ts
@@ -32,11 +32,6 @@ test.describe("route module link export", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/favicon.ico": js``,
 
diff --git a/integration/loader-test.ts b/integration/loader-test.ts
index 297db10b3..89656e431 100644
--- a/integration/loader-test.ts
+++ b/integration/loader-test.ts
@@ -12,9 +12,6 @@ test.describe("loader", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
         import { json } from "@remix-run/node";
@@ -78,11 +75,6 @@ test.describe("loader in an app", () => {
   test.beforeAll(async () => {
     appFixture = await createAppFixture(
       await createFixture({
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/root.jsx": js`
             import { Outlet } from '@remix-run/react'
diff --git a/integration/matches-test.ts b/integration/matches-test.ts
index c1b20037f..351607515 100644
--- a/integration/matches-test.ts
+++ b/integration/matches-test.ts
@@ -10,9 +10,6 @@ test.describe("useMatches", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
           import * as React from 'react';
diff --git a/integration/mdx-test.ts b/integration/mdx-test.ts
index 28e628a77..27a55b9cc 100644
--- a/integration/mdx-test.ts
+++ b/integration/mdx-test.ts
@@ -16,9 +16,6 @@ test.describe("mdx", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
diff --git a/integration/meta-test.ts b/integration/meta-test.ts
index a04a61e91..bc2260409 100644
--- a/integration/meta-test.ts
+++ b/integration/meta-test.ts
@@ -15,9 +15,6 @@ test.describe("meta", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
           import { json } from "@remix-run/node";
@@ -403,7 +400,6 @@ test.describe("v2_meta", () => {
         ignoredRouteFiles: ["**/.*"],
         future: {
           v2_meta: true,
-          v2_routeConvention: true,
         },
       },
       files: {
diff --git a/integration/multiple-cookies-test.ts b/integration/multiple-cookies-test.ts
index 7b261cb55..19ff6348d 100644
--- a/integration/multiple-cookies-test.ts
+++ b/integration/multiple-cookies-test.ts
@@ -10,11 +10,6 @@ test.describe("pathless layout routes", () => {
   test.beforeAll(async () => {
     appFixture = await createAppFixture(
       await createFixture({
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/routes/_index.jsx": js`
             import { redirect, json } from "@remix-run/node";
diff --git a/integration/navigation-state-test.ts b/integration/navigation-state-test.ts
index 319e9b58f..058586338 100644
--- a/integration/navigation-state-test.ts
+++ b/integration/navigation-state-test.ts
@@ -63,7 +63,7 @@ test.describe("navigation states", () => {
             );
           }
         `,
-        "app/routes/index.jsx": js`
+        "app/routes/_index.jsx": js`
           import { Form, Link, useFetcher } from "@remix-run/react";
           export function loader() { return null; }
           export default function() {
diff --git a/integration/path-mapping-test.ts b/integration/path-mapping-test.ts
index 6e8ea1ffb..43cfcf0af 100644
--- a/integration/path-mapping-test.ts
+++ b/integration/path-mapping-test.ts
@@ -7,9 +7,6 @@ let fixture: Fixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/components/my-lib/index.ts": js`
         export const pizza = "this is a pizza";
diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts
index 2d689632e..c5e05226e 100644
--- a/integration/postcss-test.ts
+++ b/integration/postcss-test.ts
@@ -33,11 +33,6 @@ test.describe("PostCSS enabled", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         // We provide a test plugin that replaces the strings
         // "TEST_PADDING_VALUE" and "TEST_POSTCSS_CONTEXT".
diff --git a/integration/prefetch-test.ts b/integration/prefetch-test.ts
index a70c714e1..0272552f7 100644
--- a/integration/prefetch-test.ts
+++ b/integration/prefetch-test.ts
@@ -12,9 +12,6 @@ import { PlaywrightFixture } from "./helpers/playwright-fixture";
 // Generate the test app using the given prefetch mode
 function fixtureFactory(mode: RemixLinkProps["prefetch"]): FixtureInit {
   return {
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/root.jsx": js`
         import {
@@ -278,9 +275,6 @@ test.describe("prefetch=viewport", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/_index.jsx": js`
           import { Link } from "@remix-run/react";
@@ -355,9 +349,6 @@ test.describe("other scenarios", () => {
     page,
   }) => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
             import { Links, Meta, Scripts, useFetcher } from "@remix-run/react";
diff --git a/integration/redirects-test.ts b/integration/redirects-test.ts
index 661f118f9..114708f18 100644
--- a/integration/redirects-test.ts
+++ b/integration/redirects-test.ts
@@ -10,9 +10,6 @@ test.describe("redirects", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/action.jsx": js`
           import { Outlet, useLoaderData } from "@remix-run/react";
diff --git a/integration/rendering-test.ts b/integration/rendering-test.ts
index 44a357a84..d7abc2dc5 100644
--- a/integration/rendering-test.ts
+++ b/integration/rendering-test.ts
@@ -10,9 +10,6 @@ test.describe("rendering", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
diff --git a/integration/request-test.ts b/integration/request-test.ts
index 9458f4562..37bfc4f27 100644
--- a/integration/request-test.ts
+++ b/integration/request-test.ts
@@ -9,9 +9,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/routes/_index.jsx": js`
         import { json } from "@remix-run/node";
diff --git a/integration/resource-routes-test.ts b/integration/resource-routes-test.ts
index 1282200a8..6b7240748 100644
--- a/integration/resource-routes-test.ts
+++ b/integration/resource-routes-test.ts
@@ -16,9 +16,6 @@ test.describe("loader in an app", async () => {
     _consoleError = console.error;
     console.error = () => {};
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/_index.jsx": js`
           import { Form, Link } from "@remix-run/react";
@@ -240,11 +237,6 @@ test.describe("Development server", async () => {
 
     fixture = await createFixture(
       {
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/routes/_index.jsx": js`
             import { Link } from "@remix-run/react";
diff --git a/integration/revalidate-test.ts b/integration/revalidate-test.ts
index 492245de0..58690e753 100644
--- a/integration/revalidate-test.ts
+++ b/integration/revalidate-test.ts
@@ -10,11 +10,6 @@ test.describe("Revalidation", () => {
   test.beforeAll(async () => {
     appFixture = await createAppFixture(
       await createFixture({
-        config: {
-          future: {
-            v2_routeConvention: true,
-          },
-        },
         files: {
           "app/root.jsx": js`
             import { Link, Outlet, Scripts, useNavigation } from "@remix-run/react";
diff --git a/integration/route-collisions-test.ts b/integration/route-collisions-test.ts
index f136d223c..38d16e948 100644
--- a/integration/route-collisions-test.ts
+++ b/integration/route-collisions-test.ts
@@ -32,157 +32,7 @@ let LEAF_FILE_CONTENTS = js`
   }
 `;
 
-test.describe("build failures (v1 routes)", () => {
-  let errorLogs: string[];
-  let oldConsoleError: typeof console.error;
-
-  test.beforeEach(() => {
-    errorLogs = [];
-    oldConsoleError = console.error;
-    console.error = (str) => errorLogs.push(str);
-  });
-
-  test.afterEach(() => {
-    console.error = oldConsoleError;
-  });
-
-  test("detects path collisions inside pathless layout routes", async () => {
-    try {
-      await createFixture({
-        config: {
-          future: { v2_routeConvention: false },
-        },
-        files: {
-          "app/root.tsx": ROOT_FILE_CONTENTS,
-          "app/routes/foo.jsx": LEAF_FILE_CONTENTS,
-          "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/__pathless/foo.jsx": LEAF_FILE_CONTENTS,
-        },
-      });
-      expect(false).toBe(true);
-    } catch (e) {
-      expect(errorLogs[0]).toMatch(
-        'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/foo"'
-      );
-      expect(errorLogs.length).toBe(1);
-    }
-  });
-
-  test("detects path collisions across pathless layout routes", async () => {
-    try {
-      await createFixture({
-        config: {
-          future: { v2_routeConvention: false },
-        },
-        files: {
-          "app/root.tsx": ROOT_FILE_CONTENTS,
-          "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/__pathless/foo.jsx": LEAF_FILE_CONTENTS,
-          "app/routes/__pathless2.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/__pathless2/foo.jsx": LEAF_FILE_CONTENTS,
-        },
-      });
-      expect(false).toBe(true);
-    } catch (e) {
-      expect(errorLogs[0]).toMatch(
-        'Error: Path "foo" defined by route "routes/__pathless/foo" conflicts with route "routes/__pathless2/foo"'
-      );
-      expect(errorLogs.length).toBe(1);
-    }
-  });
-
-  test("detects path collisions inside multiple pathless layout routes", async () => {
-    try {
-      await createFixture({
-        config: {
-          future: { v2_routeConvention: false },
-        },
-        files: {
-          "app/root.tsx": ROOT_FILE_CONTENTS,
-          "app/routes/foo.jsx": LEAF_FILE_CONTENTS,
-          "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/__pathless/__again.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/__pathless/__again/foo.jsx": LEAF_FILE_CONTENTS,
-        },
-      });
-      expect(false).toBe(true);
-    } catch (e) {
-      expect(errorLogs[0]).toMatch(
-        'Error: Path "foo" defined by route "routes/foo" conflicts with route "routes/__pathless/__again/foo"'
-      );
-      expect(errorLogs.length).toBe(1);
-    }
-  });
-
-  test("detects path collisions of index files inside pathless layouts", async () => {
-    try {
-      await createFixture({
-        config: {
-          future: { v2_routeConvention: false },
-        },
-        files: {
-          "app/root.tsx": ROOT_FILE_CONTENTS,
-          "app/routes/index.jsx": LEAF_FILE_CONTENTS,
-          "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/__pathless/index.jsx": LEAF_FILE_CONTENTS,
-        },
-      });
-      expect(false).toBe(true);
-    } catch (e) {
-      expect(errorLogs[0]).toMatch(
-        'Error: Path "/" defined by route "routes/index" conflicts with route "routes/__pathless/index"'
-      );
-      expect(errorLogs.length).toBe(1);
-    }
-  });
-
-  test("detects path collisions of index files across multiple pathless layouts", async () => {
-    try {
-      await createFixture({
-        config: {
-          future: { v2_routeConvention: false },
-        },
-        files: {
-          "app/root.tsx": ROOT_FILE_CONTENTS,
-          "app/routes/nested/__pathless.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/nested/__pathless/index.jsx": LEAF_FILE_CONTENTS,
-          "app/routes/nested/__oops.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/nested/__oops/index.jsx": LEAF_FILE_CONTENTS,
-        },
-      });
-      expect(false).toBe(true);
-    } catch (e) {
-      expect(errorLogs[0]).toMatch(
-        'Error: Path "nested" defined by route "routes/nested/__oops/index" conflicts with route "routes/nested/__pathless/index"'
-      );
-      expect(errorLogs.length).toBe(1);
-    }
-  });
-
-  test("detects path collisions of param routes inside pathless layouts", async () => {
-    try {
-      await createFixture({
-        config: {
-          future: { v2_routeConvention: false },
-        },
-        files: {
-          "app/root.tsx": ROOT_FILE_CONTENTS,
-          "app/routes/$param.jsx": LEAF_FILE_CONTENTS,
-          "app/routes/__pathless.jsx": LAYOUT_FILE_CONTENTS,
-          "app/routes/__pathless/$param.jsx": LEAF_FILE_CONTENTS,
-        },
-      });
-      expect(false).toBe(true);
-    } catch (e) {
-      expect(errorLogs[0]).toMatch(
-        'Error: Path ":param" defined by route "routes/$param" conflicts with route "routes/__pathless/$param"'
-      );
-      expect(errorLogs.length).toBe(1);
-    }
-  });
-});
-
-test.describe("build failures (v2 routes)", () => {
+test.describe("build failures", () => {
   let originalConsoleLog = console.log;
   let originalConsoleWarn = console.warn;
   let originalConsoleError = console.error;
@@ -204,9 +54,6 @@ test.describe("build failures (v2 routes)", () => {
     let buildOutput: string;
     await createFixture({
       buildStdio,
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files,
     });
     let chunks: Buffer[] = [];
diff --git a/integration/scroll-test.ts b/integration/scroll-test.ts
index 6f1428a1f..196100dbb 100644
--- a/integration/scroll-test.ts
+++ b/integration/scroll-test.ts
@@ -9,9 +9,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/routes/_index.jsx": js`
         import { redirect } from "@remix-run/node";
diff --git a/integration/server-code-in-browser-message-test.ts b/integration/server-code-in-browser-message-test.ts
index f1221620e..fafc6bfcb 100644
--- a/integration/server-code-in-browser-message-test.ts
+++ b/integration/server-code-in-browser-message-test.ts
@@ -14,9 +14,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "node_modules/has-side-effects/package.json": json({
         name: "has-side-effects",
diff --git a/integration/server-entry-test.ts b/integration/server-entry-test.ts
index d5eee9804..b252c57fb 100644
--- a/integration/server-entry-test.ts
+++ b/integration/server-entry-test.ts
@@ -12,9 +12,6 @@ test.describe("Custom Server Entry", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/entry.server.jsx": js`
           export default function handleRequest() {
@@ -51,7 +48,7 @@ test.describe("Default Server Entry", () => {
   test.beforeAll(async () => {
     fixture = await createFixture({
       files: {
-        "app/routes/index.jsx": js`
+        "app/routes/_index.jsx": js`
           export default function () {
             return <p>Hello World</p>
           }
@@ -72,7 +69,7 @@ test.describe("Default Server Entry (React 17)", () => {
   test.beforeAll(async () => {
     fixture = await createFixture({
       files: {
-        "app/routes/index.jsx": js`
+        "app/routes/_index.jsx": js`
           export default function () {
             return <p>Hello World</p>
           }
diff --git a/integration/server-source-maps-test.ts b/integration/server-source-maps-test.ts
index f0833d7c1..d4a5f3d72 100644
--- a/integration/server-source-maps-test.ts
+++ b/integration/server-source-maps-test.ts
@@ -9,9 +9,6 @@ let fixture: Fixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     sourcemap: true,
     files: {
       "app/routes/_index.jsx": js`
diff --git a/integration/set-cookie-revalidation-test.ts b/integration/set-cookie-revalidation-test.ts
index 8a87d0ede..bda7f490a 100644
--- a/integration/set-cookie-revalidation-test.ts
+++ b/integration/set-cookie-revalidation-test.ts
@@ -11,9 +11,6 @@ let BANNER_MESSAGE = "you do not have permission to view /protected";
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/session.server.js": js`
         import { createCookieSessionStorage } from "@remix-run/node";
diff --git a/integration/shared-route-imports-test.ts b/integration/shared-route-imports-test.ts
index 8cd54fdc5..47f78d446 100644
--- a/integration/shared-route-imports-test.ts
+++ b/integration/shared-route-imports-test.ts
@@ -10,9 +10,6 @@ let appFixture: AppFixture;
 test.describe("v1 compiler", () => {
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/routes/parent.jsx": js`
           import { createContext, useContext } from "react";
@@ -101,7 +98,9 @@ test.describe("v2 compiler", () => {
   test.beforeAll(async () => {
     fixture = await createFixture({
       config: {
-        future: { v2_routeConvention: true, v2_dev: true },
+        future: {
+          v2_dev: true,
+        },
       },
       files: {
         "app/routes/parent.jsx": js`
diff --git a/integration/splat-routes-test.ts b/integration/splat-routes-test.ts
index 17e37e5af..ae54f2774 100644
--- a/integration/splat-routes-test.ts
+++ b/integration/splat-routes-test.ts
@@ -16,9 +16,6 @@ test.describe("rendering", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
diff --git a/integration/tailwind-test.ts b/integration/tailwind-test.ts
index d16739720..59394c73b 100644
--- a/integration/tailwind-test.ts
+++ b/integration/tailwind-test.ts
@@ -43,11 +43,6 @@ function runTests(ext: typeof extensions[number]) {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         [tailwindConfigName]: tailwindConfig,
 
diff --git a/integration/transition-test.ts b/integration/transition-test.ts
index a1a99f4f3..bc3844a93 100644
--- a/integration/transition-test.ts
+++ b/integration/transition-test.ts
@@ -19,9 +19,6 @@ test.describe("rendering", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: { v2_routeConvention: true },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
diff --git a/integration/upload-test.ts b/integration/upload-test.ts
index ba08e5f00..491f49b8f 100644
--- a/integration/upload-test.ts
+++ b/integration/upload-test.ts
@@ -10,9 +10,6 @@ let appFixture: AppFixture;
 
 test.beforeAll(async () => {
   fixture = await createFixture({
-    config: {
-      future: { v2_routeConvention: true },
-    },
     files: {
       "app/routes/file-upload-handler.jsx": js`
         import {
diff --git a/integration/vanilla-extract-test.ts b/integration/vanilla-extract-test.ts
index 47cc7e335..2b5137043 100644
--- a/integration/vanilla-extract-test.ts
+++ b/integration/vanilla-extract-test.ts
@@ -12,11 +12,6 @@ test.describe("Vanilla Extract", () => {
 
   test.beforeAll(async () => {
     fixture = await createFixture({
-      config: {
-        future: {
-          v2_routeConvention: true,
-        },
-      },
       files: {
         "app/root.jsx": js`
           import { Links, Outlet } from "@remix-run/react";
diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts
index da6bbd772..90e807ad6 100644
--- a/packages/remix-dev/__tests__/readConfig-test.ts
+++ b/packages/remix-dev/__tests__/readConfig-test.ts
@@ -27,7 +27,6 @@ describe("readConfig", () => {
         future: {
           v2_headers: expect.any(Boolean),
           v2_meta: expect.any(Boolean),
-          v2_routeConvention: expect.any(Boolean),
         },
       },
       `
@@ -45,7 +44,6 @@ describe("readConfig", () => {
           "v2_dev": false,
           "v2_headers": Any<Boolean>,
           "v2_meta": Any<Boolean>,
-          "v2_routeConvention": Any<Boolean>,
         },
         "mdx": undefined,
         "postcss": true,
diff --git a/packages/remix-dev/__tests__/routesConvention-test.ts b/packages/remix-dev/__tests__/routesConvention-test.ts
deleted file mode 100644
index 93e8f635a..000000000
--- a/packages/remix-dev/__tests__/routesConvention-test.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import * as path from "path";
-
-import {
-  createRoutePath,
-  defineConventionalRoutes,
-} from "../config/routesConvention";
-
-describe("createRoutePath", () => {
-  describe("creates proper route paths", () => {
-    let tests: [string, string | undefined][] = [
-      ["routes/$", "routes/*"],
-      ["routes/sub/$", "routes/sub/*"],
-      ["routes.sub/$", "routes/sub/*"],
-      ["routes/$slug", "routes/:slug"],
-      ["routes/sub/$slug", "routes/sub/:slug"],
-      ["routes.sub/$slug", "routes/sub/:slug"],
-      ["$", "*"],
-      ["nested/$", "nested/*"],
-      ["flat.$", "flat/*"],
-      ["$slug", ":slug"],
-      ["nested/$slug", "nested/:slug"],
-      ["flat.$slug", "flat/:slug"],
-      ["flat.sub", "flat/sub"],
-      ["nested/index", "nested"],
-      ["flat.index", "flat"],
-      ["index", undefined],
-      ["__layout/index", undefined],
-      ["__layout/test", "test"],
-      ["__layout.test", "test"],
-      ["__layout/$slug", ":slug"],
-      ["nested/__layout/$slug", "nested/:slug"],
-      ["$slug[.]json", ":slug.json"],
-      ["sub/[sitemap.xml]", "sub/sitemap.xml"],
-      ["posts/$slug/[image.jpg]", "posts/:slug/image.jpg"],
-      ["$[$dollabills].[.]lol[/]what/[$].$", ":$dollabills/.lol/what/$/*"],
-      ["sub.[[]", "sub/["],
-      ["sub.]", "sub/]"],
-      ["sub.[[]]", "sub/[]"],
-      ["sub.[[]", "sub/["],
-      ["beef]", "beef]"],
-      ["[index]", "index"],
-      ["test/inde[x]", "test/index"],
-      ["[i]ndex/[[].[[]]", "index/[/[]"],
-
-      // Optional segment routes
-      ["(routes)/$", "routes?/*"],
-      ["(routes)/(sub)/$", "routes?/sub?/*"],
-      ["(routes).(sub)/$", "routes?/sub?/*"],
-      ["(routes)/($slug)", "routes?/:slug?"],
-      ["(routes)/sub/($slug)", "routes?/sub/:slug?"],
-      ["(routes).sub/($slug)", "routes?/sub/:slug?"],
-      ["(nested)/$", "nested?/*"],
-      ["(flat).$", "flat?/*"],
-      ["($slug)", ":slug?"],
-      ["(nested)/($slug)", "nested?/:slug?"],
-      ["(flat).($slug)", "flat?/:slug?"],
-      ["flat.(sub)", "flat/sub?"],
-      ["__layout/(test)", "test?"],
-      ["__layout.(test)", "test?"],
-      ["__layout/($slug)", ":slug?"],
-      ["(nested)/__layout/($slug)", "nested?/:slug?"],
-      ["($slug[.]json)", ":slug.json?"],
-      ["(sub)/([sitemap.xml])", "sub?/sitemap.xml?"],
-      ["(sub)/[(sitemap.xml)]", "sub?/(sitemap.xml)"],
-      ["(posts)/($slug)/([image.jpg])", "posts?/:slug?/image.jpg?"],
-      [
-        "($[$dollabills]).([.]lol)[/](what)/([$]).$",
-        ":$dollabills?/.lol)/(what?/$?/*",
-      ],
-      [
-        "($[$dollabills]).([.]lol)/(what)/([$]).($up)",
-        ":$dollabills?/.lol?/what?/$?/:up?",
-      ],
-      ["(sub).([[])", "sub?/[?"],
-      ["(sub).(])", "sub?/]?"],
-      ["(sub).([[]])", "sub?/[]?"],
-      ["(sub).([[])", "sub?/[?"],
-      ["(beef])", "beef]?"],
-      ["([index])", "index?"],
-      ["(test)/(inde[x])", "test?/index?"],
-      ["([i]ndex)/([[]).([[]])", "index?/[?/[]?"],
-    ];
-
-    for (let [input, expected] of tests) {
-      it(`"${input}" -> "${expected}"`, () => {
-        expect(createRoutePath(input)).toBe(expected);
-      });
-    }
-
-    describe("optional segments", () => {
-      it("will only work when starting and ending a segment with parenthesis", () => {
-        let [input, expected] = ["(routes.sub)/$", "(routes/sub)/*"];
-        expect(createRoutePath(input)).toBe(expected);
-      });
-
-      it("throws error on optional to splat routes", () => {
-        expect(() => createRoutePath("(routes)/($)")).toThrow("Splat");
-        expect(() => createRoutePath("($)")).toThrow("Splat");
-      });
-
-      it("throws errors on optional index without brackets routes", () => {
-        expect(() => createRoutePath("(nested)/(index)")).toThrow("index");
-        expect(() => createRoutePath("(flat).(index)")).toThrow("index");
-        expect(() => createRoutePath("(index)")).toThrow("index");
-      });
-    });
-  });
-});
-
-describe("defineConventionalRoutes", () => {
-  it("creates a route manifest from the routes directory", () => {
-    let routes = defineConventionalRoutes(
-      path.join(__dirname, "fixtures/indie-stack/app")
-    );
-    let keys = Object.keys(routes);
-    expect(keys).toHaveLength(1);
-    expect(keys.filter((key) => routes[key].parentId).length).toBe(1);
-    expect(keys.filter((key) => routes[key].index).length).toBe(1);
-  });
-});
diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts
index 39b06976e..11ec90c3b 100644
--- a/packages/remix-server-runtime/__tests__/server-test.ts
+++ b/packages/remix-server-runtime/__tests__/server-test.ts
@@ -417,14 +417,14 @@ describe("shared server runtime", () => {
         root: {
           default: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
         },
       });
       let handler = createRequestHandler(build, ServerMode.Test);
 
-      let request = new Request(`${baseUrl}/?_data=routes/index`, {
+      let request = new Request(`${baseUrl}/?_data=routes/_index`, {
         method: "get",
       });
 
@@ -439,7 +439,7 @@ describe("shared server runtime", () => {
         root: {
           default: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           loader: () => null,
@@ -469,7 +469,7 @@ describe("shared server runtime", () => {
         root: {
           default: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           loader: () => null,
@@ -499,7 +499,7 @@ describe("shared server runtime", () => {
           default: {},
           loader: rootLoader,
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           loader: indexLoader,
           index: true,
@@ -507,7 +507,7 @@ describe("shared server runtime", () => {
       });
       let handler = createRequestHandler(build, ServerMode.Test);
 
-      let request = new Request(`${baseUrl}/?_data=routes/index`, {
+      let request = new Request(`${baseUrl}/?_data=routes/_index`, {
         method: "get",
       });
 
@@ -760,7 +760,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           action: rootAction,
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
         },
@@ -790,7 +790,7 @@ describe("shared server runtime", () => {
           default: {},
           loader: rootLoader,
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           action: indexAction,
           index: true,
@@ -798,7 +798,7 @@ describe("shared server runtime", () => {
       });
       let handler = createRequestHandler(build, ServerMode.Test);
 
-      let request = new Request(`${baseUrl}/?index&_data=routes/index`, {
+      let request = new Request(`${baseUrl}/?index&_data=routes/_index`, {
         method: "post",
       });
 
@@ -880,7 +880,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -920,7 +920,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -942,7 +942,7 @@ describe("shared server runtime", () => {
       expect(calls.length).toBe(1);
       let context = calls[0][3].staticHandlerContext as StaticHandlerContext;
       expect(context.errors).toBeTruthy();
-      expect(context.errors!["routes/index"].status).toBe(400);
+      expect(context.errors!["routes/_index"].status).toBe(400);
       expect(context.loaderData).toEqual({
         root: "root",
       });
@@ -1011,7 +1011,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -1038,7 +1038,7 @@ describe("shared server runtime", () => {
       expect(context.errors!.root.status).toBe(400);
       expect(context.loaderData).toEqual({
         root: null,
-        "routes/index": null,
+        "routes/_index": null,
       });
     });
 
@@ -1105,7 +1105,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -1129,10 +1129,10 @@ describe("shared server runtime", () => {
       expect(calls.length).toBe(1);
       let context = calls[0][3].staticHandlerContext as StaticHandlerContext;
       expect(context.errors).toBeTruthy();
-      expect(context.errors!["routes/index"].status).toBe(400);
+      expect(context.errors!["routes/_index"].status).toBe(400);
       expect(context.loaderData).toEqual({
         root: "root",
-        "routes/index": null,
+        "routes/_index": null,
       });
     });
 
@@ -1261,7 +1261,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -1303,7 +1303,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -1325,11 +1325,11 @@ describe("shared server runtime", () => {
       expect(calls.length).toBe(1);
       let context = calls[0][3].staticHandlerContext as StaticHandlerContext;
       expect(context.errors).toBeTruthy();
-      expect(context.errors!["routes/index"]).toBeInstanceOf(Error);
-      expect(context.errors!["routes/index"].message).toBe(
+      expect(context.errors!["routes/_index"]).toBeInstanceOf(Error);
+      expect(context.errors!["routes/_index"].message).toBe(
         "Unexpected Server Error"
       );
-      expect(context.errors!["routes/index"].stack).toBeUndefined();
+      expect(context.errors!["routes/_index"].stack).toBeUndefined();
       expect(context.loaderData).toEqual({
         root: "root",
       });
@@ -1400,7 +1400,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -1429,7 +1429,7 @@ describe("shared server runtime", () => {
       expect(context.errors!.root.stack).toBeUndefined();
       expect(context.loaderData).toEqual({
         root: null,
-        "routes/index": null,
+        "routes/_index": null,
       });
     });
 
@@ -1500,7 +1500,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           index: true,
           default: {},
@@ -1524,14 +1524,14 @@ describe("shared server runtime", () => {
       expect(calls.length).toBe(1);
       let context = calls[0][3].staticHandlerContext as StaticHandlerContext;
       expect(context.errors).toBeTruthy();
-      expect(context.errors!["routes/index"]).toBeInstanceOf(Error);
-      expect(context.errors!["routes/index"].message).toBe(
+      expect(context.errors!["routes/_index"]).toBeInstanceOf(Error);
+      expect(context.errors!["routes/_index"].message).toBe(
         "Unexpected Server Error"
       );
-      expect(context.errors!["routes/index"].stack).toBeUndefined();
+      expect(context.errors!["routes/_index"].stack).toBeUndefined();
       expect(context.loaderData).toEqual({
         root: "root",
-        "routes/index": null,
+        "routes/_index": null,
       });
     });
 
@@ -1668,7 +1668,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           default: {},
           loader: indexLoader,
@@ -1713,7 +1713,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           default: {},
           loader: indexLoader,
@@ -1753,7 +1753,7 @@ describe("shared server runtime", () => {
           loader: rootLoader,
           ErrorBoundary: {},
         },
-        "routes/index": {
+        "routes/_index": {
           parentId: "root",
           default: {},
           loader: indexLoader,
@@ -1805,7 +1805,7 @@ describe("shared server runtime", () => {
         loader: rootLoader,
         ErrorBoundary: {},
       },
-      "routes/index": {
+      "routes/_index": {
         parentId: "root",
         default: {},
         loader: indexLoader,
diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx
index 27cbd7fa6..9573ddd90 100644
--- a/packages/remix-testing/create-remix-stub.tsx
+++ b/packages/remix-testing/create-remix-stub.tsx
@@ -127,7 +127,6 @@ export function createRemixStub(
           v2_dev: false,
           v2_headers: false,
           v2_meta: false,
-          v2_routeConvention: false,
           ...remixConfigFuture,
         },
         manifest: createManifest(routerRef.current.routes),