GoodTurn

css-inline 0.14.x adds mysterious vertical gap between header and title in HTML email

Transactional HTML emails showed a mysterious ~56px vertical whitespace gap between the header logo and the page title. The email pipeline renders dust-style templates and then runs the HTML through css-inline (Python css_inline 0.14.x, Rust crate with html5ever underneath) before sending. There is no error or warning at any stage — rendering, inlining, and sending all succeed silently. The template source looked correct: the base template wrapped a {+title} block in <h1 class="mb">, and each child template filled the block with <h2>Your results</h2>. Inspected the template files repeatedly expecting a stray margin utility class or an extra empty element in the source — there wasn't one; the gap only exists in the post-inlining output. Tweaking margins on the h1/h2 styles moved the gap around but never removed it, because the extra element causing it isn't visible anywhere in the source.

1 solution
ranked by outcome — not votes
✓ ACCEPTED

Root cause: heading elements cannot be nested in HTML. <h1><h2>…</h2></h1> is invalid, and any HTML5 tree-construction parser (css-inline 0.14.x uses html5ever) auto-closes the open <h1> the moment it encounters <h2> — per the HTML5 spec, an in body insertion of h1h6 while another heading is open implicitly pops it. The re-serialized DOM therefore contains an empty <h1 class="mb"> followed by the <h2> as a sibling, not a child. The empty h1 still renders its line-height (32px at font-size: 32px) plus its margin-bottom (24px) — that's the phantom ~56px gap. It never appears in the template source because the parser manufactures it at inlining time, and no stage of the pipeline emits any warning.

Fix: don't put a heading tag inside the base template's heading wrapper. Either fill the block with plain text/inline markup so it renders inside the <h1>:

{<title}{view_icon} Your {view_label} results{/title}

or change the base wrapper to a non-heading <div> styled as a heading.

Diagnosis shortcut: fetch the post-inlining HTML (preview endpoint or sent message body) and grep for <h1. If you see <h1 …></h1> immediately followed by your <h2>, this is it. Any pipeline that round-trips HTML through an HTML5 parser (css-inline, BeautifulSoup+html5lib, lxml.html5parser, juice with cheerio) will exhibit the same silent restructuring for invalid nesting — the inliner isn't broken, it's the spec's auto-close rules.