Skip to content

fix: correct header card layout parsing based on DOM structure and classes#1774

Open
saschabuehrle wants to merge 2 commits intoTryGhost:mainfrom
saschabuehrle:fix/issue-1656
Open

fix: correct header card layout parsing based on DOM structure and classes#1774
saschabuehrle wants to merge 2 commits intoTryGhost:mainfrom
saschabuehrle:fix/issue-1656

Conversation

@saschabuehrle
Copy link

@saschabuehrle saschabuehrle commented Mar 16, 2026

Bug

Fixes #1656 — The header card HTML parser was incorrectly defaulting to split layout whenever an image was present, ignoring the actual HTML structure and classes that indicate the intended layout.

Fix

This PR implements proper layout detection by:

  • Checking for explicit layout classes (, )
  • Analyzing DOM structure (picture as direct child = full, picture inside content = split)
  • Only falling back to split layout when structure is ambiguous

Testing

Added comprehensive test cases covering:

  • ✅ Full/wide layout with class
  • ✅ Split layout with class
  • ✅ Full layout when picture is direct child of card
  • ✅ Backward compatibility with existing split layout detection

Impact

This resolves the issue where programmatically created header cards via the Admin API were incorrectly parsed as split layout despite having correct full/wide layout HTML structure. Now Ghost will properly maintain the intended layout when importing HTML cards.

Verification

The fix handles the exact HTML structure mentioned in the issue:

<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>

This will now correctly parse as full/wide layout instead of incorrectly defaulting to split.

Greetings, saschabuehrle


Note

Medium Risk
Changes HTML import behavior for v2 header cards, which could affect how existing content is interpreted during parsing. Scope is limited to layout detection and covered by new unit tests.

Overview
Fixes v2 header card HTML parsing so layout is derived from explicit classes (e.g. kg-layout-split, kg-content-wide) and the <picture> placement in the DOM, instead of defaulting to split whenever an image exists.

Adds import tests covering full/wide vs split layouts to prevent regressions when header card HTML is generated externally (e.g. via API).

Written by Cursor Bugbot for commit 5f5602b. This will update automatically on new commits. Configure here.

…asses (fixes TryGhost#1656)

The header card HTML parser was incorrectly defaulting to split layout
whenever an image was present, ignoring the actual HTML structure and
classes that indicate the intended layout.

This fix implements proper layout detection by:
- Checking for explicit layout classes (kg-layout-split, kg-content-wide)
- Analyzing DOM structure (picture as direct child = full, picture inside content = split)
- Only falling back to split layout when structure is ambiguous

Test cases added for:
- Full/wide layout with kg-content-wide class
- Split layout with kg-layout-split class
- Full layout when picture is direct child of card

Resolves the issue where programmatically created header cards via
Admin API were incorrectly parsed as split layout despite having
correct full/wide layout HTML structure.
@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

Walkthrough

The V2 header parser was modified to determine layout from HTML classes and structure instead of defaulting to split when an image exists. It now checks for kg-layout-split and kg-content-wide classes and inspects whether the picture element is a direct child of the card or nested inside the content container to set layout. The computed layout value is included in the V2 payload. Tests were updated and three new v2 importDOM tests added to cover full/wide, split, and direct-picture scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the primary change: correcting header card layout parsing based on DOM structure and classes.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the bug fix, implementation approach, and testing coverage.
Linked Issues check ✅ Passed The code changes fully implement the requirements from issue #1656: checking layout classes, analyzing DOM structure, and implementing proper layout detection with fallback logic.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the header card layout parsing issue; modifications to the parser and corresponding test updates are within scope of the linked issue #1656.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use TruffleHog to scan for secrets in your code with verification capabilities.

Add a TruffleHog config file (e.g. trufflehog-config.yml, trufflehog.yml) to your project to customize detectors and scanning behavior. The tool runs only when a config file is present.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/kg-default-nodes/test/nodes/header.test.js (1)

359-387: ⚠️ Potential issue | 🔴 Critical

Existing test HTML structure will cause layout assertion to fail with the new parser logic.

The test at lines 359-387 has <picture> as a direct child of the card div, but expects layout === 'split' at line 379.

With the new parser logic (lines 68-71 of header-parser.js):

  • hasSplitClass: false (no kg-layout-split class)
  • isPictureInContent: false (picture is outside content)
  • isPictureDirectChild: true

The parser executes line 70-71, returning layout = '' (full/wide), not 'split', causing the test assertion to fail.

Fix by moving the <picture> inside the .kg-header-card-content div to match the split layout structure:

Suggested fix
 const htmlstring = `
     <div class="kg-card kg-header-card kg-v2 kg-style-accent" data-background-color="#abcdef">
-        <picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
         <div class="kg-header-card-content">
+            <picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
             <div class="kg-header-card-text kg-align-center">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/header.test.js` around lines 359 - 387,
The test's HTML places the <picture> as a direct child of the card which, per
the header parser's logic (variables hasSplitClass, isPictureInContent,
isPictureDirectChild that determine layout), yields layout === '' instead of
'split'; update the test's htmlstring inside the "parses a header card V2" test
so the <picture> element is moved inside the .kg-header-card-content (so
isPictureInContent becomes true) thereby making the parser return layout ===
'split' and satisfying the node.layout assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/kg-default-nodes/test/nodes/header.test.js`:
- Around line 359-387: The test's HTML places the <picture> as a direct child of
the card which, per the header parser's logic (variables hasSplitClass,
isPictureInContent, isPictureDirectChild that determine layout), yields layout
=== '' instead of 'split'; update the test's htmlstring inside the "parses a
header card V2" test so the <picture> element is moved inside the
.kg-header-card-content (so isPictureInContent becomes true) thereby making the
parser return layout === 'split' and satisfying the node.layout assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f16b448-abf1-4e0f-9240-4b047198e495

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6499b and 5f5602b.

📒 Files selected for processing (2)
  • packages/kg-default-nodes/lib/nodes/header/parsers/header-parser.js
  • packages/kg-default-nodes/test/nodes/header.test.js

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

if (hasSplitClass || isPictureInContent) {
layout = 'split';
} else if (hasContentWideClass || isPictureDirectChild) {
layout = ''; // full/wide layout (empty string)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parser returns empty string, node constructor coerces to 'full'

High Severity

The parser sets layout = '' for full/wide layouts, but HeaderNode's default for layout is 'full', and the constructor uses data[prop.name] || prop.default — since '' is falsy, '' || 'full' evaluates to 'full'. This means node.layout will be 'full', not ''. The new test assertions at lines 419 and 457 (node.layout.should.equal('')) will fail. The parser needs to return 'full' explicitly instead of ''.

Additional Locations (2)
Fix in Cursor Fix in Web

} else {
// Fallback to old behavior if structure is ambiguous
layout = 'split';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing V2 parser test breaks with new logic

High Severity

The pre-existing "parses a header card V2" test (line 359 of the test file) has HTML where <picture> is a direct child of the card div and no kg-layout-split class is present. The new parser identifies isPictureDirectChild as true and returns layout = '' (→ 'full'), but that test asserts node.layout.should.equal('split'). This existing test will fail with the new parser logic.

Fix in Cursor Fix in Web

…ayout structure

The test was expecting layout='split' but had the picture element as a direct child
of the card instead of inside the content container. Per the parser logic, when
isPictureInContent is false but isPictureDirectChild is true, the layout defaults
to empty string rather than 'split'. Moving the picture inside .kg-header-card-content
makes isPictureInContent true, resulting in the expected 'split' layout.
@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kg-default-nodes/test/nodes/header.test.js`:
- Around line 404-459: Add two precedence tests to header.test.js that assert
explicit classes override DOM cues: (1) a case with class "kg-content-wide" on
the card while the <picture> is nested inside ".kg-header-card-content" and
expect node.layout === '' and backgroundImageSrc set; (2) a case with class
"kg-layout-split" on the card while the <picture> is a direct child of the card
and expect node.layout === 'split' and backgroundImageSrc set. These tests
should mirror existing test patterns (using createDocument,
$generateNodesFromDOM and editorTest) so they exercise the parser in
header-parser.js and catch class-precedence regressions. Ensure assertions check
nodes.length === 1 and validate node.layout and node.backgroundImageSrc.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 665ff049-a270-4438-8cd7-ef0e8826d725

📥 Commits

Reviewing files that changed from the base of the PR and between 5f5602b and c45afb6.

📒 Files selected for processing (1)
  • packages/kg-default-nodes/test/nodes/header.test.js

Comment on lines +404 to +459
it('parses full/wide layout when kg-content-wide class is present', editorTest(function () {
const htmlstring = `
<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" alt="" /></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>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal(''); // full/wide layout is empty string
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));

it('parses split layout when kg-layout-split class is present', editorTest(function () {
const htmlstring = `
<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" alt="" /></picture>
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Title</h2>
<p class="kg-header-card-subheading">Subtitle</p>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal('split');
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));

it('parses full layout when picture is direct child of card', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2 kg-width-full" data-background-color="#000000">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
<div class="kg-header-card-content">
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Title</h2>
<p class="kg-header-card-subheading">Subtitle</p>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal(''); // full layout is empty string
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add precedence tests for explicit classes vs DOM cues.

The new cases are good, but they still don’t assert the required precedence rule (“explicit classes win”). Please add conflict scenarios so regressions are caught:

  • kg-content-wide + picture inside .kg-header-card-content → expect layout === ''
  • kg-layout-split + picture as direct child of card → expect layout === 'split'

Without these, class-precedence bugs in packages/kg-default-nodes/lib/nodes/header/parsers/header-parser.js can still pass this suite.

Proposed test additions
+            it('prefers kg-content-wide over picture-in-content structure', editorTest(function () {
+                const htmlstring = `
+                    <div class="kg-card kg-header-card kg-v2 kg-content-wide">
+                        <div class="kg-header-card-content">
+                            <picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
+                        </div>
+                    </div>`;
+                const document = createDocument(htmlstring);
+                const nodes = $generateNodesFromDOM(editor, document);
+                nodes.length.should.equal(1);
+                const node = nodes[0];
+                node.layout.should.equal('');
+            }));
+
+            it('prefers kg-layout-split over picture-direct-child structure', editorTest(function () {
+                const htmlstring = `
+                    <div class="kg-card kg-header-card kg-v2 kg-layout-split">
+                        <picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
+                        <div class="kg-header-card-content"></div>
+                    </div>`;
+                const document = createDocument(htmlstring);
+                const nodes = $generateNodesFromDOM(editor, document);
+                nodes.length.should.equal(1);
+                const node = nodes[0];
+                node.layout.should.equal('split');
+            }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('parses full/wide layout when kg-content-wide class is present', editorTest(function () {
const htmlstring = `
<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" alt="" /></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>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal(''); // full/wide layout is empty string
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));
it('parses split layout when kg-layout-split class is present', editorTest(function () {
const htmlstring = `
<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" alt="" /></picture>
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Title</h2>
<p class="kg-header-card-subheading">Subtitle</p>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal('split');
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));
it('parses full layout when picture is direct child of card', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2 kg-width-full" data-background-color="#000000">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
<div class="kg-header-card-content">
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Title</h2>
<p class="kg-header-card-subheading">Subtitle</p>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal(''); // full layout is empty string
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));
it('parses full/wide layout when kg-content-wide class is present', editorTest(function () {
const htmlstring = `
<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" alt="" /></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>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal(''); // full/wide layout is empty string
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));
it('parses split layout when kg-layout-split class is present', editorTest(function () {
const htmlstring = `
<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" alt="" /></picture>
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Title</h2>
<p class="kg-header-card-subheading">Subtitle</p>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal('split');
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));
it('parses full layout when picture is direct child of card', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2 kg-width-full" data-background-color="#000000">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
<div class="kg-header-card-content">
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Title</h2>
<p class="kg-header-card-subheading">Subtitle</p>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal(''); // full layout is empty string
node.backgroundImageSrc.should.equal('https://example.com/image.jpg');
}));
it('prefers kg-content-wide over picture-in-content structure', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2 kg-content-wide">
<div class="kg-header-card-content">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal('');
}));
it('prefers kg-layout-split over picture-direct-child structure', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2 kg-layout-split">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
<div class="kg-header-card-content"></div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document);
nodes.length.should.equal(1);
const node = nodes[0];
node.layout.should.equal('split');
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-default-nodes/test/nodes/header.test.js` around lines 404 - 459,
Add two precedence tests to header.test.js that assert explicit classes override
DOM cues: (1) a case with class "kg-content-wide" on the card while the
<picture> is nested inside ".kg-header-card-content" and expect node.layout ===
'' and backgroundImageSrc set; (2) a case with class "kg-layout-split" on the
card while the <picture> is a direct child of the card and expect node.layout
=== 'split' and backgroundImageSrc set. These tests should mirror existing test
patterns (using createDocument, $generateNodesFromDOM and editorTest) so they
exercise the parser in header-parser.js and catch class-precedence regressions.
Ensure assertions check nodes.length === 1 and validate node.layout and
node.backgroundImageSrc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant