Closed Timer closed 4 years ago
Poll: To express interest in optional parameters, please react with a "+1" this comment.
Note: Optional parameters are already possible with this RFC, they just do not have an explicit syntax (see Caveats section).
Poll: To express interest in catch-all parameters, please react with a "+1" this comment.
Note: Please share your use case for catch-all parameters in this thread! We'd love to understand the problem-space more.
reserved 3
On ricardo.ch, we use a locale prefix for each route which make routing a bit more complex.
Example of valid routes:
/
- homepage with auto-detected locale/:locale
- homepage with forced locale/:locale/search
- search page/:locale/article/:id
- article pageDo your think such prefix parameters could be supported?
At the moment, we use https://www.npmjs.com/package/next-routes
Another thing: for the article page, we also support a slug before the id like /de/article/example-article-123
where the id would be 123. This is done via a quite complex regex using next-routes
and I don't see how this could be expressed with a file-system API.
@ValentinH the provided routes are all possible using the filesystem API -- given your provided routes:
/
=> pages/index.js
/:locale
=> pages/$locale/index.js
/:locale/search
=> pages/$locale/search.js
/:locale/article/:id
=> pages/$locale/article/$id.js
we also support a slug before the id like /de/article/example-article-123 where the id would be 123
This use case is addressed above:
Named parameters cannot appear in the middle of a route name.
This means a page named
blog-$id.js
would be interpreted literally and not matched by/blog-1
. You can either restructure your pages to be/blog/$id.js
or turn the entire URL Segment into a named parameter and handle strippingblog-
in your application's code.
Does this solution not meet your needs? We'd love to learn more about your specific requirements.
Thanks a lot for the answer.
I didn't thought about using $locale/index.js
both as a folder and a file, this is really neat!
Regarding the "named parameter in middle", I overlooked it because I thought having the slug being dynamic was different. However, you are completely right and this is addressed by the paragraph you mentioned. Striping the slug in the application code will be the way to go 🙂
Would something like this (parse params from .hidden .files/.folders) be possible?
pages/ ├── .root.js ├── blog/ │ ├── .id/ │ │ ├── index.js │ │ └── comments.js <-- optional? ├── customers/ │ ├── .customer/ │ │ ├── .post/ │ │ │ └── index.js │ │ ├── index.js │ │ └── profile.js │ ├── index.js │ └── new.js ├── index.js └── terms.js
or leave the $ so one could find their files :D but always use $folder to indicate a param?
pages/ ├── $root.js ├── blog/ │ ├── $id/ │ │ ├── index.js │ │ └── comments.js <-- optional? ├── customers/ │ ├── $customer/ │ │ ├── $post/ │ │ │ └── index.js │ │ ├── index.js │ │ └── profile.js │ ├── index.js │ └── new.js ├── index.js └── terms.js
I used to have this use-case for optional parameters in an app that worked with npm packages. These could optionally have a scope. There are routes like:
/packages/express
/packages/express/dependencies
/packages/@babel/core
/packages/@babel/core/dependencies
So basically, the scope parameter is optional, but it's also only a scope when it starts with @
.
So /packages/express/dependencies
and /packages/@babel/core
have the same amount of segments, but in one case it's /dependencies
of express
and in the other it's /index
of @babel/core
.
In the end it was solved in react-router
with the following routes:
<Switch>
<Route path={`/packages/`} exact component={PackagesOverview} />
<Route path={`/packages/:name(@[^/]+/[^/]+)`} component={PackageView} />
<Route path={`/packages/:name`} component={PackageView} />
</Switch>
I'm not sure I see a solution for this use-case in this RFC.
As for catch-all use cases, I'm thinking any deep linking into recursively nested data, like folder structures, treeviews, treemaps.
My 2 cents: dollar signs in filenames are a bad idea because they're used by shells as a sigil. You're going to confuse people trying to run rm $root.js
. Underscores seem like a decent alternative.
More broadly: like many people, I've tried to leverage the file system as a solution to this in the past. Ultimately, I think the file system is never going to offer the full expressiveness you're looking for. For example, declarative routers usually let you specify a validation pattern for a dynamic parameter. In that case, part of the schema lives on the file system, and another part in the code. Separation of concerns is a good thing, but in this case, it's a technical limitation more than anything else.
Like @ValentinH we use the $locale var, but it's optional.
Should we use /page.ts and /page/$locale/page.ts?
Because we can use a "default" locale or a predefined locale ( user settings ), in those cases we don't use the $locale param.
But we have more use cases: /car/search/$optional-filter-1/$optional-filter-2/$optional-filter-3
Where optional-filter-1: color-red, optional-filter-2: brand-ford, etc...
And for optional params, something like /$required-param/ and /$$optional-param/?
Awesome that this is coming up on the roadmap!
I have to chime in supporting @timdp though. When you can't even touch $file
this will lead to a lot of confusion. You need to remember escaping at every interaction. touch \$file; vim $file
will open vim without a file (because $file isn't a defined variable).
Likewise tab completion in a shell will list all variables, once again bringing confusion.
I'm proposing two alternatives that I feel gives the right associations and should work in shells:
=
It can be read as page is a customer
for =customer
. You can even contort it mentally to be a colon just stretched out, thus resembling the most common form for named parameters.@
as it also reads somewhat well. a customer
for @customer
Another option would be to use curly braces (unless they are reserved characters on some file systems). This parameter syntax is also "prior art" and is used by many other routers:
pages/
├── {root}.js
├── blog/
│ └── {id}.js
├── customers/
│ ├── {customer}/
│ │ ├── {post}.js
│ │ ├── index.js
│ │ └── profile.js
│ ├── index.js
│ └── new.js
├── index.js
└── terms.js
This would allow to have parameters in the middle of the route segment and multiple parameters per segment as it's clear where the parameter starts and where it ends, e.g. /product-{productId}-{productColor}
.
So excited that dynamic routes is coming to Next.js!
Regarding the syntax for named parameters, this is something that has been discussed on Spectrum: https://spectrum.chat/next-js/general/rfc-move-parameterized-routing-to-the-file-system~ce289c5e-ff66-4a5b-8e49-08548adfa9c7. It might be worth using that as input for the discussion here. Personally, I like how Sapper is doing it using [brackets]
. This is also something Nuxt is going to implement in version 3. Having different frameworks use the same format for dynamic filesystem-based routes sounds like a good thing.
Regarding the usage of <Link />
, I think developers will easily forget to set both the href
and as
attributes. I get that it's not possible to "merge" these into the href
attribute because it'd introduce a breaking change, but I feel like it could be solved in a more elegant way.
Curly braces are unfortunately used by Bash to group commands.
I agree with @stephan281094 regarding usage of <Link />
, it will be source of mistakes.
Dynamic routing is an extremely useful feature, so it's really awesome you guys have looked into it and came up with a solution, huge props!
While on this topic, wildcard routes would also be a worthy addition to the proposal. You did mention catch-all parameters as something to investigate in the future, but it doesn't cover cases where you might want to do something like /category/*
, which could have N number of levels, and you want all of them to render the category
page.
Is it possible to use :
safely ? If so, that'd be my vote, because everyone is already familiar with that convention from express.
Due to $
conflicting with shell variables, I personally strongly oppose it.
Is it possible to use
:
safely ? If so, that'd be my vote, because everyone is already familiar with that convention from express.
Apparently :
is a prohibited character in Windows, so it's probably not safe. Going with _
isn't ideal either, since underscores can be used in URLs. The reason I think [brackets]
are a nice solution, is because it's more future proof. If Next.js wants to support routes like post-12345
in the future, using this syntax it can be done without introducing a breaking change.
So a list of characters to avoid would be:
:
, *
, "
, <
, >
, |
$
{
, }
Anything else?
This wouldn't eliminate our need to have a centralised route file for a couple of reasons:
We also generate our pages folder for these reasons:
index.js
definitely isn't unique, and I see places where we'd have multiple common segments like edit
.Essentially our pattern is to use our centralised route configuration to generate our pages folder, which contains files which do nothing more than import/export modules from elsewhere in the codebase.
To that end, my focus is more on whether this proposal can work simply as an enhanced output format for our existing page generation process, so that we can at least get the benefit of not needing a custom server.
I've gone over some of my use cases elsewhere: https://gist.github.com/AndrewIngram/8d4c4ccd9bd10415a375caacade9f5ca
The main thing i'm not seeing is supporting implicit parameters that aren't expressed in the file-system, for example URL overrides.
Let's say we have a URL like this:
/some-vanity-url/
Where in current Next.js terms, we'd want it to map to a product page with a number of query parameters, e.g Product.js?id=foo&language=en
.
Similarly, on our website most countries "sites" are scoped by a top-level segment eg es
or ie
, but the gb
site is mounted without that segment. This means all the gb pages have an implicit country
parameter, whilst for all other countries it's explicit.
The other downside, is that because in our case, the same 'page' can exist at multiple mount points in the URL architecture, we're going to end up with a greater number of bundles (i.e. several duplicate entry points) than we actually need in practice.
On the whole this proposal seems like it's going to work well for most common use cases, but it doesn't obviate the need for a route config or custom server in all cases. But assuming this doesn't replace my ability to use the framework the way I do today, I don't have any real objection to this being the preferred happy-path API.
I support the {id}
suggestion. It allows for multiple params and I think it looks a lot better. It also fits better with React.
I'm in favor of the file/¶m.js
character. Taken directly from urls and it doesn't look like it conflicts with files systems or bash.
I would use _
and maybe allow for an override in the next.config.js
for those who reallllly need something different.
Appreciate the work on this. Been wanting it for a while! ❤️
Amazing! 🎉🎉🎉
My only issue here is that Link
needs both href
and as
params.
I believe we could just write <Link to="blog/123" />
: since Nextjs already knows all the routes based on files in the pages folder, it could easily translate it into "/blog/$id"
.
So a list of characters to avoid would be:
&
is a control operator in bash that runs the left side of the argument in an async subshell. Plaintext: open pages/&customer
would run open pages/
in the background and the command customer
in the foreground shell.
This looks really cool.
It does seem like this will create a significant number of single file directories (like /blog/$id
in the original example). This gets even more cumbersome if you want two trailing route parameters (i.e. /git/compare/$hash1/$hash2
).
I also don't love that the filename for rending a blog post would be $id.js
. Having it named blog.js
would be much more descriptive.
Perhaps combine with a @customRoute
decorator?
// pages/blog.js
import {useRouter, @customRoute} from 'next/router'
@customRoute('/blog/:id')
function BlogPost() {
const router = useRouter()
// `blogId` will be `'how-to-use-dynamic-routes'` when rendering
// `/blog/how-to-use-dynamic-routes`
const blogId = router.query.id
return <main>This is blog post {blogId}.</main>
}
export default BlogPost
This seems to provide a cleaner solution for the proposed catch-all parameters as well.
Decorators can't be applied to functions (maybe this changed since I last read it?) and the proposal is probably a long way away anyway
Well, suppose you go that road, you'd probably do it the way AMP is configured now:
// /pages/blog.js
export const config = {
amp: true,
dynamicRoute: true // adds a [blog] property to the query object
// dynamicRoute: /\d+/ // could even support regex if you want
};
However, I think stuff like this can be added later on if it seems useful at some point. I think I'd rather see a basic support to start with, much as is described in the RFC. Get some real usage with that, then refine where it breaks. I also think the only characters that should be taken into account to avoid are the file system ones. Those are the real blockers for building this feature.
Please, make sure to use a character that is friendly with serverless solutions! (On Aws, there are some characters that could cause troubles)
Exporting a config object with a component key is something I don't hate.
You could also just use a HOC
function BlogPost(props) {
return <div />
}
export default withCustomRoute(BlogPost, "/blog/:id")
what if we add some static field to the page (like getInitialProps)?
// pages/blog.js
import {useRouter} from 'next/router'
function BlogPost() {
const router = useRouter()
// `blogId` will be `'how-to-use-dynamic-routes'` when rendering
// `/blog/how-to-use-dynamic-routes`
const blogId = router.query.id
return <main>This is blog post {blogId}.</main>
}
// By default it would be as it is now
BlogPost.route = '/blog/:id';
export default BlogPost
@dmytro-lymarenko What happens when you navigate to /blog
in the browser? A 404?
Because this needs to be determined at compile time, I guess you'd need something that is statically analyzable. a HOC or a static property wouldn't be.
you'd need something that is statically analyzable. a HOC or a static property wouldn't be
Every static property example given so far would be statically analyzable (though you could certainly break things easily). We could just insist that you export your function and set the route property on it in a statically analyzable way. The runtime could check for route properties that are set at runtime but weren't caught by our static analyzer and issue a warning / throw an error.
What happens when you navigate to /blog in the browser? A 404?
@kingdaro - IMO, yes. If you want to use both /blog
, and /blog/:blogId
paths, then you use a directory. You are overloading that path, so the directory structure is justified.
pages/
├── blog/
│ ├── $id.js
│ └── index.js
In order of appearance
Well, suppose you go that road, you'd probably do it the way AMP is configured now:
// /pages/blog.js export const config = { amp: true, dynamicRoute: true // adds a [blog] property to the query object // dynamicRoute: /\d+/ // could even support regex if you want };
However, I think stuff like this can be added later on if it seems useful at some point. I think I'd rather see a basic support to start with, much as is described in the RFC. Get some real usage with that, then refine where it breaks. I also think the only characters that should be taken into account to avoid are the file system ones. Those are the real blockers for building this feature.
I think using config is a bad idea because you need to go through multiple files, to see what is actually dynamic. If you set it in the file system you can see it from the first glance.
I wonder if more than one standard routing solution should be something to consider.
Simple file-based routing is a great selling point for those new to Next/React, or anyone wanting to quickly get a simple app up and running, but it can be rather limiting. And it seems to me that trying to shoehorn dynamic routing into this pattern could ruin that simplicity and lead to unnecessary complexity, all in the name of keeping everything file-based.
After reading this discussion and thinking about my own usage of Next.js, I think first class support for an alternative (supplementary) routing system could be the best way to solve this.
I like some of the out-of-the-box thinking in this thread (such as the proposal to use decorators) but those ideas definitely have their own problems. I hope we can come up with something great 👍
Exporting a config object with a component key is something I don't hate.
You could also just use a HOC
function BlogPost(props) { return <div /> } export default withCustomRoute(BlogPost, "/blog/:id")
That’s pretty cool, but I wonder if having route information split across many files like this could become hard to manage.
My original thinking with proposing a local config (in the file) vs a global one (route.js
), was to address the specific scenarios mentioned in my first comment (deeply nested files that are the only file in their directory, non-semantic file names, and catch-all-params).
If used strictly in those contexts, it's far less confusing, because the URL maps directly onto the file system, and only "extra" params are addressed by the local config.
That said, I'm not sure I would even try to to restrict users from doing it however they want. We can pretty print the calculated routing table to the console, or even save it to some predetermined file. That should be enough to aid troubleshooting routes
@merelinguist I don't believe =
is prohibited in Windows as you've written in the summary table. You are linking back to how :
is prohibited, but according to Microsoft Windows file naming docs the equal character is allowed.
I'm already porting with dynamic routes in a project that I use in production (hopefully I can get it live this week).
Specific question though, will the new next@canary API feature also support dynamic routing?
{ path: '/api/:customer', page: '/api/$customer/index.js' }
I've just tried it with next@8.1.1-canary.54 and I get a 404 not found, so I suspect it's not there yet. Just seems like it makes sense for these two features (API + dynamic routes) to have parity on URL routing.
@remy it's not yet implemented it's on my list to do it soon
We also should take into account not only Windows and Linux systems, but others too: https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations
I'd like to add more info about my proposal:
what if we add some static field to the page (like getInitialProps)?
// pages/blog.js import {useRouter} from 'next/router' function BlogPost() { const router = useRouter() // `blogId` will be `'how-to-use-dynamic-routes'` when rendering // `/blog/how-to-use-dynamic-routes` const blogId = router.query.id return <main>This is blog post {blogId}.</main> } // By default it would be as it is now BlogPost.route = '/blog/:id'; export default BlogPost
const route = `/blog/${somethingElse}`;
BlogPost.route = route; // is not allowed
<Trans id="msg.docs" /* id can only be static string */>
Read the <a href="https://lingui.js.org">documentation</a>
for more info.
</Trans>
Going by the list of prefixes already listed - I wonder if there's any strong reason not to use a @
symbol prefix?
I doubt if it's of value, but you get parity with Nuxt - which means someone switching from one or the other will immediately know how it works.
Alternatively, has anyone thought about making the prefix a user option? It makes it harder for people to understand one project from another, but it means if I wanted, I could make the prefix query__{...}
or something.
Just a thought.
Following on from @remy's suggestion, why not completely open up the API for how Next parses routes from the file system. Giving users as much (or as little) flexibility as they need, and inspiring reliable third-party routing solutions.
@scf4 I had a library which is a PoC , which use now.json
routes config to do universal routing with nextjs too here
I hope that Zeit team also open source the route parser on client side library, too.
Looking at Nuxt I think _id.js
is not too bad. Yes, we already use _app
and _document.js
as you mentioned and it's not publicly routable. But a dynamic route can also be viewed as not routable as this is a template for many pages
Dynamic Routes
Background
Dynamic routing (also known as URL Slugs or Pretty/Clean URLs) has been a long-time requested feature of Next.js.
Current solutions involve placing a L7 proxy, custom server, or user-land middleware in-front of your application. None of these solutions offer a sufficiently ergonomic developer experience.
Additionally, users reaching for a custom server inadvertently opt-out of advanced framework-level features like per-page serverless functions.
Goals
/blog/:post
<Link />
route transitions when possibleProposal
Next.js should support named URL parameters that match an entire URL segment. These routes would be expressed via the filesystem:
[]
would be considered a named parameterquery
object (accessible fromgetInitialProps
orrouter
viawithRouter
) — these parameters can not be overridden by a query parameterTo help understand this proposal, let's examine the following file tree:
Next.js would produce the following routes, registered in the following order:
Usage Examples
These examples all assume a page with the filename
pages/blog/[id].js
:Navigating to the Page with
<Link />
The above example will transition to the
/blog/[id].js
page and provide the followingquery
object to the Router:Reading Named Parameters from Router
Note: you can also use
withRouter
.Reading Named Parameters in
getInitialProps
Caveats
Optional route parameters are not expressible through the filesystem.
You can emulate an optional route parameter by creating a stub page that exports the parameter version (or vice versa). This increases the visibility of your application's routes when inspecting the filesystem.
Named parameters cannot appear in the middle of a route name.
This means a page named
blog-[id].js
would be interpreted literally and not matched by/blog-1
. You can either restructure your page to be/blog/[id].js
or turn the entire URL Segment into a named parameter and handle strippingblog-
in your application's code.Alternatives
Denote URL Slugs with insert symbol here instead of
[]
There are very few symbols available for use to represent a named parameter on the filesystem. Unfortunately, the most recognized way of defining a named parameter (
:name
) is not a valid filename.While surveying prior art, the most common symbols used to denote a parameter were
_
,$
and[]
.We ruled out
_
because_
is typically indicative of an internal route that is not publicly routable (e.g._app
,_document
,/_src
,/_logs
). We also ruled out$
because it is a sigil in bash for parameter expansion.Leverage
path-to-regexp
for comprehensive supportMost of the symbols required to express regex are not valid filenames. Additionally, complex regexes are sensitive to route ordering for prioritization. The filesystem cannot express order nor contain regex symbols.
In the future, we may allow
path-to-regexp
routes defined innext.config.js
or similar. This is currently out of scope for this proposal.Future Exploration
Catch-All Parameters
In the future, we may consider adding catch-all parameters. With what we know thus far, these parameters must be at the end of the URL and would potentially use
%
to denote a catch-all route (e.g.pages/website-builder/[customerName]/%.tsx
).