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.
|
// Process any pipelined markdown |
|
pipeline.processRenderedMarkdown(doc); |
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...
Bug description
On a website page,
open-graph.site-nameset in the document front matter is ignored.og:site_namealways carries the website title instead.The schema says the opposite.
It says the website or book
titleis used only whensite-nameis not explicitly provided in theopen-graphmetadata.quarto-cli/src/resources/schema/definitions.yml
Lines 649 to 656 in a8bfe40
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.
opengraphMetadataseedssite-namefrom the website title, then lets the merged site and documentopen-graphblock overwrite it.So the value handed to the tag writer is the document's
site-name.quarto-cli/src/project/types/website/website-meta.ts
Lines 240 to 271 in a8bfe40
quarto-cli/src/project/types/website/website-meta.ts
Lines 211 to 227 in a8bfe40
The second pass runs the markdown pipeline over the same document, right after.
quarto-cli/src/project/types/website/website-meta.ts
Lines 230 to 231 in a8bfe40
siteTitleMetaHandlerrenders the website title and pushes it intoog:site_nameevery time.It finds the tag with
doc.querySelector, which returns the firstog:site_nameelement in the document.It never checks that the element is the one the first pass just wrote.
quarto-cli/src/project/types/website/website-meta.ts
Lines 499 to 524 in a8bfe40
On an ordinary page there is only one
og:site_nametag, so the overwrite lands on Quarto's own tag and the document'ssite-namedisappears without a word.Put any earlier
og:site_nametag in the head, throughinclude-in-headerfor 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.titleMetaHandlerselectsog:titleandtwitter:titlethe same way, anddescriptionMetaHandlerdoes the same forog:descriptionandtwitter:description.quarto-cli/src/project/types/website/website-meta.ts
Lines 478 to 495 in a8bfe40
quarto-cli/src/project/types/website/website-meta.ts
Lines 547 to 563 in a8bfe40
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.
Set the site title in
_quarto.ymland leave the rest of the scaffold alone.Replace
index.qmd.Replace
about.qmdwith the same page, plus one hand-written tag in the head.Render, then read the tags back.
quarto render grep -n '<meta property="og:site_name' _site/index.html _site/about.htmlActual behavior
On
index.qmdthe document'ssite-namenever appears at all.On
about.qmdthe decoy tag receivesSITE 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_nameshould carryDOCUMENT SITE NAMEon both pages, since both setopen-graph.site-nameexplicitly.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
It also reproduces on a build of
mainat 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...