swup icon indicating copy to clipboard operation
swup copied to clipboard

New global option: `resolvePath`

Open hirasso opened this issue 3 years ago • 9 comments

Replaces #508

Adds a new global option to swup, that allows to run a callback for how swup should resolve a path.

What is the problem?

Right now, to be able to leave and re-enter control over the browser URL/state by swup, three steps are necessary:

  • Use skipPopStateHandling with custom logic
  • Manually ignore links with one of the given options (linkSelector, stopPropagation() during the capture phase,...)
  • Manually adjust the cache URLs so that back- and forward navigation doesn't need additional server requests

While this kind of behavior is already possible to achieve with the currently available swup API, it adds a lot of boilerplate to each new site that wants to have this. Also, the current cache implementation is less than ideal for that kind of scenario right now. It will either fill itself up with unnecessary duplicates of the same page or have cache misses for pages that were previously already loaded from the server.

Proposed solution

All three of the above topics basically boil down to always the same question: Should various paths actually be resolved to just one by swup?

  • on popState: Is the resolved version of the new browser URL the same as the previous one? > ignore
  • on clickLink: Is the resolved href of the link the same as the current browser URL? > ignore
  • Accordingly to swup ignoring the above events if paths resolve to the same one, the cache can also just keep one copy for all URLs that resolve to the same one.

Demo & Test Site

I have set-up a test site here: https://test.rassohilber.com/swup-resolve-path/test/

On that site, the whole filter logic is done by a custom component that doesn't want swup to interfere. It does everything from updating the URL to rendering the state. Navigation away from the list to one of the character detail pages or to the "about"-page and back again is being handled by swup again. To make it more obvious that there is no page load between filters, I added a staggered animation to the items that only runs if the page is being fully rendered (initially and by swup).

Code Example

const swup = new Swup({
  resolvePath: path => {
    // resolve every URL that contains '/?=filter=xyz' to '/'
    return path.replace(/\/\?filter=.+/, '/');
  }
})

Checks

  • [x] The code was linted before pushing
  • [x] All tests are passing
  • [x] New or updated tests are included
  • [ ] The documentation was updated as required
  • [ ] Make sure that restoring scroll positions in ScrollPlugin also uses the resolved path

hirasso avatar Sep 12 '22 14:09 hirasso



Test summary

27 0 0 0


Run details

Project swup
Status Passed
Commit 1e6ad7ca91
Started Nov 7, 2022 1:23 PM
Ended Nov 7, 2022 1:24 PM
Duration 00:31 💡
OS Linux Debian - 10.5
Browser Electron 102

View run in Cypress Dashboard ➡️


This comment has been generated by cypress-bot as a result of this project's GitHub integration settings. You can manage this integration in this project's settings in the Cypress Dashboard

cypress[bot] avatar Sep 12 '22 18:09 cypress[bot]

Nice! Will check it out this week.

daun avatar Sep 12 '22 18:09 daun

One thought beforehand: returning null from the resolver function will result in a warning. I think returning null or undefined could have a meaning here as well, i.e. "leave the url as-is". Probably no need to throw a warning, then?

daun avatar Sep 12 '22 18:09 daun

I would actually prefer to be quite strict here, a warning will hopefully help people with the implementation. The function should always return a non-empty string. I actually also thought about validating if the resolved path is actually a path and not a full URL, as well.

hirasso avatar Sep 12 '22 18:09 hirasso

Could you also have a look at the tests? They are passing locally, I don't understand what's going on in the CI 🙏

hirasso avatar Sep 12 '22 19:09 hirasso

Looks like we reached the monthly limit for the free CircleCI plan, it won't show the test run :(

daun avatar Sep 12 '22 20:09 daun

I think it might be the complex transitions test, the duration was a bit outside the expected range one time I let it run locally.

hirasso avatar Sep 13 '22 06:09 hirasso

You could try increasing the expected range — it's a constant somewhere at the top of main.spec.js, I think 0.1 at the moment. Setting it to 0.15 or so might already help?

daun avatar Sep 13 '22 08:09 daun

Great, that worked! 🎺

hirasso avatar Sep 13 '22 10:09 hirasso

@daun do you have any reservations against merging this?

hirasso avatar Sep 30 '22 13:09 hirasso

@hirasso Only that I've been super busy and haven't had the time to thoroughly test this. I hope I can give it a spin later this week or on the weekend.

daun avatar Oct 03 '22 14:10 daun

Sorry for the delay in checking this out. Looks good! It's something we should definitely have in the codebase.

One question about the scope: does the linked example always return the same HTML, regardless of the filter param? I'm thinking about the implications of resolving the cache urls as well here.

daun avatar Nov 04 '22 13:11 daun

One question about the scope: does the linked example always return the same HTML, regardless of the filter param? I'm thinking about the implications of resolving the cache urls as well here.

Yes, each ?filter=...-page in the example returns the same dataset with all items, regardless of the filter in the URL. The pages only differ in two things that are good SEO practice – the initial document title reflects the selected filter and the items that match the filter are being rendered initially (and then thrown away as soon as my component takes over).

The cache must use resolvePath, as well, if you think about the user journey: They have changed the filter a few times (swup ignores these), then navigated to the "about" page (wich is handled by swup again). If they now would go back in the history, they would expect the previous pages to be in the cache. Without the cache also using resolvePath, there would be no cache records and swup would needlessly have to re-fetch the page from the server.

If you need further clarification, I would be happy to walk you through it in a call – otherwise I would proceed and merge this. Documentation can be a separate PR.

hirasso avatar Nov 04 '22 21:11 hirasso

Actually, thinking about it: The concept is actually quite similar to canonical URLs:

A canonical URL is the URL of the page that Google thinks is most representative from a set of duplicate pages on your site.

So maybe renaming the option to getCanonicalURL would help with understanding it better, what do you think?

hirasso avatar Nov 05 '22 08:11 hirasso

Let's jump on a quick call whenever convenient! I agree with the reasoning but would just like to get your input on how I have been solving those filter ui situations. Ideally this PR would cover both ways of tackling it.

I'd stick with the current naming. Calling it canonical would somewhat imply being tied to a hypothetically present meta tag whereas resolve is more neutral.

daun avatar Nov 05 '22 09:11 daun

I see... Do you also prefer resolvePath over mapPath?

hirasso avatar Nov 05 '22 09:11 hirasso

I think I prefer resolve over map as well. To me, mapping implies a 1:1 relationship whereas resolving implies combining as well as rewriting. Neither sounds false, but resolve is the one that'd make me instantly understand the concept when encountering the option in the docs.

How strong is your aversion to using resolve here?

daun avatar Nov 05 '22 11:11 daun

Not strong at all. Just wanted to make sure we are naming it correctly. Let's stay with resolve, then! 👏

hirasso avatar Nov 05 '22 16:11 hirasso