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
This commit is contained in:
dualfroz committed 2026-10-03 01:33:54 +02:00
1 parent ed42ec389b
commit 573bb96204
6 files changed
+139 -5

No files matched your search

+2 -1
View File
@@ -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
@@ -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: |-
<!DOCTYPE html><html lang="en"><head></head><body><div id="search"></div>
<script src="/pagefind/pagefind-ui.js"></script>
<script>
window.pui = new PagefindUI({ element: "#search", baseUrl: "https://example.com/" });
</script></body></html>
- step: I have a "public/subs/index.html" file with the content {html}
html: |-
<!DOCTYPE html><html lang="en"><head></head><body><div id="search"></div>
<script src="/pagefind/pagefind-ui.js"></script>
<script>
window.pui = new PagefindUI({ element: "#search", baseUrl: "https://example.com/", showSubResults: true });
</script></body></html>
- step: I have a "public/blog/post/index.html" file with the content {html}
html: >-
<!DOCTYPE html><html
lang="en"><head></head><body><h1>Relative Image</h1><img
src="images/header.png"><p>A page with a relative image
URL.</p></body></html>
- step: I have a "public/about/index.html" file with the content {html}
html: >-
<!DOCTYPE html><html
lang="en"><head></head><body><h1>Absolute Image</h1><img
src="/images/hero.png"><p>A page with an absolute image
URL.</p></body></html>
- 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
@@ -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: |-
<!DOCTYPE html><html lang="en"><head>
<link href="/pagefind/pagefind-component-ui.css" rel="stylesheet">
</head><body>
<pagefind-config preload base-url="https://example.com/"></pagefind-config>
<pagefind-input></pagefind-input>
<pagefind-results show-images></pagefind-results>
<script src="/pagefind/pagefind-component-ui.js" type="module"></script>
</body></html>
- step: I have a "public/blog/post/index.html" file with the content {html}
html: >-
<!DOCTYPE html><html
lang="en"><head></head><body><h1>Relative Image</h1><img
src="images/header.png"><p>A page with a relative image
URL.</p></body></html>
- step: I have a "public/about/index.html" file with the content {html}
html: >-
<!DOCTYPE html><html
lang="en"><head></head><body><h1>Absolute Image</h1><img
src="/images/hero.png"><p>A page with an absolute image
URL.</p></body></html>
- 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
+5 -2
View File
@@ -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;
}
+3 -2
View File
@@ -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;
}
@@ -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 =