New global option: `resolvePath`
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
skipPopStateHandlingwith 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
Test summary
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
Nice! Will check it out this week.
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?
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.
Could you also have a look at the tests? They are passing locally, I don't understand what's going on in the CI 🙏
Looks like we reached the monthly limit for the free CircleCI plan, it won't show the test run :(
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.
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?
Great, that worked! 🎺
@daun do you have any reservations against merging this?
@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.
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.
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.
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?
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.
I see... Do you also prefer resolvePath over mapPath?
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?
Not strong at all. Just wanted to make sure we are naming it correctly. Let's stay with resolve, then! 👏