Blog post
A deleted page needs a durable goodbye
Removing a file from git does not remove copies already sitting at origin or at the edge. Our deploy treats disappearance as a small transaction: discover the old durable path, record the work before changing public storage, purge its visitor-facing cache keys, prove they return 404, and only then forget the queue.
First, separate two kinds of old
| Old object | What deploy does | Why |
|---|---|---|
A dropped durable URL such as /guide/index.html | Queue, remove from origin, purge, and verify 404. | The URL is reusable. A cached old response would lie about the current site. |
An old fingerprint such as /app-a1b2c3d4-20260830.js | Leave it alone. | The bytes are named by their content and date. Current pages point at a new URL, while older pages or browser caches may still need the old one. |
This distinction is why removal is not the same as the planned storage lifecycle. Deploy cleans up reusable durable names immediately. A future, slower lifecycle process will reclaim aged fingerprinted blobs for storage cost control.
A folder is a list of blobs
The origin uses Azure Blob Storage's flat namespace. A path that looks like docs/archive/ is not a directory object we can remove atomically; it is a shared prefix over individual blobs. Before upload, the workflow lists the existing $web/<PROJECT_ID>/<fqdn>/public/ subtree and compares its unhashed objects with the new staging manifest. Every durable origin blob no longer present in the manifest becomes a dropped path, including every file beneath a folder that disappeared from the project.
Visitor paths add another wrinkle. An origin object named guide/index.html can be cached as both /guide/index.html and /guide/. Both keys enter the purge and verification sets. Plain files keep their exact leading-slash path.
More than 100 objects still finish in one purge
The 100-entry limit belongs to Front Door's contentPaths request, not to origin deletion. The deploy removes every dropped durable origin blob regardless of count. When the union of pending, current, and dropped cache keys exceeds 100 entries, the queue compacts its representation by rolling up the deepest, smallest overlapping directory groups first. Exact children become a covering /dir/*, while a separate /dir/ is retained when an index response needs that cache key. /* is the deterministic final fallback.
The algorithm never takes “the first 100” and postpones the rest. Every durable path is covered by either its exact entry or an ancestor wildcard in the same purge invocation, and the verifier still checks the full exact current and dropped URL sets. There is no second purge wave for object 101 and beyond.
Write the recovery record before mutation
- Coalesce Union any unfinished queue, every current staged durable path, and every dropped durable path. Preserve an unfinished queue's original date.
- Persist Upload one
state/to-purge-YYYYMMDD.txtbesidepublic/, download it again, and compare the bytes. Any failure stops before public storage changes. - Mutate Upload the new staged site, then delete dropped durable origin blobs. Fingerprinted blobs are outside this deploy-time prune.
- Purge Ask the platform identity to invalidate the queued relative paths only after route inventory proves the repository and requested hostname belong to this project.
- Verify Publicly fetch current durable paths until their response bodies match the staged SHA-1; fetch dropped paths until they return 404.
- Commit Download and compare the unchanged queue one last time, delete it, and prove no queue remains.
If the runner stops anywhere before the last step, the queue remains under state/. The next deploy reads it, unions it with new work, and tries again. Uploading the replacement queue before deleting superseded queues means an interruption may leave two recoverable records, but never zero.
State is outside the visitor route
Front Door's fixed origin path ends at /<PROJECT_ID>/<fqdn>/public. The sibling state/ tree—version.txt and unfinished purge queues—is not routed through the tenant hostname. That is route separation, not a claim that the shared static-website origin is private: the origin endpoint remains publicly reachable, and the project id adds obscurity rather than authorization. The tenant uploader can modify blobs only beneath its project prefix and has no Front Door permission. A separate platform workload performs purge after checking platform-owned routes; creating a convincing folder name cannot authorize another hostname.
The queue itself contains FQDN-relative public paths rather than storage prefixes. That keeps the purge request narrowly scoped and avoids treating tenant-writable storage names as evidence of ownership.
Success means the absence is public
Deleting the origin blob is necessary but not sufficient: an edge cache can continue serving the old response. A removal becomes complete only when the public hostname returns 404 for every checkable dropped path. Wildcard purge entries are commands rather than URLs and are not fetched; the verifier retains the original exact dropped-path list for proof.
The live deploy contract includes the durable queue, exact path expansion, dropped-origin deletion, current-path SHA-1 verification, dropped-path 404 verification, and unchanged-queue cleanup as one operation.
Continue with the identity and authorization behind purge, the shared storage layout, or return to all posts.