Skip to content

Fix relative og:image and feed image paths for subdirectory pages - #15053

Merged
cderv merged 6 commits into
mainfrom
fix/feed-og-image-base
Oct 14, 2026
Merged

cderv merged 6 commits into
mainfrom
fix/feed-og-image-base

Conversation

@cderv

@cderv cderv commented Oct 14, 2026

Copy link
Copy Markdown
Member

For a page in a subdirectory, a relative image path used for og:image, twitter:image and the listing feed resolves against the wrong directory. With website: image: site.png in _quarto.yml and site.png at the project root, blog/index.qmd gets the og:image https://example.com/blog/site.png, a file that does not exist, and no width or height. A listing page news/index.qmd with image: thumb.png (the file is in news/) 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:

  • The website.image fallback was added for og:image doesn't propagate from _quarto.yml unless also mentioned in page front matter #5389 without a base decision, and imageMetadata resolves any relative path against the rendering document.
  • website: open-graph: image: and website: twitter-card: image: are merged into each page's metadata, so the information that they come from _quarto.yml is lost.
  • The feed reads every image as project relative, but the page image is 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: and website: twitter-card: image: in _quarto.yml are relative to the project. Quarto rebases the other paths of _quarto.yml to each document, but website: is excluded because its paths are project relative.
  • A page image:, a page open-graph: or twitter-card: image:, and the same keys from _metadata.yml are relative to the document.
  • feed: image: stays relative to the project, like xml-stylesheet in the same block. The schema description now says so.

Paths starting with / and URLs are unchanged. Without site-url the 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 (a website.image used only by subdirectory pages is still copied to the output). That branch built the URL by concatenation, which doubled the slash when site-url ends with /, so it now uses joinUrl. 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 image of a listing is joined onto the listing's directory. The helper doing this is the one the listing item images already used, moved to website-shared.ts as projectRelativeHref.

Alternative considered: making website.image document 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 working og:image on subdirectory pages.
  • Site-level open-graph / twitter-card image:: 0 of 17 break.
  • Page image used as the feed image on subdirectory listings: 20 of 27 are fixed. 3 worked by accident and change, and their og:image was already broken.

Related to #15040, which edits the same block of website-meta.ts to keep a declared image size. A companion documentation PR for quarto-web will be linked here.

Fixes #15039

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: DeepWiki MCP, local clone
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

Note: autonomous AI agents submitting PRs without human oversight are not permitted — see the Code of Conduct.

cderv added 6 commits October 14, 2026 14:38
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.
@posit-snyk-bot

posit-snyk-bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cderv
cderv merged commit b86f98d into main Oct 14, 2026
51 checks passed
@cderv
cderv deleted the fix/feed-og-image-base branch October 14, 2026 16:12
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relative image paths resolve against the wrong directory in og:image and listing feeds for pages in subdirectories

2 participants