Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

decouple route path from filename #8241

Closed
macrozone opened this issue Aug 5, 2019 · 13 comments
Closed

decouple route path from filename #8241

macrozone opened this issue Aug 5, 2019 · 13 comments

Comments

@macrozone
Copy link

Feature request

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

url structures are still visible on devices, so its a UI-element and often is bound to editorial decisions, yet in nextjs, url routes always reflect the filesystem of the project

Having the filename of a page be the route path segment might be ok, if you develop for an english target group, but leads to problems in other markets. E.g. in switzerland we often need to have an app in multiple languages and customers often wish that the url paths are also translated.

Also, there is a common practice, that source code is always in english. So this leads to the problem, that when you decide to have e.g. german url paths, you need to have german-named files in your app, which just feels wrong.

Nextjs should therefore provide a firstclass support to translate url path segments.

Describe the solution you'd like

There should be a way to provide aliases to pages in code. So a page would be delivered on all available aliases. It should be possible to use translation services to provide these aliases. They should work both on server and on client.

If you use a <Link />� to a page, you can provide some context, so that nextjs would use the right alias to make the route. E.g. you could provide the locale.

I don't know nextjs good enough to suggest an api for it yet, but suggestions are very welcome!

Describe alternatives you've considered

Doing it with dynamic routes (next 9 feature), is nearly impossible, because every page and folder would basically look like [slug].js, so you would need to deliver the right page yourself, defeating the purpose of the whole file-system-routing.

It's only possible with a custom server and some boilerplate, so that your <Link /> behave correctly. But this is needs careful setup, so that you don't have duplicated route declarations and it is therefore error-prone.

@martpie
Copy link
Contributor

martpie commented Aug 8, 2019

(as a developer in Switzerland) we also had to solve this multiple times. You have multiple ways:

  • use next-routes
  • use some helpers to get the correct as for a given view, something like:
// getPathname.ts
// implementation could be simplified, but it gives you the idea
function getPathname (page: string, lang: string) {
  if (page === '/about' && lang === 'de') return '/about'
  if (page === '/about' && lang === 'fr') return '/a-propos-de';
  if (page === '/about' && lang === 'de') return '/uber-uns'

  throw new RangeError(`Unknown page ${page} for lang ${lang}`);
}
// PageLink.tsx
import Link from 'next/link';
// ...

const PageLink: React.FC<LinkProps> = (props) => {
  const { i18n: { language } } = useTranslation(); // if you use react-i18next / next-i18next;
  const pathname = getPathname(props.href, language);

  const { children, ...rest } = props;

  return <Link {...rest} as={pathname}>{children}</Link>;
}

then you'd just have to

<PageLink href="/about">Go to the about page with the correct pathname</PageLink>

Hope it helps.

More generally, I second decoupling pages and "views", but I don't feel it's going to change soon.
Regardless of the solution, we often need to send a definition of all the routes to the client, which is an anti-pattern in Next.js, so you know... we just have to hack around 😄

@adonig
Copy link

adonig commented Aug 9, 2019

What I came up was something like this.

pages/[lang]/[about].js:

import { useRouter } from "next/router";

export default function About() {
  const { query } = useRouter();
  const { lang } = query;
  return (
    <div>
      <p>This is the {lang} about page</p>
    </div>
  );
}

pages/index.js:

import Link from "next/link";

export default function Home() {
  return (
    <ul>
      <li>
        <Link href="/[lang]/[about]" as="/en/about">
          <a>About Page en</a>
        </Link>
      </li>
      <li>
        <Link href="/[lang]/[about]" as="/de/ueber-uns">
          <a>About Page de</a>
        </Link>
      </li>
    </ul>
  );
}

Now it should be possible to detect the locale in the index page and set lang accordingly when rendering links. You can probably use the custom link components @martpie mentioned above.

@borekb
Copy link
Contributor

borekb commented Aug 20, 2019

For reference, I think the most lively discussion around this recently has been in the dynamic routing RFC: #7607. Specifically, these comments caught my attention:

The RFC description specifically mentions this:

Alternatives: Leverage path-to-regexp for comprehensive support

[...] The filesystem cannot express order nor contain regex symbols. [...] In the future, we may allow path-to-regexp routes defined in next.config.js or similar.

I think that would resolve the filesystem-based routing limitations perfectly.

@isaachinman
Copy link
Contributor

Want to chime in by saying that downstream dependencies like next-i18next need to be able to insert subpaths without making modifications to the filesystem.

Functionality which previously worked in NextJs v8 is now broken in v9.

See i18next/next-i18next#413.

If the core team is happy with merging a fix for this, and can point me in the right direction, I can get a PR through. Thanks!

@khatkarr
Copy link

khatkarr commented Aug 23, 2019

In regard to #8444, I believe the solution is already there just needs to be repurposed/modified existing functionality.

const nextConfig = {
  exportPathMap: async function () {
    //provide default path here.
    const paths = {
    };
    const res = await fetch(fetchRoutes.getUrl());
    const data = await res.json();
    const mappedUrls = fetchRoutes.processData(data);
    mappedUrls.forEach(url => {
      paths[url] = { page: '/somePage', query: { category: url } };
    });
    return paths;
  }
}

This way I can store a mapping of URL to page in an external backend system and insert new URL as new items gets added.

It has following issues:

  • At present it works only for Static site export though it work in Next.js dev mode in standard way next dev but not in production build next build.
  • It does not get updated at regular intervals because if a new path mapping is added in backend than it will not be available till next server restart.
  • This can slow down url mapping on each request if URL mapping list is very long. But URL does not follow any pattern, so it not possible to follow any reg expression. Good thing is URL are limited to less than 1000 in my case. If URL match anything else before Custom URL mappings than it would end up in good case.

I will also face same issue with locales code in near future. Maybe it's possible to take out locale info from url and put it as query-param like following.

http://www.example.com/en/us/somePage
http://www.example.com/fr/ca/somePage

If something can be configured in next.config.js and dynamically loaded by built in server, than above two request should map to somePage and /en/us or /fr/ca map as query param. The page can decide the locale easily than from query params.

          //in server .js
          app.render(req, res, '/samplePage', { ...query, locale: "/en/us" });

Possible Solution:
I have almost zero experience in JS or TS. From Java perspective, usually there can be an interface, lets says PathResolver.java, implement that and add the new PathResolver to framework provided URL Resolver list.

If Next.js provide some kind of hook or inheritance to extend or Can I provide my own implementation of Route (as shown in screenshot, route matchers) array objects that can be added to routes array provided by framework and than consumed by framework.

So this way framework will be free to provide some default way to map urls and users can implement their own in any way instead of waiting for new solution to implement at framework level.

I have implemented already something like that but in server.js. Would like to do it in Next config file for better integration and extending framework.

Screen Shot 2019-08-20 at 10 44 03 AM

Thanks

@isaachinman
Copy link
Contributor

@borekb #7607 is now closed.

Wondering if we can get some eyes on this issue? This is a genuine (and common) use case for sites which require internationalisation.

@timneutkens
Copy link
Member

A different RFC will be posted soon.

@khatkarr
Copy link

khatkarr commented Sep 14, 2019

I copied this code to next-server.js and I was able to add custom path in config file using module.exports just as Next.js does for static export. But following code is available only in dev env and have to manually copy it to next-server.js. Now I am able to add custom routes just like static export.

  // Backwards compatibility
  async prepare() {
    await this.addExportPathMapRoutes()
  }

async addExportPathMapRoutes() {
    // Makes `next export` exportPathMap work in development mode.
    // So that the user doesn't have to define a custom server reading the exportPathMap
    if (this.nextConfig.exportPathMap) {
      console.log('Defining routes from exportPathMap')
      const exportPathMap = await this.nextConfig.exportPathMap() // In development we can't give a default path mapping
      for (const path in exportPathMap) {
        const { page, query = {} } = exportPathMap[path] // We use unshift so that we're sure the routes is defined before Next's default routes
        this.router.add({
          match: (0, router_1.route)(path),
          fn: async (req, res, params, parsedUrl) => {
            const { query: urlQuery } = parsedUrl
            Object.keys(urlQuery)
              .filter(key => query[key] === undefined)
              .forEach(key =>
                console.warn(
                  `Url '${path}' defines a query parameter '${key}' that is missing in exportPathMap`
                )
              )
            const mergedQuery = { ...urlQuery, ...query }
            await this.render(req, res, page, mergedQuery, parsedUrl)
          },
        })
      }
    }
  }

The drawback is that I did not find any way to update these routes dynamically after regular interval to fetch any updated path mapping from backend. I am looking into if JS generators based method can help to achieve that.

@oswaldoacauan
Copy link

Any light on this matter? Anyone found a workaround that does not involve changing the route system :)

@BoChao-Hsu
Copy link

My Folder structure:
image
This is my solution, although it is not very good... but I can't find it better. It took me all day and I have to create a [lang] folder (pictured above) to do the middle, put it inside. All the corresponding programs, but only do the transfer action, for example:
image

@Timer
Copy link
Member

Timer commented Oct 16, 2019

Handled by #9081

@harryadel
Copy link

@martpie Thanks for the solution!

@balazsorban44
Copy link
Member

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.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests