Skip to content

Header card HTML parser incorrectly defaults to split layout when image is present #1656

@nicstark

Description

@nicstark

The header card HTML parser in header-parser.js always defaults to split layout when an image is present, ignoring the correct HTML structure and classes that indicate full/wide layout.

Related Forum Discussion: https://forum.ghost.org/t/header-card-images-displaying-side-by-side-instead-of-as-background/60982/8

Steps to Reproduce

  1. Create a header card in Ghost editor with full/wide layout (background image mode)
  2. Export the HTML from that card
  3. Send that same HTML back to Ghost via Admin API using HTML source
  4. Open the imported card in the editor

Actual Result: Card shows as split layout in the editor

Expected Result: Card should maintain full/wide layout with image as background

The Bug

In packages/kg-default-nodes/lib/nodes/header/parsers/header-parser.js (referenced in forum post), the parser has this logic:

const layout = backgroundImageSrc ? 'split' : '';

This line (line 11 in the referenced code) defaults to split whenever an image exists, ignoring:

  • The presence/absence of kg-layout-split class
  • The presence of kg-content-wide class (which indicates background image mode)
  • The DOM structure (picture as direct child = full, picture inside content = split)

Correct HTML Structure

Full/Wide Layout (Background Image)

For full/wide layout (background image), the HTML structure should be:

<div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide" data-background-color="#000000">
  <picture><img class="kg-header-card-image" src="https://example.com/image.jpg" /></picture>
  <div class="kg-header-card-content">
    <div class="kg-header-card-text kg-align-center">
      <h2 class="kg-header-card-heading" style="color: #FFFFFF;">Title</h2>
      <p class="kg-header-card-subheading" style="color: #FFFFFF;">Subtitle</p>
      <a class="kg-header-card-button" href="#">Button</a>
    </div>
  </div>
</div>

Key indicators of full layout:

  • kg-content-wide class present
  • ✅ NO kg-layout-split class
  • ✅ Picture element as direct child of card (before content div)

Split Layout

For split layout, the structure is:

<div class="kg-card kg-header-card kg-v2 kg-layout-split kg-width-full">
  <div class="kg-header-card-content">
    <picture><img class="kg-header-card-image" src="https://example.com/image.jpg" /></picture>
    <div class="kg-header-card-text kg-align-center">
      <!-- content -->
    </div>
  </div>
</div>

Key indicators of split layout:

  • kg-layout-split class present
  • ❌ NO kg-content-wide class
  • ✅ Picture element inside content div

Proposed Fix

The parser should check for the layout classes or DOM structure, not just image presence:

// Check for explicit layout class first
const hasSplitClass = div.classList.contains('kg-layout-split');
const hasContentWideClass = div.classList.contains('kg-content-wide');

// Check DOM structure: picture as direct child = full, picture inside content = split
const picture = div.querySelector('picture');
const content = div.querySelector('.kg-header-card-content');
const isPictureDirectChild = picture && picture.parentElement === div;
const isPictureInContent = picture && content && content.contains(picture);

// Determine layout
let layout = '';
if (hasSplitClass || isPictureInContent) {
    layout = 'split';
} else if (hasContentWideClass || (backgroundImageSrc && isPictureDirectChild)) {
    layout = ''; // full/wide layout (empty string)
}

Test Case

HTML that should result in full layout but currently becomes split:

<div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide" data-background-color="#000000">
  <picture><img class="kg-header-card-image" src="https://example.com/image.jpg" /></picture>
  <div class="kg-header-card-content">
    <div class="kg-header-card-text kg-align-center">
      <h2 class="kg-header-card-heading" style="color: #FFFFFF;">Title</h2>
      <p class="kg-header-card-subheading" style="color: #FFFFFF;">Subtitle</p>
    </div>
  </div>
</div>

What we're sending to API:

  • Classes: kg-v2 kg-width-full kg-content-wide (no kg-layout-split)
  • Structure: Picture as direct child of card
  • Logs confirm: split=False, full=True, wide_content=True, pic=direct child

What happens:

  • Ghost converts HTML to Mobiledoc
  • Parser sees image exists → defaults to layout = 'split'
  • Editor shows card as split layout

Impact

This affects anyone programmatically creating header cards via the Admin API using HTML. The HTML-to-Mobiledoc conversion incorrectly infers split layout, requiring:

  • Manual correction in the editor for each card, OR
  • JavaScript workarounds to fix layout after render, OR
  • Switching to Lexical format (major rewrite)

Context

  • Ghost Version: 6.9+
  • API Version: Admin API v5.0
  • Use Case: Programmatically generating monthly newsletter content via Python script
  • Method: Sending HTML directly to Admin API posts endpoint with ?source=html

The HTML structure is validated as correct before sending (matches exported HTML from working cards), but Ghost's parser misinterprets it during HTML-to-Mobiledoc conversion.

Verification

I've verified the HTML being sent:

  1. Logs show correct classes: ['kg-card', 'kg-header-card', 'kg-v2', 'kg-width-full', 'kg-content-wide']
  2. Structure verified: Picture is direct child, not inside content div
  3. No kg-layout-split class present
  4. Structure matches exported HTML from working cards

The bug is confirmed by Cathy_Sarisky in the forum, who tested and confirmed the same behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions