Personally I've never liked how tools like Remix or NextJS have mapped a nested file system to routes. Simple things like "I want to put this component in its own file" become annoying tasks.
I've always been a fan of "flatter" file systems, my files often look like this:
/App/
AppLayout.tsx
AppLayoutNav.tsx
/AppOnboarding/
AppOnboardingLayout.tsx
AppOnboardingTour.tsx
/AppOnboardingFirstSite/
AppOnboardingFirstSiteRoute.tsx
/AppOnboardingSetupLocal/
AppOnboardingSetupLocalRoute.tsx
...
This admitably takes a bit to get used to, but it's great for focusing on one part of the app at a time, and seeing all the different parts of your app in one flat directory makes discovery easier.
However, conventions are good, and using the file system for routing does have value. So I'm proposing a new way to do it:
Instead of a nested file system for declaring routes, all of the routes are declared in a flat file structure.
Dots .
get mapped to /
in the route:
a.b.c.d.e -> /a/b/c/d/e
Leading underscores _
ignore the segment in the route:
a._b.c._d.e -> /a/c/e
Leading dollar sign $
becomes a dynamic segment:
a.$b.c.$d.e -> /a/:b/c/:d/e
Trailing dollar sign $
becomes a splat:
a.b.c.d.e$ -> /a/b/c/d/*e
Trailing dots .
marks the route as a layout route:
a. -> (layout: a.*)
a.b -> /a/b (renders inside a.)
Trailing underscores _
ignore the layout:
a. -> (layout: a.*)
a_.b -> /a/b (does not render inside a.)
/pages/
/_landing./ -> (layout: _landing.*)
/_landing.index/ -> /
/_landing.company/ -> /company
/_landing.company.team/ -> /company/team
/_landing.company.careers/ -> /company/careers
/_landing.docs.doc$/ -> /docs/*
/_auth./ -> (layout: _auth.*)
/_auth.login/ -> /login
/_auth.signup/ -> /signup
/_auth.forgot-password/ -> /forgot-password
/_auth.reset-password/ -> /reset-password
/app_.loading/ -> /app/loading (does not use app.* layout)
/app./ -> (layout: app.*)
/app.onboarding./ -> (layout: app.onboarding.*)
/app.onboarding.first-site/ -> /app/onboarding/first-site
/app.onboarding.setup-local/ -> /app/onboarding/setup-local
/app.sites.$site/ -> /app/sites/:site
...
Each route directory must contain exactly one *.route.*
file at its root, but
otherwise does not care what files you put inside:
/pages/
/_landing./
Landing.route.tsx (layout route)
Landing.css
LandingLogo.svg
LandingNav.tsx
LandingFooter.tsx
/_landing.index/
LandingIndex.route.tsx (page route)
/_landing.company.index/
LandingCompanyIndex.route.tsx (page route)
...
In Remix, that same app would look something like this:
/app/
/styles/
landing.css
/components/
/landing/
LandingNav.tsx
LandingFooter.tsx
/pages/
__landing.tsx (layout route)
/__landing/
index.tsx (page route)
company.tsx (page route)
...
/public/
/images/
landing-logo.svg
When you have a few hundred or thousand files, this only gets more and more chaotic, while the flat file system "scales" linearly.