Probably because it sounds like the most poorly named breakfast cereal ever.
Sure, in the prismjs.com case, I have one of those comments in my code too. But I expect it to break one day.
If a site is a content generator and essentially idempotent for a given set of parameters, and you think the developer has a long-term commitment to the URL parameters, then it's a reasonable strategy (and they should probably formalise it).
Perhaps you implement an explicit "save to URL" in that case.
But generally speaking, we eliminated complex variable state from URLs for good reasons to do with state leakage: logged-in or identifying state ending up in search results and forwarded emails, leaking out in referrer logs and all that stuff.
It would be wiser to assume that the complete list of possible ways that user- or session-identifying state in a URL could leak has not yet been written, and to use volatile non-URL-based state until you are sure you're talking about something non-volatile.
Search keywords: obviously. Seach result filters? yeah. Sort direction: probably. Tags? ehh, as soon as you see [] in a URL it's probably bad code: think carefully about how you represent tags. Presentation customisation? No. A backlink? no.
It's also wiser to assume people want to hack on URLs and cut bits out, to reduce them to the bit they actually want to share.
So you should keep truly persistent, identifying aspects in the path, and at least try not to merge trivial/ephemeral state into the path when it can be left in the query string.
That said, I agree with the point and expose as much as possible in the URL, in the same way that I expose as much as possible as command line arguments in command line utilities.
But there are costs and trade offs with that sort of accommodation. I understand that folks can make different design decisions intentionally, rather than from ignorance/inexperience.
I genuinely don't understand why people don't get more upset over hitting refresh on a webpage and ending up in a significantly different place. It's mind-boggling and actually insulting as a user. Or grabbing a URL and sending to another person, only to find out it doesn't make sense.
Developing like this on small teams also tends, in my experience, to lead to better UX, because it makes you much more aware of how much state you're cramming into a view. I'll admit it makes development slower, but I'll take the hit most days.
I've seen some people in this thread comment on how having state in a URL is risky because it then becomes a sort of public API that limits you. While I agree this might be a problem in some scenarios, I think there are many others where that is not the case, as copied URLs tend to be short-lived (bookmarks and "browser history" are an exception), mostly used for refreshing a page (which will later be closed) or for sharing . In the remaining cases, you can always plug in some code to migrate from the old URL to the new URL when loading, which will actually solve the issue if you got there via browser history (won't fix for bookmarks though).
So what is the reality? The linked StackOverflow answer claims that, as of 2023, it is "under 2000 characters". How much state can you fit into under 2000 characters without resorting to tricks for reducing the number of characters for different parameters? And what would a rethought approach look like?
I actually use that for my self-hosted app, because hash routing doesn't require .htaccess or other URL rewriting functionality server-side. So yes, it's not ideal, but you don't fully control the deployment environment, it's better to reduce as much as you can the requirements.
Why?
I get it if we're talking about a size that flirts with browser limitations. But other than that I see absolutely no problem with this. In fact it makes me think the author is actually underrating the use-case of URL's as state containers.
https://rssrdr.com/?rss=raw.githubusercontent.com/Roald87/Ha...
https://github.com/Nanonid/rison
Super old but still a very functional library for saving state as JSON in the URL, but without all the usual JSON clutter. I first saw it used in Elastic's Kibana. I used it on a fancy internal React dashboard project around 2016, and it worked like a charm.
Sample: http://example.com/service?query=q:'*',start:10,count:10
Few years back, I built a proof-of-concept of a PDF data extraction utility, with the following characteristic - the "recipe" for extracting data from forms (think HIPAA etc) can be developed independently of confidential PDFs, signed by the server, and embedded in the URL on the client-side.
The client can work entirely offline (save the HTML to disk, airgap if you want!) off the "recipe" contained in the URL itself, process the data in WASM, all client-side. It can be trivially audited that the server does not receive any confidential information, but the software is still "web-based", "browser-based" and plays nice with the online IDE - on dummy data.
Found a working demo link - nothing gets sent to the server.
https://pdfrobots.com/robot/beta/#qNkfQYfYQOTZXShZ5J0Rw5IBgB...
PS: and i curse the day the social media brainwashed marketing freak coined the term "deep link" to mean just a normal link as its supposed to work.
Maybe a solution is some kind of browser widget that displays query params in a user-friendly way that hides the ugliness, sort of like an object explorer interface.
This is a small hobby project, I am not in IT.
https://scrobburl.com/ https://github.com/Jcparkyn/scrobburl
Youre doing two things:
1) youre moving state into an arbitrary untrusted easy to modify location.
2) youre allowing users to “deep link” into a page that is deep inside some funnel that may or may not be valid, or even exist at some future point in time, forget skipping the messages/whatever further up.
You probably dont want to do either of those two things.
I actually implemented a comment system where users just pick any arbitrary URL on the domain, ie, http://exampledomain.com/, and append /@say/ to the URL along with their comment so the URL is the UI. An example comment would be typed in the URL bar like,
http://exampledomain.com/somefolder/somepage.html/@say/Hey! Cool somepage. - Me
And then my perl script tailing the webserver log file sees the line and and adds the comment "Hey! Cool somepage. - Me" to the .html file on disk for comments.
In a previous experiment, I created a simple webpage which renders media stored in the URL. This way, it's able to store and render images, audio, and even simple webpages and games. URLs can get quite long, so can store quite a bit of data.
From a machine client perspective, it's a different story. JSON-LD is more-or-less HATEOAS, and it works fine for ActivityPub. It's good when you want to talk to an endpoint that you know what data you want to get from it, but don't necessarily need to know the exact shape or URLs.
When you control both the server and client, HATEOAS extra pain for little to no benefit, especially when it's implemented poorly (ie. when the client still needs to know the exact shape of every endpoint anyway, and HATEOAS really just makes URLs opaque), and it interacts very badly when you need to parse the URL anyway, to pull parts from it or add query parameters.
A challenge for this is that the URL is the most visible part of an HTTP request but there are many other submerged parts that are not available as UI yet are significant to the http response composition.
Additionally, aside from very basic protocol, domain, and path, the URL is a very not human friendly UI for composing the state.
The problem here is that they've implemented an application navigation feature with the same name as a browser navigation feature. As a user, you know you need to click "Back" and your brain has that wired to click the broswer back button.
Very annoying.
Having "Refresh" break things is (to me) a little more tolerable. I have the mental association of "refresh" as "start over" and so I'm less annoyed when that takes me back to some kind of front page in the app.
URL is considered a permanent string. You can break it, but that's a bad thing.
So keeping state in the URL will constrain you from evolving your system. That's bad thing.
I think, that it's more appropriate to treat URL like a protocol. You can encode some state parameters to it and you can decode URL into a state on page load. You probably could even version it, if necessary.
For very simple pages, storing entire state in the URL might work.
This is because many sites cram the URL full of tracking IDs, and people like to browse without that.
So if you are embedding state in your URL, you probably want to be sure that your application does something sane if the browser strips all of that out.
I design my SSR apps so that as much state as possible lives in the server. I find the session cookie to be far more critical than the URL. I could build most of my apps to be URL agnostic if I really wanted to. The current state of the client (as the server sees it) can determine its logical location in the space of resources. The URL can be more of an optional thing for when we do need to pin down a specific resource for future reference.
Another advantage of not urlizing everything is that you can implement very complex features without a torturous taxonomy. "/workflow/18" is about as detailed as I'd like to get in the URL scheme of a complex back office banking product.
I think the fundamental issue here is that semantics matter and URLs in isolation don't make strong enough guarantees about them.
I'm all for elegant URL design but they're just one part of the puzzle.
Depending on which mechanism you use to construct your state URLs they will see them as different pages, so you may end up with a lot of extra traffic and/or odd SEO side effects. For SEO at least there are clear directives you can set that help.
Not saying you shouldn't do this - just things to consider.
No database. No cookies. No localStorage
Themes chosen. Languages selected. Plugins enabled.
Which have the pattern of rhetoric but no substance. Clearly the author put significant effort it so why get an LLM to add noise?
If your page is server-rendered, you get saved scroll position on refresh for free. One of many ways using JS for everything can subtly break things.
Why not just use localStorage?
Do you have advice on how to achieve this (for purely client-side stuff)?
- How do you represent the state? (a list of key=value pair after the hash?)
- How do you make sure it stays in sync?
-- do you parse the hash part in JS to restore some stuff on page load and when the URL changes?
- How do you manage previous / next?
- How do you manage server-side stuff that can be updated client side? (a checkbox that's by default checked and you uncheck it, for instance)
But sometimes it’s less obvious how to keep state encoded in a URL or otherwise (i.e for the convenience of your users do you want refreshing a feed to return the user to a marker point in the feed that they were viewing? Or do you want to return to the latest point in the feed since users expect a refresh action to give them a fresh feed?).
But if you really just want your users to be able to hit refresh and not have their state change for non-navigational stuff like field contents or whatever, unless you have a really clear use case where you need to maintain state while switching devices and don’t want to do in server-side, local storage seems like the idiomatic choice.
Th web has evolved a lot, as users we're seeing an incredible amount of UX behaviors which makes any single action take different semantics depending on context.
When on mobile in particular, there's many cases where going back to the page's initial state is just a PITA the regular way, and refreshing the page is the fastest and cleanest action.
Some implementations of infinite scroll won't get you to the content top in any simple way. Some sites are a PITA regarding filtering and ordering, and you're stuck with some of the choices that are inside collapsible blocks you don't even remember where they were. And there's myriads of other situation where you just want the current page in anew and blank state.
The more you keep in the url, the more resetting the UX is a chore. Sometimes just refreshing is enough, sometimes cleaning the URL is necessary, sometimes you need to go back to the top and navigate back to the page you were on. And those are situations where the user is already in frustration over some other UX issue, so needing additional efforts just to reset is a adding insult to injury IMHO.
Basically, your approach is easier to code, and worse to use. Bookmarks, multiple tabs, the back button, sharing URLs with others, it all becomes harder for users to do with your design. I mean feel free, because with many tech stacks it is indeed easier, but don't pretend it's not a tradeoff. It's easier and worse.
If you go there, that's the URL you get. However, if you do anything with the map, your URL changes to something like
https://radar.weather.gov/?settings=v1_eyJhZ2VuZGEiOnsiaWQiO...
Which, if you take the base64 encoded string, strip off the control characters, pad it out to a valid base64 string, you get
"eyJhZ2VuZGEiOnsiaWQiOm51bGwsImNlbnRlciI6Wy0xMTUuOTI1LDM2LjAwNl0sImxvY2F0aW9uIjpudWxsLCJ6b29tIjo2LjM1MzMzMzMzMzMzMzMzMzV9LCJhbmltYXRpbmciOmZhbHNlLCJiYXNlIjoic3RhbmRhcmQiLCJhcnRjYyI6ZmFsc2UsImNvdW50eSI6ZmFsc2UsImN3YSI6ZmFsc2UsInJmYyI6ZmFsc2UsInN0YXRlIjpmYWxzZSwibWVudSI6dHJ1ZSwic2hvcnRGdXNlZE9ubHkiOmZhbHNlLCJvcGFjaXR5Ijp7ImFsZXJ0cyI6MC44LCJsb2NhbCI6MC42LCJsb2NhbFN0YXRpb25zIjowLjgsIm5hdGlvbmFsIjowLjZ9fQ==", which decodes into:
{"agenda":{"id":null,"center":[-115.925,36.006],"location":null,"zoom":6.3533333333333335},"animating":false,"base":"standard","artcc":false,"county":false,"cwa":false,"rfc":false,"state":false,"menu":true,"shortFusedOnly":false,"opacity":{"alerts":0.8,"local":0.6,"localStations":0.8,"national":0.6}}
I only know this because I've spent a ton of time working with the NWS data - I'm founding a company that's working on bringing live local weather news to every community that needs it - https://www.lwnn.news/
So that I can operate two windows/tabs of the same site in parallel without them stealing each other’s scroll position. In addition, the second window/tab may have originated from duplicating the first one.
It’s a losing battle when even the tools (web browsers hiding URLs by default, heck even Firefox on iOS does it now!) and companies (making posters with nothing more than QR codes or search terms) are what they’re up against….
Uppercase letters: A through Z (26 characters)
Lowercase letters: a through z (26 characters)
Digits: 0 through 9 (10 characters)
Special: - . _ ~ (4 characters)
So you'd get a lot of bang for your buck if you really wanted to encode a lot of information.It only strips known tracking parameters b(like those utm_ query params). It does not remove all parameters; if that's the case, YouTube video links will stop working.
Url query params are not popular in the front end developer world for some reason, probably bc the fundamentals of web dev are often skipped in favor of learning leetcode and all the react hooks. Same could be sade for SQL and CSS.
I also don't think its a good look that the author is a CTO and is just discovering how useful url query params are. that being said, its a pretty good and well-written blog post.
First of all thank you for your words about the content.
I get why you might feel that way. English isn’t my first language, so I sometimes use GPT to help me polish phrasing or find a smoother rhythm for certain lines.
But the ideas, structure, and all the writing direction are mine. I don’t ask it to write articles for me. It just help me express things more clearly. I treat it more like an editor than a writer.
Couple of weeks ago when I was publishing The Hidden Cost of URL Design I needed to add SQL syntax highlighting. I headed to PrismJS website trying to remember if it should be added as a plugin or what. I was overwhelmed with the amount of options in the download page so I headed back to my code. I checked the file for PrismJS and at the top of the file, I found a comment containing a URL:
/* https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+css-extras+markdown+scss+sql&plugins=line-highlight+line-numbers+autolinker */
I had completely forgotten about this. I clicked the URL, and it was the PrismJS download page with every checkbox, dropdown, and option pre-selected to match my exact configuration. Themes chosen. Languages selected. Plugins enabled. Everything, perfectly reconstructed from that single URL.
It was one of those moments where something you once knew suddenly clicks again with fresh significance. Here was a URL doing far more than just pointing to a page. It was storing state, encoding intent, and making my entire setup shareable and recoverable. No database. No cookies. No localStorage. Just a URL.
This got me thinking: how often do we, as frontend engineers, overlook the URL as a state management tool? We reach for all sorts of abstractions to manage state such as global stores, contexts, and caches while ignoring one of the web’s most elegant and oldest features: the humble URL.
In my previous article, I wrote about the hidden costs of bad URL design. Today, I want to flip that perspective and talk about the immense value of good URL design. Specifically, how URLs can be treated as first-class state containers in modern web applications.
Scott Hanselman famously said “URLs are UI” and he’s absolutely right. URLs aren’t just technical addresses that browsers use to fetch resources. They’re interfaces. They’re part of the user experience.
But URLs are more than UI. They’re state containers. Every time you craft a URL, you’re making decisions about what information to preserve, what to make shareable, and what to make bookmarkable.
Think about what URLs give us for free:
URLs make web applications resilient and predictable. They’re the web’s original state management solution, and they’ve been working reliably since 1991. The question isn’t whether URLs can store state. It’s whether we’re using them to their full potential.
Before we dive into examples, let’s break down how URLs encode state. Here’s a typical stateful URL:

Anatomy of a URL - Source: What is a URL - MDN Web Docs
For many years, these were considered the only components of a URL. That changed with the introduction of Text Fragments, a feature that allows linking directly to a specific piece of text within a page. You can read more about it in my article Smarter than ‘Ctrl+F’: Linking Directly to Web Page Content.
Different parts of the URL encode different types of state:
/path/to/myfile.html). Best used for hierarchical resource navigation:/users/123/posts - User 123’s posts/docs/api/authentication - Documentation structure/dashboard/analytics - Application sections?key1=value1&key2=value2). Perfect for filters, options, and configuration:?theme=dark&lang=en - UI preferences?page=2&limit=20 - Pagination?status=active&sort=date - Data filtering?from=2025-01-01&to=2025-12-31 - Date ranges#SomewhereInTheDocument). Ideal for client-side navigation and page sections:#L20-L35 - GitHub line highlighting#features - Scroll to section#/dashboard - Single-page app routing (though it’s rarely used these days)Sometimes you’ll see multiple values packed into a single key using delimiters like commas or plus signs. It’s compact and human-readable, though it requires manual parsing on the server side.
?languages=javascript+typescript+python
?tags=frontend,react,hooks
Developers often encode complex filters or configuration objects into a single query string. A simple convention uses key–value pairs separated by commas, while others serialize JSON or even Base64-encode it for safety.
?filters=status:active,owner:me,priority:high
?config=eyJyaWNrIjoicm9sbCJ9== (base64-encoded JSON)
For flags or toggles, it’s common to pass booleans explicitly or to rely on the key’s presence as truthy. This keeps URLs shorter and makes toggling features easy.
?debug=true&analytics=false
?mobile (presence = true)
?tags[]=frontend&tags[]=react&tags[]=hooks
Another old pattern is bracket notation, which represents arrays in query parameters. It originated from early web frameworks like PHP where appending [] to a parameter name signals that multiple values should be grouped together.
?tags[]=frontend&tags[]=react&tags[]=hooks
?ids[0]=42&ids[1]=73
Many modern frameworks and parsers (like Node’s qs library or Express middleware) still recognize this pattern automatically. However, it’s not officially standardized in the URL specification, so behavior can vary depending on the server or client implementation. Notice how it even breaks the syntax highlighting on my website.
The key is consistency. Pick patterns that make sense for your application and stick with them.
Let’s look at real-world examples of URLs as state containers:
PrismJS Configuration
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers
The entire syntax highlighter configuration encoded in the URL. Change anything in the UI, and the URL updates. Share the URL, and someone else gets your exact setup. This one uses anchor and not query parameters, but the concept is the same.
GitHub Line Highlighting
https://github.com/zepouet/Xee-xCode-4.5/blob/master/XeePhotoshopLoader.m#L108-L136
It links to a specific file while highlighting lines 108 through 136. Click this link anywhere, and you’ll land on the exact code section being discussed.
Google Maps
https://www.google.com/maps/@22.443842,-74.220744,19z
Coordinates, zoom level, and map type all in the URL. Share this link, and anyone can see the exact same view of the map.
Figma and Design Tools
https://www.figma.com/file/abc123/MyDesign?node-id=123:456&viewport=100,200,0.5
Before shareable design links, finding an updated screen or component in a large file was a chore. Someone had to literally show you where it lived, scrolling and zooming across layers. Today, a Figma link carries all that context like canvas position, zoom level, selected element. Literally everything needed to drop you right into the workspace.
E-commerce Filters
https://store.com/laptops?brand=dell+hp&price=500-1500&rating=4&sort=price-asc
This is one of the most common real-world patterns you’ll encounter. Every filter, sort option, and price range preserved. Users can bookmark their exact search criteria and return to it anytime. Most importantly, they can come back to it after navigating away or refreshing the page.
Before we discuss implementation details, we need to establish a clear guideline for what should go into the URL. Not all state belongs in URLs. Here’s a simple heuristic:
Good candidates for URL state:
Poor candidates for URL state:
If you are not sure if a piece of state belongs in the URL, ask yourself: If someone else clicking this URL, should they see the same state? If so, it belongs in the URL. If not, use a different state management approach.
The modern URLSearchParams API makes URL state management straightforward:
// Reading URL parameters
const params = new URLSearchParams(window.location.search);
const view = params.get('view') || 'grid';
const page = params.get('page') || 1;
// Updating URL parameters
function updateFilters(filters) {
const params = new URLSearchParams(window.location.search);
// Update individual parameters
params.set('status', filters.status);
params.set('sort', filters.sort);
// Update URL without page reload
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({}, '', newUrl);
// Now update your UI based on the new filters
renderContent(filters);
}
// Handling back/forward buttons
window.addEventListener('popstate', () => {
const params = new URLSearchParams(window.location.search);
const filters = {
status: params.get('status') || 'all',
sort: params.get('sort') || 'date'
};
renderContent(filters);
});
The popstate event fires when the user navigates with the browser’s Back or Forward buttons. It lets you restore the UI to match the URL, which is essential for keeping your app’s state and history in sync. Usually your framework’s router handles this for you, but it’s good to know how it works under the hood.
React Router and Next.js provide hooks that make this even cleaner:
import { useSearchParams } from 'react-router-dom';
// or for Next.js 13+: import { useSearchParams } from 'next/navigation';
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
// Read from URL (with defaults)
const color = searchParams.get('color') || 'all';
const sort = searchParams.get('sort') || 'price';
// Update URL
const handleColorChange = (newColor) => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
params.set('color', newColor);
return params;
});
};
return (
<div>
<select value={color} onChange={e => handleColorChange(e.target.value)}>
<option value="all">All Colors</option>
<option value="silver">Silver</option>
<option value="black">Black</option>
</select>
{/* Your filtered products render here */}
</div>
);
}
Now that we’ve seen how URLs can hold application state, let’s look at a few best practices that keep them clean, predictable, and user-friendly.
Don’t pollute URLs with default values:
// Bad: URL gets cluttered with defaults
?theme=light&lang=en&page=1&sort=date
// Good: Only non-default values in URL
?theme=dark // light is default, so omit it
Use defaults in your code when reading parameters:
function getTheme(params) {
return params.get('theme') || 'light'; // Default handled in code
}
For high-frequency updates (like search-as-you-type), debounce URL changes:
import { debounce } from 'lodash';
const updateSearchParam = debounce((value) => {
const params = new URLSearchParams(window.location.search);
if (value) {
params.set('q', value);
} else {
params.delete('q');
}
window.history.replaceState({}, '', `?${params.toString()}`);
}, 300);
// Use replaceState instead of pushState to avoid flooding history
When deciding between pushState and replaceState, think about how you want the browser history to behave. pushState creates a new history entry, which makes sense for distinct navigation actions like changing filters, pagination, or navigating to a new view — users can then use the Back button to return to the previous state. On the other hand, replaceState updates the current entry without adding a new one, making it ideal for refinements such as search-as-you-type or minor UI adjustments where you don’t want to flood the history with every keystroke.
When designed thoughtfully, URLs become more than just state containers. They become contracts between your application and its consumers. A good URL defines expectations for humans, developers, and machines alike
A well-structured URL draws the line between what’s public and what’s private, client and server, shareable and session-specific. It clarifies where state lives and how it should behave. Developers know what’s safe to persist, users know what they can bookmark, and machines know whats worth indexing.
URLs, in that sense, act as interfaces: visible, predictable, and stable.
Readable URLs explain themselves. Consider the difference between the two URLs below.
https://example.com/p?id=x7f2k&v=3
https://example.com/products/laptop?color=silver&sort=price
The first one hides intent. The second tells a story. A human can read it and understand what they’re looking at. A machine can parse it and extract meaningful structure.
Jim Nielsen calls these “examples of great URLs”. URLs that explain themselves.
URLs are cache keys. Well-designed URLs enable better caching strategies:
You can even visualize a user’s journey without any extra tracking code:
graph LR A["/products"] --> |selects category| B["/products?category=laptops"] B --> |adds price filter| C["/products?category=laptops&price=500-1000"]
style A fill:#e9edf7,stroke:#455d8d,stroke-width:2px; style B fill:#e9edf7,stroke:#455d8d,stroke-width:2px; style C fill:#e9edf7,stroke:#455d8d,stroke-width:2px;
Your analytics tools can track this flow without additional instrumentation. Every URL parameter becomes a dimension you can analyze.
URLs can communicate API versions, feature flags, and experiments:
?v=2 // API version
?beta=true // Beta features
?experiment=new-ui // A/B test variant
This makes gradual rollouts and backwards compatibility much more manageable.
Even with the best intentions, it’s easy to misuse URL state. Here are common pitfalls:
The classic single-page app mistake:
// User hits refresh and loses everything
const [filters, setFilters] = useState({});
If your app forgets its state on refresh, you’re breaking one of the web’s fundamental features. Users expect URLs to preserve context. I remember a viral video from years ago where a Reddit user vented about an e-commerce site: every time she hit “Back,” all her filters disappeared. Her frustration summed it up perfectly. If users lose context, they lose patience.
This one seems obvious, but it’s worth repeating:
// NEVER DO THIS
?password=secret123
URLs are logged everywhere: browser history, server logs, analytics, referrer headers. Treat them as public.
// Unclear and inconsistent
?foo=true&bar=2&x=dark
// Self-documenting and consistent
?mobile=true&page=2&theme=dark
Choose parameter names that make sense. Future you (and your team) will thank you.
?config=eyJtZXNzYWdlIjoiZGlkIHlvdSByZWFsbHkgdHJpZWQgdG8gZGVjb2RlIHRoYXQ_IiwiZmlsdGVycyI6eyJzdGF0dXMiOlsiYWN0aXZlIiwicGVuZGluZyJdLCJwcmlvcml0eSI6WyJoaWdoIiwibWVkaXVtIl0sInRhZ3MiOlsiZnJvbnRlbmQiLCJyZWFjdCIsImhvb2tzIl0sInJhbmdlIjp7ImZyb20iOiIyMDI0LTAxLTAxIiwidG8iOiIyMDI0LTEyLTMxIn19LCJzb3J0Ijp7ImZpZWxkIjoiY3JlYXRlZEF0Iiwib3JkZXIiOiJkZXNjIn0sInBhZ2luYXRpb24iOnsicGFnZSI6MSwibGltaXQiOjIwfX0==
If you need to base64-encode a massive JSON object, the URL probably isn’t the right place for that state.
Browsers and servers impose practical limits on URL length (usually between 2,000 and 8,000 characters) but the reality is more nuanced. As this detailed Stack Overflow answer explains, limits come from a mix of browser behavior, server configurations, CDNs, and even search engine constraints. If you’re bumping against them, it’s a sign you need to rethink your approach.
// Replacing state incorrectly
history.replaceState({}, '', newUrl); // Used when pushState was needed
Respect browser history. If a user action should be “undoable” via the back button, use pushState. If it’s a refinement, use replaceState.
That PrismJS URL reminded me of something important: good URLs don’t just point to content. They describe a conversation between the user and the application. They capture intent, preserve context, and enable sharing in ways that no other state management solution can match.
We’ve built increasingly sophisticated state management libraries like Redux, MobX, Zustand, Recoil and others. They all have their place but sometimes the best solution is the one that’s been there all along.
In my previous article, I wrote about the hidden costs of bad URL design. Today, we’ve explored the flip side: the immense value of good URL design. URLs aren’t just addresses. They’re state containers, user interfaces, and contracts all rolled into one.
If your app forgets its state when you hit refresh, you’re missing one of the web’s oldest and most elegant features.
Browsers running Javascript referenced from HTML is a perfect example of HATEOAS, for example. browsers and web server creators agreed on the semantics of these two data formats, and now any browser in the world can talk to any web server in the world and display what was intended to be displayed to the user.
If the web design hadn't been HATEOAS, you'd need server specific code in your browser, like AOL had a long time ago, where your browser would know how to look up specific parts of the AOL site and display them. This is also how most client apps are developed, since both the client and the server are controlled by the same entity, and there is no problem in hardcoding URLs in the client.
This URI for example:
https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/...
Links to an instance of "The Referer" narrowed down via a start prefix ("downgrade:") and end suffix ("to origins").
These are used across Google I believe so many have probably seen them.
[0] https://developer.mozilla.org/en-US/docs/Web/URI/Reference/F...
We all are trying to understand a problem and trying to figure out the best solution.
How each role approaches this has some low level specializations but high level learnings can be shared.
Our company does phishing tests like most, and their checklist of suspicious behavior is 1 to 1 useless. Every item on the list is either 1: something that our company actually does with its real emails or 2: useless because outlook sucks a huge wang. So I basically never open emails and report almost everything I get. I’m sure the IT department enjoys the 80% false report rate.
I just used Pako.js which accepts a `{ dictionary: string }` option. Concat a bunch of common URL together, done.
The only downside (with both our approaches) is if you add substantially many new fields / common values later on, you need to update the dictionary, and then old URLs don't work, so you'd need some sort of versioning scheme and use the right dictionary for the right version.
But something that can bite you with these solutions if that browsers allow you to duplicate tabs, so you also need some inter-tab mechanisms (like the broadcast API or local storage with polling) to resolve duplicate ids
Actually it would be amazing if desktop applications were like this too, and we had a separate way to go back to the initial screen
https://stackoverflow.com/questions/11896160/any-way-to-iden...
Same with search ahead.
I've almost entirely moved to Rust/WASM for browser logic, and I just use serde crate to produce compact representation of the record, but I've seen protobufs used as well.
Otherwise you end up with parsing monsters like ?actions[3].replay__timestamp[0]=0.444 vs {"actions": [,,,{"replay":{"timestamp":[0.444, 0.888]}]}
Both approaches (appending/rewriting) have their uses, the tricky part is using the right thing for the right action, fuck up either and the experience is abysmal.
If you want to argue against the use of URLs to represent state, I would concentrate on the “R” (resource) aspect.
Navigational state need not be confused with app state. Also talking about "state" as in "state machine" etc used to sound pretty academic with obscure meaning of the word "state". When someone says "state machine" they are basically saying "I'm a PhD and you are not". There are simpler and more crisp ways to convey things rather than via obscurity.
I was referring to mostly everything else
Interacting with the URL from JS within the page load cycle is inherently complex.
For what it's worth, I'd also argue that the right behavior here is to replace.
But that of course also means that now the URL on the history stack for this particular view will always have the filter in it (as opposed to an initial visit without having touched anything).
Of course the author's case is the good/special one where they already visited the site with a filter in the URL.
But when you might be interested in using the view/page with multiple queries/filters/paramerers, it might also be unexpected: for example, developers not having a dedicated search results page and instead updating the query parameters of the current URL.
Also, from the history APIs perspective, path and query parameters are interchangeable as long as the origin matches, but user expectations (and server behavior) might assign them different roles.
Still, we're commenting on a site where the main view parameter (item ID, including submission pages) is a query parameter. So this distinction is pretty arbitrary.
And the most extreme case of misusing pushState (instead if replace) are sites where each keystroke in some typeahead filter creates a new history entry.
All of this doesn't even touch the basic requirement that is most important and addressed in the article: being able to refresh the page without losing state and being able to bookmark things.
Manually implementing stuff like this on top of a basic routing functionality (which should use pushState) in an SPA is complex very quickly.
Then a developer gets the task to create this, and they too don't push back on what exact URIs are being used, nor how the history is being treated. Either they don't have time, don't have the power to send back tasks to product, simply don't care or just don't think of it. They happily carry along creating whatever URIs make sense to them.
No one is responsible for URLs, no one considers that part of UX and design, so no one ends up thinking about it, people implement things as they feel is right, without having a full overview over how things are supposed to fit together.
Anyways, that's just based on my experience, I'm sure there are other holes in the process that also exacerbates the issue.
That said, I've also worked with some developers that didn't like intruding on their turf, so to speak. Though I've also worked with others that were more than happy to collaborate and very proactive about these sorts of things.
Furthermore, as a UX designer this is the sort of topic that we're unlikely to be able to meaningfully discuss with PMs and other stakeholders as it's completely non-visual and often trying to bring this up with them and discuss it ends up feeling like pulling teeth and them wondering why we're even spending time on it. So usually it just ended up being a discussion between me and the developers with no PM oversight.