Skip to content

Listing contents glob silently matches files in sibling subdirectories (smart-glob **/ prefix leaks across the project) #14512

@MilkClouds

Description

@MilkClouds

I have:

  • searched the issue tracker for similar issues
  • installed the latest version of Quarto CLI (1.9.37)
  • formatted my issue following the Bug Reports guide

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:

project:
  type: website

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglistings

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions