From 4a43bd28f27d3d50b76514cbfc5be5746fe5ce7c Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Sun, 25 Oct 2026 11:49:32 -0400
Subject: [PATCH 1/8] Updates to improve handling of 404s for bibliography
entires
Custom 404 message is provided for bibliography entries. If we have a
record of the page being removed we supply both the page name and item
title. In either case, provide a link to the bibliography, Zotero and
the Biblography intake page.
Also maps so links that Google reports as 404s to relevant pages
on the website.
---
README.md | 10 +++
content/en/history/_index.md | 1 +
content/en/project/partners/_index.md | 1 +
content/en/software/install-and-run/_index.md | 1 +
.../install-and-run/windows/_index.md | 2 +
.../en/software/using-medley/online-using.md | 2 +
layouts/404.html | 85 +++++++++++++++++--
layouts/_partials/hooks/head-end.html | 5 ++
scripts/update_bibliography.sh | 18 ++++
tests/test_hugo_build.py | 65 ++++++++++++++
10 files changed, 185 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 4f84ff804..c52ed35aa 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,8 @@ cd scripts
This script retrieves the bibliography from Zotero, formats it appropriately, and places the individual JSON files in `static/data/bibItems/`.
+Entries marked as deleted in Zotero are recorded in `data/bibliography/removed.json`. The custom 404 page uses that list to recognize former bibliography URLs and show the removed entry's title. The file is merged, not replaced, so entries that Zotero has already purged are retained.
+
---
## Local Development
@@ -358,6 +360,7 @@ The repository follows the standard [Hugo directory structure](https://gohugo.io
| `assets/` | Custom global resources (CSS, JS, icons, SCSS) |
| `config/` | Site configuration for different environments |
| `content/en/` | All website content (English only) |
+| `data/` | Build-time data files |
| `layouts/` | Hugo layout templates and overrides |
| `static/` | Files copied directly to the built site |
@@ -395,6 +398,13 @@ The repository follows the standard [Hugo directory structure](https://gohugo.io
+
+data/ — Build-time data
+
+- `bibliography/removed.json` — Keys and titles of bibliography entries removed from Zotero (read by the 404 page)
+
+
+
static/ — Static files
diff --git a/content/en/history/_index.md b/content/en/history/_index.md
index a4dd04593..71626f8c9 100644
--- a/content/en/history/_index.md
+++ b/content/en/history/_index.md
@@ -4,6 +4,7 @@ weight: 500
type: docs
aliases:
- /medley/history/
+ - /doc/info/history.html
---
## The Short History
diff --git a/content/en/project/partners/_index.md b/content/en/project/partners/_index.md
index bb8e505e7..a753bbcc7 100644
--- a/content/en/project/partners/_index.md
+++ b/content/en/project/partners/_index.md
@@ -4,6 +4,7 @@ weight: 18
type: docs
aliases:
- /medley/project/partners
+ - /about/partners/
---
### [Software Preservation Network](https://www.softwarepreservationnetwork.org/about/)
diff --git a/content/en/software/install-and-run/_index.md b/content/en/software/install-and-run/_index.md
index f9a6df6ab..fe5449567 100644
--- a/content/en/software/install-and-run/_index.md
+++ b/content/en/software/install-and-run/_index.md
@@ -7,6 +7,7 @@ aliases:
- /developing/
- /software/emu-compat/
- /using/running/
+ - /running/running-with-docker
weight: 10
type: docs
---
diff --git a/content/en/software/install-and-run/windows/_index.md b/content/en/software/install-and-run/windows/_index.md
index fd29c1632..dcde3bfb8 100644
--- a/content/en/software/install-and-run/windows/_index.md
+++ b/content/en/software/install-and-run/windows/_index.md
@@ -3,6 +3,8 @@ title: Install and Run on Windows
linkTitle: Windows
weight: 40
type: docs
+aliases:
+ - /medley/using/running/runnning-on-win
---
There are two ways to install and run Medley on Windows. These two ways result in (nearly) identical Medley experiences, but differ in how Medley is installed and started.
diff --git a/content/en/software/using-medley/online-using.md b/content/en/software/using-medley/online-using.md
index 5e2f341b7..4ab6a88ea 100644
--- a/content/en/software/using-medley/online-using.md
+++ b/content/en/software/using-medley/online-using.md
@@ -2,6 +2,8 @@
title: Using Medley Online
weight: 40
type: docs
+aliases:
+ - /running/online/usingonline
---
diff --git a/layouts/404.html b/layouts/404.html
index 2471a8337..a9ab7f745 100644
--- a/layouts/404.html
+++ b/layouts/404.html
@@ -5,11 +5,36 @@
-
Page Not Found
-
-
The page you were looking for does not exist. It may have been moved or removed.
-
You can use the search box or browse the site sections on the left to find what you are looking for.
-
Or return to the homepage.
+
+
Page Not Found
+
+
The page you were looking for does not exist. It may have been moved or removed.
+
You can use the search box or browse the site sections on the left to find what you are looking for.
+
Or return to the homepage.
+
+
+
+
Bibliography Entry Not Found
+
+
+ The bibliography entry —
+ is no longer available.
+
+
+ The Interlisp bibliography is generated from our
+ Zotero group library.
+ Entries may be removed or reclassified when the library changes, which removes
+ the corresponding page from this site. If you followed a link from another
+ site, that link may be out of date.
+
+
You can:
+
+
Or return to the homepage.
+
+
+{{- $bibData := index hugo.Data "bibliography" -}}
+{{- $removed := slice -}}
+{{- with $bibData -}}
+ {{- $removed = .removed | default (slice) -}}
+{{- end -}}
+
+
{{ end }}
diff --git a/layouts/_partials/hooks/head-end.html b/layouts/_partials/hooks/head-end.html
index 14f526e3c..25c9439ff 100644
--- a/layouts/_partials/hooks/head-end.html
+++ b/layouts/_partials/hooks/head-end.html
@@ -8,4 +8,9 @@
{{ with .Params.robots }}
{{ end }}
+{{- /* The 404 page is served with an HTTP 404 status. Tell crawlers not to
+ index it even if they ignore the status code. */ -}}
+{{ if eq .Kind "404" }}
+
+{{ end }}
{{ partial "bibliography-json-ld.html" . }}
diff --git a/scripts/update_bibliography.sh b/scripts/update_bibliography.sh
index ab9dae234..2e27a3671 100755
--- a/scripts/update_bibliography.sh
+++ b/scripts/update_bibliography.sh
@@ -355,6 +355,24 @@ if $debugFiles ; then
fi
showInfo 5 "Delete items that Zotero indicated as such."
+# Save the deleted entries before filtering them out. The custom 404 page uses
+# this list to identify bibliography URLs that were removed and show their
+# titles. Keep only keys and titles; the list is embedded in 404.html at build
+# time. Merge with the existing file instead of replacing it: once Zotero
+# purges an item, the API no longer reports it as deleted, but its old URL can
+# still be requested by search engines and external links.
+REMOVED_DATA_FILE="./../data/bibliography/removed.json"
+mkdir -p "$(dirname "$REMOVED_DATA_FILE")"
+deletedItems=$(jq 'map(select(.deleted)) | map({key: .key, title: (.title // "")})' <<< "$items")
+if [[ -f "$REMOVED_DATA_FILE" ]]; then
+ # Newly deleted entries override older records with the same key.
+ jq --argjson deleted "$deletedItems" \
+ '(INDEX(.key) + ($deleted | INDEX(.key))) | [.[]] | sort_by(.key)' \
+ "$REMOVED_DATA_FILE" > "$REMOVED_DATA_FILE.tmp"
+else
+ printf '%s\n' "$deletedItems" > "$REMOVED_DATA_FILE.tmp"
+fi
+mv "$REMOVED_DATA_FILE.tmp" "$REMOVED_DATA_FILE"
beforeCount=$(jq '. | length' <<< "$items")
items=$(jq 'map(select(.deleted|not))' <<< "$items")
afterCount=$(jq '. | length' <<< "$items")
diff --git a/tests/test_hugo_build.py b/tests/test_hugo_build.py
index 55c22fee1..d733b7234 100644
--- a/tests/test_hugo_build.py
+++ b/tests/test_hugo_build.py
@@ -20,6 +20,7 @@
PYTEST_FORCE_HUGO_BUILD=1 pytest tests/test_hugo_build.py -v
"""
+import json
import os
import re
import xml.etree.ElementTree as ET
@@ -119,6 +120,70 @@ def test_page_exists(self, path: str) -> None:
)
+class TestNotFoundPage:
+ """The custom 404 page must handle removed bibliography entries."""
+
+ @pytest.fixture(autouse=True)
+ def page(self):
+ path = PROD_PUBLIC / "404.html"
+ if not path.exists():
+ pytest.skip("404.html not found — run a production build first")
+ self.content = path.read_text(encoding="utf-8", errors="ignore")
+
+ def test_has_bibliography_fallback(self) -> None:
+ assert 'id="not-found-bibliography"' in self.content
+ assert "/history/bibliography/" in self.content
+ assert "bibliography-removed-data" in self.content
+
+ def test_embeds_removed_entries_as_json(self) -> None:
+ match = re.search(
+ r'',
+ self.content,
+ re.DOTALL,
+ )
+ assert match, "Removed-bibliography data block not found in 404.html"
+ removed = json.loads(match.group(1))
+ assert isinstance(removed, list)
+ for entry in removed:
+ assert isinstance(entry.get("key"), str) and entry["key"]
+ assert isinstance(entry.get("title"), str)
+
+ def test_is_noindex(self) -> None:
+ assert re.search(
+ r' None:
+ redirect = PROD_PUBLIC / path
+ if not redirect.exists():
+ pytest.skip("public/ not built — run hugo first")
+ content = redirect.read_text(encoding="utf-8", errors="ignore")
+ assert target in content, (
+ f"Redirect {path} does not point at {target}:\n{content[:200]}"
+ )
+
+
class TestSitemapIntegrity:
"""The sitemap must be valid XML and reference only absolute URLs."""
From a702a13c9ed93a21e48cb09cb5d53e853c377398 Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Sun, 25 Oct 2026 23:39:35 -0400
Subject: [PATCH 2/8] Fix 404 test to correctly parse staging site URLs.
---
README.md | 4 +-
config/staging/hugo.yaml | 5 +-
data/bibliography/removed.json | 66 +++++++++++++++++
layouts/404.html | 32 ++++++---
staging-site-design.md | 6 +-
static/IRM_files/content.htm | 3 +
tests/test_hugo_build.py | 128 +++++++++++++++++++++++++++++++++
7 files changed, 230 insertions(+), 14 deletions(-)
create mode 100644 data/bibliography/removed.json
create mode 100644 static/IRM_files/content.htm
diff --git a/README.md b/README.md
index c52ed35aa..8fb388daf 100644
--- a/README.md
+++ b/README.md
@@ -325,8 +325,8 @@ repository's GitHub Pages site:
| Deployment | URL |
|------------|-----|
-| Staging root | `https://interlisp.github.io/Interlisp.staging/` |
-| PR #123 preview | `https://interlisp.github.io/Interlisp.staging/pr-123/` |
+| Staging root | `https://interlisp.org/Interlisp.staging/` |
+| PR #123 preview | `https://interlisp.org/Interlisp.staging/pr-123/` |
When a PR is opened or updated, the production workflow (`gh-pages.yml`)
triggers the `deploy-preview` workflow in the staging repository, which:
diff --git a/config/staging/hugo.yaml b/config/staging/hugo.yaml
index 09e8b8ee2..2b6526125 100644
--- a/config/staging/hugo.yaml
+++ b/config/staging/hugo.yaml
@@ -1,4 +1,7 @@
-baseURL: https://interlisp.github.io/Interlisp.staging/
+baseURL: https://interlisp.org/Interlisp.staging/
+
+# Per-PR previews override this at build time so that links stay inside the
+# preview, e.g. https://interlisp.org/Interlisp.staging/pr-/
locale: en-us
diff --git a/data/bibliography/removed.json b/data/bibliography/removed.json
new file mode 100644
index 000000000..629d58868
--- /dev/null
+++ b/data/bibliography/removed.json
@@ -0,0 +1,66 @@
+[
+ {
+ "key": "47P4BUHS",
+ "title": "Script for testing defstruct and the inspector with defstruct"
+ },
+ {
+ "key": "4R2EXMY4",
+ "title": "TEST APPRENTICE"
+ },
+ {
+ "key": "59UEUE22",
+ "title": "THE INTERLISP-D TESTING SYSTEM - USER GUIDE"
+ },
+ {
+ "key": "7KUMIRNC",
+ "title": "Special Files on ARs>"
+ },
+ {
+ "key": "D8B2SFFU",
+ "title": "Running AR Test Cases & Recording the Results"
+ },
+ {
+ "key": "ERU4YWDT",
+ "title": "Script for INSPECTALLFIELDSFLG"
+ },
+ {
+ "key": "F7XLXRG5",
+ "title": "Lyric Manual Test procedures"
+ },
+ {
+ "key": "JFGREFUI",
+ "title": "THE INTERLISP-D TESTING SYSTEM"
+ },
+ {
+ "key": "KHWFGC5M",
+ "title": "Debugger, Error System, and Unwinder test report"
+ },
+ {
+ "key": "LJQYU5HX",
+ "title": "Scripts for testing the record package"
+ },
+ {
+ "key": "RJLLRQDR",
+ "title": "SEdit test report"
+ },
+ {
+ "key": "SHRDJ9J4",
+ "title": "Testing Report"
+ },
+ {
+ "key": "SVIQ724G",
+ "title": "Test Utilities"
+ },
+ {
+ "key": "TAYYCYB8",
+ "title": "DEdit test report"
+ },
+ {
+ "key": "TTHB6JCX",
+ "title": "Testing the programmatic interface to INSPECTW"
+ },
+ {
+ "key": "UIS8C4AI",
+ "title": "Script to check code inspectors on frames"
+ }
+]
diff --git a/layouts/404.html b/layouts/404.html
index a9ab7f745..90cd711b6 100644
--- a/layouts/404.html
+++ b/layouts/404.html
@@ -29,11 +29,11 @@ Bibliography Entry Not Found
You can:
- Or return to the homepage.
+ Or return to the homepage.
@@ -51,14 +51,22 @@ Bibliography Entry Not Found
{{- $removed = .removed | default (slice) -}}
{{- end -}}
-
diff --git a/tests/test_hugo_build.py b/tests/test_hugo_build.py
index d733b7234..ef44ecac7 100644
--- a/tests/test_hugo_build.py
+++ b/tests/test_hugo_build.py
@@ -23,6 +23,8 @@
import json
import os
import re
+import shutil
+import subprocess
import xml.etree.ElementTree as ET
from pathlib import Path
from urllib.parse import urlparse
@@ -54,6 +56,86 @@ def _get_baseurl_path() -> str:
return ""
+def _run_404_script(content: str, path: str, tmp_path: Path) -> dict:
+ """Execute the built 404 page script in Node with a minimal fake DOM.
+
+ Returns the visible state and rewritten links for ``path``.
+ """
+ node = shutil.which("node")
+ if not node:
+ pytest.skip("Node.js is required to exercise the 404 page script")
+
+ script_match = re.search(
+ r'', content, re.DOTALL
+ )
+ assert script_match, "not-found-script block not found in 404.html"
+
+ data_match = re.search(
+ r'',
+ content,
+ re.DOTALL,
+ )
+ assert data_match, "bibliography removed data not found in 404.html"
+
+ harness = "\n".join(
+ [
+ "function makeElement(id) {",
+ " const classes = new Set();",
+ " return {",
+ " id,",
+ ' textContent: "",',
+ ' href: "",',
+ " classes,",
+ " classList: {",
+ " add: (c) => classes.add(c),",
+ " remove: (c) => classes.delete(c),",
+ " contains: (c) => classes.has(c),",
+ " },",
+ " };",
+ "}",
+ "const ids = [",
+ ' "not-found-generic", "not-found-bibliography",',
+ ' "not-found-bib-key", "not-found-bib-title",',
+ ' "not-found-bib-title-wrap", "not-found-bib-zotero",',
+ ' "not-found-bib-browse", "not-found-bib-intake",',
+ ' "not-found-bib-home",',
+ "];",
+ "const elements = Object.fromEntries(ids.map((id) => [id, makeElement(id)]));",
+ 'elements["not-found-bibliography"].classList.add("d-none");',
+ 'elements["not-found-bib-title-wrap"].classList.add("d-none");',
+ "const removedData = { textContent: "
+ + json.dumps(data_match.group(1))
+ + " };",
+ "const byId = Object.assign({}, elements, {",
+ ' "bibliography-removed-data": removedData,',
+ "});",
+ "const window = { location: { pathname: "
+ + json.dumps(path)
+ + " } };",
+ 'const document = { title: "", getElementById: (id) => byId[id] || null };',
+ script_match.group(1),
+ "console.log(JSON.stringify({",
+ ' visible: !elements["not-found-bibliography"].classes.has("d-none"),',
+ ' key: elements["not-found-bib-key"].textContent,',
+ ' browse: elements["not-found-bib-browse"].href,',
+ ' intake: elements["not-found-bib-intake"].href,',
+ ' home: elements["not-found-bib-home"].href,',
+ ' title: document.title,',
+ "}));",
+ ]
+ )
+
+ script_file = tmp_path / "check-404.js"
+ script_file.write_text(harness, encoding="utf-8")
+ result = subprocess.run(
+ [node, str(script_file)],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ return json.loads(result.stdout.strip().splitlines()[-1])
+
+
# ---------------------------------------------------------------------------
# Build process tests
# ---------------------------------------------------------------------------
@@ -154,6 +236,52 @@ def test_is_noindex(self) -> None:
self.content,
), "404 page must be marked noindex"
+ def test_bibliography_match_ignores_site_prefix(self) -> None:
+ """Staging previews live under /Interlisp.staging/pr-/ and are
+ served by the domain-level 404 page, so detection must match the end
+ of the path rather than a build-time baseURL prefix."""
+ assert r"/\/history\/bibliography\/([A-Za-z0-9]{8})\/?$/" in self.content
+ assert "startsWith(bibRoot)" not in self.content
+ for element_id in (
+ "not-found-bib-browse",
+ "not-found-bib-intake",
+ "not-found-bib-home",
+ ):
+ assert f'id="{element_id}"' in self.content
+
+ @pytest.mark.parametrize(
+ ("path", "expected_root"),
+ (
+ ("/history/bibliography/wxmqhqnz/", "/"),
+ ("/history/bibliography/WXMQHQNZ", "/"),
+ (
+ "/Interlisp.staging/pr-355/history/bibliography/wxmqhqnz/",
+ "/Interlisp.staging/pr-355/",
+ ),
+ ),
+ )
+ def test_bibliography_links_follow_site_prefix(
+ self, path: str, expected_root: str, tmp_path: Path
+ ) -> None:
+ result = _run_404_script(self.content, path, tmp_path)
+ assert result["visible"] is True
+ assert result["key"] == "WXMQHQNZ"
+ assert result["browse"] == expected_root + "history/bibliography/"
+ assert result["intake"] == expected_root + "history/bibliography/intake/"
+ assert result["home"] == expected_root
+
+ @pytest.mark.parametrize(
+ "path",
+ (
+ "/history/bibliography/",
+ "/some/other/page/",
+ "/Interlisp.staging/pr-355/history/bibliography/",
+ ),
+ )
+ def test_non_entry_paths_stay_generic(self, path: str, tmp_path: Path) -> None:
+ result = _run_404_script(self.content, path, tmp_path)
+ assert result["visible"] is False
+
class TestLegacyRedirects:
"""Aliases and static redirects must exist for known legacy URLs."""
From 0641c1c857095dbb56290196af67fe97680f38fd Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Mon, 26 Oct 2026 07:43:49 -0400
Subject: [PATCH 3/8] Force rebuild.
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 8fb388daf..38c2ecc76 100644
--- a/README.md
+++ b/README.md
@@ -478,3 +478,4 @@ Modifying search scope requires updating the Google Custom Search engine setting
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
Copyright (c) 2026 Interlisp.org
+
From eb43fa6d6bf2ee7dba078547684bcdb496646203 Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Mon, 26 Oct 2026 08:01:13 -0400
Subject: [PATCH 4/8] Force rebuild
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 38c2ecc76..8fb388daf 100644
--- a/README.md
+++ b/README.md
@@ -478,4 +478,3 @@ Modifying search scope requires updating the Google Custom Search engine setting
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
Copyright (c) 2026 Interlisp.org
-
From d8803daef009ec973f83783ec52bc828ade1c57b Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Mon, 26 Oct 2026 17:35:37 -0400
Subject: [PATCH 5/8] Force rebuild
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 8fb388daf..38c2ecc76 100644
--- a/README.md
+++ b/README.md
@@ -478,3 +478,4 @@ Modifying search scope requires updating the Google Custom Search engine setting
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
Copyright (c) 2026 Interlisp.org
+
From 5c6fd7d87a7b825b162dcaeeb06b01ea6224dbe1 Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Wed, 28 Oct 2026 07:30:51 -0400
Subject: [PATCH 6/8] Loosen up rules on what gets a bibliography 404 page.
---
layouts/404.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/layouts/404.html b/layouts/404.html
index 90cd711b6..82e22f87a 100644
--- a/layouts/404.html
+++ b/layouts/404.html
@@ -61,7 +61,7 @@ Bibliography Entry Not Found
// GitHub Pages supports only one custom 404 per site, and a preview can be
// served by the domain-level 404 page, so the build-time baseURL cannot be
// used to decide what kind of URL this is.
- const match = path.match(/\/history\/bibliography\/([A-Za-z0-9]{8})\/?$/);
+ const match = path.match(/\/history\/bibliography\/([^?#]+)\/?(?:[?#]|$)/);
if (!match) return;
const key = match[1];
From 39e87b18693c5d5ce30d43b12ecb7dcc8dac97fe Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Wed, 28 Oct 2026 07:52:22 -0400
Subject: [PATCH 7/8] Update processing to handle ending slash correctly.
---
layouts/404.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/layouts/404.html b/layouts/404.html
index 82e22f87a..e78dba37b 100644
--- a/layouts/404.html
+++ b/layouts/404.html
@@ -61,10 +61,10 @@ Bibliography Entry Not Found
// GitHub Pages supports only one custom 404 per site, and a preview can be
// served by the domain-level 404 page, so the build-time baseURL cannot be
// used to decide what kind of URL this is.
- const match = path.match(/\/history\/bibliography\/([^?#]+)\/?(?:[?#]|$)/);
+ const match = path.match(/\/history\/bibliography\/([^/?#]+(?:\/[^/?#]+)*)\/?(?:[?#]|$)/);
if (!match) return;
- const key = match[1];
+ const key = match[1].replace(/\/+$/, "");
// Everything before "history/", including any staging/PR prefix.
const siteRoot = path.slice(0, match.index + 1);
From 8ceb8ed84d8667043824f9bec305133faa56b1b7 Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Wed, 28 Oct 2026 08:04:40 -0400
Subject: [PATCH 8/8] Update test to support regex changes.
---
tests/test_hugo_build.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_hugo_build.py b/tests/test_hugo_build.py
index ef44ecac7..135be2874 100644
--- a/tests/test_hugo_build.py
+++ b/tests/test_hugo_build.py
@@ -240,7 +240,7 @@ def test_bibliography_match_ignores_site_prefix(self) -> None:
"""Staging previews live under /Interlisp.staging/pr-/ and are
served by the domain-level 404 page, so detection must match the end
of the path rather than a build-time baseURL prefix."""
- assert r"/\/history\/bibliography\/([A-Za-z0-9]{8})\/?$/" in self.content
+ assert r"/\/history\/bibliography\/([^/?#]+(?:\/[^/?#]+)*)\/?(?:[?#]|$)/" in self.content
assert "startsWith(bibRoot)" not in self.content
for element_id in (
"not-found-bib-browse",