Skip to content

body-header/footer and margin-header/footer do not resolve /-prefixed paths as project-relative #14057

@mcanouil

Description

@mcanouil

Bug description

body-header, body-footer, margin-header, and margin-footer do not follow Quarto's path resolution convention for paths starting with /.

In Quarto, a leading / in a path means "relative to the project root" (similar to here::here() in R). However, these options treat /path as a filesystem-absolute path instead.

There are two distinct problems in the path resolution:

  1. expandMarkdownFilePath treats / as filesystem-absolute
    • function expandMarkdownFilePath(source: string, path: string): string {
      const absPath = isAbsolute(path) ? path : join(dirname(source), path);
      if (safeExistsSync(absPath)) {
      const fileContents = Deno.readTextFileSync(absPath);
      // If we are reading raw HTML, provide raw block indicator
      const ext = extname(absPath);
      if (ext === ".html") {
      return "```{=html}\n" + fileContents + "\n```";
      } else {
      return fileContents;
      }
      } else {
      return path;
      }
      }

The function that resolves file paths for all four options (body-header, body-footer, margin-header, margin-footer) uses isAbsolute(path) to decide whether to join with the source directory. When a path starts with /, it is treated as a filesystem-absolute path. Since the file does not exist at the filesystem root, the raw path string is inserted as markdown content instead of the file contents.

  1. resolveProjectPaths only applied to margin-header/margin-footer, not body-header/body-footer
    • const resolveProjectPaths = (maybePath: string) => {
      if (existsSync(maybePath)) {
      return join(projectDir, maybePath);
      } else {
      return maybePath;
      }
      };
      if (siteMeta[kMarginHeader]) {
      siteMeta[kMarginHeader] = ensureArray(siteMeta[kMarginHeader])?.map(
      resolveProjectPaths,
      );
      }
      if (siteMeta[kMarginFooter]) {
      siteMeta[kMarginFooter] = ensureArray(siteMeta[kMarginFooter])?.map(
      resolveProjectPaths,
      );
      }

The resolveProjectPaths helper in website-config.ts is only applied to margin-header and margin-footer. It is never applied to body-header or body-footer, meaning these two options skip the path resolution step entirely. Additionally, resolveProjectPaths itself has the same filesystem-root issue: it calls existsSync(maybePath) on the raw value, so /footer.html checks if /footer.html exists on the filesystem root rather than resolving it relative to the project directory.

Steps to reproduce

Create a Quarto website project with the following structure:

project/
├── _quarto.yml
├── _footer.html
├── index.qmd
└── pages/
    └── mypage.qmd

_footer.html:

<footer>Custom footer content</footer>

_quarto.yml:

project:
  type: website

website:
  title: "Demo"
  navbar:
    left:
      - href: index.qmd
        text: Home
      - href: pages/mypage.qmd
        text: My Page
  body-footer: /_footer.html

format:
  html:
    theme: cosmo

index.qmd:

---
title: "Home"
---

Home page content.

pages/mypage.qmd:

---
title: "My Page"
---

Page in subdirectory.

Then render with quarto render.

Actual behavior

  • With body-footer: /_footer.html (leading /): the literal string /_footer.html is rendered as text in the footer on all pages, because the path is treated as filesystem-absolute and the file is not found.
  • With body-footer: _footer.html (no leading /): works for index.qmd at the project root, but for pages/mypage.qmd the path resolves to pages/_footer.html (relative to the source file), which does not exist, so the literal string is rendered as text.

The same behaviour applies to body-header, margin-header, and margin-footer.

Expected behavior

  • body-footer: /_footer.html should resolve the path relative to the project root directory, following Quarto's convention for /-prefixed paths.
  • body-footer: _footer.html should also resolve relative to the project root (since it is set in _quarto.yml), or at least the documentation should clarify that relative paths are resolved relative to each source file.
  • All four options (body-header, body-footer, margin-header, margin-footer) should follow the same path resolution logic consistently.

Quarto check output

Quarto 99.9.9
[✓] Checking environment information...
      Quarto cache location: /Users/mcanouil/Library/Caches/quarto
[✓] Checking versions of quarto binary dependencies...
      Pandoc version 3.8.3: OK
      Dart Sass version 1.87.0: OK
      Deno version 2.4.5: OK
      Typst version 0.14.2: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
      Version: 99.9.9
      commit: eef6d945afbdafe9dcc619c10ff97bcb8910063c
      Path: /Users/mcanouil/Projects/quarto-dev/quarto-cli/package/dist/bin

[✓] Checking tools....................OK
      TinyTeX: v2026.02
      VeraPDF: 1.28.2
      Chromium: (not installed)
      Chrome Headless Shell: (not installed)

[✓] Checking LaTeX....................OK
      Using: TinyTex
      Path: /Users/mcanouil/Library/TinyTeX/bin/universal-darwin
      Version: 2025

[✓] Checking Chrome Headless....................OK
      Using: Chrome from QUARTO_CHROMIUM
      Path: /Applications/Brave Browser.app/Contents/MacOS/Brave Browser

[✓] Checking basic markdown render....OK

(-) Checking R installation...........ℹ R version 4.5.2 (2025-10-31)
! Config '~/.Rprofile' was loaded!
[✓] Checking R installation...........OK
      Version: 4.5.2
      Path: /Library/Frameworks/R.framework/Resources
      LibPaths:
        - /Users/mcanouil/Projects/quarto-dev/quarto-playground/renv/library/macos/R-4.5/aarch64-apple-darwin20
        - /Users/mcanouil/Library/Caches/org.R-project.R/R/renv/sandbox/macos/R-4.5/aarch64-apple-darwin20/4cd76b74
      knitr: 1.50
      rmarkdown: 2.30

[✓] Checking Knitr engine render......OK

[✓] Checking Python 3 installation....OK
      Version: 3.14.0
      Path: /Users/mcanouil/Projects/quarto-dev/quarto-playground/.venv/bin/python3
      Jupyter: 5.9.1
      Kernels: uv, julia-1.12, python3

[✓] Checking Jupyter engine render....OK

[✓] Checking Julia installation...

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthtmlIssues with HTML and related web technology (html/css/scss/js)layoutwebsitesIssues creating websites

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions