State as navigation

Make application state portable without turning the URL into a database.

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.

Mermaid
Uses a compressed state snapshot in the URL fragment.
Query
Best for concise, semantic, server-visible view state.
Fragment
Best for large, client-only, shareable snapshots.
Fallback
Use a governed share ID when state becomes large or sensitive.

Core finding

Mermaid stores its portable editor snapshot in the fragment.

The familiar Mermaid Live Editor link uses a fragment such as #pako:<compressed-state>. It is not primarily a query-parameter design.

The architectural lesson

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.

Research progression

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.

1 · Serialize

Project the state

Build a deliberately shareable object. Do not serialize the entire application store.

2 · Compress

Reduce the payload

Mermaid uses DEFLATE through Pako, currently at compression level 9.

3 · Encode

Make bytes URL-safe

Use Base64URL or another URL-safe binary-to-text representation.

4 · Replace

Update without history noise

Debounce continuous changes and write them with history.replaceState().

Reference implementation

The Mermaid Live Editor pipeline

The editor converts a JSON snapshot into a compact fragment and independently persists raw state locally.

Mermaid editor state serialization flow Editor state code + config JSON UTF-8 bytes DEFLATE Pako level 9 Base64URL URL-safe text #pako:<payload> replaceState after debounce localStorage local recovery
Conceptual Mermaid-style URL
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

Put each class of state in the channel that matches its semantics.

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
Useful test

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

Do not force every state concern into one persistence mechanism.

The strongest design combines readable navigation state, a portable snapshot, local recovery, and a governed fallback.

Hybrid URL state architecture decision flow Application state classify before persistence State category identity · view · snapshot · recovery Path resource identity /projects/123 Query small semantic state ?tab=preview Fragment portable snapshot #s=v1.deflate... Local / server recovery or governance draft or share ID Bookmarkable experience
Path

Identity

Use stable identifiers. Avoid encoding temporary UI state in route segments.

Query

Intent

Keep parameters readable, canonical, typed, and limited to application-owned keys.

Fragment

Snapshot

Version, compress, Base64URL-encode, validate, and enforce decoded-size limits.

Storage

Recovery

Keep local drafts separate from portable state. Add timestamps and expiration rules.

Navigation behavior

Browser history should represent user intent, not every state mutation.

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.
Subtle browser behavior

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

Compare readable query state with a portable snapshot.

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.

URL-state playground

Change the controls, inspect the projected state, then write either representation.

0 URL characters
Filters
1.00×
Readable query projection
Portable fragment snapshot
Projected state

              

Implementation pattern

Project, normalize, validate, encode, and hydrate.

A small boundary module should own URL serialization. UI components should not independently invent parameter formats.

TypeScript · query state
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);
}
TypeScript · snapshot envelope
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)
    )
  )
);
Canonicalization rule

Equivalent state should produce the same URL. Omit defaults, sort unordered filters, deduplicate arrays, normalize numbers, and never serialize transient framework state.

Trust boundary

A shared URL is untrusted input.

Compression and encoding are transport transformations. They provide neither confidentiality nor integrity.

Validate

Use a strict schema

Constrain types, enum values, unknown keys, string lengths, arrays, ranges, and nesting.

Bound

Limit every stage

Cap URL length, encoded length, compressed bytes, decompressed bytes, and object complexity.

Sanitize

Render safely

Never route URL state directly into HTML, script execution, CSS URLs, imports, or network targets.

Separate

Keep secrets out

Do not put credentials, access tokens, personal data, or confidential source content in a URL.

Fragments are not secret

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.

  • Reject unsupported codec and schema versions.
  • Use bounded decompression to mitigate compression bombs.
  • Disallow prototype keys such as __proto__, constructor, and prototype.
  • Apply a deliberate Referrer-Policy.
  • Strip or redact URL state before analytics capture.
  • Offer a controlled recovery path when a link is malformed.

Operational budget

Treat URL length as a product constraint, not a browser constant.

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

A shared URL becomes a public persistence format.

Once users bookmark or send links, the encoding contract must survive application releases.

Versioned decoder
switch (envelope.v) {
  case 1:
    return migrateV1ToCurrent(envelope.state);

  case 2:
    return CurrentSchema.parse(envelope.state);

  default:
    throw new UnsupportedStateVersionError(envelope.v);
}
  • Never reinterpret an existing version.
  • Add a new version when state semantics change.
  • Keep migrations deterministic and test them against captured links.
  • Preserve old decoders for a documented compatibility window.
  • Show a controlled error and an “open with defaults” recovery option.

Tooling choices

Start with native browser primitives. Add abstraction when routing complexity earns it.

The central design decisions remain the same regardless of framework.

Native

URL + History API

Best for generated single-file SPAs, small applications, and minimal dependency surfaces.

TanStack Router

Typed search state

Strong fit for complex routing, structured search parameters, and schema-driven validation.

nuqs

React query hooks

Useful when component state should feel like useState while remaining URL-synchronized.

React Router / Next.js

Router integration

Use each framework’s navigation semantics, but preserve explicit URL ownership and validation.

Provenance

Evidence, methodology, and document lineage

Claims were synthesized from primary source code, browser platform documentation, specifications, security guidance, and framework documentation.

Method

Source-code inspection

Examined Mermaid Live Editor serialization, state hydration, History API writes, and local persistence behavior.

Method

Platform verification

Cross-checked fragment, query, History API, referrer, and navigation semantics against browser documentation.

Method

Threat modeling

Evaluated URL state as untrusted input and identified confidentiality, validation, rendering, and resource-exhaustion risks.

Method

Research presentation

Organized the findings as decision tables, architecture flows, implementation patterns, and an interactive state model.

Document metadata

Generated 2026-07-18. The page includes HTML metadata and JSON-LD describing purpose, authorship, subject, date, and upstream sources.