From caf3990f90a778ec4a362e0c04cfd983cd83d504 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 17:46:08 +0200 Subject: [PATCH 1/2] Read listing feed image size relative to the project createFeed passed the raw feed image (listing feed.image, document image, or website image) to imageSize, so a relative path resolved against the process cwd. Rendering a project from outside its directory (quarto render path/to/project) silently dropped / from the feed , and a leading "/" path read from the drive root on every render. The feed already treats the image as project-relative through absoluteUrl, and per-item images already join project.dir, so the size is now read from that same project file. Renders from the project directory produce the same output as before. The smoke test renders a fixture project from the tests directory and checks the listing option, the site image fallback and a leading "/" path. --- .../website/listing/website-listing-feed.ts | 5 ++- tests/docs/listings-feed-image/.gitignore | 4 ++ tests/docs/listings-feed-image/_quarto.yml | 7 +++ .../listings-feed-image/archive/index.qmd | 7 +++ .../docs/listings-feed-image/archive/post.qmd | 6 +++ tests/docs/listings-feed-image/feed.png | Bin 0 -> 10660 bytes tests/docs/listings-feed-image/index.qmd | 9 ++++ tests/docs/listings-feed-image/posts/post.qmd | 6 +++ .../listings-feed-image/updates/index.qmd | 8 ++++ .../docs/listings-feed-image/updates/post.qmd | 6 +++ .../site/render-listing-feed-image.test.ts | 40 ++++++++++++++++++ 11 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/docs/listings-feed-image/.gitignore create mode 100644 tests/docs/listings-feed-image/_quarto.yml create mode 100644 tests/docs/listings-feed-image/archive/index.qmd create mode 100644 tests/docs/listings-feed-image/archive/post.qmd create mode 100644 tests/docs/listings-feed-image/feed.png create mode 100644 tests/docs/listings-feed-image/index.qmd create mode 100644 tests/docs/listings-feed-image/posts/post.qmd create mode 100644 tests/docs/listings-feed-image/updates/index.qmd create mode 100644 tests/docs/listings-feed-image/updates/post.qmd create mode 100644 tests/smoke/site/render-listing-feed-image.test.ts diff --git a/src/project/types/website/listing/website-listing-feed.ts b/src/project/types/website/listing/website-listing-feed.ts index d33e09d59b9..af0f12abbc7 100644 --- a/src/project/types/website/listing/website-listing-feed.ts +++ b/src/project/types/website/listing/website-listing-feed.ts @@ -175,7 +175,10 @@ export async function createFeed( link: link, url: absoluteUrl(siteUrl, image), }; - const size = isAbsoluteRef(image) ? undefined : imageSize(image); + // Read the size from the project file the url points at, not the cwd + const size = isAbsoluteRef(image) + ? undefined + : imageSize(join(project.dir, image)); if (size) { const [height, width] = feedImageSize(size.height, size.width); feed.image.height = height; diff --git a/tests/docs/listings-feed-image/.gitignore b/tests/docs/listings-feed-image/.gitignore new file mode 100644 index 00000000000..812977e5f74 --- /dev/null +++ b/tests/docs/listings-feed-image/.gitignore @@ -0,0 +1,4 @@ +/.quarto/ +_site/ + +**/*.quarto_ipynb diff --git a/tests/docs/listings-feed-image/_quarto.yml b/tests/docs/listings-feed-image/_quarto.yml new file mode 100644 index 00000000000..cbe2bc9d137 --- /dev/null +++ b/tests/docs/listings-feed-image/_quarto.yml @@ -0,0 +1,7 @@ +project: + type: website + +website: + title: "listings-feed-image" + site-url: https://example.com + image: feed.png diff --git a/tests/docs/listings-feed-image/archive/index.qmd b/tests/docs/listings-feed-image/archive/index.qmd new file mode 100644 index 00000000000..0b528523f4e --- /dev/null +++ b/tests/docs/listings-feed-image/archive/index.qmd @@ -0,0 +1,7 @@ +--- +title: "Feed image from the site" +listing: + feed: true +--- + +This listing sets no image, so the feed falls back to the site `image`. diff --git a/tests/docs/listings-feed-image/archive/post.qmd b/tests/docs/listings-feed-image/archive/post.qmd new file mode 100644 index 00000000000..3cafb5a31cc --- /dev/null +++ b/tests/docs/listings-feed-image/archive/post.qmd @@ -0,0 +1,6 @@ +--- +title: "An archived post" +date: "2026-10-12" +--- + +A post listed by the archive feed. diff --git a/tests/docs/listings-feed-image/feed.png b/tests/docs/listings-feed-image/feed.png new file mode 100644 index 0000000000000000000000000000000000000000..219cc7a3aa8981c79becce7eda7122cc039abbc7 GIT binary patch literal 10660 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 F{{W07$4USI literal 0 HcmV?d00001 diff --git a/tests/docs/listings-feed-image/index.qmd b/tests/docs/listings-feed-image/index.qmd new file mode 100644 index 00000000000..5f505a96602 --- /dev/null +++ b/tests/docs/listings-feed-image/index.qmd @@ -0,0 +1,9 @@ +--- +title: "Feed image from the listing option" +listing: + contents: posts + feed: + image: feed.png +--- + +The feed image comes from the listing `feed: image:` option. diff --git a/tests/docs/listings-feed-image/posts/post.qmd b/tests/docs/listings-feed-image/posts/post.qmd new file mode 100644 index 00000000000..f56fd132724 --- /dev/null +++ b/tests/docs/listings-feed-image/posts/post.qmd @@ -0,0 +1,6 @@ +--- +title: "A post" +date: "2026-10-12" +--- + +A post listed by the root feed. diff --git a/tests/docs/listings-feed-image/updates/index.qmd b/tests/docs/listings-feed-image/updates/index.qmd new file mode 100644 index 00000000000..c89fdb2ecb1 --- /dev/null +++ b/tests/docs/listings-feed-image/updates/index.qmd @@ -0,0 +1,8 @@ +--- +title: "Feed image with a leading slash" +listing: + feed: + image: /feed.png +--- + +The feed image is given relative to the project root with a leading `/`. diff --git a/tests/docs/listings-feed-image/updates/post.qmd b/tests/docs/listings-feed-image/updates/post.qmd new file mode 100644 index 00000000000..0d0bf45dc3f --- /dev/null +++ b/tests/docs/listings-feed-image/updates/post.qmd @@ -0,0 +1,6 @@ +--- +title: "An update" +date: "2026-10-12" +--- + +A post listed by the updates feed. diff --git a/tests/smoke/site/render-listing-feed-image.test.ts b/tests/smoke/site/render-listing-feed-image.test.ts new file mode 100644 index 00000000000..8ffeb3b9d6a --- /dev/null +++ b/tests/smoke/site/render-listing-feed-image.test.ts @@ -0,0 +1,40 @@ +/* + * render-listing-feed-image.test.ts + * + * Copyright (C) 2026 Posit Software, PBC + */ +import { join } from "../../../src/deno_ral/path.ts"; +import { removeIfExists } from "../../../src/core/path.ts"; +import { testQuartoCmd, Verify } from "../../test.ts"; +import { docs } from "../../utils.ts"; +import { ensureFileRegexMatches, noErrorsOrWarnings } from "../../verify.ts"; + +// The project is rendered from the test cwd, not from its own directory, so +// the feed image size must be read relative to the project, not the cwd. +const projectDir = docs("listings-feed-image"); +const outputDir = join(projectDir, "_site"); + +// feed.png is 600x400, scaled down to fit the RSS 144px maximum width. +const feedImage = [ + /\s*https:\/\/example\.com\/feed\.png<\/url>/, + /96<\/height>\s*144<\/width>\s*<\/image>/, +]; + +const verify: Verify[] = [ + noErrorsOrWarnings, + // Listing `feed: image:` option + ensureFileRegexMatches(join(outputDir, "index.xml"), feedImage), + // Site `image` fallback + ensureFileRegexMatches(join(outputDir, "archive", "index.xml"), feedImage), + // Project-relative path with a leading slash + ensureFileRegexMatches(join(outputDir, "updates", "index.xml"), feedImage), +]; + +testQuartoCmd("render", [projectDir], verify, { + name: "Listing feed image size is read relative to the project", + teardown: () => { + removeIfExists(outputDir); + removeIfExists(join(projectDir, ".quarto")); + return Promise.resolve(); + }, +}); From 035ad05062a5205153cd50f499dc86ecc6900fee Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 18:07:35 +0200 Subject: [PATCH 2/2] Add changelog entry for listing feed image size fix --- news/changelog-1.11.md | 1 + 1 file changed, 1 insertion(+) diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 25b754954a5..7484892f3e2 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -79,6 +79,7 @@ All changes included in 1.11: - ([#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. - ([#15022](https://github.com/quarto-dev/quarto-cli/issues/15022)): Fix rendering a listing with a `feed` failing on Windows with `os error 123` when a listing item's `image` or the feed `image` is a web URL. The image URL is used as is in the feed and no local size lookup is attempted. (author: @Masad791) +- ([#15032](https://github.com/quarto-dev/quarto-cli/pull/15032)): Fix the listing feed `image` missing its height and width when a website is rendered from a directory other than the project directory, or when the image path starts with `/`. ## Lua API