fix(import): report loop references the importer cannot resolve - #321
Merged
Conversation
`<instatic-loop data-source-id="posts">` — a table slug where a source id
belongs — imported cleanly. The id was copied verbatim into props,
`site_insert_html` reported success, `verify-docs` found the document intact,
and at publish `loopPrefetch` turned the unregistered source into a
well-formed `{ items: [], totalItems: 0 }`, indistinguishable from "this table
has no rows". The section rendered as nothing and every gate passed, because
the route still returned 200 with a unique title and one h1.
`loopSourceRegistry` has had `has()`/`getOrThrow()` the whole time and nothing
under `htmlImport` ever called either, so the shape `site_list_loop_sources`
documents and the shape the importer accepts were checked against nothing in
common. They now share the registry: the walk reports an unregistered source,
a missing source, and a `data.rows` loop with no table — the second silent
path, where `filters: {}` makes `dataRows` return empty just as quietly.
These are warnings, not rejections. A paste of thirty elements should not be
thrown away over one mistyped attribute, and the importer also serves
arbitrary user-pasted HTML. What matters is that the caller is told at author
time rather than finding an empty section in a screenshot later, so
`site_insert_html` and `site_replace_node_html` return them alongside the ids
they already report. Warnings live on the walk result rather than on
`ImportFragment`, which is the structural shape callers hand back to the store.
Also corrected two descriptions that promised behaviour the composer does not
implement. `<instatic-outlet>` is spliced away on page routes — the page's
content replaces it — so `data-tag` only takes effect on entry routes, and a
template that wants pages to have a `<main>` has to wrap the outlet itself.
The module's own doc comment describes this polymorphism correctly; the tool
description and system prompt did not, which is how a layout ends up with no
main landmark on any public route.
Test: a table slug, a missing id, and a missing table each warn with the fix
in the message; a well-formed loop is silent; the node still imports; several
bad loops in one payload all report.
DavidBabinec
added a commit
that referenced
this pull request
Aug 1, 2026
…322) * fix(import): warn when a loop has no rows to repeat A loop authored inside a table is destroyed by HTML parsing before the importer ever sees it. `<tbody>` may only contain `<tr>`, so the parser foster-parents `<instatic-loop>` out to before the `<table>` AND strips its children, which stay behind in the tbody. Both halves survive as perfectly valid nodes: an empty loop that can never render, and an orphan `<tr>` whose `{currentEntry.*}` bindings resolve to nothing. So `site_insert_html` reports success, the document persists, every existing check passes — the source id and table id on the relocated loop are still correct — and the route publishes a table with exactly one blank row. Found building template pack 023, where six data tables published as six blank rows and cleared every gate. The fix is the general form of the diagnosis: a loop with no children has no row template and can never render anything, whatever emptied it. That is checked after recursion, when the child count is known, and reported through the same `site_insert_html` warnings channel as #321. The message names the table cause, because there is essentially only one way this happens. Also documents the constraint in the `site_insert_html` description and the site system prompt, so the shape that works is stated where a caller reads it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(import): warn when data-order-by is not a sort key `data-order-by` accepts any string. `fetchPublishedDataRowItems` narrows it against a four-value whitelist — publishedAt, createdAt, updatedAt, slug — and falls back to the source default on a miss. So `data-order-by="sortHour"`, naming a real and populated field on the table, imports cleanly, publishes, and produces an order that is not the one asked for. It reads as a mistake in the content rather than a loop that never sorted. The allowed values were already declared on the source as `orderByOptions` and already advertised through `site_list_loop_sources`; nothing checked an authored loop against them. Now the importer does, and names them in the message. Found in pack 023, where a night published as 22:00, 00:30, 23:00. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An invalid loop imported cleanly and published nothing
postsis a table slug; the source id isdata.rowsand the table goes indata-table-id. The importer copied the string verbatim into props,site_insert_htmlreported success,verify-docsfound the document intact, and at publishloopPrefetch.ts:288-296turned the unregistered source into a well-formed{ items: [], totalItems: 0 }— indistinguishable from "this table has no rows".renderLoopthen rendered''.The route still returned 200 with a unique title and one
<h1>, so every downstream check passed. Two template packs shipped with an empty journal index before anyone looked at the page.loopSourceRegistryhas exposedhas()andgetOrThrow()the whole time, and nothing undersrc/core/htmlImport/ever called either — so the shapesite_list_loop_sourcesdocuments and the shape the importer accepts were validated against nothing in common. The one place that does look the id up,dynamicDetection.ts:143-152, explicitly treats a miss as "static".The fix
The walk now checks each loop against that same registry and reports three cases: an unregistered source, no source at all, and a
data.rowsloop with notableId— the second silent path, wherefilters: {}makesdataRows.ts:418return empty just as quietly.Live, through the real MCP surface:
All three still insert. Warnings, not rejections: a paste of thirty elements should not be thrown away over one mistyped attribute, and the importer also serves arbitrary user-pasted HTML.
site_insert_htmlandsite_replace_node_htmlreturn them alongside the ids they already report, so the caller is told at author time.Warnings live on a
WalkResultrather than onImportFragment— that is the structural shape callers hand back toinsertImportedNodes, and a diagnostic has no business in it.Two descriptions that promised what the composer does not do
<instatic-outlet>is spliced away on page routes —templateCompose.ts:83deletes the node and the page's content takes its position — sodata-tagonly takes effect on entry routes, where the outlet stays and wraps the entry body. The module's own doc comment describes this polymorphism correctly; the tool description and system prompt claimed omittingdata-tagmakes the outlet "own the page's main landmark", which is never true on a page route. That is how a layout ends up with no<main>on any public route.Both now say what actually happens and tell authors to wrap the outlet themselves. I did not change the rendering: making the outlet a wrapper needs
canHaveChildrenand a render change against a deliberately polymorphic module, which is a redesign rather than a fix, and it would add a wrapper element to every templated page route.Verification
Unit — a table slug, a missing id, and a missing table each warn with the fix in the message; a well-formed loop is silent; the node still imports; several bad loops in one payload all report.
Live — inserted all three shapes through MCP against a real site (above), then restored the page and re-audited: 19/19 routes, 0 problems.
Suite: 6505 pass / 0 fail. Lint clean.
tsc -bclean.Worth knowing
bunx tsc --noEmittypechecks nothing in this repo —tsconfig.jsonis solution-style with onlyreferencesand nofiles/include. I confirmed it by planting a syntax error in aserver/file and getting exit 0.tsc -b(whatbun run buildand CI use) catches it. Anyone verifying locally with--noEmitis running a no-op.Related
Follows #318 and #320 from the same investigation.
🤖 Generated with Claude Code