From fb8b2afef89aba7a8e10fb81fa73612691ba2b97 Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Fri, 5 Jun 2026 21:29:20 -0400
Subject: [PATCH 1/4] Update bibliographies to include additional metadaa.
Metadata is encoded as JSON-LD and provides information for Google and
other search engines. A first step in enabling richer data in our
searches.
---
layouts/_partials/bibliography-json-ld.html | 290 ++++++++++++++++++++
layouts/_partials/hooks/head-end.html | 1 +
2 files changed, 291 insertions(+)
create mode 100644 layouts/_partials/bibliography-json-ld.html
diff --git a/layouts/_partials/bibliography-json-ld.html b/layouts/_partials/bibliography-json-ld.html
new file mode 100644
index 00000000..b466452f
--- /dev/null
+++ b/layouts/_partials/bibliography-json-ld.html
@@ -0,0 +1,290 @@
+{{- /*
+ bibliography-json-ld.html
+ Emits a
+
+{{- else if .IsSection -}}
+
+{{- /* ── COLLECTION PAGE (bibliography index) ──────────────────────────── */ -}}
+{{- $pages := .RegularPages -}}
+{{- $pages = sort $pages "Date" "desc" -}}
+{{- $pages = sort $pages "Title" "asc" -}}
+
+{{- $itemList := slice -}}
+{{- range $i, $p := $pages -}}
+ {{- $itemList = append (dict
+ "@type" "ListItem"
+ "position" (add $i 1)
+ "name" $p.Title
+ "url" $p.Permalink
+ ) $itemList -}}
+{{- end -}}
+
+{{- $desc := .Description | default "A bibliography of publications, patents, reports, and other artifacts related to the Interlisp programming environment." -}}
+
+{{- $ld := dict
+ "@context" "https://schema.org"
+ "@type" "CollectionPage"
+ "name" .Title
+ "url" .Permalink
+ "description" $desc
+ "inLanguage" "en-US"
+ "mainEntity" (dict
+ "@type" "ItemList"
+ "name" .Title
+ "numberOfItems" (len $pages)
+ "itemListElement" $itemList
+ )
+-}}
+
+
+
+{{- end -}}
+
+{{- end -}}
diff --git a/layouts/_partials/hooks/head-end.html b/layouts/_partials/hooks/head-end.html
index 9daa28c0..f800ce9f 100644
--- a/layouts/_partials/hooks/head-end.html
+++ b/layouts/_partials/hooks/head-end.html
@@ -1,2 +1,3 @@
+{{ partial "bibliography-json-ld.html" . }}
From 3940d92b1e4c1ed7352355758eaee3c194e31002 Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Thu, 18 Jun 2026 23:21:03 -0400
Subject: [PATCH 2/4] Clean up book metadata. - Remove incorrect usage of
locationCreated field - Fix formatting of titles. Remove newline character
at end of string
---
layouts/_partials/bibliography-json-ld.html | 3 ---
scripts/bibSplit.pl | 8 +++++---
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/layouts/_partials/bibliography-json-ld.html b/layouts/_partials/bibliography-json-ld.html
index b466452f..b83b770f 100644
--- a/layouts/_partials/bibliography-json-ld.html
+++ b/layouts/_partials/bibliography-json-ld.html
@@ -172,9 +172,6 @@
{{- with .Params.publisher -}}
{{- $ld = merge $ld (dict "publisher" (dict "@type" "Organization" "name" .)) -}}
{{- end -}}
- {{- with .Params.place -}}
- {{- $ld = merge $ld (dict "locationCreated" .) -}}
- {{- end -}}
{{- end -}}
{{- /* ── Chapter ──────────────────────────────────────────────────────── */ -}}
diff --git a/scripts/bibSplit.pl b/scripts/bibSplit.pl
index 8a9b77be..97356543 100755
--- a/scripts/bibSplit.pl
+++ b/scripts/bibSplit.pl
@@ -58,10 +58,12 @@ sub sanitize_text {
my $type = $obj->{type} // '';
- # Titles can have colons and other special characters. Place YAML keyword on one line
- # and follow it with the title indented on subsequent line
+ # Titles can have colons and other special characters. Use a YAML double-quoted scalar
+ # so the decoded value has no trailing newline (the literal-block `|` style would add one).
my $itemTitle = sanitize_text($obj->{title} // '');
- my $title = $itemTitle eq '' ? "title: ''" : "title: |\n $itemTitle\n";
+ (my $escapedTitle = $itemTitle) =~ s/\\/\\\\/g;
+ $escapedTitle =~ s/"/\\"/g;
+ my $title = $itemTitle eq '' ? "title: ''" : "title: \"$escapedTitle\"";
# Abstracts can be multi-line and contain multiple paragraphs. Place YAML keyword on
# one line and follow it with the abstract indented on subsequent lines.
From 9222f83fa069bd201a2e3a541a9a882566839a45 Mon Sep 17 00:00:00 2001
From: Bill Stumbo
Date: Thu, 2 Jul 2026 23:44:19 -0400
Subject: [PATCH 3/4] Add tests for metadata types. Update code to fix minor
mistakes.
Building test cases revealed minor, incorrect or poor usages of
metadata.
---
.gitignore | 1 +
config/_default/params.yaml | 3 -
config/testing/hugo.yaml | 50 +
content/en/history/bibliography/_index.md | 1 +
layouts/_partials/bibliography-date.html | 27 +
layouts/_partials/bibliography-json-ld.html | 72 +-
layouts/bibliography/single.html | 14 +-
scripts/bibSplit.pl | 15 +-
tests/fixtures/bibliography/ZTEST-BLOG.md | 20 +
tests/fixtures/bibliography/ZTEST-BOOK.md | 20 +
tests/fixtures/bibliography/ZTEST-CHAPTER.md | 23 +
.../fixtures/bibliography/ZTEST-CONFERENCE.md | 23 +
.../bibliography/ZTEST-ENCYCLOPEDIA.md | 20 +
tests/fixtures/bibliography/ZTEST-JOURNAL.md | 27 +
tests/fixtures/bibliography/ZTEST-MAGAZINE.md | 23 +
tests/fixtures/bibliography/ZTEST-MESSAGE.md | 18 +
tests/fixtures/bibliography/ZTEST-NO-URLS.md | 21 +
tests/fixtures/bibliography/ZTEST-PATENT.md | 23 +
tests/fixtures/bibliography/ZTEST-REPORT.md | 20 +
tests/fixtures/bibliography/ZTEST-SOFTWARE.md | 19 +
tests/fixtures/bibliography/ZTEST-THESIS.md | 20 +
tests/fixtures/bibliography/ZTEST-VIDEO.md | 22 +
tests/fixtures/bibliography/ZTEST-WEBPAGE.md | 20 +
tests/test_bibliography_jsonld.py | 863 ++++++++++++++++++
24 files changed, 1321 insertions(+), 44 deletions(-)
create mode 100644 config/testing/hugo.yaml
create mode 100644 layouts/_partials/bibliography-date.html
create mode 100644 tests/fixtures/bibliography/ZTEST-BLOG.md
create mode 100644 tests/fixtures/bibliography/ZTEST-BOOK.md
create mode 100644 tests/fixtures/bibliography/ZTEST-CHAPTER.md
create mode 100644 tests/fixtures/bibliography/ZTEST-CONFERENCE.md
create mode 100644 tests/fixtures/bibliography/ZTEST-ENCYCLOPEDIA.md
create mode 100644 tests/fixtures/bibliography/ZTEST-JOURNAL.md
create mode 100644 tests/fixtures/bibliography/ZTEST-MAGAZINE.md
create mode 100644 tests/fixtures/bibliography/ZTEST-MESSAGE.md
create mode 100644 tests/fixtures/bibliography/ZTEST-NO-URLS.md
create mode 100644 tests/fixtures/bibliography/ZTEST-PATENT.md
create mode 100644 tests/fixtures/bibliography/ZTEST-REPORT.md
create mode 100644 tests/fixtures/bibliography/ZTEST-SOFTWARE.md
create mode 100644 tests/fixtures/bibliography/ZTEST-THESIS.md
create mode 100644 tests/fixtures/bibliography/ZTEST-VIDEO.md
create mode 100644 tests/fixtures/bibliography/ZTEST-WEBPAGE.md
create mode 100644 tests/test_bibliography_jsonld.py
diff --git a/.gitignore b/.gitignore
index 66e48255..2a1a5fd2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ _site
.sass-cache
.jekyll-metadata
public/
+tests/public_test/
node_modules/
_gen/
.hugo_build.lock
diff --git a/config/_default/params.yaml b/config/_default/params.yaml
index 0ccd9e4d..c4f0ba91 100644
--- a/config/_default/params.yaml
+++ b/config/_default/params.yaml
@@ -163,9 +163,6 @@ ui:
# Sidebar generation is slow otherwise
sidebar_cache_limit: 100
- # We have almost 200 attributes; don't truncate the sidebar to max 50 contents.
- sidebar_menu_truncate: 0
-
# Set to true to disable breadcrumb navigation.
breadcrumb_disable: false
diff --git a/config/testing/hugo.yaml b/config/testing/hugo.yaml
new file mode 100644
index 00000000..f625566c
--- /dev/null
+++ b/config/testing/hugo.yaml
@@ -0,0 +1,50 @@
+# Hugo testing environment configuration
+#
+# Used exclusively when running the bibliography JSON-LD test suite:
+#
+# hugo --environment testing --destination tests/public_test
+#
+# This environment mounts the test fixtures from tests/fixtures/bibliography/
+# alongside the real bibliography content, so the JSON-LD template is exercised
+# against controlled data without those pages ever appearing in a production build.
+#
+# The module.mounts block must re-declare all standard project-level mounts
+# because adding any mount replaces Hugo's defaults for the project.
+# Theme (Docsy) mounts defined within that module are unaffected.
+
+baseURL: "http://localhost/"
+
+publishDir: tests/public_test
+
+# Broken refs in unrelated content files must not block the test build.
+refLinksErrorLevel: WARNING
+
+# Explicit mounts (see module section below) require this to be re-stated
+# so that English content URLs do not gain an /en/ prefix.
+defaultContentLanguageInSubdir: false
+
+module:
+ mounts:
+ # ── Standard project mounts (preserve defaults) ──────────────────────────
+ # Mount content/en/ directly as the English language content root so that
+ # URLs are generated without an /en/ prefix (matching production behavior).
+ - source: content/en
+ target: content
+ lang: en
+ - source: static
+ target: static
+ - source: layouts
+ target: layouts
+ - source: data
+ target: data
+ - source: assets
+ target: assets
+ - source: i18n
+ target: i18n
+ - source: archetypes
+ target: archetypes
+ # ── Test fixtures ─────────────────────────────────────────────────────────
+ # Mount alongside the real bibliography content for the English language.
+ - source: tests/fixtures/bibliography
+ target: content/history/bibliography
+ lang: en
diff --git a/content/en/history/bibliography/_index.md b/content/en/history/bibliography/_index.md
index a4ad1df1..4a499831 100644
--- a/content/en/history/bibliography/_index.md
+++ b/content/en/history/bibliography/_index.md
@@ -4,6 +4,7 @@ heading: Interlisp Bibliography
type: bibliography
cascade:
type: bibliography
+ toc_hide: true
weight: 5
aliases:
- /bibliography/
diff --git a/layouts/_partials/bibliography-date.html b/layouts/_partials/bibliography-date.html
new file mode 100644
index 00000000..69c1bc9c
--- /dev/null
+++ b/layouts/_partials/bibliography-date.html
@@ -0,0 +1,27 @@
+{{- /*
+ bibliography-date.html
+ Resolves a bibliography page's publication date.
+
+ The front-matter `date` param is a plain YYYY-MM-DD string produced by
+ bibSplit.pl. Hugo's built-in .Date can be a full datetime parsed from git
+ history; this partial prefers the explicit param when it is well-formed.
+
+ Accepts: page context (.)
+ Returns: dict
+ dateISO – YYYY-MM-DD string; "0001-01-01" when no date is known
+ hasDate – true when dateISO is not the zero value
+ dateLabel – .Params.readabledate when present, otherwise dateISO
+*/ -}}
+{{- $dateTime := .Date -}}
+{{- with .Params.date -}}
+ {{- $iso := trim . " " -}}
+ {{- if and (ne $iso "") (findRE `^\d{4}-\d{2}-\d{2}$` $iso) -}}
+ {{- $dateTime = time $iso -}}
+ {{- end -}}
+{{- end -}}
+{{- $dateISO := $dateTime.Format "2006-01-02" -}}
+{{- return dict
+ "dateISO" $dateISO
+ "hasDate" (ne $dateISO "0001-01-01")
+ "dateLabel" (or .Params.readabledate $dateISO)
+-}}
diff --git a/layouts/_partials/bibliography-json-ld.html b/layouts/_partials/bibliography-json-ld.html
index b83b770f..1cbbec25 100644
--- a/layouts/_partials/bibliography-json-ld.html
+++ b/layouts/_partials/bibliography-json-ld.html
@@ -11,18 +11,13 @@
*/ -}}
{{- if eq .Type "bibliography" -}}
+{{- /* ── Date (resolved once, used by the IsPage branch below) ───────────── */ -}}
+{{- $date := partial "bibliography-date.html" . -}}
+
{{- if .IsPage -}}
-{{- /* ── Date: mirror the logic in single.html ──────────────────────────── */ -}}
-{{- $dateTime := .Date -}}
-{{- with .Params.date -}}
- {{- $iso := trim . " " -}}
- {{- if and (ne $iso "") (findRE `^\d{4}-\d{2}-\d{2}$` $iso) -}}
- {{- $dateTime = time $iso -}}
- {{- end -}}
-{{- end -}}
-{{- $dateISO := $dateTime.Format "2006-01-02" -}}
-{{- $hasDate := ne $dateISO "0001-01-01" -}}
+{{- $dateISO := $date.dateISO -}}
+{{- $hasDate := $date.hasDate -}}
{{- /* ── Map item_type → Schema.org @type ──────────────────────────────── */ -}}
{{- $itemType := .Params.item_type | default "document" -}}
@@ -91,11 +86,36 @@
{{- with .Params.url_source -}}{{- $sameAs = append (trim . " ") $sameAs -}}{{- end -}}
{{- with .Params.zotero_url -}}{{- $sameAs = append (trim . " ") $sameAs -}}{{- end -}}
+{{- /* ── CS Concepts: about[] + keywords[] ────────────────────────────
+ Reads the `concepts:` list from front matter (list of concept IDs).
+ Looks each ID up in data/bibliography/concepts.json to get the name.
+ Emits schema:about (array of DefinedTerm) and schema:keywords (string).
+*/ -}}
+{{- $conceptData := site.Data.bibliography.concepts -}}
+{{- $conceptIds := .Params.concepts -}}
+{{- $aboutObjs := slice -}}
+{{- $keywordNames := slice -}}
+{{- if reflect.IsSlice $conceptIds -}}
+ {{- range $conceptIds -}}
+ {{- $id := trim . " " -}}
+ {{- $concept := index $conceptData $id -}}
+ {{- if $concept -}}
+ {{- $aboutObjs = append (dict
+ "@type" "DefinedTerm"
+ "@id" (printf "https://interlisp.org/concepts/#%s" $id)
+ "name" $concept.name
+ "inDefinedTermSet" "https://interlisp.org/data/cs-concepts.jsonld"
+ ) $aboutObjs -}}
+ {{- $keywordNames = append $concept.name $keywordNames -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
{{- /* ── Base JSON-LD object (properties common to all types) ─────────── */ -}}
{{- $ld := dict
"@context" "https://schema.org"
"@type" $schemaType
- "name" .Title
+ "name" (.Title | strings.TrimRight "\n ")
"url" .Permalink
"inLanguage" "en-US"
-}}
@@ -124,6 +144,14 @@
{{- $ld = merge $ld (dict "sameAs" $sameAs) -}}
{{- end -}}
+{{- if gt (len $aboutObjs) 0 -}}
+ {{- $ld = merge $ld (dict "about" $aboutObjs) -}}
+{{- end -}}
+
+{{- if gt (len $keywordNames) 0 -}}
+ {{- $ld = merge $ld (dict "keywords" (delimit $keywordNames ", ")) -}}
+{{- end -}}
+
{{- /* ── Patent: additionalType + structured identifiers ──────────────── */ -}}
{{- if eq $itemType "patent" -}}
{{- $ld = merge $ld (dict "additionalType" "Patent") -}}
@@ -140,10 +168,10 @@
{{- end -}}
{{- with .Params.assignee -}}
- {{- $ld = merge $ld (dict "funder" (dict "@type" "Organization" "name" .)) -}}
+ {{- $ld = merge $ld (dict "copyrightHolder" (dict "@type" "Organization" "name" .)) -}}
{{- end -}}
{{- with .Params.issuing_authority -}}
- {{- $ld = merge $ld (dict "countryOfOrigin" (dict "@type" "Country" "name" .)) -}}
+ {{- $ld = merge $ld (dict "validIn" (dict "@type" "DefinedRegion" "name" .)) -}}
{{- end -}}
{{- end -}}
@@ -160,11 +188,14 @@
{{- /* ── Conference paper ─────────────────────────────────────────────── */ -}}
{{- if eq $itemType "paper-conference" -}}
{{- with .Params.proceedings_title -}}
- {{- $ld = merge $ld (dict "isPartOf" (dict "@type" "Periodical" "name" .)) -}}
+ {{- $ld = merge $ld (dict "isPartOf" (dict "@type" "Book" "name" .)) -}}
{{- end -}}
{{- with .Params.publisher -}}
{{- $ld = merge $ld (dict "publisher" (dict "@type" "Organization" "name" .)) -}}
{{- end -}}
+ {{- with .Params.place -}}
+ {{- $ld = merge $ld (dict "locationCreated" (dict "@type" "Place" "name" .)) -}}
+ {{- end -}}
{{- end -}}
{{- /* ── Book ─────────────────────────────────────────────────────────── */ -}}
@@ -191,10 +222,10 @@
{{- with .Params.publisher -}}
{{- $ld = merge $ld (dict "publisher" (dict "@type" "Organization" "name" .)) -}}
{{- end -}}
- {{- with .Params.reportType -}}
+ {{- with .Params.report_type -}}
{{- $ld = merge $ld (dict "additionalType" .) -}}
{{- end -}}
- {{- with .Params.reportNumber -}}
+ {{- with .Params.report_number -}}
{{- $ld = merge $ld (dict "identifier" (dict "@type" "PropertyValue" "propertyID" "report-number" "value" .)) -}}
{{- end -}}
{{- end -}}
@@ -240,15 +271,12 @@
{{- end -}}
{{- end -}}
-
+{{- printf "" ($ld | jsonify (dict "indent" " ")) | safeHTML -}}
{{- else if .IsSection -}}
{{- /* ── COLLECTION PAGE (bibliography index) ──────────────────────────── */ -}}
{{- $pages := .RegularPages -}}
-{{- $pages = sort $pages "Date" "desc" -}}
{{- $pages = sort $pages "Title" "asc" -}}
{{- $itemList := slice -}}
@@ -278,9 +306,7 @@
)
-}}
-
+{{- printf "" ($ld | jsonify (dict "indent" " ")) | safeHTML -}}
{{- end -}}
diff --git a/layouts/bibliography/single.html b/layouts/bibliography/single.html
index 62dd9a4e..7f6a0a9b 100644
--- a/layouts/bibliography/single.html
+++ b/layouts/bibliography/single.html
@@ -30,18 +30,10 @@ {{ .Title }}
{{- end -}}
- {{- $dateTime := .Date -}}
- {{- with .Params.date -}}
- {{- $isoDate := trim . " " -}}
- {{- if and (ne $isoDate "") (findRE `^\d{4}-\d{2}-\d{2}` $isoDate) -}}
- {{- $dateTime = time $isoDate -}}
- {{- end -}}
- {{- end -}}
- {{- $dateISO := $dateTime.Format "2006-01-02" -}}
- {{- $dateLabel := or .Params.readabledate $dateISO -}}
+ {{- $date := partial "bibliography-date.html" . -}}
{{- /* Don't display bogus date */ -}}
- {{- if ne $dateISO "0001-01-01" -}}
-
+ {{- if $date.hasDate -}}
+
{{- end -}}
diff --git a/scripts/bibSplit.pl b/scripts/bibSplit.pl
index 97356543..71ce637a 100755
--- a/scripts/bibSplit.pl
+++ b/scripts/bibSplit.pl
@@ -80,13 +80,13 @@ sub sanitize_text {
my $itemAuthors = '';
if (ref($obj->{authorsFormatted}) eq 'ARRAY' && @{$obj->{authorsFormatted}}) {
$itemAuthors = "\n";
- for my $a (@{$obj->{authorsFormatted}}) {
+ for my $author (@{$obj->{authorsFormatted}}) {
# The encode_json is where extended unicode chars get corrupted, e.g., "Emanuelson, Pär"
# There may be other things that now don't work!!
# my $quoted = encode_json($a // '');
# sanitize_text seems to handle them correctly
- my $san = sanitize_text($a // '');
- $itemAuthors .= " - \"$san\"\n";
+ my $san_author = sanitize_text($author // '');
+ $itemAuthors .= " - \"$san_author\"\n";
}
$itemAuthors =~ s/\n$//u; # strip trailing newline
}
@@ -94,11 +94,11 @@ sub sanitize_text {
my $itemEditors = '';
if (ref($obj->{editorsFormatted}) eq 'ARRAY' && @{$obj->{editorsFormatted}}) {
$itemEditors = "\n";
- for my $a (@{$obj->{editorsFormatted}}) {
+ for my $editor (@{$obj->{editorsFormatted}}) {
# as above...
- # my $quoted = encode_json($a // '');
- my $san = sanitize_text($a // '');
- $itemEditors .= " - \"$san\"\n";
+ # my $quoted = encode_json($editor // '');
+ my $san_editor = sanitize_text($editor // '');
+ $itemEditors .= " - \"$san_editor\"\n";
}
$itemEditors =~ s/\n$//u; # strip trailing newline
}
@@ -187,6 +187,7 @@ sub sanitize_text {
$extraFields
tags:
+concepts:
url_source: $urlSource
zotero_url: "https://www.zotero.org/groups/2914042/items/$key"
diff --git a/tests/fixtures/bibliography/ZTEST-BLOG.md b/tests/fixtures/bibliography/ZTEST-BLOG.md
new file mode 100644
index 00000000..02928951
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-BLOG.md
@@ -0,0 +1,20 @@
+---
+title: "Test Blog Post"
+date: 2021-08-20
+readabledate: 2021-08-20
+type: bibliography
+item_type: post-weblog
+authors:
+ - "Blogger, Test"
+editors:
+abstract: |
+ A test abstract for a blog post used by automated test fixtures.
+
+blog_title: "Test Tech Blog"
+
+tags:
+url_source: "https://example.com/blog/post"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTBLG"
+
+lastmod: 2023-08-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-BOOK.md b/tests/fixtures/bibliography/ZTEST-BOOK.md
new file mode 100644
index 00000000..b0ecd9b4
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-BOOK.md
@@ -0,0 +1,20 @@
+---
+title: "Test Book Title"
+date: 1995-01-01
+readabledate: 1995
+type: bibliography
+item_type: book
+authors:
+ - "Author, Test"
+editors:
+abstract: |
+ A test abstract for a book used by automated test fixtures.
+
+publisher: "Test Publisher"
+
+tags:
+url_source: "https://example.com/book"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTBOOK"
+
+lastmod: 2022-01-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-CHAPTER.md b/tests/fixtures/bibliography/ZTEST-CHAPTER.md
new file mode 100644
index 00000000..5fee537b
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-CHAPTER.md
@@ -0,0 +1,23 @@
+---
+title: "Test Chapter Title"
+date: 2000-01-01
+readabledate: 2000
+type: bibliography
+item_type: chapter
+authors:
+ - "Writer, Test"
+editors:
+ - "Editor, Test"
+abstract: |
+ A test abstract for a book chapter used by automated test fixtures.
+
+book_title: "Test Book of Chapters"
+publisher: "Chapter Publisher"
+pages: "100-120"
+
+tags:
+url_source: "https://example.com/chapter"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTCHAP"
+
+lastmod: 2022-03-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-CONFERENCE.md b/tests/fixtures/bibliography/ZTEST-CONFERENCE.md
new file mode 100644
index 00000000..14cc9176
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-CONFERENCE.md
@@ -0,0 +1,23 @@
+---
+title: "Test Conference Paper"
+date: 2019-07-20
+readabledate: 2019-07-20
+type: bibliography
+item_type: paper-conference
+authors:
+ - "Brown, Carol K."
+ - "White, David"
+editors:
+abstract: |
+ A test abstract for a conference paper used by automated test fixtures.
+
+proceedings_title: "Proceedings of the Test Conference 2019"
+publisher: "ACM"
+place: "San Francisco, CA"
+
+tags:
+url_source: "https://example.com/conference-paper"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTCONF"
+
+lastmod: 2022-08-15T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-ENCYCLOPEDIA.md b/tests/fixtures/bibliography/ZTEST-ENCYCLOPEDIA.md
new file mode 100644
index 00000000..93a35792
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-ENCYCLOPEDIA.md
@@ -0,0 +1,20 @@
+---
+title: "Test Encyclopedia Entry"
+date: 2003-01-01
+readabledate: 2003
+type: bibliography
+item_type: entry-encyclopedia
+authors:
+ - "Scholar, Test"
+editors:
+abstract: |
+ A test abstract for an encyclopedia entry used by automated test fixtures.
+
+encyclopedia_title: "Encyclopedia of Test Computing"
+
+tags:
+url_source: "https://example.com/encyclopedia/entry"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTENC"
+
+lastmod: 2023-11-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-JOURNAL.md b/tests/fixtures/bibliography/ZTEST-JOURNAL.md
new file mode 100644
index 00000000..6d7bbd71
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-JOURNAL.md
@@ -0,0 +1,27 @@
+---
+title: "Test Scholarly Article"
+date: 2020-03-15
+readabledate: 2020-03-15
+type: bibliography
+item_type: article-journal
+authors:
+ - "Smith, Alice J."
+ - "Jones, Bob"
+editors:
+abstract: |
+ A test abstract for a journal article used by automated test fixtures.
+
+publication_title: "Journal of Test Science"
+volume: "5"
+issue: "2"
+pages: "45-67"
+
+tags:
+url_source: "https://example.com/journal-article"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTJNL"
+concepts:
+ - "Interlisp"
+ - "ProgrammingLanguages"
+
+lastmod: 2023-01-10T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-MAGAZINE.md b/tests/fixtures/bibliography/ZTEST-MAGAZINE.md
new file mode 100644
index 00000000..bb560eb1
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-MAGAZINE.md
@@ -0,0 +1,23 @@
+---
+title: "Test Magazine Article"
+date: 2021-06-01
+readabledate: 2021-06-01
+type: bibliography
+item_type: article-magazine
+authors:
+ - "Doe, Jane"
+editors:
+abstract: |
+ A test abstract for a magazine article used by automated test fixtures.
+
+publication_title: "Computing Monthly"
+volume: "10"
+issue: "6"
+pages: "12-14"
+
+tags:
+url_source: "https://example.com/magazine-article"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTMAG"
+
+lastmod: 2023-02-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-MESSAGE.md b/tests/fixtures/bibliography/ZTEST-MESSAGE.md
new file mode 100644
index 00000000..b75745e4
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-MESSAGE.md
@@ -0,0 +1,18 @@
+---
+title: "Test Personal Communication"
+date: 2012-07-04
+readabledate: 2012-07-04
+type: bibliography
+item_type: personal_communication
+authors:
+ - "Sender, Test"
+editors:
+abstract: |
+ A test abstract for a personal communication used by automated test fixtures.
+
+tags:
+url_source: "https://example.com/communication"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTMSG"
+
+lastmod: 2023-12-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-NO-URLS.md b/tests/fixtures/bibliography/ZTEST-NO-URLS.md
new file mode 100644
index 00000000..fe676412
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-NO-URLS.md
@@ -0,0 +1,21 @@
+---
+title: "Test Entry Without URLs"
+date: 2008-06-15
+readabledate: 2008-06-15
+type: bibliography
+item_type: report
+authors:
+ - "Nobody, Test"
+editors:
+abstract: |
+ A test abstract for a report with no external URLs, used to verify that
+ the sameAs field is omitted when neither url_source nor zotero_url is set.
+
+publisher: "Test Publisher"
+
+tags:
+url_source:
+zotero_url:
+
+lastmod: 2024-01-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-PATENT.md b/tests/fixtures/bibliography/ZTEST-PATENT.md
new file mode 100644
index 00000000..33c23e82
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-PATENT.md
@@ -0,0 +1,23 @@
+---
+title: "Test Patent for Widget"
+date: 2015-03-10
+readabledate: 2015-03-10
+type: bibliography
+item_type: patent
+authors:
+ - "Inventor, Test"
+editors:
+abstract: |
+ A test abstract for a patent used by automated test fixtures.
+
+assignee: "Test Corporation"
+application_number: "US12345678"
+patent_number: "US9876543B2"
+issuing_authority: "United States"
+
+tags:
+url_source: "https://patents.google.com/patent/US9876543B2"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTPAT"
+
+lastmod: 2023-06-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-REPORT.md b/tests/fixtures/bibliography/ZTEST-REPORT.md
new file mode 100644
index 00000000..9cad7896
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-REPORT.md
@@ -0,0 +1,20 @@
+---
+title: "Test Technical Report"
+date: 2005-09-01
+readabledate: 2005-09-01
+type: bibliography
+item_type: report
+authors:
+ - "Researcher, Test"
+editors:
+abstract: |
+ A test abstract for a technical report used by automated test fixtures.
+
+publisher: "Test Research Lab"
+
+tags:
+url_source: "https://example.com/report"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTRPT"
+
+lastmod: 2023-04-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-SOFTWARE.md b/tests/fixtures/bibliography/ZTEST-SOFTWARE.md
new file mode 100644
index 00000000..c0ab51dc
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-SOFTWARE.md
@@ -0,0 +1,19 @@
+---
+title: "Test Software"
+date: 2016-02-14
+readabledate: 2016-02-14
+type: bibliography
+item_type: software
+authors:
+ - "Developer, Test"
+ - "Singularity"
+editors:
+abstract: |
+ A test abstract for a software entry used by automated test fixtures.
+
+tags:
+url_source: "https://github.com/example/test-software"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTSOFT"
+
+lastmod: 2023-10-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-THESIS.md b/tests/fixtures/bibliography/ZTEST-THESIS.md
new file mode 100644
index 00000000..bd41d49f
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-THESIS.md
@@ -0,0 +1,20 @@
+---
+title: "Test Doctoral Thesis"
+date: 2010-05-01
+readabledate: 2010-05-01
+type: bibliography
+item_type: thesis
+authors:
+ - "Student, Test"
+editors:
+abstract: |
+ A test abstract for a doctoral thesis used by automated test fixtures.
+
+university: "Test University"
+
+tags:
+url_source: "https://example.com/thesis"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTTHES"
+
+lastmod: 2023-05-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-VIDEO.md b/tests/fixtures/bibliography/ZTEST-VIDEO.md
new file mode 100644
index 00000000..6a3b0833
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-VIDEO.md
@@ -0,0 +1,22 @@
+---
+title: "Test Motion Picture"
+date: 2018-04-01
+readabledate: 2018-04-01
+type: bibliography
+item_type: motion_picture
+authors:
+ - "Director, Test"
+editors:
+abstract: |
+ A test abstract for a motion picture used by automated test fixtures.
+
+studio: "Test Film Studio"
+video_recording_format: "MP4"
+series_title: "Test Film Series"
+
+tags:
+url_source: "https://example.com/video"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTVID"
+
+lastmod: 2023-09-01T00:00:00Z
+---
diff --git a/tests/fixtures/bibliography/ZTEST-WEBPAGE.md b/tests/fixtures/bibliography/ZTEST-WEBPAGE.md
new file mode 100644
index 00000000..220418fd
--- /dev/null
+++ b/tests/fixtures/bibliography/ZTEST-WEBPAGE.md
@@ -0,0 +1,20 @@
+---
+title: "Test Web Page"
+date: 2022-11-15
+readabledate: 2022-11-15
+type: bibliography
+item_type: webpage
+authors:
+ - "Webber, Test"
+editors:
+abstract: |
+ A test abstract for a web page used by automated test fixtures.
+
+website_title: "Test Website"
+
+tags:
+url_source: "https://example.com/page"
+zotero_url: "https://www.zotero.org/groups/test/items/ZTESTWEB"
+
+lastmod: 2023-07-01T00:00:00Z
+---
diff --git a/tests/test_bibliography_jsonld.py b/tests/test_bibliography_jsonld.py
new file mode 100644
index 00000000..ecd11e92
--- /dev/null
+++ b/tests/test_bibliography_jsonld.py
@@ -0,0 +1,863 @@
+"""
+tests/test_bibliography_jsonld.py
+==================================
+Validates the JSON-LD structured data emitted by
+``layouts/_partials/bibliography-json-ld.html`` for every Schema.org type
+used in the Interlisp bibliography.
+
+How it works
+------------
+Each ``ZTEST-*.md`` fixture in ``tests/fixtures/bibliography/`` is the
+**mock input**: it provides fully-controlled front-matter values that Hugo
+feeds into the template. A test-environment Hugo build mounts those fixtures
+into the content tree without touching production content. The tests then
+read the rendered ``tests/public_test/history/bibliography//index.html``,
+extract the ``