Closed borekb closed 4 years ago
Wow, thank you for this feedback. We'll definitely address each one of these feedback points in the coming days.
Thanks, here is some more:
UPDATE: I now think it would be better to have non-cascading behavior by default, like routing in now.json
. See the discussion below with @Janpot.
Sometimes, I want to finish immediately so that I don't need to worry about interactions with other rewrite rules below. Something like this would be useful:
{ source: '/post/:id', destination: '/post', stopHere: true }
Apache calls this the "last" flag – [L]
which is probably a better name 😄.
I'd like to implement a catch-all rule like this:
{ source: '/(.*)', destination: '/catchall' }
I'd use that component to check if the route exists (fetch from backend) and display the appropriate component (or 404 page).
I think it's currently impossible as all rules will eventually reduce down to this catch-all rule. The "stop here" feature would probably help resolve this.
Note: this comment https://github.com/zeit/next.js/issues/8053#issuecomment-542654273 indicates that the Custom Routes RFC should indeed handle catch-all behavior.
The catch-all route above would be useful for vanity / marketing URLs like this: https://example.com/any-string-really
. Another solution in our case would be to route based on HTTP headers because the load balancer adds a small HTTP header to each request:
GET /any-string-really
X-Page-Type: product
So when the request reaches Next.js, there's enough information in it about which page component to render and maybe, the next.config.js
could support something like this:
{
source: {
httpHeaders: 'X-Page-Type: :pageComponent',
path: '/:id',
},
destination: '/:pageComponent/:id',
}
Or even less dynamic would be fine too:
{
source: {
httpHeaders: 'X-Page-Type: product',
path: '/:id',
},
destination: '/product/:id',
},
{
source: {
httpHeaders: 'X-Page-Type: category',
path: '/:id',
},
destination: '/category/:id',
},
// ...
I'm trying to avoid custom server as much as possible 😄.
@borekb Wouldn't the custom _error
page be enough as a catch-all mechanism?
Personally, I feel like all these rule modifiers in apache have turned it into some sort of half-baked pseudo programming language. In my experience, the effect of this is that people try to cram as much logic in them as they can and turn it into a hard to debug black boxes of routing logic.
@Janpot I agree in general, there needs to be a balance.
With the "stop here" mechanism, I'd argue that it actually makes reasoning about the rules easier.
As for catch-all, it's a routing feature to me, even if it could be emulated via _error
. Generally, I believe that the more the system knows about routing, the more it can optimize the deployments. Then there is also the question of DX / intuitiveness and I would be looking for a catch-all feature in the routing layer.
As for HTTP headers, they're in the "not likely" department even in my head 😄. They would be a powerful feature but also possibly complex and with benefits for only a small number of users. As long as there is a good solution for vanity URLs (and custom server is not that), I'm happy.
With the "stop here" mechanism, I'd argue that it actually makes reasoning about the rules easier.
But if the catch-all case is solved, would there still be a need for "stop here"? I can't think of another case that needs "stop here" that wouldn't be solvable with the current RFC.
It's purely about reasoning about the rules.
In fact, when we're talking about it, I just realized that Now routing doesn't cascade by default, so when I'm reading the now.json
and find a match, I know how the request is routed straight away. When I was reading the Next.js routing in the tests, it was mentally harder to tell how a request will be routed, and I even guessed incorrectly for the /test-overwrite/a/b
route.
With Now routing, you have to explicitly pass continue: true
which I actually quite like.
🤔 I see. I was not very familiar with now
routing.
Next round:
I have a demo app with two page components, one "normal", one dynamic:
pages/product.js
pages/category/[id].js
Locally, both work fine but when deployed to Now with @now/next@1.0.6-canary.0
, only the product routes work – categories don't. You can try it here: https://nextjs-custom-routes-a0pz7flvg.now.sh/
_error.js
Motivated by the discussion above, I tried implementing dynamic route resolution via _error.js
, it looks like this:
import React from 'react';
import { useRouter } from 'next/router';
import NextError from 'next/error';
import dynamic from 'next/dynamic';
const ProductPage = dynamic(() => import('./product'));
const CategoryPage = dynamic(() => import('./category/[slug]'));
function Error({ pageToRender, route }) {
const router = useRouter();
switch (pageToRender) {
case 'category':
router.query.slug = route.substring(1); // quick & dirty
return <CategoryPage />;
case 'product':
router.query.id = route.substring(1);
return <ProductPage />;
default:
return <NextError statusCode={404} />;
}
}
Error.getInitialProps = async ({ req, res }) => {
const pageToRender = await resolvePageType(req.url);
res.statusCode = pageToRender ? 200 : 404;
return { pageToRender, route: req.url };
};
async function resolvePageType(route) {
// would fetch from backend...
return {
'/dynamically-resolved-category': 'category',
'/dynamically-resolved-product': 'product',
}[route];
}
export default Error;
It mostly works, just in dev mode, the browser keeps reloading routes that fall back to _error.js
. Production build seems to be fine.
Still, catch-all feels like it should be a routing feature, not this hack 😄.
yeah, I agree, that's less than ideal.
Thanks, here is some more:
...
Routing based on HTTP headers?
The catch-all route above would be useful for vanity / marketing URLs like this:
https://example.com/any-string-really
. Another solution in our case would be to route based on HTTP headers because the load balancer adds a small HTTP header to each request:GET /any-string-really X-Page-Type: product
So when the request reaches Next.js, there's enough information in it about which page component to render and maybe, the
next.config.js
could support something like this:{ source: { httpHeaders: 'X-Page-Type: :pageComponent', path: '/:id', }, destination: '/:pageComponent/:id', }
Or even less dynamic would be fine too:
{ source: { httpHeaders: 'X-Page-Type: product', path: '/:id', }, destination: '/product/:id', }, { source: { httpHeaders: 'X-Page-Type: category', path: '/:id', }, destination: '/category/:id', }, // ...
I'm trying to avoid custom server as much as possible 😄.
Just want to add to the discussion that we also use a custom server mostly for something like that.
Our pages structure:
pages/
www.site1.com/
index.tsx
www.site2.com/
index.tsx
and in our custom server:
server.get('/', (req, res) => {
return app.render(req, res, `/${req.header('x-site')}`) // or could be req.header('host')
})
This let's us have site specific pages but still have "normal" routes, so we would very much like this feature 👼
Hey Guys,
I was experimenting with the new feature on canary-8. I set up one rewrite rule to fetch all urls with one file (I use a headless CMS):
serverless: true,
experimental: {
async rewrites () {
return [
{source: '/:slug+', destination: '/index'}
]
}
},
Everything works as expected, but every click on a link is now a full page reload and not a client-side route. Is this a bug or am I overlooking something?
I just updated to 9.1.4
and I combined the new catchAllRouting
and rewrites
as following:
experimental:{
async rewrites () {
return [
{source: '/', destination: '/[...index]'}
]
},
catchAllRouting: true
}
Below the issue I am facing, basically all links work server and client side but only the link to the root of the page is not working. I can remove the as
or href
but then the link would require a page reload
<Link as={as} href="/[...index]" /> // => works perfectly for /path, /path/subpath
<Link as="/" href="/[...index]" /> // => throws error on client side routing. SSR works
With this setup only one [...index].tsx
file is needed to serve all requests. Everything works great except the little issue with the root link.
Wow, catch-all / "...rest" exists already? Missed #9416, that's great news!
@dohomi How does the [...index].tsx
impact chunking?
@borekb not sure.. I just started playing around with it. I honestly think that for my case (working with a headless API approach) it should not matter at all.
I just use a second lambda which simply re-export the [...index].tsx
content like following:
// index.tsx
import INDEX from './[...index].tsx'
export default INDEX
Now I could remove the rewrites
section and my client as well as server side routing was working as expected. Would love to hear some feedback from the core devs if thats the approach to take
@dohomi How does the [...index].tsx impact chunking?
Very badly. The catch-all route should never be used to mount a subrouter in your application.
It's strictly meant for CMS use cases, the RFC is being posted soon!
Very badly. The catch-all route should never be used to mount a subrouter in your application.
It's strictly meant for CMS use cases, the RFC is being posted soon!
@Timer what do you mean by very badly? I ended up using https://github.com/zeit/next.js/issues/9390#issuecomment-555310907 so I only have one single [...index].tsx
file which serves all requests of an endpoint connected to a CMS API. It feels pretty solid though..
@dohomi it means, there is no code splitting besides our chunking mechanism, meaning you're more likely to "overship" code as you're routing 100% of routes to one page, so that bundle will be very large depending on how you set it up.
For example if you route all routes to a single page you're likely to include multiple different layouts etc.
Unless it's something very basic and there is only one page view and all content is the exact same type from a CMS. With the exact same type I mean you don't have multiple different "post types", like in WordPress you could have a blog + pages and those both warrant different layouts. For that case it's better to create multiple pages like for example:
/blog/[...slug].js
and /[...slug].js
this would mean /blog/hello-world
gets served a different bundle than /support
for example.
@timneutkens thanks for your explanation. As the layout and content blocks are all based on the JSON output of the headless API I think I have to bite the bullet and "overship" code because of the unknown amount and type of content needs to be delivered to the frontend. I use React.createElement(VARIOUS_CONTENT_TYPE, PROPS)
to render every content block and layout section dynamically. I can post a website based on this approach if you guys are interested in the outcome.
Hello, I think some points raised in this feedback should be answered with the latest version of path-to-regexp. Being a major version of path-to-regexp, I guess it would be necessary to rewrite the implementation of custom routes, but it's worth it. Looking forward to see that!
Hello, I started testing this feature like the @dohomi code above. With /[...index]
to take all routes. However, internal routes, such as favicon.ico
and other resources, are captured. Is this the expected behavior?
This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.
I noticed that custom routes (RFC #9081) landed in canary a few days ago so yummy, I had to try them!
Overall, it's an awesome feature ❤️. Even in this first pre-release, it allowed me to do a lot so huge congrats to @Timer, @timneutkens, @ijjk and everyone involved. 👍
Params & pre-rendering
As most of our routes are dynamic, I first tried this:
This should be roughly equivalent to a dynamically-routed component
pages/post/[id].js
which would have this behavior:getInitialProps
so this page is pre-rendered to HTML.query
object is{}
.With custom routes, the third step doesn't happen which left me initially confused – I thought my rewrite rules are wrong. If this could be aligned with dynamic routing's behavior, with the same set of tradeoffs, that would be great. 👍
The fix / workaround is to add
getInitialProps
. (Here's a related test; "GIPGIP" actually has a lot of significance 😄.)Dynamic routing
I initially thought that dynamic routing & rewrites don't work together but that was only because of the pre-rendering issue above. This works fine:
Partial path matching doesn't work with non-dynamic page components
This might be a bug, I'm not sure.
Given the two components from above, this is what I'm seeing:
Console errors
When rewriting goes wrong and the result is 404, it would be nice to see some sort of info in the console.
Future routing wishes
This section is not strictly about Custom Routes but while I'm writing this, I wanted to mention that we're dealing with complex routing requirements in an e-commerce solution we're working on. The users can choose to use vanity URLs like
/nike
or/adidas-air
which makes it impossible to tell just from the path which page component should be used.Some assorted thoughts:
/three/levels/deep
but Next.js only sees it as/two/levels
, things likehref
s could be off. But again, I'd have to try this.Overall, thanks for all the improvements in the routing department! You're doing great work. 👏