Skip to content

environment variables not substituted with empty title #13550

Open

Description

@aronatkins

I have:

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

Bug description

This is related to #7754.

When the Quarto website title includes the environment variable shortcode and the (not default) page does not include a title, substitution does not occur.

This is a regression from Quarto 1.4.

Steps to reproduce

project:
  type: website

website:
  title: "{{< env SITE_TITLE >}}"
---
title: "index"
---

this is index.qmd
---
title: ""
---

this is about.qmd
 SITE_TITLE="hiya" ~/quarto/quarto-1.4.557-macos/bin/quarto render --output-dir _site_1.4
 SITE_TITLE="hiya" ~/quarto/quarto-1.5.57-macos/bin/quarto render --output-dir _site_1.5
 SITE_TITLE="hiya" ~/quarto/quarto-1.6.43-macos/bin/quarto render --output-dir _site_1.6
 SITE_TITLE="hiya" ~/quarto/quarto-1.7.34-macos/bin/quarto render --output-dir _site_1.7
 SITE_TITLE="hiya" ~/quarto/quarto-1.8.25-macos/bin/quarto render --output-dir _site_1.8

Looking across all of the generated HTML:

grep '<title' _site*/about.html

Only Quarto 1.4 replaces the environment variable.

_site_1.4/about.html:<title>hiya -</title>
_site_1.5/about.html:<title>about – {{&lt; env SITE_TITLE &gt;}}</title>
_site_1.6/about.html:<title>about – {{&lt; env SITE_TITLE &gt;}}</title>
_site_1.7/about.html:<title>about – {{&lt; env SITE_TITLE &gt;}}</title>
_site_1.8/about.html:<title>about – {{&lt; env SITE_TITLE &gt;}}</title>

To workaround, use title-prefix and pagetitle.

---
title-prefix: ""
pagetitle: '{{< env SITE_TITLE >}} the fix'
---

this is about.qmd

Actual behavior

Environment variables are not substituted.

Expected behavior

Environment variables are not substituted.

Your environment

Tested with multiple Quarto versions (see above).

Quarto check output

Quarto 1.8.25
[✓] Checking environment information...
      Quarto cache location: /Users/aron/Library/Caches/quarto
[✓] Checking versions of quarto binary dependencies...
      Pandoc version 3.6.3: OK
      Dart Sass version 1.87.0: OK
      Deno version 2.3.1: OK
      Typst version 0.13.0: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
      Version: 1.8.25
      Path: /Users/aron/quarto/quarto-1.8.25-macos/bin

[✓] Checking tools....................OK
      TinyTeX: (not installed)
      Chromium: (not installed)

[✓] Checking LaTeX....................OK
      Using: Installation From Path
      Path: /Library/TeX/texbin
      Version: 2024

[✓] Checking Chrome Headless....................OK
      Using: Chrome found on system
      Path: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
      Source: MacOS known location

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

[✓] Checking Python 3 installation....OK
      Version: 3.12.2
      Path: /opt/homebrew/opt/python@3.12/bin/python3.12
      Jupyter: (None)

      Jupyter is not available in this Python installation.
      Install with python3 -m pip install jupyter

[✓] Checking R installation...........OK
      Version: 4.3.3
      Path: /Library/Frameworks/R.framework/Resources
      LibPaths:
        - /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/library
      knitr: 1.50
      rmarkdown: 2.29

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

Activity

  1. self-assigned this
    on Oct 14, 2025
  2. modified the milestones: v1.9, Hot-fix on Oct 14, 2025
  3. cderv commented on Oct 17, 2025

    @cderv
    Member

    Looking into that I found

    we do retrieve the unprocessed website title to pass it as Pandoc metadata to use in template

    // title prefix if the project has a title and this isn't the home page
    const title = websiteTitle(project.config);
    if (title) {
    extras.pandoc = {
    [kTitlePrefix]: title,
    };
    }

    So we always set a title-prefix. But we do resolve pagetitle later too

    // resolve some title variables
    const title = allDefaults?.[kVariables]?.[kTitle] ||
    options.format.metadata[kTitle];
    const pageTitle = allDefaults?.[kVariables]?.[kPageTitle] ||
    options.format.metadata[kPageTitle];
    const titlePrefix = allDefaults?.[kTitlePrefix];
    // provide default page title if necessary
    if (!title && !pageTitle && isHtmlFileOutput(options.format.pandoc)) {
    const [_dir, stem] = dirAndStem(options.source);
    args.push(
    "--metadata",
    `pagetitle:${pandocAutoIdentifier(stem, false)}`,
    );
    }
    // don't ever duplicate pagetite/title and title-prefix
    if (
    (pageTitle !== undefined && pageTitle === titlePrefix) ||
    (pageTitle === undefined && title === titlePrefix)
    ) {
    delete allDefaults[kTitlePrefix];
    }

    Specifically when there is no title and not pagetitle we do set pagetitle metadata

    // provide default page title if necessary
    if (!title && !pageTitle && isHtmlFileOutput(options.format.pandoc)) {
    const [_dir, stem] = dirAndStem(options.source);
    args.push(
    "--metadata",
    `pagetitle:${pandocAutoIdentifier(stem, false)}`,
    );
    }

    which I believe will have an impact. However, for both files we have the unprocessed shortcode.

    So processing of shortcode in title happens elsewhere. And this is in the Markdown pipeline logic.

    The titles metadate are also handled in a Markdown pipeline, prbably so that shortcode is rendered

    function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
    const resolvedTitle = computePageTitle(format);

    This is setup before the previous processing I shared, and the resolved after within this markdownpipeline

    processRendered(rendered: Record<string, Element>, doc: Document) {
    const renderedEl = rendered[kMetaTitleId];
    if (renderedEl) {
    // Update the document title
    const el = doc.querySelector(
    `head title`,
    );
    if (el && el.innerText === resolvedTitle) {
    if (format.pandoc[kNumberSections] === false) {
    // Remove chapter numbers if not numbered
    const numberEl = renderedEl.querySelector("span.chapter-number");
    if (numberEl) {
    numberEl.remove();
    }
    // Collapse contiguous whitespace
    const collapsed = renderedEl.innerText.replaceAll(/\s+/g, " ");
    el.innerHTML = collapsed;
    } else {
    el.innerHTML = renderedEl.innerText;
    }
    }
    }

    The issue seems to be in there.

    The first problem I see is at this comparaison

    if (el && el.innerText === resolvedTitle) {

    el.innerText
    'about – {{< env SITE_TITLE >}}'
    resolvedTitle
    ' – {{< env SITE_TITLE >}}'
    

    The rendered document as the about pagetitle we added as metadata and that applied in template

    <title>$pagetitle$$if(title-prefix)$ – $title-prefix$$endif$</title>

    but the computed is different

    const resolvedTitle = computePageTitle(format);

    So we don't go to replace the shortcode in the HTML by its resolved version from the markdown pipeline

    } else {
    el.innerHTML = renderedEl.innerText;
    }

    renderedEl.innerText
    '– hiya'
    

    So at the end this is about the pagetitle handling. we must have modified something in 1.5 that changed this. I'll keep looking

  4. self-assigned this
    on Oct 17, 2025
  5. cderv commented on Oct 17, 2025

    @cderv
    Member

    This is the bad commit: c8676ca and change is about exactly the line found

    if (el && el.innerText === resolvedTitle) {

    This was done to fix

    So this needs to be fixed while not breaking #7318 fix

  6. cderv commented on Oct 17, 2025

    @cderv
    Member

    So this needs to be fixed while not breaking #7318 fix

    I have added a regression test for #7318 in #13576 so that we are covered

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions