Part of the Dashboard State & URL Sharing guide.
Operative rule: name a saved view by the hash of its own content — it is then immutable, infinitely cacheable, and identical for two readers who saved the same thing.
When a URL Stops Being Enough
Self-contained links are better than saved views in every way except one: length. They need no fetch, they can be read by a human, they cannot 404, and they work offline. A saved view gives up all of that, so it should only be reached for when the alternative is a link that breaks.
The threshold in practice is somewhere around three hundred characters. Below it, links survive being pasted into a chat message, an email body or a ticket description. Above it, clients begin wrapping and truncating in ways that produce a link which looks fine to the sender and fails for the recipient — the worst failure mode available, because nobody can see it.
Production-Ready Implementation
A saved view is a JSON object whose filename is the hash of its own bytes. Writing it needs nothing more than the credentials the build already has for publishing tiles.
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any
SAVED_VIEW_VERSION = 1
def canonical_bytes(state: dict[str, Any]) -> bytes:
"""Stable serialisation: sorted keys, no incidental whitespace.
Two readers who saved the same view must produce identical bytes, or the
content hash stops deduplicating and the storage fills with near-copies.
"""
payload = {"v": SAVED_VIEW_VERSION, "state": state}
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
def saved_view_key(state: dict[str, Any]) -> str:
"""Short, collision-resistant, URL-safe key."""
return hashlib.sha256(canonical_bytes(state)).hexdigest()[:12]
def write_saved_view(root: Path, state: dict[str, Any]) -> str:
key = saved_view_key(state)
target = root / "views" / f"{key}.json"
if target.exists():
return key # identical view already stored
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(canonical_bytes(state))
return key
def read_saved_view(root: Path, key: str) -> dict[str, Any] | None:
if not key.isalnum() or len(key) != 12:
return None # reject anything that is not a key
path = root / "views" / f"{key}.json"
if not path.exists():
return None
payload = json.loads(path.read_text(encoding="utf-8"))
if payload.get("v") != SAVED_VIEW_VERSION:
return None # migrate explicitly, never guess
return payload.get("state")
The key-format check in read_saved_view matters more than it looks. The key arrives from a URL, and it is used to build a path — validating its shape before touching the filesystem is what stops a crafted key from reaching anywhere it should not.
The Read Path in the Browser
Retention, Deduplication and Cost
Because the key is a content hash, two readers saving identical views write the same object once. That property does most of the housekeeping for free: a busy dashboard accumulates far fewer objects than it has share events, and re-sharing an existing view costs nothing at all.
What remains is retention. Views created ad hoc during a working week rarely need to outlive the quarter; views referenced from documentation need to last as long as the documentation does. The simplest policy that works is two prefixes — views/tmp/ with a lifecycle rule, and views/keep/ without one — and a share control that writes to the second only when the reader explicitly asks for a permanent link.
Verification Steps
- Save the same view twice and confirm one object exists and both keys match.
- Request a key that does not exist and confirm the map opens at the fragment’s camera with a quiet notice.
- Confirm saved-view objects are served with a long cache lifetime — they are immutable by construction.
- Confirm the share control writes to the temporary prefix unless the reader asked for a permanent link.
- Confirm a key with unexpected characters is rejected before any storage lookup.
Common Errors & Fixes
Two identical views produce different keys
Serialisation is not canonical — key order or whitespace is varying. Sort keys and use compact separators, as canonical_bytes does.
Saved views work in development and 404 in production
The build writes them under a path the deployment does not publish. Write them into the same output directory the tiles go to, so one publish step covers both.
Old documentation links stopped working
A lifecycle rule expired objects under the permanent prefix, or a permanent link was written to the temporary one. Audit which prefix the share control writes to, and exclude the permanent prefix from every lifecycle rule explicitly.
The saved view opens the right layers but the wrong place
The camera was moved into the saved object and taken out of the fragment. Keep it in both: the fragment is what makes a failed fetch survivable.
Writing a Saved View Without a Backend
The obvious objection to storing saved views in object storage is that a static page cannot write to it. That is true of the storage’s normal credentials and not true of the mechanisms designed for exactly this case.
The most common arrangement is a presigned upload: a small endpoint — an edge function, a serverless handler, a single route in whatever already authenticates the dashboard — issues a short-lived signed URL that permits a single PUT to one key under the views prefix. The page computes the content hash, requests a signature for that key, uploads and is done. The endpoint stays tiny because it makes no decisions about content; it only decides whether this reader may save a view at all.
Two constraints keep it safe. Restrict the signature to the exact key the hash produces, so a client cannot write anywhere else under the prefix. And cap the object size, because a saved view is a few kilobytes and anything larger is either a bug or an attempt to use the bucket as free storage.
If even that endpoint is unwelcome, there is a lower-tech option: the dashboard can offer a copyable blob of state that a reader pastes into a ticket, and a small internal tool writes into the views prefix when a permanent link is genuinely needed. It is clumsier, but it keeps the deployment entirely static and covers the case where saved views are an occasional need rather than a daily one.
What is not viable is writing from the client with long-lived credentials embedded in the page. Anything in the bundle is public, and a bucket writable by everyone who has loaded the dashboard will be found and used for something else.
Naming, Listing and Cleaning Up
Because keys are content hashes, they carry no meaning to a reader. That is fine for links pasted into a conversation, where the surrounding text supplies the context, and less fine for a set of standard views a team returns to weekly. For those, a small hand-maintained index — a JSON file mapping human names to keys, published with the site — turns opaque keys into a menu without introducing a database.
The index also solves retention. A view referenced from the index is one the cleanup process must never expire, which makes the rule mechanical rather than a matter of judgement: everything in the index is permanent, everything else follows the lifecycle policy on the temporary prefix.
Related
- Dashboard State & URL Sharing — the parent guide covering when state belongs in a link at all
- Encoding Map Camera State in the URL Fragment — the part that must stay inline
- Versioning Tile URLs with Content Hashes for Cache-Busting — the same content-addressing idea applied to tiles
- Static vs Dynamic Map Export Methods — why a static dashboard needs this pattern at all