Skip to main content

chives-{image,video}: allow setting a basePath for media files

ID
9e5d7ce
date
2026-10-31 20:20:42+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
e70630e
message
chives-{image,video}: allow setting a basePath for media files

This allows you to prepend a path for the rendered media files. This is
useful if the HTML file is in a subfolder and not the root of the media
collection.
changed files
9 files, 169 additions, 13 deletions

Changed files

CHANGELOG.md (9111 → 9375)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c404286..a7c411d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # CHANGELOG
 
+## v59 - 2026-10-31
+
+The `<chives-image>` and `<chives-video>` web components now allow setting a `basePath`, which prepends a path to the rendered media files.
+This is useful if they're being used in an HTML file which isn't in the root of the media collection.
+
 ## v58 - 2026-10-29
 
 Add a missing `pytest` dependency for the `browser_fixtures` extra.

javascript/chives-image.js (6561 → 6918)

diff --git a/javascript/chives-image.js b/javascript/chives-image.js
index 92f2d99..f058b0f 100644
--- a/javascript/chives-image.js
+++ b/javascript/chives-image.js
@@ -1,5 +1,5 @@
 // javascript/chives-image.js
-// chives version: 58
+// chives version: 59
 
 // ChivesImage provides a <chives-image> component that renders
 // an ImageEntity on a web page.
@@ -26,6 +26,9 @@ class ChivesImage extends HTMLElement {
       // Defaults to "full".
       "variant",
       //
+      // Optional path prepended to the path/thumbnail path.
+      "base-path",
+      //
       // Fields from ImageEntity
       "path",
       "thumbnail-path",
@@ -99,6 +102,11 @@ class ChivesImage extends HTMLElement {
     this.setAttribute("variant", "thumbnail");
   }
 
+  // basePath allows you to set the basePath where media is loaded from.
+  set basePath(p) {
+    this.setAttribute("base-path", p);
+  }
+
   // Rather than re-rendering the component on every attribute change
   // (which will fire seven times when we set imageEntity), batch render
   // changes together into the next event loop tick.
@@ -127,6 +135,9 @@ class ChivesImage extends HTMLElement {
     const href = this.getAttribute("href") || "";
 
     const variant = this.getAttribute("variant") || "full";
+    const basePath = this.getAttribute("base-path") !== null
+      ? this.getAttribute("base-path") + "/"
+      : "";
 
     const path = this.getAttribute("path");
     const thumbnailPath = this.getAttribute("thumbnail-path");
@@ -137,12 +148,12 @@ class ChivesImage extends HTMLElement {
     const hasTransparency = this.hasAttribute("has-transparency");
 
     // Decide which variant of the image will be displayed.
-    const displayPath =
-      variant === "full"
+    const displayPath = basePath +
+      (variant === "full"
         ? path
         : typeof thumbnailPath === 'string'
         ? thumbnailPath
-        : path;
+        : path);
 
     // Choose which wrapper element to use. Use a link if there's
     // an href, otherwise a wrapper div.

javascript/chives-pagination.js (4929 → 4929)

diff --git a/javascript/chives-pagination.js b/javascript/chives-pagination.js
index 1c7e6c8..44a0028 100644
--- a/javascript/chives-pagination.js
+++ b/javascript/chives-pagination.js
@@ -1,5 +1,5 @@
 // javascript/chives-pagination.js
-// chives version: 58
+// chives version: 59
 
 // ChivesPagination manages the pagination of a collection of items.
 //

javascript/chives-sorting.js (5034 → 5034)

diff --git a/javascript/chives-sorting.js b/javascript/chives-sorting.js
index 28df98e..dc98319 100644
--- a/javascript/chives-sorting.js
+++ b/javascript/chives-sorting.js
@@ -1,5 +1,5 @@
 // javascript/chives-sorting.js
-// chives version: 58
+// chives version: 59
 
 // ChivesSorting manages the sorting of a collection of items.
 //

javascript/chives-video.js (5147 → 5521)

diff --git a/javascript/chives-video.js b/javascript/chives-video.js
index abab4fd..be5514c 100644
--- a/javascript/chives-video.js
+++ b/javascript/chives-video.js
@@ -1,5 +1,5 @@
 // javascript/chives-video.js
-// chives version: 58
+// chives version: 59
 
 // ChivesVideo provides a <chives-video> component that renders
 // a VideoEntity on a web page.
@@ -22,6 +22,9 @@ class ChivesVideo extends HTMLElement {
       "poster-path",
       "subtitles",
       "autoplay",
+      //
+      // Optional path prepended to the path/poster path.
+      "base-path",
     ];
   }
 
@@ -56,6 +59,11 @@ class ChivesVideo extends HTMLElement {
     }
   }
 
+  // basePath allows you to set the basePath where media is loaded from.
+  set basePath(p) {
+    this.setAttribute("base-path", p);
+  }
+
   // Rather than re-rendering the component on every attribute change
   // (which will fire seven times when we set imageEntity), batch render
   // changes together into the next event loop tick.
@@ -87,6 +95,9 @@ class ChivesVideo extends HTMLElement {
     const height = this.getAttribute("height");
 
     const posterPath = this.getAttribute("poster-path");
+    const basePath = this.getAttribute("base-path") !== null
+      ? this.getAttribute("base-path") + "/"
+      : "";
 
     const subtitles = (this.getAttribute("subtitles") || "")
       .split(";")
@@ -134,14 +145,14 @@ class ChivesVideo extends HTMLElement {
     }
 
     video.preload = "none";
-    video.src = path;
-    video.poster = posterPath;
+    video.src = basePath + path;
+    video.poster = basePath + posterPath;
 
     subtitles.forEach(s => {
       const track = document.createElement("track");
       track.kind = "subtitles";
       track.label = s.label;
-      track.src = s.path;
+      track.src = basePath + s.path;
       track.srclang = s.label.substring(0, 2).toLowerCase()
       video.appendChild(track);
     });

python/chives/__init__.py (391 → 391)

diff --git a/python/chives/__init__.py b/python/chives/__init__.py
index 0ce13e4..35b9dcc 100644
--- a/python/chives/__init__.py
+++ b/python/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
 
 """
 
-__version__ = "58"
+__version__ = "59"

python/chives/browser_fixtures.py (12115 → 12144)

diff --git a/python/chives/browser_fixtures.py b/python/chives/browser_fixtures.py
index c26f5a3..3c0715b 100644
--- a/python/chives/browser_fixtures.py
+++ b/python/chives/browser_fixtures.py
@@ -123,10 +123,10 @@ def page(browser: Browser) -> Iterator[Page]:
         for msg in console_messages
         if msg.type == "error" or msg.type == "warning"
     ]
-    assert console_errors == []
+    assert console_errors == [], console_errors
 
     # Check there weren't any page errors
-    assert page_errors == []
+    assert page_errors == [], page_errors
 
 
 class PageError(Exception):

tests/javascript/test_chives_image.py (7026 → 9206)

diff --git a/tests/javascript/test_chives_image.py b/tests/javascript/test_chives_image.py
index e083e51..00ffb08 100644
--- a/tests/javascript/test_chives_image.py
+++ b/tests/javascript/test_chives_image.py
@@ -2,12 +2,15 @@
 Tests for the <chives-image> web component.
 """
 
+import contextlib
 import json
 from pathlib import Path
+import shutil
 
 from playwright.sync_api import expect, Page
 import pytest
 
+from chives.browser_fixtures import file_uri
 from chives.media import create_image_entity
 
 
@@ -245,3 +248,71 @@ def test_animated_gif_with_video_thumbnail(page: Page, tmp_path: Path) -> None:
     video_elem = chives_elem.locator("video")
     expect(video_elem).to_be_visible()
     assert (video_elem).get_attribute("src") == e["thumbnail_path"]
+
+
+def test_image_with_base_path(page: Page, tmp_path: Path) -> None:
+    """
+    If you set the basePath on an image, it's prepended to the path.
+    """
+    # Create an HTML file at tmp_path/viewer/index.html
+    with open("javascript/chives-image.js") as f:
+        component_js = f.read()
+
+    (tmp_path / "viewer").mkdir()
+    html_path = tmp_path / "viewer/index.html"
+    html_path.write_text("<script>" + component_js + "</script>")
+
+    page.goto(file_uri(html_path))
+
+    # Copy the media files to tmp_path/media
+    (tmp_path / "media").mkdir()
+    shutil.copyfile(
+        "tests/fixtures/media/470906.png",
+        tmp_path / "media/470906.png",
+    )
+
+    # Create the video entity with the working directory set to tmp_path/media
+    with contextlib.chdir(tmp_path / "media"):
+        e = create_image_entity(
+            "470906.png",
+            thumbnail_config={
+                "out_dir": ".thumbnails",
+                "width": 25,
+            },
+        )
+
+    # Check we get the correct path if we render the full-sized image.
+    page.evaluate(
+        """
+        const elem = document.createElement("chives-image");
+        elem.imageEntity = %s;
+        elem.basePath = '../media';
+        document.body.appendChild(elem);
+        """
+        % json.dumps(e),
+    )
+
+    chives_elem = page.locator("chives-image")
+
+    img_elem = chives_elem.locator("img")
+    expect(img_elem).to_be_visible()
+    assert (img_elem).get_attribute("src") == "../media/470906.png"
+
+    # Check we get the correct path if we render the thumbnail image.
+    page.evaluate(
+        """
+        document.querySelector("chives-image").remove();
+        const elem = document.createElement("chives-image");
+        elem.imageEntity = %s;
+        elem.variant = 'thumbnail';
+        elem.basePath = '../media';
+        document.body.appendChild(elem);
+        """
+        % json.dumps(e),
+    )
+
+    chives_elem = page.locator("chives-image")
+
+    img_elem = chives_elem.locator("img")
+    expect(img_elem).to_be_visible()
+    assert (img_elem).get_attribute("src") == "../media/.thumbnails/470906.png"

tests/javascript/test_chives_video.py (5095 → 7022)

diff --git a/tests/javascript/test_chives_video.py b/tests/javascript/test_chives_video.py
index b91ef6b..78db232 100644
--- a/tests/javascript/test_chives_video.py
+++ b/tests/javascript/test_chives_video.py
@@ -2,11 +2,15 @@
 Tests for the <chives-image> web component.
 """
 
+import contextlib
 import json
+from pathlib import Path
+import shutil
 
 from playwright.sync_api import expect, Page
 import pytest
 
+from chives.browser_fixtures import file_uri
 from chives.media import create_video_entity
 
 
@@ -79,6 +83,60 @@ def test_video_with_autoplay(page: Page) -> None:
     assert video_elem.get_attribute("autoplay") == ""
 
 
+def test_video_with_base_path(page: Page, tmp_path: Path) -> None:
+    """
+    If you set the basePath on a video, it's prepended to the path.
+    """
+    # Create an HTML file at tmp_path/viewer/index.html
+    with open("javascript/chives-video.js") as f:
+        component_js = f.read()
+
+    (tmp_path / "viewer").mkdir()
+    html_path = tmp_path / "viewer/index.html"
+    html_path.write_text("<script>" + component_js + "</script>")
+
+    page.goto(file_uri(html_path))
+
+    # Copy the media files to tmp_path/media
+    (tmp_path / "media").mkdir()
+    shutil.copyfile(
+        "tests/fixtures/media/wings_tracking_shot.mp4",
+        tmp_path / "media/wings_tracking_shot.mp4",
+    )
+    shutil.copyfile(
+        "tests/fixtures/media/wings_tracking_shot.jpg",
+        tmp_path / "media/wings_tracking_shot.jpg",
+    )
+
+    # Create the video entity with the working directory set to tmp_path/media
+    with contextlib.chdir(tmp_path / "media"):
+        e = create_video_entity(
+            video_path="wings_tracking_shot.mp4",
+            poster_path="wings_tracking_shot.jpg",
+            subtitles_path="wings_tracking_shot.en.vtt",
+        )
+
+    page.evaluate(
+        """
+        const elem = document.createElement("chives-video");
+        elem.videoEntity = %s;
+        elem.basePath = "../media";
+        document.body.appendChild(elem);
+        """
+        % json.dumps(e),
+    )
+
+    chives_elem = page.locator("chives-video")
+
+    video_elem = chives_elem.locator("video")
+    expect(video_elem).to_be_visible()
+    assert video_elem.get_attribute("src") == "../media/wings_tracking_shot.mp4"
+    assert video_elem.get_attribute("poster") == "../media/wings_tracking_shot.jpg"
+
+    track_elem = video_elem.locator("track")
+    assert track_elem.get_attribute("src") == "../media/wings_tracking_shot.en.vtt"
+
+
 @pytest.mark.parametrize(
     "width, height, orientation",
     [