diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js
index daf6f419..5e074cd0 100644
--- a/src/js/views/listDetail.view.js
+++ b/src/js/views/listDetail.view.js
@@ -1,7 +1,7 @@
import { html, render } from "/js/lib/lit-html.js";
import { Signal, ReactiveStore } from "/js/signals.js";
import { classnames } from "/js/utils.js";
-import { isModerationList } from "/js/dataHelpers.js";
+import { cdnImageUrl, isModerationList } from "/js/dataHelpers.js";
import { postFeedTemplate } from "/js/templates/postFeed.template.js";
import { profileFeedTemplate } from "/js/templates/profileFeed.template.js";
import { auth } from "/js/auth.js";
@@ -232,7 +232,7 @@ export default async function listDetailView({
${list.avatar
? html`
@@ -110,7 +111,10 @@ export default async function notificationsView({
${video
? html`
`
diff --git a/src/js/views/search.view.js b/src/js/views/search.view.js
index 5523695f..b7be4ed7 100644
--- a/src/js/views/search.view.js
+++ b/src/js/views/search.view.js
@@ -4,7 +4,11 @@ import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js";
import { headerTemplate } from "/js/templates/header.template.js";
import { avatarTemplate } from "/js/templates/avatar.template.js";
import { classnames } from "/js/utils.js";
-import { getDisplayName, MISSING_HANDLE } from "/js/dataHelpers.js";
+import {
+ cdnImageUrl,
+ getDisplayName,
+ MISSING_HANDLE,
+} from "/js/dataHelpers.js";
import { Signal, ReactiveStore } from "/js/signals.js";
import {
linkToFeed,
@@ -523,7 +527,7 @@ export default async function searchView({
${feedGenerator.avatar
? html`

`
diff --git a/tests/e2e/mockServer.js b/tests/e2e/mockServer.js
index 7d2d904b..55c45dd8 100644
--- a/tests/e2e/mockServer.js
+++ b/tests/e2e/mockServer.js
@@ -1,3 +1,4 @@
+import { CDN_URL } from "../../src/js/config.js";
import { createPost } from "../shared/factories.js";
import { bskyLabeler, notificationService, userProfile } from "./testData.js";
import {
@@ -1888,16 +1889,18 @@ export class MockServer {
});
});
- await page.route("https://cdn.bsky.app/img/**", (route) => {
- return route.fulfill({
- status: 200,
- contentType: "image/png",
- body: Buffer.from(
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
- "base64",
- ),
+ for (const cdnUrl of new Set(["https://cdn.bsky.app", CDN_URL])) {
+ await page.route(`${cdnUrl}/img/**`, (route) => {
+ return route.fulfill({
+ status: 200,
+ contentType: "image/png",
+ body: Buffer.from(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
+ "base64",
+ ),
+ });
});
- });
+ }
await page.route("https://ogcard.cdn.bsky.app/**", (route) => {
return route.fulfill({
diff --git a/tests/unit/specs/components/plugin-blob-image.test.js b/tests/unit/specs/components/plugin-blob-image.test.js
index 67ebd7d5..9eee5ab2 100644
--- a/tests/unit/specs/components/plugin-blob-image.test.js
+++ b/tests/unit/specs/components/plugin-blob-image.test.js
@@ -1,6 +1,7 @@
import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import "/js/components/plugin-blob-image.js";
+import { CDN_URL } from "/js/config.js";
describe("plugin-blob-image", () => {
const VALID_CID =
@@ -106,7 +107,7 @@ describe("plugin-blob-image", () => {
assert(img !== null);
assert.deepEqual(
img.getAttribute("src"),
- `https://cdn.bsky.app/img/avatar_thumbnail/plain/${did}/${VALID_CID}@jpeg`,
+ `${CDN_URL}/img/avatar_thumbnail/plain/${did}/${VALID_CID}@jpeg`,
);
assert.deepEqual(img.getAttribute("alt"), "avatar");
});
diff --git a/tests/unit/specs/dataHelpers.test.js b/tests/unit/specs/dataHelpers.test.js
index 9637bf8f..15b6156c 100644
--- a/tests/unit/specs/dataHelpers.test.js
+++ b/tests/unit/specs/dataHelpers.test.js
@@ -1,8 +1,10 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
+import { CDN_URL } from "/js/config.js";
import {
avatarThumbnailUrl,
buildProfileFromRecord,
+ cdnImageUrl,
getRKey,
getIsLiked,
isListFeed,
@@ -83,8 +85,8 @@ describe("buildProfileFromRecord", () => {
handle: "me.test",
displayName: "Me",
description: "hello",
- avatar: `https://cdn.bsky.app/img/avatar/plain/${did}/avatarcid@jpeg`,
- banner: `https://cdn.bsky.app/img/banner/plain/${did}/bannercid@jpeg`,
+ avatar: `${CDN_URL}/img/avatar/plain/${did}/avatarcid@jpeg`,
+ banner: `${CDN_URL}/img/banner/plain/${did}/bannercid@jpeg`,
pinnedPost: { uri: `at://${did}/app.bsky.feed.post/1`, cid: "abc" },
createdAt: "2024-01-01T00:00:00.000Z",
labels: [],
@@ -128,6 +130,42 @@ describe("avatarThumbnailUrl", () => {
});
});
+describe("cdnImageUrl", () => {
+ it("should rewrite the bsky CDN origin to the configured CDN", () => {
+ assert.deepEqual(
+ cdnImageUrl(
+ "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:123/imagecid@jpeg",
+ ),
+ `${CDN_URL}/img/feed_thumbnail/plain/did:plc:123/imagecid@jpeg`,
+ );
+ });
+
+ it("should leave other hosts alone", () => {
+ const videoThumb =
+ "https://video.bsky.app/watch/did:plc:123/videocid/thumbnail.jpg";
+ assert.deepEqual(cdnImageUrl(videoThumb), videoThumb);
+ const ogCard = "https://ogcard.cdn.bsky.app/start/did:plc:123/rkey";
+ assert.deepEqual(cdnImageUrl(ogCard), ogCard);
+ });
+
+ it("should leave non-absolute and non-URL values alone", () => {
+ assert.deepEqual(
+ cdnImageUrl("/img/avatar-fallback.svg"),
+ "/img/avatar-fallback.svg",
+ );
+ assert.deepEqual(
+ cdnImageUrl("data:image/png;base64,abc"),
+ "data:image/png;base64,abc",
+ );
+ });
+
+ it("should pass through empty values", () => {
+ assert.deepEqual(cdnImageUrl(""), "");
+ assert.deepEqual(cdnImageUrl(null), null);
+ assert.deepEqual(cdnImageUrl(undefined), undefined);
+ });
+});
+
describe("getRKey", () => {
it("should extract rkey from post URI", () => {
const post = { uri: "at://did:plc:123/app.bsky.feed.post/3l7q2wm5ws22k" };
diff --git a/tests/unit/specs/dataLayer/mutations.test.js b/tests/unit/specs/dataLayer/mutations.test.js
index 572b25bc..1d2be3a8 100644
--- a/tests/unit/specs/dataLayer/mutations.test.js
+++ b/tests/unit/specs/dataLayer/mutations.test.js
@@ -8,6 +8,11 @@ import { Derived } from "/js/dataLayer/derived.js";
import { Preferences } from "/js/preferences.js";
import { Signal } from "/js/signals.js";
import { HiddenFeedItemsStore } from "/js/dataLayer/hiddenFeedItemsStore.js";
+import { CDN_URL } from "/js/config.js";
+
+function escapeRegExp(value) {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
const mockIdentityResolver = {
resolveHandle: async () => null,
@@ -1007,11 +1012,15 @@ describe("updateProfile", () => {
const updatedProfile = dataStore.$profiles.get(testProfile.did);
assert.match(
updatedProfile.avatar,
- /^https:\/\/cdn\.bsky\.app\/img\/avatar\/plain\/did:plc:test123\/bafkreiblob\d+@jpeg$/,
+ new RegExp(
+ `^${escapeRegExp(CDN_URL)}/img/avatar/plain/did:plc:test123/bafkreiblob\\d+@jpeg$`,
+ ),
);
assert.match(
updatedProfile.banner,
- /^https:\/\/cdn\.bsky\.app\/img\/banner\/plain\/did:plc:test123\/bafkreiblob\d+@jpeg$/,
+ new RegExp(
+ `^${escapeRegExp(CDN_URL)}/img/banner/plain/did:plc:test123/bafkreiblob\\d+@jpeg$`,
+ ),
);
});
@@ -4301,7 +4310,7 @@ describe("updateList", () => {
assert.equal(
dataStore.$lists.get(listUri).avatar,
- "https://cdn.bsky.app/img/avatar/plain/did:plc:test123/bafkreiavatarcid@jpeg",
+ `${CDN_URL}/img/avatar/plain/did:plc:test123/bafkreiavatarcid@jpeg`,
);
});
diff --git a/tests/unit/specs/templates/avatar.template.test.js b/tests/unit/specs/templates/avatar.template.test.js
index e54b1e0f..2b8df71e 100644
--- a/tests/unit/specs/templates/avatar.template.test.js
+++ b/tests/unit/specs/templates/avatar.template.test.js
@@ -1,6 +1,7 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { avatarTemplate } from "/js/templates/avatar.template.js";
+import { cdnImageUrl } from "/js/dataHelpers.js";
import { post } from "../../testData.js";
import { render } from "/js/lib/lit-html.js";
@@ -96,7 +97,10 @@ describe("avatarTemplate", () => {
const container = document.createElement("div");
render(result, container);
const img = container.querySelector("[data-testid='avatar-image']");
- assert.deepEqual(img.getAttribute("data-lightbox-src"), post.author.avatar);
+ assert.deepEqual(
+ img.getAttribute("data-lightbox-src"),
+ cdnImageUrl(post.author.avatar),
+ );
});
it("should use thumbnail URL for src and full-size URL for data-lightbox-src", () => {