Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ All changes included in 1.11:
- ([#10756](https://github.com/quarto-dev/quarto-cli/issues/10756)): Fix the notebook preview of an embedded `.qmd` file going missing from the output directory when a project is rendered again, which broke its source notebook link.
- ([#12014](https://github.com/quarto-dev/quarto-cli/issues/12014)): Fix `twitter-card` `image-width` and `image-height` being listed as options although Twitter Cards have no image size tags. They are no longer documented, and setting them prints a warning pointing to `open-graph`, where an image size is used. The size Quarto measures from a local image is no longer written as `twitter:image-height` and `twitter:image-width` tags, only as `og:image:height` and `og:image:width`.
- ([#12016](https://github.com/quarto-dev/quarto-cli/issues/12016)): Fix `open-graph` `image-width` and `image-height` being dropped when `site-url` is set and Quarto cannot measure the image, such as a remote image. A size declared in the site `open-graph` options no longer applies when a page sets its own `image`, even the same file: declare `image-width` and `image-height` in the page `open-graph` options or in shared metadata such as `_metadata.yml`.
- ([#14917](https://github.com/quarto-dev/quarto-cli/issues/14917)): Fix `og:site_name` ignoring a page's `open-graph.site-name` and always showing the website title. Quarto also no longer rewrites `og:site_name`, `og:title`, `og:description`, `twitter:title` or `twitter:description` tags it did not write, such as ones added with `include-in-header` or by an extension. (author: @Masad791)
- ([#14972](https://github.com/quarto-dev/quarto-cli/issues/14972)): Fix the website navbar, sidebar toggle bar, or announcement covering the header of notebook preview pages, which hid the back link, notebook title, and download link. The notebook preview header now sits below the site header.
- ([#14974](https://github.com/quarto-dev/quarto-cli/issues/14974)): Fix math in `.llms.md` files from `llms-txt` coming out garbled with the default `mathjax` method, and with `katex` and `webtex`. Math is now written as `$...$` and `$$...$$` with the `mathjax`, `katex`, `webtex` and `mathml` methods.
- ([#15004](https://github.com/quarto-dev/quarto-cli/issues/15004)): Fix notebook preview pages and notebook sources missing from the output directory when a website with embedded notebooks is rendered from a directory other than the project directory.
Expand Down
7 changes: 6 additions & 1 deletion src/format/html/format-html-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,11 @@ export function createCodeBlock(
return preEl;
}

export function writeMetaTag(name: string, content: string, doc: Document) {
export function writeMetaTag(
name: string,
content: string,
doc: Document,
): Element {
// Meta tag
const m = doc.createElement("META");
if (name.startsWith("og:")) {
Expand All @@ -520,6 +524,7 @@ export function writeMetaTag(name: string, content: string, doc: Document) {
// Insert the nodes
doc.querySelector("head")?.appendChild(m);
doc.querySelector("head")?.appendChild(nl);
return m;
}

export function writeLinkTag(rel: string, href: string, doc: Document) {
Expand Down
57 changes: 38 additions & 19 deletions src/project/types/website/website-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,19 @@ export function metadataHtmlDependencies(
format: Format,
extras: FormatExtras,
) {
const pipeline = metaMarkdownPipeline(format, extras);
// The social meta tags Quarto writes for this page, by name. The markdown
// pipeline updates these elements rather than the first matching tag in the
// head, which may come from include-in-header or an extension (#14917).
const written = new Map<string, Element>();
const pipeline = metaMarkdownPipeline(format, extras, written);
return {
[kHtmlPostprocessors]: metadataHtmlPostProcessor(
source,
project,
format,
extras,
pipeline,
written,
),
[kMarkdownAfterBody]: pipeline.markdownAfterBody(),
};
Expand All @@ -92,6 +97,7 @@ export function metadataHtmlPostProcessor(
format: Format,
extras: FormatExtras,
pipeline: MarkdownPipeline,
written: Map<string, Element>,
) {
return (doc: Document): Promise<HtmlPostProcessResult> => {
// read document level metadata
Expand Down Expand Up @@ -267,7 +273,8 @@ export function metadataHtmlPostProcessor(
key = provider.filter(key);
}

writeMetaTag(`${provider.prefix}:${key}`, value, doc);
const name = `${provider.prefix}:${key}`;
written.set(name, writeMetaTag(name, value, doc));
}
});
});
Expand Down Expand Up @@ -503,9 +510,14 @@ const kTwitterDesc = "quarto-twittercarddesc";
const kOgTitle = "quarto-ogcardtitle";
const kOgDesc = "quarto-ogcardddesc";
const kMetaSideNameId = "quarto-metasitename";
const kOgSiteName = "quarto-ogsitename";
const kMetaSiteDescId = "quarto-metasitedesc";

function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
function metaMarkdownPipeline(
format: Format,
extras: FormatExtras,
written: Map<string, Element>,
) {
const resolvedTitle = computePageTitle(format);

const titleMetaHandler = {
Expand Down Expand Up @@ -550,15 +562,15 @@ function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
// Process social metadata titles
[{
key: kOgTitle,
sel: 'meta[property="og:title"]',
name: "og:title",
}, {
key: kTwitterTitle,
sel: 'meta[name="twitter:title"]',
name: "twitter:title",
}].forEach(
(obj) => {
const renderedObjEl = rendered[obj.key];
if (renderedObjEl) {
const metaEl = doc.querySelector(obj.sel);
const metaEl = written.get(obj.name);
if (metaEl) {
metaEl.setAttribute("content", renderedObjEl.innerText);
}
Expand All @@ -570,12 +582,20 @@ function metaMarkdownPipeline(format: Format, extras: FormatExtras) {

const siteTitleMetaHandler = {
getUnrendered() {
const inlines: Record<string, string> = {};
const siteMeta = format.metadata[kWebsite] as Metadata;
if (siteMeta && siteMeta[kTitle]) {
return { inlines: { [kMetaSideNameId]: siteMeta[kTitle] as string } };
inlines[kMetaSideNameId] = siteMeta[kTitle] as string;
}
// og:site_name is the page's open-graph.site-name when set, else the
// website title, the same value the tag was written with
const ogSiteName = opengraphMetadata(format)[kSiteName];
if (typeof ogSiteName === "string") {
inlines[kOgSiteName] = ogSiteName;
}
return { inlines };
},
processRendered(rendered: Record<string, Element>, doc: Document) {
processRendered(rendered: Record<string, Element>) {
const renderedEl = rendered[kMetaSideNameId];
if (renderedEl) {
// Write resolved title back into format.metadata so it flows
Expand All @@ -584,13 +604,12 @@ function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
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);
}
}
// Update the og:site_name meta tag
const siteNameEl = rendered[kOgSiteName];
const metaEl = written.get("og:site_name");
if (siteNameEl && metaEl) {
metaEl.setAttribute("content", siteNameEl.innerText);
}
},
};
Expand All @@ -616,17 +635,17 @@ function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
inlines[kOgDesc] = ogMeta.description as string || description || "";
return { inlines };
},
processRendered(rendered: Record<string, Element>, doc: Document) {
processRendered(rendered: Record<string, Element>) {
// Meta values
const metaVals = [{
sel: 'meta[property="og:description"]',
name: "og:description",
key: kOgDesc,
}, { sel: 'meta[name="twitter:description"]', key: kTwitterDesc }];
}, { name: "twitter:description", key: kTwitterDesc }];

metaVals.forEach((metaVal) => {
const renderedEl = rendered[metaVal.key];
if (renderedEl) {
const metaEl = doc.querySelector(metaVal.sel);
const metaEl = written.get(metaVal.name);
if (metaEl) {
metaEl.setAttribute("content", renderedEl.innerText);
}
Expand Down
3 changes: 3 additions & 0 deletions tests/docs/smoke-all/2026/10/19/issue-14917/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.quarto/
_site/
**/*.quarto_ipynb
8 changes: 8 additions & 0 deletions tests/docs/smoke-all/2026/10/19/issue-14917/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
project:
type: website

website:
title: "SITE TITLE"
site-url: https://example.com
open-graph: true
twitter-card: true
30 changes: 30 additions & 0 deletions tests/docs/smoke-all/2026/10/19/issue-14917/about.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: "About"
description: "About page description"
open-graph:
site-name: "DOCUMENT SITE NAME"
format:
html:
include-in-header:
- text: |
<meta property="og:site_name" content="DECOY">
<meta property="og:title" content="DECOY TITLE">
<meta property="og:description" content="DECOY DESCRIPTION">
<meta name="twitter:title" content="DECOY TITLE">
<meta name="twitter:description" content="DECOY DESCRIPTION">
_quarto:
tests:
html:
ensureHtmlElements:
- - 'meta[property="og:site_name"][content="DECOY"]'
- 'meta[property="og:site_name"][content="DOCUMENT SITE NAME"]'
- 'meta[property="og:title"][content="DECOY TITLE"]'
- 'meta[property="og:description"][content="DECOY DESCRIPTION"]'
- 'meta[name="twitter:title"][content="DECOY TITLE"]'
- 'meta[name="twitter:description"][content="DECOY DESCRIPTION"]'
- 'meta[property="og:title"][content^="About"]'
- 'meta[name="twitter:description"][content="About page description"]'
- ['meta[property="og:site_name"][content="SITE TITLE"]']
---

Tags written by `include-in-header` must keep their values; Quarto updates only the tags it wrote itself.
14 changes: 14 additions & 0 deletions tests/docs/smoke-all/2026/10/19/issue-14917/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: "Home"
open-graph:
site-name: "DOCUMENT SITE NAME"
_quarto:
render-project: true
tests:
html:
ensureHtmlElements:
- ['meta[property="og:site_name"][content="DOCUMENT SITE NAME"]']
- ['meta[property="og:site_name"][content="SITE TITLE"]']
---

`og:site_name` should come from this page's `open-graph.site-name`, not the website title.
22 changes: 22 additions & 0 deletions tests/docs/smoke-all/2026/10/19/issue-14917/opt-out.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: "Opt out"
open-graph: false
twitter-card: false
format:
html:
include-in-header:
- text: |
<meta property="og:site_name" content="PAGE SITE NAME">
<meta property="og:title" content="PAGE TITLE">
<meta name="twitter:title" content="PAGE TITLE">
_quarto:
tests:
html:
ensureHtmlElements:
- - 'meta[property="og:site_name"][content="PAGE SITE NAME"]'
- 'meta[property="og:title"][content="PAGE TITLE"]'
- 'meta[name="twitter:title"][content="PAGE TITLE"]'
- ['meta[property="og:site_name"][content="SITE TITLE"]']
---

This page turns off Quarto's social metadata and writes its own tags, which must keep their values.
Loading