Skip to content
Back to skills

Ejs Listing Port

ASecurity

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

  • 265 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added August 30, 2026
documentationjavascriptgojavabashnodeexpressdocumentation

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned August 30, 2026

npx -y skills add quarto-dev/q2 --skill ejs-listing-port --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ejs Listing Port?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ejs Listing Port
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/quarto-dev-ejs-listing-port/badge)](https://www.skillsdirectory.com/skills/quarto-dev-ejs-listing-port)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ejs-listing-port
description: 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.
---

# 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 href
```

For images the same split costs **two** things — rewriting *and* copying:

```
![$it.image-alt$]($it.image$)     ✔ rewritten, and the file is copied
`<img src="$it.image$">`{=html}   ✘ neither: the src 404s in the deployed site
```

Raw 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. The built-in templates gate the whole
envelope on `$if(description)$` — so **copying the built-in shape loses
previews for precisely the items that need them.** A custom template can and
should do better.

## 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: custom` **and** `template:` are both required. `template:` alone
  warns `Q-12-7` and silently uses the built-in layout.
- Give the file a neutral extension (`.template`) so `Q-12-9` stops firing.

## Worked examples

- **The built-ins** — `crates/quarto-core/src/project/listing/templates/`
  (`item-default`, `item-grid`, `item-table`, and the `listing-*` wrappers).
  Short, idiomatic, and the reference for class names a custom template must
  match to inherit the listing CSS.
- **`references/worked-examples.md`** in 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 three
  `custom_template_*` tests. They pin both silent failure modes and the
  unconditional envelope, so they are executable documentation of the rules
  above.

## Procedure

1. **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 or `listing-item.extra`).
2. **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.
3. **Convert `<div class="…">` to `::: {.…}`.** Same output element, but the
   contents are markdown, so links and images inside resolve.
4. **Convert every anchor and image to markdown** (rule 1). Keep raw HTML
   only as link *text* or as genuinely inert markup.
5. **Add the description envelope unconditionally** (rule 2) if the listing
   wants previews.
6. **Guard every optional read with `$if$`** — an absent value warns
   `Q-12-10` rather than rendering blank.
7. **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**:

```bash
cargo run --bin q2 -- render <project>     # in q2; `quarto render .` for users
```

1. **Read the `href` values** in the listing's host page. Every link to a
   project document must end in `.html`. A surviving `.qmd` means that anchor
   is still raw HTML.
2. **Read the `src` values, then confirm each file exists** under the output
   directory. A `src` that 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.
3. **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.
4. **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.

Files in this skill

  • SKILL.md7.5 KB
  • references/worked-examples.md9.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.