blitz icon indicating copy to clipboard operation
blitz copied to clipboard

Routes manifest: Extract real/dynamic paths from RouteUrlObject

Open nickluger opened this issue 4 years ago • 7 comments

What do you want and why?

Routes manifest is a great Single Source of Truth for pages and routes. There are certain places in the app, though, where we would need the filled out URL-path instead of the Next.js dynamic-placeholder path, i.e. we would like to extract

/posts/123

instead of

/posts/[postId]

Currently, the RouteUrlObject generated by a Routes.Post() call only offers us the properties pathname and query. Neither can be directly used, for example in:

  • Redirects in getStaticProps etc.
  • Canonical URL link tags like <link rel="canonical" href={canonicalUrl} />

Possible implementation(s)

Add a field to the RouteUrlObject like href or as (following Next.js Link-component naming) which returns /posts/123, instead of /posts/[postId].

Of course, using pathname and queryone can easily construct the filled out path oneself, but I think it would be more natural to embed it into this than rolling your own helper.

nickluger avatar Oct 09 '21 08:10 nickluger

Thanks for reporting. I think this is a nice idea! In other issues (#2816, blitz-js/legacy-framework#79), a suggested solution was to add toString method to RouteUrlObject, which handles filled-out path scenario. Open for discussion about what would work better!

beerose avatar Oct 11 '21 13:10 beerose

I vote for Routes.Post().href. href matches other nextjs/blitz href usage and also new Url() usage.

flybayer avatar Oct 14 '21 16:10 flybayer

Would love if this also exposed a method that prepended the baseUrl. My specific use case is copying a link to the clipboard for sharing.

margalit avatar Oct 17 '21 01:10 margalit

@margalit should that be a separate field? I'm not personally sure of which places nextjs automatically handles basePath and which they don't.

flybayer avatar Oct 21 '21 20:10 flybayer

As things are going to pivot soon, i did not contribute a PR, but here's a workaround. This is type safe on the consumer side.

import { Routes } from "blitz";
// ** HINT ** You can use any other mapper function ** HINT **
import { mapValues } from "lodash";

type RoutesType = typeof Routes;
type RoutesWithHrefType = {
  [K in keyof RoutesType]: (
    ...params: Parameters<RoutesType[K]>
  ) => ReturnType<RoutesType[K]> & { href: string };
};

export const routes: RoutesWithHrefType = mapValues(
  Routes,
  (Route) =>
    function RouteWithHref(...args: Parameters<typeof Route>) {
      // eslint-disable-next-line prefer-spread
      const res: ReturnType<typeof Route> = Route.apply(null, args);
      return {
        ...res,
        // converts a pathname like "/item-types/[itemTypeId]/items/[itemId]"
        // into an href like "/item-types/123/items/456", using the query parameters
        // and their values, already passed to this Routes function
        // ** HINT ** This helper function replaces multiple strings at once ** HINT **
        href: args[0] ? replaceMultiple(
          res.pathname,
          Object.keys(args[0] as any).map((parameterName) => `[${parameterName}]`),
          Object.values(args[0] as any)
        ): res.pathname,
      };
    }
);

nickluger avatar Dec 29 '21 09:12 nickluger

As things are going to pivot soon, i did not contribute a PR, but here's a workaround. This is type safe on the consumer side.

import { Routes } from "blitz";
// ** HINT ** You can use any other mapper function ** HINT **
import { mapValues } from "lodash";

type RoutesType = typeof Routes;
type RoutesWithHrefType = {
  [K in keyof RoutesType]: (
    ...params: Parameters<RoutesType[K]>
  ) => ReturnType<RoutesType[K]> & { href: string };
};

export const routes: RoutesWithHrefType = mapValues(
  Routes,
  (Route) =>
    function RouteWithHref(...args: Parameters<typeof Route>) {
      // eslint-disable-next-line prefer-spread
      const res: ReturnType<typeof Route> = Route.apply(null, args);
      return {
        ...res,
        // converts a pathname like "/item-types/[itemTypeId]/items/[itemId]"
        // into an href like "/item-types/123/items/456", using the query parameters
        // and their values, already passed to this Routes function
        // ** HINT ** This helper function replaces multiple strings at once ** HINT **
        href: args[0] ? replaceMultiple(
          res.pathname,
          Object.keys(args[0] as any).map((parameterName) => `[${parameterName}]`),
          Object.values(args[0] as any)
        ): res.pathname,
      };
    }
);

Could you please provide an example of using this ? With slugs like /books/ID/ or organisations/ID/books/ID type of slugs in urls.

Jarrodsz avatar Feb 07 '22 12:02 Jarrodsz

for those who want to use @nickluger snippet but are missing replaceMultiple, here it is:

function replaceMultiple(str: string, targets: string[], values: string[]): string {
  let result = str;
  // eslint-disable-next-line
  for (const val in values) {
    result = result.replace(targets[val], values[val]);
  }
  return result;
}

zernie avatar Jun 20 '22 15:06 zernie

Hi I'd like to take this up - adding a .href property to RouteUrlObject with the "real" path that is.

a11rew avatar Oct 02 '22 13:10 a11rew

Go ahead @a11rew! Let us know if you have any questions

beerose avatar Oct 03 '22 03:10 beerose