Repository navigation
Fix relative og:image and feed image paths for subdirectory pages - #15053
Merged
Merged
Conversation
A relative `website: image:` is written in `_quarto.yml`, so it is relative to the project, but og:image and twitter:image resolved it against the page. On a page in a subdirectory that points to a file that does not exist and loses the image size. `website:` is excluded from the path rebasing Quarto applies to the rest of `_quarto.yml` because its paths are project relative, and the schema describes `website.image` as the site thumbnail, which only works that way. The site image is now turned into its project relative form (leading `/`) before it enters the page metadata, so the existing project relative branch gives the URL, the size and the page resource. That branch also built the URL with a plain concatenation, which doubled the slash when `site-url` ends with `/`. The listing feed did the opposite for the page `image`: it is documented as relative to the document, but the feed read it as project relative, so a listing in `news/` with `image: thumb.png` got `/thumb.png` and no size. It is now joined onto the listing directory. `feed: image:` stays project relative, like `xml-stylesheet` in the same block, and the schema now says so. Without `site-url` the image value is left untouched. ^ Conflicts: ^ news/changelog-1.11.md ^ src/project/types/website/website-meta.ts
…ject `website: open-graph: image:` and `website: twitter-card: image:` are written in `_quarto.yml`, so like `website: image:` they are relative to the project, but they were merged into each page's metadata and resolved against the page. On a page in a subdirectory that gave a URL to a file that does not exist. The merge now works on a copy of the site object, because it used to write the page image into the object shared by every page, and turns a string site image into its project relative form when `site-url` is set. Which value wins is unchanged: a page `image` still replaces the site one. Page-level `open-graph: image:` and an `image` from `_metadata.yml` stay relative to the document. Without `site-url` the value is left as written. Related to #12016, which edits the same block to keep a declared image size. The new test asserts the twitter image from `_quarto.yml` on a page in a subdirectory, which failed before this change. The assertions for the page `open-graph` image, the `_metadata.yml` image, the page image replacing the site twitter image and the `feed: image:` URL pass without it; they guard the document relative cases and the unchanged feed behavior.
The previous two commits added two small functions that did what the listing item image code already did in a private function: leave URLs and project relative paths alone, otherwise join the path onto the document directory under a leading `/`. Move that function to `website-shared.ts` as `projectRelativeHref` and use it for the listing item images, the feed page image and the site-level images, instead of keeping three copies. The forward-slash conversion moves into the helper, so the listing item call no longer wraps it.
The listing item image code normalized every path it got back from the href helper. After the helper moved to a shared module, a project relative path (leading `/`) was returned untouched, so one written with a backslash kept it and the feed then looked for a file with that name when reading the image size. Normalize that branch too; URLs are still returned as they are.
join treats a backslash as part of the file name on Linux and macOS, so the `..\thumb.png` case only held on Windows. The backslash case that matters, after a leading slash, is platform independent and stays.
Collaborator
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
2 of 5 tasks
mcanouil
added a commit
to mcanouil/quarto-mcanouil
that referenced
this pull request
Oct 14, 2026
Quarto PR quarto-dev/quarto-cli#15053 normalises the join of site-url and the image path. The note says when the no-trailing-slash rule can go.
cderv
added a commit
that referenced
this pull request
Oct 15, 2026
The feed-level listing image skipped the project.dir join that the per-item image had (#15032), because the same resolve-and-size logic was hand-written in three places. Move it into resolveProjectImage in website-shared.ts so the href, the URL check and the size lookup live in one place. - The helper builds the project-relative href and reads the pixel size from the project file. Web URLs get no size lookup and are not joined onto the project dir, since on Windows that makes stat throw (#15022). - The feed-level image, the per-item feed image and the page metadata image all call it. Relative feed image paths are now normalized like page metadata images: ./feed.png becomes /feed.png, and img//feed.png becomes /img/feed.png. Leading-slash paths keep their href as written. Images outside the project are not supported. Related to #15032 Related to #15022 See #15053
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For a page in a subdirectory, a relative image path used for
og:image,twitter:imageand the listing feed resolves against the wrong directory. Withwebsite: image: site.pngin_quarto.ymlandsite.pngat the project root,blog/index.qmdgets theog:imagehttps://example.com/blog/site.png, a file that does not exist, and no width or height. A listing pagenews/index.qmdwithimage: thumb.png(the file is innews/) gets the feed<image><url>https://example.com/thumb.png, also without a size.Root cause
Each source was resolved against one fixed base, whatever file it was written in:
website.imagefallback was added forog:imagedoesn't propagate from_quarto.ymlunless also mentioned in page front matter #5389 without a base decision, andimageMetadataresolves any relative path against the rendering document.website: open-graph: image:andwebsite: twitter-card: image:are merged into each page's metadata, so the information that they come from_quarto.ymlis lost.imageis documented as relative to the document.Fix
An image path is relative to the file it is written in:
website: image:,website: open-graph: image:andwebsite: twitter-card: image:in_quarto.ymlare relative to the project. Quarto rebases the other paths of_quarto.ymlto each document, butwebsite:is excluded because its paths are project relative.image:, a pageopen-graph:ortwitter-card:image:, and the same keys from_metadata.ymlare relative to the document.feed: image:stays relative to the project, likexml-stylesheetin the same block. The schema description now says so.Paths starting with
/and URLs are unchanged. Withoutsite-urlthe value is written as is, as before.A site image is turned into its leading-
/form before it reaches the page metadata, so the existing project relative branch gives the URL and the size, and registers the file as a page resource (awebsite.imageused only by subdirectory pages is still copied to the output). That branch built the URL by concatenation, which doubled the slash whensite-urlends with/, so it now usesjoinUrl. The merge of site and page metadata also stopped writing the page image into the site object shared by all pages.For the feed, the page
imageof a listing is joined onto the listing's directory. The helper doing this is the one the listing item images already used, moved towebsite-shared.tsasprojectRelativeHref.Alternative considered: making
website.imagedocument relative was rejected, since the schema describes it as the site thumbnail and it only works from a single base.Existing sites
A sample of public sites found through GitHub code search suggests few relative values would change meaning:
website: image:: 0 of 58 sites break, and about 43 get a workingog:imageon subdirectory pages.open-graph/twitter-cardimage:: 0 of 17 break.imageused as the feed image on subdirectory listings: 20 of 27 are fixed. 3 worked by accident and change, and theirog:imagewas already broken.Related to #15040, which edits the same block of
website-meta.tsto keep a declared image size. A companion documentation PR for quarto-web will be linked here.Fixes #15039
Checklist
I have (if applicable):
AI-assisted PR
Note: autonomous AI agents submitting PRs without human oversight are not permitted — see the Code of Conduct.