Your blocks, your scripts, your DOM — unchanged.
ledge serves an Edge Delivery Services project straight from disk and renders the pre-decoration DOM your blocks already expect. This site’s own scripts/ is a byte-for-byte copy of the sample project’s, and a script in the repository fails if any file drifts — so the central claim of this page is checked on every commit rather than asserted here.
The post-pass is a pass over the finished string rather than a change inside the image helper, because that helper is not the only thing on the page emitting an image URL: block templates, plugin renderers, raw rich text, head.html and the og:image metadata all emit their own. One scan over the output catches every producer at once. It is also the reason a media URL that no longer matches the file it names is served with a short lifetime instead of the year-long immutable one — the URL still resolves to the current bytes, but a stale digest must never be pinned in a CDN for a year.
Byte identity
What “byte-identical” is actually claiming
Three separate statements, each with its own guard. The delivery port and the published port emit the same bytes for the same page. The authoring port emits that same markup plus data-aue-* instrumentation and nothing else — a section’s editor-only name reaches the DOM as an instrument-only label, a default-content node is wrapped only under instrumentation, an empty section shows its placeholder only under instrumentation, and delivery carries none of it. And the smaller EDS-only build is checked against the same render snapshots as the full build: there are no per-profile snapshot variants, so compiling half the feature set out cannot change the HTML.
Templated blocks hold the same line by a different route. A minijinja block template owns its whole markup, so the renderer cannot wrap it from outside; the template author places aue_block(), aue_item() and aue_cell() where the editor needs handles, and the attribute strings those emit are copied verbatim from the non-templated path so the two are structurally identical. On delivery, every one of them expands to the empty string.
Icons are the same story from the other direction. A :name: token or an empty icon span is expanded server-side from icons/ on every port, so delivery and published stay identical to each other — and the inlined SVG survives client decoration, because the stock decorator leaves a span that already has children alone.
HTML minification was designed, built, and then deleted after review. The minifier removes whitespace between inline elements inside layout elements, and the EDS pre-decoration DOM is almost entirely <div>s: an ordinary space between two inline elements in author rich text would have silently disappeared, on the published port only. CSS and JavaScript are still minified there — HTML is not, and the byte-identity guard stayed an unconditional strict equality in both build profiles.
Check it
The claim is a three-line check
# the same page from both public ports, byte for byte
# (the published port only has it once you have published it)
curl -s http://localhost:3000/eds-compatibility.plain.html | sha256sum
curl -s http://localhost:3002/eds-compatibility.plain.html | sha256sum
# no editor attributes anywhere on delivery — match the attribute
# form, because this page's own prose mentions the name
curl -s http://localhost:3000/eds-compatibility \
| grep -oE 'data-aue-[a-z]+=' | wc -l
The .plain.html form is the EDS fragment contract, and it is how the header and footer on this page are loaded — by the stock client runtime, through the same render path, with hashed media URLs but no LCP hint, so a client-loaded fragment never competes with the page’s real LCP candidate.
The importer
Bringing an existing site over
ledge import ./my-project --base https://main--repo--org.aem.page /products
The importer is the inverse of the render, and it is driven by your component-definition.json, component-models.json and component-filters.json rather than by a fixed schema: a block class resolves to a model through your own definition, and its cells are reversed through the same function the renderer uses forward. It fetches a page, follows its fragment links recursively — depth-limited and cycle-safe — and writes the page and its assets into the preview store.
Some details are there because a real site tripped over them. A full page render that 404s is retried as .plain.html. Button styling round-trips through the <strong> and <em> wrapping EDS’s own decorator reads. Icons are harvested into icons/ and web fonts are parsed out of fonts.css and downloaded into the code store — both no-clobber, so a file you already committed is never overwritten. A columns-shaped block, which has no single model to reduce to, comes in as a real container with each cell as a child rather than as one opaque blob.
A block that does not reduce to a single model — nested, or with heterogeneous cells — is preserved verbatim as raw HTML and counted as passthrough in the report the import prints. Nothing disappears silently, but those blocks arrive as rich text rather than as fields, and they stay that way until you model them. Import is also not a perfect mirror of render in one place: a repeating container field renders to rows, and the importer has no reverse for those rows, so the two directions are asymmetric for that field type.
Keep reading
Two more pieces of the same system
The loop the byte-identical contract exists to protect: edit in the page, re-render on the server, publish one page at a time.
The other half of EDS parity: sheets served as sheet JSON, a generated query-index.json, sitemaps from the same helix-sitemap.yaml, and redirects that win over real pages.
Waitlist
One mail, when there is a binary.
No newsletter, no drip sequence, no passing your address to anyone. One message when ledge is downloadable, and that is the entire promise.