-
Notifications
You must be signed in to change notification settings - Fork 410
Description
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:
expandMarkdownFilePathtreats/as filesystem-absolutequarto-cli/src/project/types/website/website-navigation-md.ts
Lines 716 to 731 in eef6d94
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.
resolveProjectPathsonly applied tomargin-header/margin-footer, notbody-header/body-footerquarto-cli/src/project/types/website/website-config.ts
Lines 385 to 401 in eef6d94
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.
- Discussion: How to add body-footer containing several logos across all pages on Quarto website? #6566
- Issue
margin-header,body-header, etc. should document they allow HTML #13270 (documentation for these options)
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: cosmoindex.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.htmlis 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 forindex.qmdat the project root, but forpages/mypage.qmdthe path resolves topages/_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.htmlshould resolve the path relative to the project root directory, following Quarto's convention for/-prefixed paths.body-footer: _footer.htmlshould 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...