Skip to content

Commit e49ca05

Browse files
shsteimerclaude
andcommitted
fix: extract main innerHTML for .plain.html fallback and improve test fixture
Parse the full HTML document and return only the innerHTML of <main> rather than sending the raw file content. Uses existing hast-util-select and hast-util-to-html deps. Test fixture updated to use a realistic full document and a multiline fragment string for readability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 89d12d5 commit e49ca05

3 files changed

Lines changed: 35 additions & 5 deletions

File tree

src/server/HelixServer.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,15 +476,16 @@ export class HelixServer extends BaseServer {
476476
}
477477
}
478478
if (isPlainFallback) {
479-
// .plain.html callers want the raw body fragment — skip head wrapping
479+
// .plain.html callers want the raw fragment — extract <main> content
480480
if (liveReload) {
481481
liveReload.registerFile(ctx.requestId, servedFilePath);
482482
}
483+
const fragment = utils.extractMainContent(htmlContent);
483484
res.set({
484485
'content-type': 'text/html; charset=utf-8',
485486
'access-control-allow-origin': '*',
486487
});
487-
res.send(htmlContent);
488+
res.send(fragment);
488489
log.debug(`${pfx}served from ${CONTENT_DIR}/: ${ctx.path}`);
489490
return;
490491
}

src/server/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import { fileURLToPath } from 'url';
1919
import cookie from 'cookie';
2020
import { unified } from 'unified';
2121
import rehypeParse from 'rehype-parse';
22+
import { select } from 'hast-util-select';
23+
import { toHtml } from 'hast-util-to-html';
2224
import { getFetch } from '../fetch-utils.js';
2325

2426
// Load console interceptor script at startup
2527
// eslint-disable-next-line no-underscore-dangle
2628
const __dirname = path.dirname(fileURLToPath(import.meta.url));
29+
const htmlParser = unified().use(rehypeParse);
2730
const CONSOLE_INTERCEPTOR = readFileSync(
2831
path.join(__dirname, '../../packages/browser-injectables/src/console-interceptor.js'),
2932
'utf-8',
@@ -787,6 +790,19 @@ window.LiveReloadOptions = {
787790
const fullHead = headHtml + metaTags;
788791
return `<html><head>${fullHead}</head><body><header></header><main>${content}</main><footer></footer></body></html>`;
789792
},
793+
794+
/**
795+
* Extracts the innerHTML of the <main> element from a full HTML document.
796+
* Returns the original content unchanged if no <main> is found.
797+
* @param {string} html full HTML document
798+
* @returns {string} innerHTML of <main>, or original html if no <main> present
799+
*/
800+
extractMainContent(html) {
801+
const ast = htmlParser.parse(html);
802+
const main = select('main', ast);
803+
if (!main) return html;
804+
return main.children.map((child) => toHtml(child)).join('');
805+
},
790806
};
791807

792808
export default Object.freeze(utils);

test/server.test.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,9 +1353,18 @@ describe('Helix Server', () => {
13531353
const cwd = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot);
13541354
await fse.ensureDir(path.join(cwd, CONTENT_DIR));
13551355
// Only content/foo.html exists on disk — no content/foo.plain.html
1356+
// The stored .html file is a full document; .plain.html must return only the inner fragment
1357+
const innerFragment = `<div>
1358+
<p><a href="/">Home</a></p>
1359+
</div>
1360+
<div>
1361+
<ul>
1362+
<li><a href="/about">About</a></li>
1363+
</ul>
1364+
</div>`;
13561365
await fse.writeFile(
13571366
path.join(cwd, CONTENT_DIR, 'foo.html'),
1358-
'<body><header></header><main><p>plain fragment</p></main><footer></footer></body>',
1367+
`<html><head><title>Foo</title></head><body><header></header><main>${innerFragment}</main><footer></footer></body></html>`,
13591368
);
13601369

13611370
// Proxy must not be called — if it is, the test will fail via nock
@@ -1369,9 +1378,13 @@ describe('Helix Server', () => {
13691378
const resp = await getFetch()(`http://127.0.0.1:${project.server.port}/foo.plain.html`);
13701379
assert.strictEqual(resp.status, 200);
13711380
const body = await resp.text();
1372-
// Should return the raw fragment — no <head> wrapping, no head.html injection
1373-
assert.ok(body.includes('plain fragment'), 'body should contain the fragment content');
1381+
// Must return the inner fragment only — no document structure
1382+
assert.ok(body.includes('<a href="/">Home</a>'), 'body should contain the fragment content');
13741383
assert.ok(!body.includes('<head>'), '.plain.html response must not contain a <head> element');
1384+
assert.ok(!body.includes('<body>'), '.plain.html response must not contain a <body> element');
1385+
assert.ok(!body.includes('<main>'), '.plain.html response must not contain a <main> element');
1386+
assert.ok(!body.includes('<header>'), '.plain.html response must not contain a <header> element');
1387+
assert.ok(!body.includes('<footer>'), '.plain.html response must not contain a <footer> element');
13751388
} finally {
13761389
await project.stop();
13771390
}

0 commit comments

Comments
 (0)