Listings
listing:.
Contents
contents: names what a listing shows. Each entry is either a glob that
selects project documents, or an inline record — a small map that is an
item.
Globs
A glob resolves against the file it is written in — the listing page’s
own directory for front-matter patterns, the directory holding a
_metadata.yml, or the project root for patterns in _quarto.yml. *
matches one directory level; use **/ or name a directory to search
subdirectories. A leading / anchors at the project root from anywhere.
See Q-12-19 for the full rules and the
common ways a pattern matches nothing.
Records
listing:
id: cards
type: grid
contents:
- title: "Get started"
description: "Download and install"
image: images/rocket.png
- title: "Release notes"
path: https://example.com/releasesA record’s title, subtitle, description, author, date,
date-modified, image, image-alt, categories, order, path,
reading-time-minutes and word-count (and other fields besides) are the
same fields a document supplies. Any other key becomes a custom field a
custom template can read as $it.key$ (also $it.extra.key$) — which
also makes a misspelled field name easy to miss, since it’s kept rather
than rejected; watch for Q-12-22 if an
item is missing a field you thought you set. A record needs a title: or
a path: to render meaningfully — one with neither produces an
empty-titled item (Q-12-21). Relative
image: and path: values resolve against the file the listing is
written in.
Titles and descriptions are markdown
An item’s title, subtitle and description are markdown, whether
they come from a document’s front matter or from a record. Their
formatting shows in the listing as it does on the page: a title such as
Fix `_scope`: a *lexical* regression keeps its code span and its
emphasis. Links inside them are shown as plain text, since each item is
already a link, and footnotes are left out.
A listing shows each of them as a single paragraph. Write a
description: as one paragraph; one written as several (a YAML block
scalar with a blank line in it, or a list) is joined into one, with a
warning (Q-12-26).
Where descriptions and images come from
For an item that is a project document, the listing uses the first description it finds in this order:
description:underlisting-item:in the document’s front matter;description:;abstract:;- the first paragraph of the rendered page.
The image follows the same pattern: image: under listing-item:, then
image:, then the page’s preview image. The preview image is an image
with the class .preview-image, or else one whose file name contains
preview, feature, cover or thumbnail, or else the first image on
the page.
A value you write is used as written; Quarto derives one from the page
only when you didn’t. listing-item: is there for the case where the
listing should say something different from the page’s own
description: or image:. A description longer than the listing’s
max-description-length (default 175 characters; 0 turns the limit
off) is cut at a word boundary, whichever way it was chosen. Derived
descriptions appear in every layout, tables included.
The derived description is taken from the rendered page, so a page whose text comes from running code still gets one. In the browser-based editor, which does not do that last step, the derived description is the page’s first paragraph as written instead.
A record’s own description: and image: are always used as written.
A record that points at a document but leaves them out gets the
document’s, chosen as above.
Records that point at a document
path: guide/install.qmd merges the record over that document’s own
entry: the record’s fields win, the document supplies the rest (link,
description, date, …). A path: that is an external URL or a
non-document file is used as the link as written. A path: that looks
like a document (has a .qmd/.md/.Rmd/.ipynb extension) but names
one the project doesn’t render — a typo, or a file outside
project.render — builds the item from the record’s own fields alone
and keeps the link as written, and warns
(Q-12-20).
Order
Items are sorted by the listing’s sort:. With no sort: key, the
default is order asc, title asc — but that default applies only to a
listing that contains at least one document: an item matched by a
contents: glob, or a record whose path: names a document the project
renders.
A listing built entirely from records — the usual shape of a hand-curated card list — gets no default sort, and its items appear in the order you wrote them:
listing:
contents:
- title: "Get started" # renders first
- title: "Explore features"
- title: "Advanced usage"Add one glob, or one record whose path: points at a page in the
project, and the default sort applies to the whole listing again —
alphabetizing the records along with it, since records carry no order:
of their own. To keep such a list in the order you wrote it, set
sort: false, or give each record an explicit order:.
With sort: false, items appear in the order of the contents: entries
— records included — with one exception: when contents: holds only
negated patterns (e.g. ["!draft.qmd", {title: Featured}]), the implicit
“everything else” positive counts as declared before every entry, so a
record sorts after those items no matter where it’s written. (Quarto 1
placed all records after all glob matches.)
Fields
fields: lists which parts of each item a listing shows. In the default
and grid layouts it decides what each card draws:
listing:
type: grid
fields: [date, title, description]These cards show only a date, a title and a description. There is no
image area, and no author, subtitle, reading time or categories, even
for items that have them. Without fields:, a listing uses its type’s
default set and keeps only the fields at least one item has, plus
image. An item that is a project document always counts as having a
description and an image, since Quarto can derive both from the
rendered page.
image is the exception: when it’s in the set, every card gets an image
area. An item with no image of its own (see
Where descriptions and images come from)
gets the listing’s image-placeholder:, or an empty placeholder. If none of your items have images, leave image
out of fields: to avoid a row of empty boxes.
Not yet supported
YAML files in contents: (Quarto 1’s contents: items.yml) are skipped
with a warning (Q-12-23); move the
records inline.
Pages and categories in the browser
A default or grid listing with more items than its page-size:
shows one page at a time, with page links below it. The default page
size is 25 for default listings, 18 for grid and 30 for table.
When categories: is on, clicking a category — in the sidebar list or
on an item — narrows the listing to the items in that category, and a
“No matching items” message stands in when nothing matches. The current
page and category are kept in the address (#category=…), so a
filtered view can be linked to.
The listing’s element on the page has the id listing- followed by the
listing’s id:. A page’s only listing, when it sets no id:, is
#listing-listing.
Not yet supported:
tablelistings paginate and filter in Quarto 1, but not yet in Quarto 2: every row is shown.sort-uiandfilter-uicontrols are not rendered yet.customlistings are left as your template wrote them, without paging or category filtering in the browser.- The live preview — the default
q2 preview, and Quarto Hub — shows every item on one page.q2 renderandq2 preview --staticproduce the interactive page.
Custom templates
Beyond the built-in default, grid and table layouts, a listing can
render through a template you write:
listing:
id: cards
type: custom
template: card.template
contents: "guides/*.qmd"template: resolves relative to the page that declares the listing.
Quarto 2 custom listing templates are doctemplates — the
$variable$ syntax Pandoc templates use — not the EJS templates of
Quarto 1. A doctemplate cannot run code; it interpolates values,
branches on them and loops over them. The output is markdown, parsed
back into the page, so raw HTML goes in a ```{=html} block just as
it would in a .qmd file.
That last point has a consequence worth knowing before you write a template: paths inside raw HTML are not resolved. A link or image written as raw HTML keeps the path you gave it, so links to project documents ship unrewritten and images are never copied into the output — with no warning either way. Write links and images as markdown. See Listing templates for the idiom, and Paths in websites for the same rule as it applies to page content.
Syntax
| Directive | Meaning |
|---|---|
$listing.id$, $it.title$
|
Interpolate a value; dotted paths walk into maps. |
${it.title}
|
The same, brace-delimited — use it when the value is followed immediately by text that would otherwise run into the name. |
$for(items)$ … $endfor$
|
Loop. Inside the body the current item is $it$ (also $items$). Nested loops over a list-valued field work, and the inner $it$ shadows the outer.
|
$sep$
|
Inside a loop: emitted between items, not after the last. |
$if(it.image)$ … $elseif(it.icon)$ … $else$ … $endif$
|
Branch on truthiness (empty strings and missing keys are false). |
$it.title/uppercase$
|
Apply a pipe. Available: uppercase, lowercase, chomp, nowrap, alpha, roman, first, last, rest, allbutlast, length, pairs, reverse, left, center, right.
|
$it:item-default()$
|
Apply a partial to a value — here the built-in item-default partial, or a same-named .template file next to your template file.
|
$-- text
|
Comment; not rendered. |
$$
|
A literal dollar sign — on its own this is still literal text and does not make a file a template. |
A template that contains none of these is not a template: Quarto 2
warns (Q-12-24) and skips the listing
rather than paste the file into the page.
Values a template can read
listing.*— the listing’s own configuration:id,type,fields,field-links,table-header(a pre-rendered markdown header row for thetablelayout),sort-ui,filter-ui,page-size,grid-columns,template-params(anything undertemplate-params:in the YAML, passed through for your template’s own options).items— one entry per item, each withtitle,subtitle,description,author,authors,date,date-modified,image,image-alt,categories,path(the link target, already page-relative),filename,reading-time,reading-time-minutes,word-count,order, plusimage-html,category-htmlandtable-rowpre-rendered snippets. Custom fields from an inline record or from a document’slisting-item.extraare readable directly ($it.icon$) and under$it.extra.icon$.title,subtitleanddescriptionare markdown on one line, already escaped: use them where the template writes markdown (### [$it.title$]($it.path$)). Inside a raw HTML block, where markdown would show as source, use their rendered twinstitle-html,subtitle-htmlanddescription-htmlinstead.project.*—site-urlandtitlefrom the website configuration.
Only title is guaranteed present on every item. path is present
whenever the item has a link target — a document, or a record with
href: — and absent for a record with neither. The rest are absent
when the item lacks them, and reading an absent value warns with
Q-12-10 — so guard optional reads:
$if(it.subtitle)$…$endif$.
Four more per-item values exist for specific jobs:
outputHref— the item’s rendered output path. For feeds and cross-references, not for links: it is already resolved, so it bypasses the path rewriting that makespathwork.description-placeholder-begin/-endandimage-placeholder-begin/-end— envelope markers that let Quarto fill in a missing description or image after both documents render. See Listing templates.show.<field>— true for each field in the listing’s field set: itsfields:, or the type’s defaults (see Fields). Atype: customlisting withoutfields:uses every field that at least one item has, as in Quarto 1.metadata-attrs—data-index,data-categoriesanddata-listing-<field>-sortattributes, the equivalent of Quarto 1’smetadataAttrs(). Emit it in attribute position — inside a div’s braces (::: {.item $it.metadata-attrs$}) or on a tag in a```{=html}block; interpolated as markdown text its quotes are curled into invalid HTML.
These are the most useful values; the built-in templates show the rest.
The built-in layouts are themselves doctemplates and make the best
reference: listing-default.template, listing-grid.template,
listing-table.template and the item-default partial live under
crates/quarto-core/src/project/listing/templates/
in the Quarto 2 repository.
Example: a card grid
card.template:
::: {.custom-card-grid}
$for(items)$
::: {.custom-card}
### [$it.title$]($it.path$)
$if(it.description)$
$it.description$
$endif$
:::
$endfor$
:::
Style .custom-card-grid and .custom-card in your site’s CSS.
Going further
Listing templates covers the rest: the two rules that fail silently (links and images, the description envelope), what each built-in layout emits so your template can inherit its styling, and a worked port of a Quarto 1 EJS template.
If you are carrying a template over from Quarto 1, start there: Quarto 2
does not run EJS, and a carried-over template warns with
Q-12-9 at the extension and
Q-12-24 at the contents.
Listings as data
A doctemplate interpolates, branches and loops, and nothing more. When a
listing needs logic — grouping by year, a count, a layout that depends
on the items — write the markup in Lua instead. Every listing a page
declares is also published into the page’s metadata, resolved: the
items are matched, filtered, sorted and truncated exactly as the
rendered listing would show them, and a Lua filter or an
lmd cell reads them from meta.listings.<id>.
listing:
id: posts
type: metadata
contents: posts/*.qmd
sort: "date desc"
filters: [posts.lua]type: metadata is a listing that resolves its items and renders
nothing: no container, no “No matching items” placeholder, no paging
script. The markup is yours to build. Everything that reads the resolved
items rather than the rendered markup still works — the categories
sidebar, an RSS feed:, and the
dependency tracking that re-renders the page when a post changes.
The metadata is published for every listing type, not only
metadata, so a filter can also read the items of a listing the page
renders in the usual way.
The same route makes a listing interactive: build the no-script markup in
Lua and hand the items to a React component with quarto.component. See
Components.
What a filter reads
posts.lua:
function Pandoc(doc)
local items = doc.meta.listings.posts.items
local entries = pandoc.List()
for _, it in ipairs(items) do
entries:insert({ pandoc.Plain({ pandoc.Link(it.title, it.path),
pandoc.Str(" (" .. it.date .. ")") }) })
end
doc.blocks:insert(pandoc.BulletList(entries))
return doc
endmeta.listings.<id> is a map with the listing’s own settings (id,
type, fields, sort, page-size, max-items,
max-description-length, date-format, image-placeholder,
categories, template-params) and items, one entry per item. The
item keys are the ones a custom template
reads, in their natural Lua types rather than as text:
| Key | Type in Lua | Notes |
|---|---|---|
title, subtitle, description
|
Inlines
|
Markdown kept as inlines: pandoc.Link(it.title, it.path) keeps the title’s emphasis and code. Links and footnotes inside them are dropped, as the templates drop them. description is cut at the listing’s max-description-length.
|
description-source, image-source
|
string | Where the value came from: listing-item, document, abstract, record, derived, or absent.
|
path
|
string | The link target, relative to the page, as a .qmd source path. Put it in a pandoc.Link or pandoc.Image and Quarto rewrites it to the output URL, as it does for a template’s $it.path$.
|
output-href, filename
|
string | The rendered output path and the file name. |
date, date-modified
|
string | Display strings, formatted with the listing’s or the document’s date-format, as a template shows them.
|
date-iso, date-modified-iso
|
string | The stored values, for sorting and grouping. |
author
|
string | The authors joined with “,”. |
authors, categories
|
list of strings | Always present; empty when the item has none. |
image, image-alt
|
string | The image path and its alt text. |
order, word-count, reading-time-minutes
|
number | reading-time is the display string (“4 min read”).
|
origin
|
string | document, record, or record-over-document.
|
extra
|
table | Custom fields from an inline record or a document’s listing-item.extra. Only here, never mixed into the item itself.
|
Keys for values an item does not have are absent (it.subtitle is
nil), except the two lists, which are empty. The strings are plain
Lua strings, not markdown, so it.path and it.date can be
concatenated and compared; a YAML string in a document’s own front
matter would instead reach a filter as inlines.
Two things a rendered listing does that a filter has to do itself, or
go without. The description of an item that has none of its own is
the first paragraph of the item page as written; a rendered listing
upgrades it after both pages render, a filter gets the written form.
And the default and grid layouts’ paging and category filtering
run in the browser on markup the filter does not emit; a listing you
build is shown in full, as a custom listing is.
Links and images obey the same two rules as templates:
build them as pandoc.Link and pandoc.Image nodes with the path
and image values, not as raw HTML, or the targets are not rewritten
and the images are not copied.