Skip to content

Website og:site_name ignores open-graph.site-name, because the site-title handler overwrites the first matching meta tag #14917

Description

@mcanouil

Bug description

On a website page, open-graph.site-name set in the document front matter is ignored.
og:site_name always carries the website title instead.

The schema says the opposite.
It says the website or book title is used only when site-name is not explicitly provided in the open-graph metadata.

  • site-name:
    string:
    description:
    short: "Name that should be displayed for the overall site"
    long: |
    Name that should be displayed for the overall site. If not explicitly
    provided in the `open-graph` metadata, Quarto will use the website or
    book `title` by default.

The cause is that two passes build the tag, they disagree about which value wins, and the second one picks its target by position in the document rather than by ownership.

The first pass gets it right.
opengraphMetadata seeds site-name from the website title, then lets the merged site and document open-graph block overwrite it.
So the value handed to the tag writer is the document's site-name.

  • function opengraphMetadata(
    format: Format,
    ): Record<string, unknown> {
    const metadata: Record<string, unknown> = {};
    // populate defaults
    const openGraph = mergedSiteAndDocumentData(kOpenGraph, format);
    // Check the site for title
    const siteMeta = format.metadata[kWebsite] as Metadata;
    if (siteMeta && siteMeta[kTitle]) {
    metadata[kSiteName] = siteMeta[kTitle];
    }
    // Read open graph data in
    if (openGraph && typeof openGraph === "object") {
    [
    kTitle,
    kDescription,
    kImage,
    kImageAlt,
    kImageHeight,
    kImageWidth,
    kLocale,
    kSiteName,
    ].forEach((key) => {
    if (openGraph[key] !== undefined) {
    metadata[key] = openGraph[key];
    }
    });
    }
    return metadata;
    }
  • // Append the metadata
    Object.keys(metadata).forEach((key) => {
    if (metadata[key] !== undefined) {
    // Resolve the value
    const data = metadata[key] as string;
    const value = provider.resolveValue
    ? provider.resolveValue(key, data)
    : data;
    // Filter the key
    if (provider.filter) {
    key = provider.filter(key);
    }
    writeMetaTag(`${provider.prefix}:${key}`, value, doc);
    }
    });

The second pass runs the markdown pipeline over the same document, right after.

siteTitleMetaHandler renders the website title and pushes it into og:site_name every time.
It finds the tag with doc.querySelector, which returns the first og:site_name element in the document.
It never checks that the element is the one the first pass just wrote.

  • const siteTitleMetaHandler = {
    getUnrendered() {
    const siteMeta = format.metadata[kWebsite] as Metadata;
    if (siteMeta && siteMeta[kTitle]) {
    return { inlines: { [kMetaSideNameId]: siteMeta[kTitle] as string } };
    }
    },
    processRendered(rendered: Record<string, Element>, doc: Document) {
    const renderedEl = rendered[kMetaSideNameId];
    if (renderedEl) {
    // Write resolved title back into format.metadata so it flows
    // through to ProjectOutputFile.format for post-render consumers
    const siteMeta = format.metadata[kWebsite] as Metadata;
    if (siteMeta) {
    siteMeta[kTitle] = renderedEl.innerText;
    }
    // Update the og:site_name meta tag
    const el = doc.querySelector(
    `meta[property="og:site_name"]`,
    );
    if (el) {
    el.setAttribute("content", renderedEl.innerText);
    }
    }
    },
    };

On an ordinary page there is only one og:site_name tag, so the overwrite lands on Quarto's own tag and the document's site-name disappears without a word.
Put any earlier og:site_name tag in the head, through include-in-header for instance, and the overwrite lands on that tag instead.
Quarto's own tag is then left carrying the document site-name, which shows what the first pass really wrote.

titleMetaHandler selects og:title and twitter:title the same way, and descriptionMetaHandler does the same for og:description and twitter:description.

  • // Process social metadata titles
    [{
    key: kOgTitle,
    sel: 'meta[property="og:title"]',
    }, {
    key: kTwitterTitle,
    sel: 'meta[name="twitter:title"]',
    }].forEach(
    (obj) => {
    const renderedObjEl = rendered[obj.key];
    if (renderedObjEl) {
    const metaEl = doc.querySelector(obj.sel);
    if (metaEl) {
    metaEl.setAttribute("content", renderedObjEl.innerText);
    }
    }
    },
    );
  • processRendered(rendered: Record<string, Element>, doc: Document) {
    // Meta values
    const metaVals = [{
    sel: 'meta[property="og:description"]',
    key: kOgDesc,
    }, { sel: 'meta[name="twitter:description"]', key: kTwitterDesc }];
    metaVals.forEach((metaVal) => {
    const renderedEl = rendered[metaVal.key];
    if (renderedEl) {
    const metaEl = doc.querySelector(metaVal.sel);
    if (metaEl) {
    metaEl.setAttribute("content", renderedEl.innerText);
    }
    }
    });
    },

Those four never show a wrong value, because both passes work them out from the same expression.
They do still write into a tag the project did not create, so a page that supplies its own social tags has them rewritten.

This matters for any extension or template that writes Open Graph tags of its own.
The effect is easy to misread as the extension corrupting Quarto's output.

Steps to reproduce

Create a stock website project.

quarto create project website ogsite --no-open --no-prompt

Set the site title in _quarto.yml and leave the rest of the scaffold alone.

project:
  type: website

website:
  title: "SITE TITLE"
  navbar:
    left:
      - href: index.qmd
        text: Home
      - about.qmd

format:
  html:
    theme:
      - cosmo
      - brand
    css: styles.css
    toc: true

Replace index.qmd.

---
title: "Home"
open-graph:
  site-name: "DOCUMENT SITE NAME"
---

`og:site_name` in the head of this page should be `DOCUMENT SITE NAME`.

Replace about.qmd with the same page, plus one hand-written tag in the head.

---
title: "About"
open-graph:
  site-name: "DOCUMENT SITE NAME"
format:
  html:
    include-in-header:
      - text: |
          <meta property="og:site_name" content="DECOY">
---

The same page, plus one hand-written `og:site_name` tag placed in the head before
Quarto writes its own.

Render, then read the tags back.

quarto render
grep -n '<meta property="og:site_name' _site/index.html _site/about.html

Actual behavior

_site/index.html:81:<meta property="og:site_name" content="SITE TITLE">
_site/about.html:76:<meta property="og:site_name" content="SITE TITLE">
_site/about.html:82:<meta property="og:site_name" content="DOCUMENT SITE NAME">

On index.qmd the document's site-name never appears at all.

On about.qmd the decoy tag receives SITE TITLE, even though a header include wrote it and it never held the website title.
Quarto's own tag keeps DOCUMENT SITE NAME, which is what it was written with.

Expected behavior

og:site_name should carry DOCUMENT SITE NAME on both pages, since both set open-graph.site-name explicitly.

The website title should only be the fallback, as the schema describes.

A tag the project did not write should also be left alone.
Selecting the element by ownership, or narrowing the selection to the tag the first pass created, would fix both halves.

Your environment

  • IDE: none, run from the terminal.
  • OS: macOS 26.6.2 on Apple Silicon.

It also reproduces on a build of main at a8bfe40, which is where the line references above point.

Quarto check output

$ quarto check
Quarto 1.10.18
[✓] Checking environment information...
      Quarto cache location: /Users/mcanouil/Library/Caches/quarto
[✓] Checking versions of quarto binary dependencies...
      Pandoc version 3.10.0: OK
      Dart Sass version 1.101.0: OK
      Deno version 2.7.14: OK
      Typst version 0.15.1: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
      Version: 1.10.18
      Path: /Applications/Positron.app/Contents/Resources/app/quarto/bin

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

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

[✓] 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...........OK
      Version: 4.6.1
      Path: /Library/Frameworks/R.framework/Versions/4.6/Resources
      LibPaths:
        - /Users/mcanouil/Library/R/arm64/4.6/library
        - /Library/Frameworks/R.framework/Versions/4.6/Resources/library
      knitr: 1.51
      rmarkdown: 2.31

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

[✓] Checking Python 3 installation....OK
      Version: 3.9.6
      Path: /Library/Developer/CommandLineTools/usr/bin/python3
      Jupyter: (None)

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

[✓] Checking Julia installation...

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmetadataIssues involving metadata resolution in quarto-cli cells, documents, and projects.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions