← Back to blog

The last four templates, and what our own postmortem got wrong

We published a score of 11/15 and named four remaining causes. Two of the causes were wrong. Chasing the real ones got us to 14/15, one honest boundary, and a defect channel that caught our own regression before it shipped.

Last release we ran 15 real production invoice templates through our engine and published the score: 11 rendered correctly, 4 degraded legibly, 0 broken. We also published our diagnosis of the four that remained. One used position: running(). One was AdminLTE's invoice with a remnant of the admin shell surviving. And two, we wrote, were responsive grids that only activate above a 768px viewport, which made them "a semantics question we would rather answer deliberately than patch around."

The score is now 14 USABLE, 1 DEGRADED, 0 BROKEN, shipped in 0.20.0. That is the headline. The more useful part of this post is that when we sat down to answer the semantics question deliberately, the investigation killed its own premise: two of the four causes we had published were wrong.

The postmortem rot

The "two templates gated on a 768px viewport" claim came from early-campaign notes, written when eight templates were failing at once and attribution was fast and loose. Nobody re-checked it after the float work landed. When we finally did:

  • The Bootstrap 2 template has zero media queries in the file. Its entire grid lives behind [class*="span"] { float: left }, an attribute selector our selector engine skipped (with a warning, to its credit). No viewport on earth would have activated those columns.
  • The other "gated" template is Bootstrap 3 with ungated col-xs-* classes, and its two-column layout doesn't use the float grid at all. It uses display: table and display: table-cell on divs, the pre-flexbox equal-height-columns idiom.

And the actual semantics question, once isolated, had a clean answer that moved nothing. Media Queries Level 4 defines width in paged media as the width of the page box, not the content area. A4 is 794 CSS px, so (min-width: 768px) is true on A4, and Chrome's print path agrees. Our content-box viewport was a spec misreading, wrapped in a README sentence about honesty that turned out to be wrong. We fixed it, said so plainly in the README, and measured the corpus: zero change. Not a lever, just a correction.

So the four remaining templates were four unrelated gaps: a rowspan bug, a missing selector feature, a viewport-clipping behavior, and a missing display type. Postmortems rot. Re-derive the cause before you build the fix.

Attribute selectors: one selector class, one framework generation

[class*="span"] is all of Bootstrap 2's grid, and attribute selectors generally are a place where a selector engine that skips them silently un-applies rules across framework CSS you never tested. We shipped all seven operators plus the i case flag, with spec specificity.

The measurement made the case for the whole approach: the Bootstrap 2 template flipped to its real two-column layout, structurally matching Chrome, and the other fourteen templates rendered byte-identically. Sixty-six "attribute selector skipped" warnings vanished from one template's output. A change exactly as wide as its selector.

The dark block that browsers never show you

The AdminLTE "sidebar remnant" turned out to be an idiom we had never modeled: off-viewport parking. The admin shell parks its control sidebar at right: -230px and relies on body { overflow-x: hidden } to make the overhang invisible. In a browser you never see it. In our renderer, position: fixed was unsupported, the element fell back into normal flow, and a dark block landed in the middle of the invoice.

Two policies fixed it, both stated in the docs rather than implied:

  • body { overflow-x: hidden } is now a page-level clip: content is clipped horizontally to the page content box, full page height, nothing vertical lost. The paged equivalent of what the browser was doing all along.
  • position: fixed renders as position: absolute: anchored on the page where it occurs, not repeated on every page. A paged renderer's viewport is the page. Repeating per page would be a guess at intent, and margin boxes already exist for running content; a warning names the difference every time.

The fixed policy improved a template we weren't even fixing. A delivery note in the corpus uses #footer { position: fixed; bottom: 0 }, the classic wkhtmltopdf print-footer idiom, and its footer now sits flush at the page bottom instead of floating mid-page. If you are migrating wkhtmltopdf templates, that pattern is everywhere in your codebase; the migration guide now documents exactly what it does.

CSS tables, and the regression we didn't ship

The equal-height-columns template needed display: table / table-cell on divs. We mapped it onto the engine's native table machinery, which is the obvious move and was measurably wrong: this template wraps its entire document in one equal-height row, engine table rows are atomic by design, and the result was a 966-point unbreakable row with overlapping overflow. Worse than before.

What caught it was the render-defect channel we built at the start of the campaign, in its own words: "table row needs 966pt but a page holds 734pt". The channel exists to answer "what did we get wrong?", and this time the answer was a regression we had introduced that same afternoon, flagged before it shipped.

The failure forced a better design. Multi-row CSS tables are genuine grids and use the table machinery. A single-row CSS table is the columns idiom, and real templates put arbitrarily tall content in it, so it becomes a breakable flex row instead: equal heights from cross-axis stretch, cell widths as column widths, page breaks allowed.

The one that stayed, on purpose

That template now renders complete, legible, and at the correct one-third / two-thirds geometry. It is still graded DEGRADED, and we kept it that way deliberately.

When a flex row taller than a page splits, our engine lays the children out sequentially: the sidebar column, then the content column on the following pages. Chrome continues both columns side by side on every page. Parallel fragmentation of a split row is a real layout-engine feature we don't have, and faking the grade would mean shipping something that looks fixed while producing a different document than the author intended, which is worse than the honest version.

So the boundary is loud instead: a new render defect fires whenever a multi-child flex row actually splits across pages. On the corpus it fires for exactly one template. The feature is on the board with the finding attached, waiting for a real document with a real requester, the same way our streaming and half-leading work waited. One narrow idiom in one template is not a buyer.

The score, and the release

Same 15 templates, same Chrome references, same grading: 14 USABLE, 1 DEGRADED, 0 BROKEN. From 3, 8, and 4 when the corpus was first assembled.

Everything above shipped in 0.20.0, along with a global typographic change worth reading before you upgrade: half-leading. Text baselines now sit half the leading lower in their line boxes, per the CSS line box model and every browser, which is what finally makes the line-height centering idiom actually center. Layout geometry and page breaks are unchanged; only the ink inside each line box moves. The release notes list all three behavior changes.

The corpus keeps its job. It runs as a release gate before every publish: render all fifteen, diff the warnings and page counts against committed baselines, and disposition anything that moved as either an intended improvement recorded in the notes or a stop-the-release regression. This release, the gate's diff was the campaign itself, line by line, and one line it surfaced is already the next small fix: background-color: transparent inside newly-matching rules now reaches our value parser and warns, when it should clear an inherited background. The gate found the next bug. That is what it is for.