solidjs / solid-router

A universal router for Solid inspired by Ember and React Router
MIT License
1.1k stars 137 forks source link

feat: add URL transformation hook #410

Closed jchatard closed 2 months ago

jchatard commented 2 months ago

This PR introduces a new feature allowing URL rewriting through a hook.

New Prop Added: A new property transformUrl?: (url: string) => string has been added to the RouterProps and StaticRouterProps types. This optional function allows developers to transform the URL before it is processed by the router.

Routing Logic: The Router function now checks for the presence of transformUrl. If provided, this function is used to alter the URL, allowing for scenarios such as URL rewriting:

Example: /some-fancy-page.html to /product/123

Usage example with Solid Start, app.tsx:

// @refresh reload
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";

export default function App() {

  const rewriteMap: Record<string, string> = {
    "/some-fancy-keyboard.html": "/product/1",
    "/some-fancy-mouse.html": "/product/2",
    "/some-fancy-headset.html": "/product/3",
    "/u/ryan-carniato": "/user/1",
    "/u/jchatard": "/user/123",
    // ...
  };

  const rewrite = (url: string) => {
    // Of course can be what ever you want...
    const u = new URL(url, "http://localhost/");
    return rewriteMap[u.pathname] ? rewriteMap[u.pathname] + u.search : url;
  };

  return (
    <Router
      transformUrl={rewrite}
      root={(props) => <Suspense>{props.children}</Suspense>}
    >
      <FileRoutes />
    </Router>
  );
}

I couldn't figure out how to implement tests, sorry about that. I would love to have feedbacks on this. And maybe transformUrl is not the greatest name. Happy to chat about that.

Cheers, Jérémy

changeset-bot[bot] commented 2 months ago

🦋 Changeset detected

Latest commit: 6740fe25399efa96ede7a2a5b96e8fff5fa42d80

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package | Name | Type | | --------------- | ----- | | @solidjs/router | Patch |

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

ryansolid commented 2 months ago

I like the direction here for rewrites. Understanding the use case.. this only impacts URLs coming in from navigation/url bar.. I think the biggest risk here is missing a location. I'm gathering the approach here is do all the transforms before matching etc... Have you tested this with your own project?

jchatard commented 2 months ago

Yeah, I tested on my project, it's working great. But there are some caveats in some corner cases.

Let me do a simple pet project tomorrow or tonight, to demonstrate it. It also works for client side navigation as far as my test goes.

jchatard commented 2 months ago

You were right, I forgot to wire client side routing (I messed up with the 5 Solid router forks on my machine 😅).

PR updates

Demo project

https://github.com/Breek/solid-rewrite-demo

It’s a small blog listing 3 posts on the front page, and each link is rewritten, client & server side, with preloading enabled 🎉.

The relevant code consists only of the patched version of the Solid router and app.tsx:

export default function App() {

  const rewriteMap: Record<string, string> = {
    "/blog/sunt-aut-facere.html": "/post/1",
    "/blog/qui-est-esse.html": "/post/2",
    "/blog/ea-molestias.html": "/post/3",
    // ...
  };

  const rewrite = (url: string) => {
    const u = new URL(url, "http://localhost/");
    return rewriteMap[u.pathname] ? rewriteMap[u.pathname] : url;
  };

  return (
    <Router
      transformUrl={rewrite}
      root={props => (
        <MetaProvider>
          <Title>SolidStart - Basic</Title>
          <a href="/">Index</a>
          <a href="/about">About</a>
          <Suspense>{props.children}</Suspense>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  );
}

Preloading

I wired the hook to:

But not to : handleFormSubmit maybe I’m wrong, but I don’t see a use case and I guess it would break actions and stuff.

I’m not sure if I missed a location here.

Thoughts

I’m pretty happy with the PR. It works great in my use case, but without client side routing.

The reason is that the projects I’m working on are heavy on content, and this content is stored in a headless Drupal CMS (more than 100000 addressable content, will reach a million at some point).

So it’s not reasonable to inject the whole url rewriting mapping in app.tsx:

So what I’m doing as of now, is disabling client side routing all together (anyway I need to turn it off for other subjects related to SEO).

The middleware is responsible for catching the incoming request, asking Drupal if the requested URL is known and mapping it to a Solid router’s route.

Everything is cached in Redis afterwards, so it’s quite fast, like really really fast.

I still need to map the rewritten URL to a Solid route on the client for hydration to kick in. It’s quite dirty as of now, I inject it in the global scope, like this:

EntryServer.tsx

  const event = getRequestEvent();
  const pathname = removeSearchParams(event!.nativeEvent!.path);

  const rewriteUrlsMapping: Record<string, string> = {};
  rewriteUrlsMapping[pathname] = event?.locals.startPath; // <-- middleware mapping is done here
  const mapping = JSON.stringify(rewriteUrlsMapping);

  return (
    <StartServer
      document={({ assets, children, scripts }) => (
        <html lang="en">
          <head>
            <meta charset="utf-8" />
            <meta
              name="viewport"
              content="width=device-width, initial-scale=1"
            />
            <link rel="icon" href="/favicon.ico" />
            <script>{`rewriteUrlsMapping = ${mapping};`}</script> // <-- 🥹 don't hit me

app.tsx

  if (isServer) {
    const event = getRequestEvent();
    const pathname = removeSearchParams(event!.nativeEvent!.path);

    if (pathname) {
      rewriteMap[pathname] = event?.locals.startPath;
    }
  } else {
    // On the client, we're stuck with a dirty hack.
    rewriteMap = globalThis.rewriteUrlsMapping;
  }

  const rewrite = (url: string) => {
    const u = new URL(url, "http://localhost/");
    return rewriteMap[u.pathname] ? rewriteMap[u.pathname] + u.search : url;
  };

I asked on Discord how to have something cleaner, with no luck so far. I think I need to create a kind of manifest or get inspiration from solid-meta, but I still need to explore these options.

Once I have a clean strategy for sharing the URL mapping on both server and client, what I imagine is using a user land kind of a <Link> component, which would wrap <A> tags and collect them as they are encountered in the rendering process to update the manifest for the client.

If you're still reading this far, thanks!

Let me know what you think,

Cheers, Jérémy

ryansolid commented 2 months ago

Yeah this looks pretty good. Given that it doesn't impact people not using the feature I'm ok if we are missing a few edge cases and we improve as time goes on. I guess there is no real fix for client-side routing if you need to bring in the whole map. I'm not sure what that would look like really. I don't see an easy solution here that doesn't involve async coordination which is beyond our scope.