diff --git a/.gitignore b/.gitignore index 66e482551..2a1a5fd2c 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 90adbd391..9b7c6ffef 100644 --- a/config/_default/params.yaml +++ b/config/_default/params.yaml @@ -166,9 +166,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 000000000..f625566c3 --- /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 a4ad1df13..4a4998312 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 000000000..69c1bc9c5 --- /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 b466452ff..1cbbec255 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 ─────────────────────────────────────────────────────────── */ -}} @@ -172,9 +203,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 ──────────────────────────────────────────────────────── */ -}} @@ -194,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 -}} @@ -243,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 -}} @@ -281,9 +306,7 @@ ) -}} - +{{- printf "" ($ld | jsonify (dict "indent" " ")) | safeHTML -}} {{- end -}} diff --git a/layouts/bibliography/single.html b/layouts/bibliography/single.html index 62dd9a4e4..7f6a0a9be 100644 --- a/layouts/bibliography/single.html +++ b/layouts/bibliography/single.html @@ -30,18 +30,10 @@
- {{- $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 8a9b77be2..71ce637aa 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.
@@ -78,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
}
@@ -92,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
}
@@ -185,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 000000000..02928951c
--- /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 000000000..b0ecd9b45
--- /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 000000000..5fee537bf
--- /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 000000000..14cc9176d
--- /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 000000000..93a35792d
--- /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 000000000..6d7bbd71a
--- /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 000000000..bb560eb1f
--- /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 000000000..b75745e44
--- /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 000000000..fe6764125
--- /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 000000000..33c23e828
--- /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 000000000..9cad7896b
--- /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 000000000..c0ab51dc8
--- /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 000000000..bd41d49fc
--- /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 000000000..6a3b08335
--- /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 000000000..220418fd5
--- /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 000000000..ecd11e92d
--- /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/