Mycelium

II · Relationship

The URL Is the Honest One

Durable places deserve durable addresses

Every way of moving a person through an interface says something before it's touched. A sidebar link says you'll go somewhere. A tab says you'll stay, and see another side of the same thing. A breadcrumb says up. A back button says the way you came.

Under all of them sits one line the person leans on: navigation moves you; actions do something. Navigation you can walk back. Actions, often, you can't. So the sidebar item that quietly filters the list, the nav-bar button that submits a form, the "New record" link that's secretly a create action: each wears the clothes of going somewhere while doing something else, and takes away the person's sense of what's safe to try.

Where a way of moving sits says something too. What's true everywhere lives in the top bar. What's true inside one section lives in the sidebar, and changes with it. What's true for one record lives on the page itself. Put a section's links in the global shell and you've told the person they're universal; put one record's actions in the sidebar and you've told them it applies to the whole section. A phone makes the test for you: fold the sidebar into the top bar, and whatever stays reachable was global, whatever folds was local. A "global" item that vanishes on a phone was never global, and the people on phones pay for the mislabel.

The screen tells you whether you moved. One thing outside the screen has to agree with it.

Durable places deserve durable addresses. Not every state on a screen is a place: a hover, an open menu, a half-typed draft don't need one. But a place a person might come back to, refresh, or send to someone else does, and for those the address is the one witness that can't be talked into anything.

Everything else on the screen can lie about where you are. The sidebar highlight, the page title, the breadcrumb: all of it is the application describing itself, and an application can describe itself wrong. The URL is harder to fool. An app can still make it lie, but it has to try. It's the one piece of location that's universal, persistent, and independent of whatever the interface happens to be pretending in the moment. It's the coordinate system of the web, and for anything web-based, single-page apps most of all, it's the deepest promise navigation makes.

The promise is plain: a move to a durable place shows up in the address. The address bar sits outside the app, a record of location the app doesn't get to paint, and that's what makes it worth keeping honest. A screen that says you've gone somewhere new while the address says you never moved has split the person's record of where they are in two.

The test I trust most for whether something earns a URL is whether a person could paste it into a browser on another device and land somewhere that makes sense. If they could send this exact view to a colleague, if the back button should return here, if the screen has its own title and purpose, if a refresh should restore it, it needs a URL. Filters, a tab, a step in a long form, even a modal can pass that test, and then they deserve an address too. What usually doesn't: an expanded accordion, a hover, a spinner, a half-typed draft. Put those in the URL and you've stuffed the history stack with states the back button was never meant to walk through.

A URL isn't only a technical address, either. It's readable text a person sees, types, copies, and sends. So its shape is interface. The hierarchy of the path should mirror the hierarchy of the app: /section/subsection/record/view tells the person, and the browser, and the crawler, exactly where they are and how the place is organized.

Which is why, in a place people come back to and send each other, a raw UUID in the URL is rude. /projects/12847362-f8a3-4c1d-b93e-7f2a18dc4501/settings makes a person decode a database key before they can understand where they're standing. The UUID was generated for the system's convenience, not theirs; it can't be read, remembered, guessed, typed, or understood by a colleague who receives it as a link. It is the system talking to itself through a channel that belongs to the user. The fix isn't always a pretty slug (some records have no natural name, and that's fine), but the structure around the identifier should always give it meaning: /orders/7f2a18dc beats /7f2a18dc, and /projects/7f2a18dc/settings beats both. Where a thing does have a name, a project, an organization, an article, use it. /projects/spring-catalog can be read, remembered, and shared. It respects the person's relationship with the address. (A readable name isn't always the right one, either. Some names shouldn't be in an address someone might forward. The point isn't to hide every system identifier; it's to give it enough structure around it that a person can tell what place it names.)

<figure style="margin:1.6rem 0;text-align:center"><svg width="400" height="134" viewBox="0 0 400 134" role="img" aria-label="two addresses: /projects/spring-catalog/settings with each segment labeled, and the same address with a long database key where the name was"><rect x="8" y="8" width="384" height="26" rx="13" fill="#eeeae3"/><text x="22" y="25" font-family="'JetBrains Mono', ui-monospace, monospace" font-size="9" font-weight="400" fill="#3a3631" text-anchor="start">/projects/spring-catalog/settings</text><path d="M22.0 39 v4 H70.6 v-4" stroke="#8f887d" stroke-width="1" fill="none"/><text x="46.3" y="55" font-family="Inter, system-ui, sans-serif" font-size="8" font-weight="400" fill="#8f887d" text-anchor="middle">section</text><path d="M76.0 39 v4 H151.60000000000002 v-4" stroke="#7a1f1f" stroke-width="1" fill="none"/><text x="113.80000000000001" y="55" font-family="Inter, system-ui, sans-serif" font-size="8" font-weight="400" fill="#7a1f1f" text-anchor="middle">by name</text><path d="M157.0 39 v4 H200.20000000000002 v-4" stroke="#8f887d" stroke-width="1" fill="none"/><text x="178.60000000000002" y="55" font-family="Inter, system-ui, sans-serif" font-size="8" font-weight="400" fill="#8f887d" text-anchor="middle">the view</text><rect x="8" y="74" width="384" height="26" rx="13" fill="#eeeae3"/><text x="22" y="91" font-family="'JetBrains Mono', ui-monospace, monospace" font-size="9" font-weight="400" fill="#3a3631" text-anchor="start">/projects/12847362-f8a3-4c1d-b93e-7f2a18dc4501/settings</text><path d="M22.0 105 v4 H70.6 v-4" stroke="#8f887d" stroke-width="1" fill="none"/><text x="46.3" y="121" font-family="Inter, system-ui, sans-serif" font-size="8" font-weight="400" fill="#8f887d" text-anchor="middle">section</text><path d="M76.0 105 v4 H270.4 v-4" stroke="#7a1f1f" stroke-width="1" fill="none"/><text x="173.2" y="121" font-family="Inter, system-ui, sans-serif" font-size="8" font-weight="400" fill="#7a1f1f" text-anchor="middle">a database key</text><path d="M275.8 105 v4 H319.0 v-4" stroke="#8f887d" stroke-width="1" fill="none"/><text x="297.4" y="121" font-family="Inter, system-ui, sans-serif" font-size="8" font-weight="400" fill="#8f887d" text-anchor="middle">the view</text></svg><figcaption style="font-size:.8rem;color:#a8a298;font-style:italic;margin-top:.4rem">Both addresses point to the same place. Only one tells a person where they are.</figcaption></figure>

(The same structure that serves the person serves the search engine: hierarchy in the path, readable slugs, consistent patterns, every segment earning its place. Not always, and not for free, but where an address legibly says where a thing lives and what it is, the person and the crawler are reading the same thing.)

Single-page apps are where this promise quietly dies, because they can swap the entire view without touching the URL, and it's easy, so it happens. The screen says you've gone somewhere new; the URL says you never moved; the back button, reading the URL, says there's nowhere to return to; and a refresh drops you somewhere else entirely. The major frameworks ship routing tools for exactly this reason, and using them isn't optional polish. It's how a client-side app keeps the same promises a plain server-rendered page kept for free: every named route gets a URL, navigation writes to history, view state stays out of the address unless it passes the share test, and a link opened cold restores the right place.

The whole contract collapses to one move: close the tab, reopen the last URL. Land where you were, and the URL told the truth. Land anywhere else, and it was lying the entire time, along with everything on the screen that agreed with it.