blitz
blitz copied to clipboard
Routes manifest: Extract real/dynamic paths from RouteUrlObject
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
getStaticPropsetc. - 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.
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!
I vote for Routes.Post().href. href matches other nextjs/blitz href usage and also new Url() usage.
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 should that be a separate field? I'm not personally sure of which places nextjs automatically handles basePath and which they don't.
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,
};
}
);
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.
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;
}
Hi I'd like to take this up - adding a .href property to RouteUrlObject with the "real" path that is.
Go ahead @a11rew! Let us know if you have any questions