Next.js: catch-all with dynamic routes

Created on 22 Jul 2019  路  10Comments  路  Source: vercel/next.js

Feature request

Is your feature request related to a problem? Please describe.

Since next 9 we have finally dynamic routes: https://github.com/zeit/next.js/issues/7607 馃憤

The one important thing missing, but mentioned in the RFC is the ability to define catch-all pages and i did not find an open issue that tracks the progress of that.

E.g. if you have a list of pages (from a cms or so) that can be nested, where every page has a path (that might contain slashes /), you can't currently match them with dynamic routes. A possible workaround would be (if you limit the depth of the paths) to do nested multiple pages.

Describe the solution you'd like

i think the solution proposed in https://github.com/zeit/next.js/issues/7607 would be good ( pages/website-builder/[customerName]/%.tsx))

One thing to consider is:

can the page define whether it accepts a path or not? E.g. consider this pattern: content/%.tsx. Given this url-path: "content/about/team", i would receive this segment: about/team and then e.g. fetch a page somewhere from a cms with this path. If this page does not exist, i would like to show a 404, so the page should "reject" this path.

Describe alternatives you've considered

A first-class code-driven routing approach that uses path-to-regexp without custom server would be much more flexible in the future. I think forcing the route pattern to be in the filenames has too much downsides (filesystem restrictions) without adding much value to developers, but that's my opinion.

Most helpful comment

ok i found a (somewhat silly) workaround, assuming the depth of your dynamic pages paths is limited:

  1. Create this directory structure in pages:

Bildschirmfoto 2019-07-27 um 23 41 31

you can uses as many subfolders as you like, depending on the depth of your dynamic pages

  1. in the most inner index.js, you can load your page:
import * as React from "react";

import { withRouter } from "next/router";

const ContentPage = withRouter(({ router }) => {
  // assuming usePage loads a page from a database/graphql with the given path
  const { page } = usePage(router.asPath);
  return <Whatever page={page} />
});

export default ContentPage;

and in any other index.js in this tree:

import page from "./[slug]";
export default page;
  1. Now if you want to generate a <Link /> to that page, you need a little trick, e.g. a component like this:
import Link from "next/link";
import React from "react";

// assuming page is an object with .path , e.g. path=/about/team/bigboss
const PageLink = ({ page, ...props }) => {
  const depth = page.path.split("/").length - 1;
  return <Link href={"/" + new Array(depth).fill("[slug]").join("/")} as={page.path} {...props} />;
};
export default PageLink;

This works, however it has a huge problem: When going to a childpage, an addition, identical bundle of that page is also loaded

All 10 comments

I'm building out a prototype of my company's site with NextJs. With the updates in NextJs 9, I can build all of our pages EXCEPT one, that uses a "pretty" url setup for its search results page: /search/keyword/somekeyword/category/somecategory. For the whole site, I would need to build a server.js, JUST so I can handle this one page.

If this was implemented, I could set up a component in /pages/search/%.js and reasonably handle everything after /search in the getInitialProps.

This is a fantastic idea and in my opinion would convince a lot of people to ditch whatever server they're using and use NextJs and their filesystem instead.

by the way, I can't find a workaround at all for this use case. Even with a custom server i am lost.

i want to send all non-static page to one catch-all page. This is only possible if the custom server knows either about all my dynamic pages or about all my static next pages.

imaging you have these paths somewhere in a database. This list will be loaded through graphql or similar.

/about
/about/team
/service
/service/you-name-it

and you have these static next pages:

index.jsx
profile.tsx
user/[userId].tsx

Now, with a custom server, i would need to declare a route like this, e.g. in express:

app.get('*', function(req, res) {
  // here you use const handle = app.getRequestHandler(); normally
  // I would already have to know which routes exist in my db to either call
// handle(req, res)
// or to send it to my custom catch-all-route
// so i would already have to know which request would be accepted by nextjs without error, or which paths exist in my db.
});

ok i found a (somewhat silly) workaround, assuming the depth of your dynamic pages paths is limited:

  1. Create this directory structure in pages:

Bildschirmfoto 2019-07-27 um 23 41 31

you can uses as many subfolders as you like, depending on the depth of your dynamic pages

  1. in the most inner index.js, you can load your page:
import * as React from "react";

import { withRouter } from "next/router";

const ContentPage = withRouter(({ router }) => {
  // assuming usePage loads a page from a database/graphql with the given path
  const { page } = usePage(router.asPath);
  return <Whatever page={page} />
});

export default ContentPage;

and in any other index.js in this tree:

import page from "./[slug]";
export default page;
  1. Now if you want to generate a <Link /> to that page, you need a little trick, e.g. a component like this:
import Link from "next/link";
import React from "react";

// assuming page is an object with .path , e.g. path=/about/team/bigboss
const PageLink = ({ page, ...props }) => {
  const depth = page.path.split("/").length - 1;
  return <Link href={"/" + new Array(depth).fill("[slug]").join("/")} as={page.path} {...props} />;
};
export default PageLink;

This works, however it has a huge problem: When going to a childpage, an addition, identical bundle of that page is also loaded

Best workaround is to use https://github.com/fridays/next-routes

Would probably a good idea to integrate that into core. That would solve so much problems.

EDIT: after further testing, this does also not work properly: https://github.com/fridays/next-routes/issues/315

basically, you can't have file system routes anymore, you have to define all routes in next-routes

Edit2: I now got rid of next-routes again and do it like this:

  server.get("*", async (req, res, n) => {
    if (req.url) {
      const hasPage = await hasPageForPath(req.url);
      if (hasPage) {
        return app.render(req, res, "/content", { path: req.url });
      } else {
        n();
      }
    }
  });

where hasPageForPath checks whether the url exists.

In my case hasPageForPath looks like this:

const hasPageForPath = async (path) => {
  const { page } = await request(
    GRAPHQL_ENDPOINT,
    /* GraphQL */ `
      query GetPage($path: String!) {
        page(path: $path) {
          id
        }
      }
    `,
    { path },
  );

  return Boolean(page);
};

this might look expensive, but can be cached.

@timneutkens is it possible to get a "order by priority" list (not ETA) for some next features?

I guess this one looks "important" for most ppl using Dynamic routes, and 9.2 sounds like "far away" from current priorities.

Right now working around it using rewrite in our reverse proxy, but forces us to add "route logic" to an external service.

I think a catch-all would be great. Lets say you would have a headless CMS of any depth of routes are built inside. It would be great to tell NextJS that [index].tsx would handle everything, either "/about" or "depp/nested/page". Currently I use next-routes but it would be great if the logic could be avoided.

That was my exact use case, using it with a headless cms, but had to discard NextJS for now.

It's been a few months, and looks it will take a few more until it's available.

That was my exact use case, using it with a headless cms, but had to discard NextJS for now.

It's been a few months, and looks it will take a few more until it's available.

I would not discard next.js because of that, because there are workarounds.

But you need a custom router for that:

// e.g. if someone accesses /depp/nested/page, we check whether a page at that path exists
// we exclude some static paths like fonts and static
server.get(/^\/(?!(_|fonts|static).*)/, async (req, res, n) => {
    if (req.url) {
      try {
       // hasPageForPath checks whether a page exists (in our case it makes a small graphql request to the cms-api
        const hasPage = await hasPageForPath(req.url);
        if (hasPage) {
          // there exists a page at that path. We render our content.tsx page with ?path=/deep/nested/page
          return app.render(req, res, "/content", { path: req.url });
        } else {
          n();
        }
      } catch (e) {
        // tslint:disable-next-line:no-console
        console.error(e);
        n();
      }
    }
  });
// if the previous route did not catch the route, this one will catch it and pass it to nextjs to perform the default behaviour
  server.all("*", (req, res) => {
    handle(req, res);
  });
Was this page helpful?
0 / 5 - 0 ratings

Related issues

olifante picture olifante  路  3Comments

sospedra picture sospedra  路  3Comments

wagerfield picture wagerfield  路  3Comments

irrigator picture irrigator  路  3Comments

timneutkens picture timneutkens  路  3Comments