Skip to content

Add the ejs-listing-port skill and the listing-template docs it points at (bd-hzsi) - #621

Merged
cscheid merged 4 commits into
mainfrom
feature/bd-hzsi-listing-template-migration-docs
Aug 27, 2026
Merged

Add the ejs-listing-port skill and the listing-template docs it points at (bd-hzsi)#621
cscheid merged 4 commits into
mainfrom
feature/bd-hzsi-listing-template-migration-docs

Conversation

@gordonwoodhull

Copy link
Copy Markdown
Member

Brings the ejs-listing-port skill into the repo, and fills the documentation gap it needs in order to point at q2's own docs rather than an external guide.

The skill has been used to complete two Q1 → Q2 listing ports — the Posit Connect documentation and the Positron website. Its prototype body was a pointer to a guide living outside this repo; that guide also predated fcd76aebd, which added the "Custom templates" section to the Listings guide and superseded much of it. So the skill here points at docs/guides/projects/, and those docs carry what the external guide carried.

The skill

.claude/skills/ejs-listing-port/SKILL.md — the Q1 EJS → Q2 doctemplate contract, the canonical spellings ($it.<key>$ inside $for(items)$; type: custom and template:; a neutral extension so Q-12-9 stops firing), a port procedure, and a verification step.

It carries two semantics in the body rather than by reference, because they are what a port gets wrong and neither is visible in the template's text:

Links and images must be markdown, not raw HTML. A template's output is markdown, re-parsed into the page; only then are paths resolved. LinkRewriteTransform rewrites .qmd targets to output URLs, and resource collection notes every image so the file is copied. Both walk parsed markdown nodes, and RawBlock/RawInline are no-op leaves in each, so a path inside a raw-HTML attribute is invisible to both.

[$it.title$]($it.path$)          → href="posts/intro.html"
`<a href="$it.path$">`{=html}    → href="posts/intro.qmd"

For images the same split costs two things rather than one: a markdown image is rewritten and copied, a raw <img> is neither, so the asset never reaches the output tree. Raw HTML stays fine inside link text — the built-ins do exactly this with [`$image-html$`{=html}]($path$).

The skill also records that this is settled design — q2 emits HTML from the AST and has no post-Pandoc DOM stage — so the fix is the markdown path, not HTML parsing.

The description placeholder envelope must be emitted unconditionally. Quarto fills a missing item description from the first paragraph of the rendered item page, but only inside the description-placeholder-begin/-end markers, and only if they sit outside the $if$. The built-in templates gate the whole envelope on $if(description)$, so a template copying that shape loses previews for exactly the items that need them.

The image rule has one wrinkle the skill calls out: when the image comes from an item document's own front-matter image:, that page's render copies the file regardless, so a raw-<img> template looks correct. It only fails for inline-record and custom fields — which is where a ported gallery gets its thumbnails.

Triggers are the symptoms a user reports: Q-12-7, Q-12-9, Q-12-10, Q-12-24, a listing rendering with the built-in layout instead of the custom one, a template dumped verbatim into the page, .qmd hrefs, 404ing images. Verification is inspecting the rendered href and src values and confirming the referenced files exist, since neither failure produces a diagnostic or a text diff.

references/worked-examples.md has three annotated ports: the minimal link-and-description shape; a card grid reimplementing the grid built-in, where the JavaScript prologue becomes template-params: and the first question is whether type: grid would do; and a whole-card link, where a markdown link's auto-<p> restricts the card body to phrasing content. That last constraint is shown with a parse tree — nesting a <div> force-closes the <p>, moves the card outside the anchor, and reconstructs the anchor three times.

The docs

docs/guides/projects/listing-templates.qmd (new) — both rules above, the anatomy of each built-in layout so a custom template can inherit its CSS, the Q1 → Q2 mapping table (moved here from listings.qmd and extended with the rows that fail silently), what doctemplates cannot do, and a verification checklist. The worked before/after is quarto-web's own docs/gallery/gallery.ejs: one {=html} fence around everything, raw anchors, a raw thumbnail, a metadataAttrs() call, and a nested loop over a custom field, which ports to $for(it.tiles)$ with the inner $it$ shadowing the outer.

docs/guides/projects/listings.qmd keeps a tight "Custom templates" section and gains what its tables were missing: ${var}, $elseif$, the pipe list, outputHref, the four placeholder markers, show.<field>, table-row, metadata-attrs. Its line about raw HTML going in a ```{=html} block "just as it would in a .qmd file" was true and read as unqualified permission; it now carries the consequence and links to paths.qmd, which states the same rule for page content.

Two values needed a caveat. show.<field> is false for everything under type: custom unless the listing declares fields:, since custom listings have no default field set. And metadata-attrs must go inside a ```{=html} block — interpolated as markdown its quotes are curled into data-index=“0”.

Q-12-9 and Q-12-24 stopped at syntax; Q-12-24's "after" example used a markdown link without saying why. Both now name what the syntax mapping does not cover, and Q-12-24's table gains rows for the two patterns that fail silently. Both move from status: stub to complete.

Tests

Three tests in crates/quarto-core/tests/integration/listing_pipeline.rs pin what the skill and the guide assert: the $it.* spelling with an unconditional envelope yielding a derived description, and both halves of the raw-HTML split. They pass against unmodified code — they keep the documented idioms from rotting, and hold the two failure modes as deliberate contract. The image test uses inline-record fields so it exercises the case the front-matter path masks.

Notes

metadata-attrs is bound but has no consumer: no built-in template emits it and nothing reads data-index/data-categories. The comment at helpers.rs:117 saying the list.js sort/filter UI is gated on these attributes cannot be true today — a built-in listing with sort-ui: true, filter-ui: true emits no valueNames, no new List, no data-index. bd-nbv80e33 owns that gap; the docs describe the value and how to emit it safely, and claim nothing about the filter UI.

bd-o1meelim (a leading / in template: resolving filesystem-absolute) is a bug whose fix owns the docs sentence about /, so nothing here documents the current behaviour as correct.

The epic's suggestion to source the custom-template example from the Quarto extension catalogue turned out not to apply — the catalogue has no listing-template extension, since a listing template is a per-site file rather than something packaged and installed.

Closes bd-hzsi.

Three integration tests pin what the listing-template documentation
asserts: the `$it.*` spelling with an unconditionally-emitted description
envelope, and both halves of the raw-HTML split — a markdown anchor is
link-rewritten while a raw `<a href>` is not, and a markdown image is
resource-collected while a raw `<img>` is not.

All three pass against unmodified production code. They exist so the
documented idioms cannot silently rot, and so the two silent failure
modes stay pinned as deliberate contract — `RawInline` is a no-op leaf in
both `LinkRewriteTransform` and `ResourceCollector` — rather than
drifting into an accidental "fix".

The image test uses inline-record fields deliberately. When the image is
an item document's own front-matter `image:`, that page's own render
copies the file regardless, which masks the failure completely.
Add docs/guides/projects/listing-templates.qmd as a sibling of the
Listings guide, and keep listings.qmd's "Custom templates" section tight
with pointers into it.

The new page states what the documentation did not. A template's output
is markdown re-parsed into the page, so a raw-HTML anchor is never
link-rewritten and a raw `<img>` is never resource-collected. Both fail
with no diagnostic and no visible difference in the template text, and
the image case hides itself whenever the image is an item document's own
front-matter `image:` — that page's render copies the file regardless.
The page also covers the description placeholder envelope (the markers
belong outside the `$if$`, not inside, or the items that need a preview
are exactly the ones that lose it), what each built-in layout emits so a
custom template can inherit its CSS, and the fact that reading an absent
value warns with Q-12-10.

The worked before/after is quarto-web's own docs/gallery/gallery.ejs —
raw anchors, a raw thumbnail, a `metadataAttrs()` call, and a nested
loop over a custom field. The Q1 → Q2 mapping table moves here from
listings.qmd and gains the rows that fail silently.

listings.qmd gains the syntax and values it was missing: `${var}`,
`$elseif$`, the pipe list, `outputHref`, the four placeholder markers,
`show.<field>` (with the note that `type: custom` has no default field
set, so every `show.*` is false unless `fields:` is declared),
`table-row`, and `metadata-attrs` — the one value that is unsafe to
interpolate directly, since as markdown its quotes are curled into
invalid HTML.

`path` is described precisely: present whenever an item has a link
target, a document or a record with `href:`, and absent only for a
record with neither.
A thin pointer to docs/guides/projects/listing-templates.qmd with the two
silent failure modes stated inline rather than merely named — a skill
that only names them gets them skipped, which is what the two ports this
is generalized from demonstrated.

Fires on the symptoms a user actually sees: Q-12-7, Q-12-9, Q-12-10,
Q-12-24, a listing rendering with the built-in layout instead of the
custom one, a template dumped verbatim into the page, listing links
pointing at .qmd files, listing images 404ing. Ends in a required
verification step, because a passing render proves nothing here —
neither failure mode produces a diagnostic or a text diff.

references/worked-examples.md carries three annotated ports: the minimal
link-and-envelope shape, a card grid that was reimplementing the `grid`
built-in (its JavaScript prologue becomes `template-params:`, and the
first question is whether `type: grid` would do), and the whole-card
link, where a markdown link's auto-`<p>` restricts the card body to
phrasing content. That last constraint is demonstrated rather than
asserted: nesting a `<div>` there force-closes the `<p>`, reparents the
card outside the anchor as a sibling, and reconstructs the anchor three
times via the adoption-agency algorithm, destroying the whole-card link.
Both pages presented syntax translation as the whole job. Q-12-24 even
demonstrated the real fix — its "after" example uses a markdown link —
without ever saying why, which is the most reliable way to have a reader
copy the shape and miss the reason.

Each page now names what the syntax mapping does not cover: a template's
output is markdown and Quarto resolves paths only after parsing it, so a
raw-HTML anchor keeps its .qmd href and a raw `<img>` is never copied
into the site, neither with a warning. Q-12-24's mapping table gains the
two rows for exactly those patterns.

Prose revised through the reader-expectations methodology, which
docs/errors/README.md requires for the stub → complete promotion. The
finding both pages shared was a missing connective, not misplaced topic
or stress positions.
@posit-snyk-bot

posit-snyk-bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cscheid

cscheid commented Aug 27, 2026

Copy link
Copy Markdown
Member

This is great. Relatedly, I recently added q2 docs llms so the binary hosts the llms.txt version of our website. I think we should consider hosting skills too!

@cscheid
cscheid merged commit fc5bac4 into main Aug 27, 2026
10 checks passed
@cscheid
cscheid deleted the feature/bd-hzsi-listing-template-migration-docs branch August 27, 2026 17:56
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.

3 participants