ejs-listing-port
Skill302 starsChanged 2 months agoMITView on GitHub
Port a Quarto 1 EJS custom listing template to a Quarto 2 doctemplate, or write a new one. Use when a render warns Q-12-7 ("template: was set but type: is not custom"), Q-12-9 (".ejs" / ".ejs.md" extension), Q-12-24 ("template contains no doctemplate directives"), or Q-12-10 ("Undefined variable"); when a listing renders with the built-in layout instead of the custom one, or dumps the template verbatim into the page; when listing links point at .qmd files or listing images 404; or when writing any custom listing template for Quarto 2.
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- autonomously-fix-small-issueSkill
- braidSkill
- investigate-beadsSkill
- preview-render-paritySkill
- reader-expectations-proseSkill
- snyk-prSkill
- triageSkill
- upgrade-cargo-depsSkill
- upgrade-vendored-depsSkill
- weekly-summarySkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 163
After porting, render the project and inspect the output:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
ejs-listing-port Skill
Quarto 1 custom listing templates were EJS — embedded JavaScript. Quarto 2
does not run EJS: custom listing templates are doctemplates, the
$variable$ / $if(…)$ / $for(…)$ syntax Pandoc templates use, so a
template can never execute code.
The syntax translation is mechanical and self-announcing — a half-finished port either warns or looks obviously wrong. The two things that make ports fail are not syntax, and both are silent: no error, no warning, and no visible difference in the template's text. State them out loud before you start, or you will skip them.
The two silent failure modes
1. Links and images must be markdown, never raw HTML
A template's output is markdown, re-parsed into the page. Only then are
paths resolved: LinkRewriteTransform rewrites .qmd link targets to output
URLs, and resource collection notes every image so the file gets copied.
Both walk parsed markdown nodes. Block::RawBlock and Inline::RawInline
are no-op leaves in both
(crates/quarto-core/src/transforms/link_rewrite.rs,
.../resource_collector.rs), so a path inside a raw-HTML attribute is
invisible to each.
$it.path$ is deliberately a source path, which is what makes the markdown
form work:
[$it.title$]($it.path$) ✔ becomes href="….html"
`<a href="$it.path$">`{=html} ✘ ships a dead .qmd hrefFor images the same split costs two things — rewriting and copying:
 ✔ rewritten, and the file is copied
`<img src="$it.image$">`{=html} ✘ neither: the src 404s in the deployed siteItem prose follows the same split. $it.title$, $it.subtitle$ and
$it.description$ are markdown (formatting kept, special characters
escaped); inside a raw HTML block use their rendered twins:
### [$it.title$]($it.path$) ✔ markdown in markdown
`<span class="t">$it.title-html$</span>`{=html} ✔ HTML in HTML
`<span class="t">$it.title$</span>`{=html} ✘ shows `Fix \_scope` sourceRaw HTML is fine inside the link text — the built-ins do exactly this with
[`$image-html$`{=html}]($path$). Anchor markdown, contents raw.
This is settled design, not a gap: Quarto 2 emits HTML from the AST and will
not parse HTML you author (claude-notes/plans/2026-04-24-websites-phase-6.md
Decision 1; claude-notes/plans/2026-08-13-site-root-relative-paths.md
Case C). Do not propose an HTML post-processor. Make the markdown path
obvious instead.
Why this survives review. The image failure hides itself whenever the
image is an item document's own front-matter image: — that page's render
copies the file regardless, so a raw-<img> template looks perfect. It only
bites for inline-record fields and custom fields, which is exactly where a
ported gallery or card grid gets its thumbnails.
2. The description placeholder envelope must be unconditional
Quarto 1 auto-filled a missing item description from the first paragraph of
the rendered item page. Quarto 2 does the same, via a post-render
substitution — but only inside the
description-placeholder-begin / -end markers, and only if they are
emitted outside the $if$:
::: {.listing-description}
```{=html}
$it.description-placeholder-begin$
```
$if(it.description)$
$it.description$
$endif$
```{=html}
$it.description-placeholder-end$
```
:::The markers delimit the region to substitute, so they must exist for
exactly the items the $if$ skips. They are empty strings for an item whose
description the author wrote (listing-item.description, description:,
abstract:), so emitting them unconditionally never overrides an authored
description; for a derived one, $it.description$ holds the pre-render
first paragraph and the markers let the post-render pass replace it. The
built-in default and grid templates follow this shape (grid puts the
envelope inside the description link — see docs/guides/projects/listing-templates.qmd).
Read this first
docs/guides/projects/listing-templates.qmd in the q2 repo is the
authoritative treatment: both rules above, the doctemplate language subset,
what each built-in layout emits, the Q1 → Q2 mapping table, a full worked
before/after port, and what doctemplates cannot do. Its sibling
docs/guides/projects/listings.qmd (§"Custom templates") carries the syntax
table and the per-item value list.
Canonical spellings, since Q1 habits produce the wrong ones:
$it.<key>$inside$for(items)$. ($items.<key>$is an accepted alias; prefer$it$.) Inside a partial applied to an item, keys are bare:$title$.type: customandtemplate:are both required.template:alone warnsQ-12-7and silently uses the built-in layout.- Give the file a neutral extension (
.template) soQ-12-9stops firing.
Worked examples
- The built-ins —
crates/quarto-core/src/project/listing/templates/(item-default,item-grid,item-table, and thelisting-*wrappers). Short, idiomatic, and the reference for class names a custom template must match to inherit the listing CSS. references/worked-examples.mdin this skill — three annotated ports in increasing difficulty, including the phrasing-content constraint that bites card grids.- The in-repo contract tests —
crates/quarto-core/tests/integration/listing_pipeline.rs, the threecustom_template_*tests. They pin both silent failure modes and the unconditional envelope, so they are executable documentation of the rules above.
Procedure
- Read the Q1 template and inventory what it does beyond
interpolation: helper calls, JS prologues, expressions, nested loops,
grouping. Those need decisions, not translation — see "What doctemplates
cannot do" in the guide (JS constants →
template-params:; string manipulation → pre-computed record key orlisting-item.extra). - Drop the outer
{=html}fence. A Q1 listing template is usually one big raw-HTML block; that fence is what makes every path inside it invisible. Removing it is most of the port. - Convert
<div class="…">to::: {.…}. Same output element, but the contents are markdown, so links and images inside resolve. - Convert every anchor and image to markdown (rule 1). Keep raw HTML only as link text or as genuinely inert markup.
- Add the description envelope unconditionally (rule 2) if the listing wants previews.
- Guard every optional read with
$if$— an absent value warnsQ-12-10rather than rendering blank. - Verify against the rendered output (below). Do not stop at "it renders."
Verification — required, and not optional
Neither silent failure produces a diagnostic, and neither changes the template's text in a way review catches. A passing render proves nothing. After porting, render the project and inspect the output:
cargo run --bin q2 -- render <project> # in q2; `quarto render .` for users- Read the
hrefvalues in the listing's host page. Every link to a project document must end in.html. A surviving.qmdmeans that anchor is still raw HTML. - Read the
srcvalues, then confirm each file exists under the output directory. Asrcthat looks right with no file behind it is a raw<img>— this is the check that catches the masked case, and the one people skip. - Check an item with no front-matter
description:. It should show a first-paragraph preview. Nothing shown means the envelope is inside the$if$instead of around it. - Grep the render output for
Q-12-. Any warning here is real.
Report what you inspected, not just that the render succeeded. If you could not check one of the four, say which.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

