A quarto preview session resolves _brand.yml once and caches the result for the lifetime of the process. Adding, removing, or changing a sibling _brand.yml while preview is running has no effect on the output until preview is stopped and started again.
Reproduction
Start from a single document with no _brand.yml next to it:
---
title: "Report"
format: typst
---
Voir la [documentation](https://example.com).
quarto preview report.qmd --to typst --no-watch-inputs --no-browser
Now add a _brand.yml in the same directory:
color:
palette:
imperial-red: "#BC1E22"
primary: imperial-red
Trigger a re-render (request a render, or edit and save the document). The link keeps its default color. Stop preview and render again and the brand is applied — the link turns red. The reverse case behaves the same way: start with _brand.yml present, remove it, and the brand stays applied until preview is restarted.
This is also what shows up from the RStudio IDE. The Render button runs quarto preview ... --no-watch-inputs and routes the following renders through the same long-lived preview server, so creating a _brand.yml and clicking Render again produces no visible change.
Cause
The preview server keeps a single ProjectContext for the whole session. projectResolveBrand populates project.brandCache on the first resolve and returns it on every subsequent call without re-scanning for _brand.yml:
|
if (fileName === undefined) { |
|
if (project.brandCache) { |
|
return project.brandCache.brand; |
|
} |
|
project.brandCache = {}; |
|
let fileNames = [ |
|
"_brand.yml", |
|
"_brand.yaml", |
|
"_brand/_brand.yml", |
|
"_brand/_brand.yaml", |
|
].map((file) => join(project.dir, file)); |
|
const brand = (project?.config?.brand ?? |
|
project?.config?.project.brand) as |
|
| boolean |
|
| string |
|
| { |
|
light?: string; |
|
dark?: string; |
|
}; |
|
if (brand === false) { |
|
project.brandCache.brand = undefined; |
|
return project.brandCache.brand; |
|
} |
|
if ( |
|
typeof brand === "object" && brand && |
|
("light" in brand || "dark" in brand) |
|
) { |
|
project.brandCache.brand = { |
|
light: brand.light |
|
? await loadSingleBrand(resolveBrandPath(brand.light, project.dir)) |
|
: undefined, |
|
dark: brand.dark |
|
? await loadSingleBrand(resolveBrandPath(brand.dark, project.dir)) |
|
: undefined, |
|
enablesDarkMode: !!brand.dark, |
|
}; |
|
return project.brandCache.brand; |
|
} |
|
if (typeof brand === "string") { |
|
fileNames = [join(project.dir, brand)]; |
|
} |
|
|
|
for (const brandPath of fileNames) { |
|
if (!existsSync(brandPath)) { |
|
continue; |
|
} |
|
project.brandCache.brand = await loadUnifiedBrand(brandPath); |
|
} |
|
return project.brandCache.brand; |
Re-renders invalidate the per-file fileInformationCache but never brandCache:
|
if (project?.fileInformationCache) { |
|
project.fileInformationCache.invalidateForFile(file); |
|
} |
A separate quarto render process resolves brand correctly because it builds a fresh ProjectContext; only the long-lived preview process is affected. Single-file and project (_quarto.yml) previews are both affected since they share the same cache field, and the behavior is format-independent (projectResolveBrand takes no format).
Suggestion
We could give brandCache a fingerprint over the candidate brand paths (_brand.yml, _brand.yaml, _brand/_brand.yml, _brand/_brand.yaml) covering existence and modification time, and re-resolve when it changes — similar to the mtime guard already used for the full-markdown cache.
A
quarto previewsession resolves_brand.ymlonce and caches the result for the lifetime of the process. Adding, removing, or changing a sibling_brand.ymlwhile preview is running has no effect on the output until preview is stopped and started again.Reproduction
Start from a single document with no
_brand.ymlnext to it:Now add a
_brand.ymlin the same directory:Trigger a re-render (request a render, or edit and save the document). The link keeps its default color. Stop preview and render again and the brand is applied — the link turns red. The reverse case behaves the same way: start with
_brand.ymlpresent, remove it, and the brand stays applied until preview is restarted.This is also what shows up from the RStudio IDE. The Render button runs
quarto preview ... --no-watch-inputsand routes the following renders through the same long-lived preview server, so creating a_brand.ymland clicking Render again produces no visible change.Cause
The preview server keeps a single
ProjectContextfor the whole session.projectResolveBrandpopulatesproject.brandCacheon the first resolve and returns it on every subsequent call without re-scanning for_brand.yml:quarto-cli/src/project/project-shared.ts
Lines 586 to 634 in 16257ef
Re-renders invalidate the per-file
fileInformationCachebut neverbrandCache:quarto-cli/src/command/preview/preview.ts
Lines 465 to 467 in 16257ef
A separate
quarto renderprocess resolves brand correctly because it builds a freshProjectContext; only the long-lived preview process is affected. Single-file and project (_quarto.yml) previews are both affected since they share the same cache field, and the behavior is format-independent (projectResolveBrandtakes no format).Suggestion
We could give
brandCachea fingerprint over the candidate brand paths (_brand.yml,_brand.yaml,_brand/_brand.yml,_brand/_brand.yaml) covering existence and modification time, and re-resolve when it changes — similar to the mtime guard already used for the full-markdown cache.