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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions