Project the state
Build a deliberately shareable object. Do not serialize the entire application store.
State as navigation
A research-backed guide to query parameters, fragments, the History API, compressed snapshots, local recovery, and server-backed share links. Architecture, evidence, implementation patterns, and interactive examples form one cohesive technical narrative.
Core finding
The familiar Mermaid Live Editor link uses a fragment such as
#pako:<compressed-state>. It is not primarily a query-parameter design.
The URL is a portable checkpoint. Local storage is a recovery mechanism. A server-backed record is the durable or governed source when sharing requirements exceed the URL.
The document moves from observed behavior to state-placement decisions, architecture, browser semantics, implementation, threat controls, and a reusable application standard. Diagrams communicate flow. Tables expose tradeoffs. The playground demonstrates the contract.
Build a deliberately shareable object. Do not serialize the entire application store.
Mermaid uses DEFLATE through Pako, currently at compression level 9.
Use Base64URL or another URL-safe binary-to-text representation.
Debounce continuous changes and write them with history.replaceState().
Reference implementation
The editor converts a JSON snapshot into a compact fragment and independently persists raw state locally.
https://mermaid.live/edit#pako:eNp...
Fragments are processed by the browser and are not included in the HTTP request. That keeps the snapshot away from ordinary server request routing and access logs. It does not make the state secret.
Placement model
URL design works best when identity, navigation intent, portable snapshots, local recovery, and governed persistence are separated.
| Channel | Example | Server-visible | Best use | Primary risk |
|---|---|---|---|---|
| Path | /projects/123/editor |
Yes | Stable resource identity | Breaking links when identity semantics change |
| Query | ?tab=preview&zoom=1.25 |
Yes | Filters, sorting, tabs, small view state | Logging, analytics, and URL-length constraints |
| Fragment | #s=v1.deflate.eNp... |
No | Large client-only snapshots | Opaque links and browser-side exposure |
| History state | Hidden entry metadata | No | Navigation optimization | Not portable or bookmarkable |
| Local storage | Origin-scoped data | No | Recovery, drafts, device preferences | Stale state and shared-device exposure |
| Share ID | ?share=01JZA72... |
Yes | Large, controlled, expiring, collaborative state | Backend dependency and lifecycle management |
Ask whether the server needs the value before rendering. If not, and the value is a large portable snapshot, the fragment is usually the cleaner channel.
Hybrid architecture
The strongest design combines readable navigation state, a portable snapshot, local recovery, and a governed fallback.
Use stable identifiers. Avoid encoding temporary UI state in route segments.
Keep parameters readable, canonical, typed, and limited to application-owned keys.
Version, compress, Base64URL-encode, validate, and enforce decoded-size limits.
Keep local drafts separate from portable state. Add timestamps and expiration rules.
Navigation behavior
Treat the Back button as navigation. Do not accidentally turn it into a character-by-character undo system.
| Interaction | Recommended operation | Reason |
|---|---|---|
| Typing, dragging, zooming | replaceState() |
Updates the shareable checkpoint without generating history noise. |
| Applying a filter set | pushState() or replace |
Push only when Back should restore the prior result set. |
| Changing documents | pushState() |
Represents distinct navigation. |
| Autosave | No URL history entry | Persistence is not navigation. |
| Back or Forward | Handle popstate |
Rehydrate application state from the traversed URL. |
Calling pushState() or replaceState() does not emit
popstate. Updating a fragment through the History API also does not emit
hashchange. Update the live application directly after writing.
Interactive reference
This dependency-free demo writes small state to the query string and a complete JSON snapshot to the fragment using Base64URL. Production systems can replace the Base64 step with DEFLATE plus Base64URL.
Change the controls, inspect the projected state, then write either representation.
Implementation pattern
A small boundary module should own URL serialization. UI components should not independently invent parameter formats.
type UrlState = {
v: 1;
tab: "editor" | "preview" | "settings";
query: string;
filters: string[];
zoom: number;
};
function writeQueryState(state: UrlState): void {
const url = new URL(window.location.href);
// Delete only keys owned by this state module.
["v", "tab", "q", "filter", "zoom"]
.forEach((key) => url.searchParams.delete(key));
url.searchParams.set("v", "1");
if (state.tab !== "editor") {
url.searchParams.set("tab", state.tab);
}
if (state.query) {
url.searchParams.set("q", state.query);
}
[...new Set(state.filters)]
.sort()
.forEach((filter) => url.searchParams.append("filter", filter));
if (state.zoom !== 1) {
url.searchParams.set("zoom", String(state.zoom));
}
history.replaceState(null, "", url);
}
type SnapshotEnvelope = {
v: 1;
codec: "deflate";
state: ShareableState;
};
// Recommended wire format:
const fragment =
`#s=v1.deflate.${base64Url(deflate(JSON.stringify(envelope)))}`;
// Decode in the reverse order, then validate the complete object.
const decoded = schema.parse(
JSON.parse(
inflate(
base64UrlDecode(encodedPayload)
)
)
);
Equivalent state should produce the same URL. Omit defaults, sort unordered filters, deduplicate arrays, normalize numbers, and never serialize transient framework state.
Trust boundary
Compression and encoding are transport transformations. They provide neither confidentiality nor integrity.
Constrain types, enum values, unknown keys, string lengths, arrays, ranges, and nesting.
Cap URL length, encoded length, compressed bytes, decompressed bytes, and object complexity.
Never route URL state directly into HTML, script execution, CSS URLs, imports, or network targets.
Do not put credentials, access tokens, personal data, or confidential source content in a URL.
They avoid normal server request transmission, but remain visible to the browser, client-side scripts, extensions, browser history, screenshots, clipboard operations, and anyone receiving the link.
__proto__, constructor, and prototype.Referrer-Policy.Operational budget
Browsers, proxies, load balancers, API gateways, security products, messaging clients, and QR workflows impose different practical limits.
| Approximate state size | Preferred representation | Rationale |
|---|---|---|
| Under 1 KB | Readable query parameters | Easy to inspect, debug, share, and support. |
| 1–4 KB | Query or fragment | Choose based on server visibility and semantic meaning. |
| 4–8 KB | Compressed fragment | Avoids request-line infrastructure limits and query logging. |
| Over 8 KB | Server-backed share ID | More reliable across enterprise infrastructure and communication channels. |
| Over 32 KB | Do not depend on URL transport | Use durable storage, lifecycle controls, and explicit access policy. |
These are engineering budgets, not normative browser limits. Base64URL adds roughly one third to compressed binary size. Compress first, then encode.
Compatibility
Once users bookmark or send links, the encoding contract must survive application releases.
switch (envelope.v) {
case 1:
return migrateV1ToCurrent(envelope.state);
case 2:
return CurrentSchema.parse(envelope.state);
default:
throw new UnsupportedStateVersionError(envelope.v);
}
Tooling choices
The central design decisions remain the same regardless of framework.
Best for generated single-file SPAs, small applications, and minimal dependency surfaces.
Strong fit for complex routing, structured search parameters, and schema-driven validation.
Useful when component state should feel like useState while remaining URL-synchronized.
Use each framework’s navigation semantics, but preserve explicit URL ownership and validation.
Recommended standard
This convention keeps URLs understandable while preserving full-state portability when it adds value.
/path/to/resource
?tab=<semantic-view-state>
&filter=<small-filter-state>
#s=v1.deflate.<complete-portable-snapshot>
replaceState handles continuous interaction.pushState handles meaningful navigation checkpoints.Use the URL as a contract between application state and navigation. Do not use it as an unbounded serialization dump.
Provenance
Claims were synthesized from primary source code, browser platform documentation, specifications, security guidance, and framework documentation.
Examined Mermaid Live Editor serialization, state hydration, History API writes, and local persistence behavior.
Cross-checked fragment, query, History API, referrer, and navigation semantics against browser documentation.
Evaluated URL state as untrusted input and identified confidentiality, validation, rendering, and resource-exhaustion risks.
Organized the findings as decision tables, architecture flows, implementation patterns, and an interactive state model.
Generated 2026-07-18. The page includes HTML metadata and JSON-LD describing purpose, authorship, subject, date, and upstream sources.