I have:
Bug description
A listing configured with contents: "posts/*.qmd" (or any non-/-anchored path/glob) silently matches files in any directory named posts/ anywhere in the project, not just the posts/ directory adjacent to the listing page.
This breaks the natural use case of a multi-section / bilingual website where two listings live at different depths and each owns its own posts/ subdirectory.
Likely related (but a different symptom of the same path-handling area): #13677.
Steps to reproduce
Minimal project:
project/
├── _quarto.yml # project.type: website
├── index.qmd # listing.contents: "posts/*.qmd"
├── posts/foo.qmd
└── ko/
├── index.qmd # listing.contents: "posts/*.qmd"
└── posts/foo.qmd
_quarto.yml:
index.qmd:
---
title: EN
listing:
contents: "posts/*.qmd"
---
ko/index.qmd:
---
title: KO
listing:
contents: "posts/*.qmd"
---
Each posts/foo.qmd and ko/posts/foo.qmd just contains a title + date.
Render and inspect _site/listings.json (or open the rendered index.html). The English listing in index.html includes both /posts/foo.html and /ko/posts/foo.html.
quarto render index.qmd --log-level debug confirms:
[listing] Contents: posts/*.qmd
[listing] matches 2 files:
[listing] Reading file .../project/posts/foo.qmd
[listing] Reading file .../project/ko/posts/foo.qmd ← unexpected
Expected behavior
contents: "posts/*.qmd" from a listing in index.qmd should match only dirname(index.qmd)/posts/*.qmd — i.e., posts under the listing's own directory. Cross-section matching is surprising for the multi-section / i18n use case.
Actual behavior
The glob posts/*.qmd is silently rewritten to **/posts/*.qmd, so it also matches ko/posts/foo.qmd.
Root cause
In src/core/path.ts, resolveGlobs.asFullGlob:
if (!glob.startsWith("/")) {
if (smartGlob && (!options || !options.explicitSubfolderSearch)) {
return "**/" + glob; // prepends `**/` to relative globs
}
...
}
useSmartGlobs() returns true by default when no options is passed.
In src/project/types/website/listing/website-listing-read.ts, filterListingFiles calls resolvePathGlobs(...) (and filterPaths(...)) without GlobOptions, so smartGlob is true and every relative entry in contents is silently widened to **/<glob>. This is the design that breaks parallel-section listings.
GlobOptions.explicitSubfolderSearch already exists in path.ts precisely for this case (per its inline comment: "set this to true to never prepend **/ to globs to create nested directory searching") — it's just never wired through from the YAML.
Workaround
Project-rooted leading-/ paths work for a listing whose directory equals the project root:
listing:
contents: "/posts/*.qmd"
asFullGlob strips the leading / and skips the **/ prefix. But this is asymmetric: from a non-root listing (e.g. ko/index.qmd) the /-prefix is resolved against project.dir in expandGlob and against dirname(source) in resolvePathGlobs, so the path doubles up and matches nothing. The remaining workarounds are:
- rename one of the colliding directories (e.g.
ko/articles/ instead of ko/posts/),
- enumerate files explicitly in
contents (brittle, and also fails when basenames collide across sections — same **/-prefix bug).
Suggested fix
Expose the existing internal explicitSubfolderSearch knob as a per-listing YAML option, preserving today's default (smart-glob true). E.g.:
listing:
contents: "posts/*.qmd"
recursive: false # anchored to dirname(listing.qmd); does not match sibling subdirs
Happy to send a PR (small: ~25 lines across website-listing-shared.ts, website-listing-read.ts, and the website-listing schema in definitions.yml). Naming open to maintainer preference (recursive / anchored / match-subfolders / …).
Your environment
- Quarto CLI 1.9.37
- Linux (Ubuntu 22.04, kernel 5.15)
- Project type:
website
I have:
Bug description
A listing configured with
contents: "posts/*.qmd"(or any non-/-anchored path/glob) silently matches files in any directory namedposts/anywhere in the project, not just theposts/directory adjacent to the listing page.This breaks the natural use case of a multi-section / bilingual website where two listings live at different depths and each owns its own
posts/subdirectory.Likely related (but a different symptom of the same path-handling area): #13677.
Steps to reproduce
Minimal project:
_quarto.yml:index.qmd:ko/index.qmd:Each
posts/foo.qmdandko/posts/foo.qmdjust contains a title + date.Render and inspect
_site/listings.json(or open the renderedindex.html). The English listing inindex.htmlincludes both/posts/foo.htmland/ko/posts/foo.html.quarto render index.qmd --log-level debugconfirms:Expected behavior
contents: "posts/*.qmd"from a listing inindex.qmdshould match onlydirname(index.qmd)/posts/*.qmd— i.e., posts under the listing's own directory. Cross-section matching is surprising for the multi-section / i18n use case.Actual behavior
The glob
posts/*.qmdis silently rewritten to**/posts/*.qmd, so it also matchesko/posts/foo.qmd.Root cause
In
src/core/path.ts,resolveGlobs.asFullGlob:useSmartGlobs()returnstrueby default when nooptionsis passed.In
src/project/types/website/listing/website-listing-read.ts,filterListingFilescallsresolvePathGlobs(...)(andfilterPaths(...)) withoutGlobOptions, sosmartGlobistrueand every relative entry incontentsis silently widened to**/<glob>. This is the design that breaks parallel-section listings.GlobOptions.explicitSubfolderSearchalready exists inpath.tsprecisely for this case (per its inline comment: "set this to true to never prepend**/to globs to create nested directory searching") — it's just never wired through from the YAML.Workaround
Project-rooted leading-
/paths work for a listing whose directory equals the project root:asFullGlobstrips the leading/and skips the**/prefix. But this is asymmetric: from a non-root listing (e.g.ko/index.qmd) the/-prefix is resolved againstproject.dirinexpandGloband againstdirname(source)inresolvePathGlobs, so the path doubles up and matches nothing. The remaining workarounds are:ko/articles/instead ofko/posts/),contents(brittle, and also fails when basenames collide across sections — same**/-prefix bug).Suggested fix
Expose the existing internal
explicitSubfolderSearchknob as a per-listing YAML option, preserving today's default (smart-globtrue). E.g.:Happy to send a PR (small: ~25 lines across
website-listing-shared.ts,website-listing-read.ts, and thewebsite-listingschema indefinitions.yml). Naming open to maintainer preference (recursive/anchored/match-subfolders/ …).Your environment
website