Intercepting a next-js catch-all route into a modal
So this is one heck of an edge-case I guess... Still a combination of common NextJS patterns that I've just spent way too long wrapping my head around.
The UX we want to create:
- User lands on a Page
/my-blogpost
and clicks a link to/my-image
- We open the image in a modal overlay
- But the URL is still updated accordingly, so that they can bookmark, share etc.
- When going directly to
/my-image
we want to open the image without the post-context
This pattern is well documented in the NextJS docs for parallel routing
Details
Note how there is no content-type indicator in /my-blogpost
and /my-image
.
The docs assume we'll use /post/my-blogpost
and /image/my-image
and when that's your case, stop reading now.
What if the route-handler for all content is at app/[...slug]/page.tsx
and we only know the content type after we've loaded it from CMS? (I'm doing this on hannesdiem.de )
Both the parallel routing and the route interception work on a static level, so we can not dynamically handle this.
Also, we only want to intercept some content from within some other content - even if we could place the interception inside the catch-all route, it would intercept every route, and to my knowledge there is no good way to dynamically decide "no, actually I don't want to intercept that route..." (which is probably why it's not even supported)
What about doing this in the client?
After trying around a lot with this (and finally landing at the solution I'll share shortly), I was like "Ok, let's just do this manually in the client..." but that then works completely against the core react-server-component idea of modern NextJS apps.
- The content I want to display in my modal comes from a DB -> So I'd need to build a server-action, loader, api, ... something to access the data securely
- The content is a quite complex Rich-Text format and processing that in the frontend would require loading a lot more libraries into the users browser than when we do this on the server.
Both are solvable challenges, but felt wrong to do when the other solution is so much more elegant.
My solution
I ended up with this design:
app/
├─ [...slug]/page.tsx
│ 👆 dynamic content page
├─ @modal/
│ ├─ (.)m/[...slug]/page.tsx
│ │ 👆 renders the modal or notFound
│ ├─ default.tsx
│ 👆 always renders null
├─ (modal-fallback)/m/[...slug]/page.tsx
│ 👆 redirects to URL without /m prefix
├─ layout.tsx
│ 👆 renders children and @modal slot
- Modals are always prefixed with
/m
in their route
This way we can explicitly intercept them. And also explicitly link to them from anywhere in the app using<Link href="/m/my-image">Show</Link>
. - Direct links to
/m
are redirected When a user opens a modal URL like/m/my-image
, they're redirected to/my-image
- A content-type that can not be rendered in a modal produces a 404
The
@modal/(.)m/[...slug]/page.tsx
has to mimic the rendering of[...slug]/page.tsx
but inside a modal. There is no need to implement this for every content-type. Just the ones that we want to support in modals. Everything else can rendernotFound()
and we'll never link there.
That's it
Feels a bit like three core patterns of NextJS routing (catch-all routes, intercepting-routes, and parallel routes) not playing well together. I'm wondering If I miss out something.
In reality, this is even a bit more complex on my end, because I'm rendering multiple websites from within a single next-stack 💪 and at this point the file-system syntax for routing features is a DSL that would benefit so much from better documentation and debugging tools – Really much underlining the move of React-Router to go back to declare the routing in a typescript file.
Hope this helps, let me know when I've missed something, or when you've solved this differently.