From 573bb96204aeb446943bc6c76e4e8dddbce4db44 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 3 Oct 2026 01:33:54 +0200 Subject: [PATCH] Resolve result images against an absolute baseUrl Relative images were resolved against the page URL, but only the pathname was kept, so with a baseUrl on another domain the image still loaded from the search site. Keep the origin when the page URL has one, in the Component UI's resolveUrl filter and in both Default UI result templates. result_with_subs.svelte didn't resolve images at all before. Fixes #573 --- docs/content/docs/components/results.md | 3 +- ...-image-urls-against-base-url.toolproof.yml | 67 +++++++++++++++++++ ...relative-image-urls-base-url.toolproof.yml | 49 ++++++++++++++ pagefind_ui/component/components/index.ts | 7 +- pagefind_ui/default/svelte/result.svelte | 5 +- .../default/svelte/result_with_subs.svelte | 13 ++++ 6 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 pagefind/integration_tests/ui/ui_base/pagefind-ui-resolves-image-urls-against-base-url.toolproof.yml create mode 100644 pagefind/integration_tests/web_components/relative-image-urls-base-url.toolproof.yml diff --git a/docs/content/docs/components/results.md b/docs/content/docs/components/results.md index c82fafc..0a84bdd 100644 --- a/docs/content/docs/components/results.md +++ b/docs/content/docs/components/results.md @@ -88,10 +88,11 @@ The template engine provides [built-in filters](https://github.com/bglw/adequate #### `resolveUrl(pageUrl)` -Resolves a relative URL against a page URL. Absolute URLs are returned unchanged. +Resolves a relative URL against a page URL. Absolute URLs are returned unchanged. If the page URL includes a domain, for example from a `baseUrl` on a merged index, the resolved URL keeps that domain. ``` "images/hero.png" | resolveUrl("/blog/post/") → /blog/post/images/hero.png +"images/hero.png" | resolveUrl("https://example.com/blog/post/") → https://example.com/blog/post/images/hero.png ``` ### Placeholder Template diff --git a/pagefind/integration_tests/ui/ui_base/pagefind-ui-resolves-image-urls-against-base-url.toolproof.yml b/pagefind/integration_tests/ui/ui_base/pagefind-ui-resolves-image-urls-against-base-url.toolproof.yml new file mode 100644 index 0000000..2387819 --- /dev/null +++ b/pagefind/integration_tests/ui/ui_base/pagefind-ui-resolves-image-urls-against-base-url.toolproof.yml @@ -0,0 +1,67 @@ +name: Base UI Tests > Pagefind UI resolves image URLs against an absolute base URL +steps: + - ref: ./background.toolproof.yml + - step: I have a "public/index.html" file with the content {html} + html: |- + + + + + - step: I have a "public/subs/index.html" file with the content {html} + html: |- + + + + + - step: I have a "public/blog/post/index.html" file with the content {html} + html: >- +

Relative Image

A page with a relative image + URL.

+ - step: I have a "public/about/index.html" file with the content {html} + html: >- +

Absolute Image

A page with an absolute image + URL.

+ - macro: I run Pagefind + - step: stdout should contain "Running Pagefind" + - step: The file "public/pagefind/pagefind.js" should not be empty + - step: I serve the directory "public" + - step: In my browser, I load "/" + - step: In my browser, I evaluate {js} + js: window.pui.triggerSearch("image"); + - step: In my browser, the console should be empty + - snapshot: In my browser, the result of {js} + js: |- + await toolproof.waitFor(() => document.querySelectorAll(".pagefind-ui__result-image").length === 2); + let items = Array.from(document.querySelectorAll(".pagefind-ui__result")); + items.sort((a, b) => a.textContent.localeCompare(b.textContent)); + return items.map(li => { + let title = li.querySelector(".pagefind-ui__result-link").textContent; + return title + ": " + li.querySelector(".pagefind-ui__result-image").getAttribute("src"); + }).join("\n"); + snapshot_content: |- + ╎Absolute Image: https://example.com/images/hero.png + ╎Relative Image: https://example.com/blog/post/images/header.png + - step: In my browser, I load "/subs/" + - step: In my browser, I evaluate {js} + js: window.pui.triggerSearch("image"); + - step: In my browser, the console should be empty + - snapshot: In my browser, the result of {js} + js: |- + await toolproof.waitFor(() => document.querySelectorAll(".pagefind-ui__result-image").length === 2); + let items = Array.from(document.querySelectorAll(".pagefind-ui__result")); + items.sort((a, b) => a.textContent.localeCompare(b.textContent)); + return items.map(li => { + let title = li.querySelector(".pagefind-ui__result-link").textContent; + return title + ": " + li.querySelector(".pagefind-ui__result-image").getAttribute("src"); + }).join("\n"); + snapshot_content: |- + ╎Absolute Image: https://example.com/images/hero.png + ╎Relative Image: https://example.com/blog/post/images/header.png diff --git a/pagefind/integration_tests/web_components/relative-image-urls-base-url.toolproof.yml b/pagefind/integration_tests/web_components/relative-image-urls-base-url.toolproof.yml new file mode 100644 index 0000000..02350cd --- /dev/null +++ b/pagefind/integration_tests/web_components/relative-image-urls-base-url.toolproof.yml @@ -0,0 +1,49 @@ +name: Web Components Tests > Image URLs are resolved against an absolute base URL +steps: + - step: I have the environment variable "PAGEFIND_SITE" set to "public" + - step: I have a "public/index.html" file with the content {html} + html: |- + + + + + + + + + - step: I have a "public/blog/post/index.html" file with the content {html} + html: >- +

Relative Image

A page with a relative image + URL.

+ - step: I have a "public/about/index.html" file with the content {html} + html: >- +

Absolute Image

A page with an absolute image + URL.

+ - macro: I run Pagefind + - step: stdout should contain "Running Pagefind" + - step: I serve the directory "public" + - step: In my browser, I load "/" + - step: In my browser, I click the selector "pagefind-input input" + - step: In my browser, I type "image" + - step: In my browser, the console should be empty + - step: In my browser, I evaluate {js} + js: |- + let results = await toolproof.querySelector("pagefind-results ul"); + await toolproof.waitFor(() => results.querySelectorAll("li a").length === 2); + - snapshot: In my browser, the result of {js} + js: |- + let results = await toolproof.querySelector("pagefind-results ul"); + let items = Array.from(results.querySelectorAll("li")); + items.sort((a, b) => a.textContent.localeCompare(b.textContent)); + return items.map(li => { + let title = li.querySelector("a").textContent; + let img = li.querySelector("img"); + return title + ": " + (img ? img.getAttribute("src") : "no image"); + }).join("\n"); + snapshot_content: |- + ╎Absolute Image: https://example.com/images/hero.png + ╎Relative Image: https://example.com/blog/post/images/header.png diff --git a/pagefind_ui/component/components/index.ts b/pagefind_ui/component/components/index.ts index c9ce0e5..6f38920 100644 --- a/pagefind_ui/component/components/index.ts +++ b/pagefind_ui/component/components/index.ts @@ -4,9 +4,12 @@ import { registerFunction } from "adequate-little-templates"; registerFunction("resolveUrl", (url, pageUrl) => { const s = String(url ?? ""); - if (!s || /^[a-z][a-z0-9+.-]*:/i.test(s) || /^\/\//.test(s) || s.startsWith("/")) return s; + if (!s || /^[a-z][a-z0-9+.-]*:/i.test(s) || /^\/\//.test(s)) return s; try { - return new URL(s, new URL(String(pageUrl ?? "/"), "https://p")).pathname; + const resolved = new URL(s, new URL(String(pageUrl ?? "/"), "https://p")); + return resolved.origin === "https://p" + ? resolved.pathname + resolved.search + resolved.hash + : resolved.href; } catch { return s; } diff --git a/pagefind_ui/default/svelte/result.svelte b/pagefind_ui/default/svelte/result.svelte index eb44288..e8978b5 100644 --- a/pagefind_ui/default/svelte/result.svelte +++ b/pagefind_ui/default/svelte/result.svelte @@ -9,9 +9,10 @@ let meta = []; const resolveImageUrl = (src, pageUrl) => { - if (!src || /^[a-z][a-z0-9+.-]*:/i.test(src) || /^\/\//.test(src) || src.startsWith("/")) return src; + if (!src || /^[a-z][a-z0-9+.-]*:/i.test(src) || /^\/\//.test(src)) return src; try { - return new URL(src, new URL(pageUrl || "/", "https://p")).pathname; + const url = new URL(src, new URL(pageUrl || "/", "https://p")); + return url.origin === "https://p" ? url.pathname + url.search + url.hash : url.href; } catch { return src; } diff --git a/pagefind_ui/default/svelte/result_with_subs.svelte b/pagefind_ui/default/svelte/result_with_subs.svelte index 5e0cd4d..1392161 100644 --- a/pagefind_ui/default/svelte/result_with_subs.svelte +++ b/pagefind_ui/default/svelte/result_with_subs.svelte @@ -10,6 +10,16 @@ let non_root_sub_results = []; let has_root_sub_result = false; + const resolveImageUrl = (src, pageUrl) => { + if (!src || /^[a-z][a-z0-9+.-]*:/i.test(src) || /^\/\//.test(src)) return src; + try { + const url = new URL(src, new URL(pageUrl || "/", "https://p")); + return url.origin === "https://p" ? url.pathname + url.search + url.hash : url.href; + } catch { + return src; + } + }; + const thin_sub_results = (results, limit) => { if (results.length <= limit) { return results; @@ -26,6 +36,9 @@ const load = async (r) => { data = await r.data(); data = process_result?.(data) ?? data; + if (data.meta?.image) { + data = { ...data, meta: { ...data.meta, image: resolveImageUrl(data.meta.image, data.meta.url || data.url) } }; + } meta = Object.entries(data.meta).filter(([key]) => !skipMeta.includes(key)); if (Array.isArray(data.sub_results)) { has_root_sub_result =