From 066c879dfeda8739617f7a348a35c8bc11cdd60c Mon Sep 17 00:00:00 2001 From: Stephan Heunis Date: Mon, 13 Apr 2026 10:53:44 +0200 Subject: [PATCH] New approach to extracting publication data and filtering/searching it This commit introduces the process for individual publication term page generation, which includes a new jinja template and workflow. Since 'publications' is a taxonomy, the workflow renders the term page at 'content/publications/pub-pid/_index.md'. For the same reason, the (more) correct html template to use would be 'publications/taxonomy.html' instead of 'publications/list.html' (which is actually the fallback), hence the replacement. Since, individual publication metadata are included in each individual term page front matter, the taxonomy page has been updated to grab that metadata instead of reading the json object from 'data/publications.json'. This also allows the associated workflow to be dropped. A new 'publication-item' partial is introduced to allow better control over the rendering of individual items in the publication list on the taxonomy page. This intentionally shifts the rendering that was previously done in JS code to Hugo templating. Because hugo only runs the rendering on app build, the searching/filtering approach had to be changed so that all publications are rendered by default and required items are hidden depending on the filtering options selected or search terms entered. This is done in updated JS code, by assigning 'display: none' when applicable. Other JS additions include: - adding a count of filtered items - a new 'Clear all' button for clearing filters - allowing Topic pills to be clicked in order to add filter options TODO: customize the publications term page template for improved individual publication display. --- ...ata.yaml => update_publication_pages.yaml} | 16 +- content/publications/_index.md | 3 - layouts/_partials/publication-item.html | 67 ++++++++ .../publications/{list.html => taxonomy.html} | 18 +- page_templates/publication.md.j2 | 59 +++++++ static/publications.css | 13 ++ static/publications.js | 158 +++++++++++------- 7 files changed, 255 insertions(+), 79 deletions(-) rename .forgejo/workflows/{update_publications_data.yaml => update_publication_pages.yaml} (52%) delete mode 100644 content/publications/_index.md create mode 100644 layouts/_partials/publication-item.html rename layouts/publications/{list.html => taxonomy.html} (68%) create mode 100644 page_templates/publication.md.j2 diff --git a/.forgejo/workflows/update_publications_data.yaml b/.forgejo/workflows/update_publication_pages.yaml similarity index 52% rename from .forgejo/workflows/update_publications_data.yaml rename to .forgejo/workflows/update_publication_pages.yaml index c6ef4c8..d8720c5 100644 --- a/.forgejo/workflows/update_publications_data.yaml +++ b/.forgejo/workflows/update_publication_pages.yaml @@ -16,19 +16,11 @@ jobs: uses: actions/checkout@v4 - name: Prepare environment uses: ./.forgejo/actions/prep-metadata-query - - name: Install jq - run: | - apt-get update - apt-get install -y jq - - name: Update pages + - name: Update publication pages run: | dtc get-records ${{ env.DUMPTHINGS_APIURL }} public -C XYZPublication \ | qri inline-records -p about -p attributed_to -c public \ - | jq -sc '{pid: "xyzri:this-is-not-important", publications: .}' \ - | qri render-record page_templates/publications.json.j2 'data/publications.json' + | qri render-record page_templates/publication.md.j2 \ + 'content/{__pid_curie_reference}/_index.md' - name: Deposit changes - run: | - git add data - git diff --quiet --cached \ - && echo "Already up-to-date" \ - || ( git commit -m "chore: auto-generate content from metadata" && git push origin ) + uses: ./.forgejo/actions/deposit-changes diff --git a/content/publications/_index.md b/content/publications/_index.md deleted file mode 100644 index 7c38345..0000000 --- a/content/publications/_index.md +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Publications ---- \ No newline at end of file diff --git a/layouts/_partials/publication-item.html b/layouts/_partials/publication-item.html new file mode 100644 index 0000000..d4fe990 --- /dev/null +++ b/layouts/_partials/publication-item.html @@ -0,0 +1,67 @@ +
+ + +

+ + {{ .Title }} + + {{ if .Params.date }} + {{ $year := substr .Params.date 0 4 }} + + ({{ $year }}) + + {{ end }} + {{ with .Params.doi }} + + + DOI: {{ . }} + + + {{ end }} + {{ with .Params.kind }} + + {{ . | replaceRE "^.*:" "" }} + + {{ end }} +

+ + + {{ with .Params.author }} +
+ {{ range $i, $a := . }} + + {{ $a.given_name }} {{ $a.family_name }} + + {{ end }} +
+ {{ end }} + + + {{ with .Params.topic }} +
+ {{ range . }} + + {{ .display_label }} +   + {{ end }} +
+ {{ end }} + +
\ No newline at end of file diff --git a/layouts/publications/list.html b/layouts/publications/taxonomy.html similarity index 68% rename from layouts/publications/list.html rename to layouts/publications/taxonomy.html index 4dea629..9199550 100644 --- a/layouts/publications/list.html +++ b/layouts/publications/taxonomy.html @@ -9,6 +9,9 @@
- -
+ + + {{ $pubPages := where .Site.Pages "Section" "publications" }} + {{ $pubPagesTerms := where $pubPages "Kind" "term" }} +
+ {{ range $i, $p := $pubPagesTerms }} + {{ partial "publication-item.html" $p }} + {{ end }} +
- - diff --git a/page_templates/publication.md.j2 b/page_templates/publication.md.j2 new file mode 100644 index 0000000..cbfb886 --- /dev/null +++ b/page_templates/publication.md.j2 @@ -0,0 +1,59 @@ +{% macro taxonomy_terms(taxonomy, terms_list) -%} +{% if terms_list -%} +{{ taxonomy }}: +{% for el in terms_list -%} +{% if el.pid -%} + - {{ el.pid.split('/')[-1] }} +{% endif -%} +{% endfor -%} +{% endif -%} +{%- endmacro -%} + +{%- set doi = ( + __rec.identifiers + | selectattr('schema_type', 'equalto', 'dlthings:DOI') + | list + | first +) if __rec.identifiers is defined and __rec.identifiers is sequence else none -%} + +{%- set generation = ( + __rec.generated_by + | selectattr('object', 'equalto', 'obo:IAO_0000444') + | list + | first +) if __rec.generated_by is defined and __rec.generated_by is sequence else none -%} + +{%- set authors = [] -%} +{%- for auth in __rec.attributed_to -%} + {%- set _ = authors.append({ + "pid": auth.object.pid | default(none), + "given_name": auth.object.given_name | default(none), + "family_name": auth.object.family_name | default(none) + }) -%} +{%- endfor -%} + +{%- set topics = [] -%} +{%- for top in __rec.about -%} + {%- set _ = topics.append({ + "pid": top.pid | default(none), + "display_label": top.display_label | default(none) + }) -%} +{%- endfor -%} + +{%- set publication = { + "pid": __rec.pid | default(none), + "doi": doi.notation | default(none) if doi else none, + "date": generation.at_time | default(none) if generation else none, + "title": __rec.title | default(none), + "kind": __rec.kind | default(none), + "author": authors | default(none), + "topic": topics | default(none) +} -%} +--- +title: {{ publication.title | toyaml | replace('...\n','') | trim }} +{{ taxonomy_terms('persons', authors) -}} +{{ taxonomy_terms('topics', topics) -}} +params: + graphRootNodePID: {{ pid }} + {{ publication | toyaml | indent(2) | trim }} +--- diff --git a/static/publications.css b/static/publications.css index 58e1cd5..8c6ef31 100644 --- a/static/publications.css +++ b/static/publications.css @@ -18,6 +18,12 @@ max-height: calc(100vh - 4rem); overflow-y: auto; scroll-behavior: smooth; + display: flex; + flex-direction: column; +} + +.clear-button { + margin-left: auto; } .results-panel { @@ -49,4 +55,11 @@ color: #292524; padding-left: 0.5em; border-radius: 3px; + margin-bottom: 1em; + border: 1px solid grey; +} + +.topic-chip.active { + background-color: rgb(59 130 246 / 0.15); + color: rgb(59 130 246); } \ No newline at end of file diff --git a/static/publications.js b/static/publications.js index 9056ef8..8bbb1ce 100644 --- a/static/publications.js +++ b/static/publications.js @@ -1,13 +1,5 @@ -// Load data -const rawData = JSON.parse(document.getElementById("pub-data").textContent); - -// Normalize data -const publications = rawData.map(p => ({ - ...p, - year: p.date ? new Date(p.date).getFullYear() : 'unknown', - topics: (p.topic || []).map(t => t.display_label), - authors: (p.author || []).map(a => `${a.given_name} ${a.family_name}`) -})); +// Load data from all pre-rendered publication item divs +const publications = Array.from(document.querySelectorAll(".pub")); // State let state = { @@ -20,15 +12,13 @@ let state = { // Build filter options dynamically function getUniqueValues(field) { const values = new Set(); - - publications.forEach(p => { - if (Array.isArray(p[field])) { - p[field].forEach(v => values.add(v)); - } else if (p[field]) { - values.add(p[field]); + publications.forEach(el => { + if (field === "topic") { + getTopics(el).forEach(v => values.add(v)); + } else { + values.add(el.dataset[field]); } }); - return Array.from(values).sort(); } @@ -42,7 +32,6 @@ function formatValue(field, value) { return value } -// Render checkboxes for filters function renderFilter(containerId, field, values) { const container = document.getElementById(containerId); values.forEach(value => { @@ -52,6 +41,8 @@ function renderFilter(containerId, field, values) { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = value; + checkbox.dataset.field = field; + checkbox.id = id; checkbox.addEventListener("change", () => { if (checkbox.checked) { state[field].add(value); @@ -67,67 +58,118 @@ function renderFilter(containerId, field, values) { } // Filtering logic -function matchesFilters(p) { - // Kind - if (state.kind.size && !state.kind.has(p.kind)) return false; - // Year - if (state.year.size && !state.year.has(String(p.year))) return false; - // Topic (multi-value) +function matchesFilters(el) { + const kind = el.dataset.kind; + const year = el.dataset.year; + const topics = getTopics(el); + + // kind filter + if (state.kind.size && !state.kind.has(kind)) { + return false; + } + // year filter + if (state.year.size && !state.year.has(year)) { + return false; + } + // topic filter (multi-match) if (state.topic.size) { - const match = p.topics.some(t => state.topic.has(t)); + const match = topics.some(t => state.topic.has(t)); if (!match) return false; } return true; } -// Search -function matchesSearch(p) { +// Helper to get topics from an element +function getTopics(el) { + try { + return JSON.parse(el.dataset.topics || "[]").map(t => t.display_label); + } catch { + return []; + } +} + +// Search logic +function matchesSearch(el) { if (!state.search) return true; const text = ( - p.title + - " " + - p.kind + - " " + - p.topics.join(" ") + - " " + - p.authors.join(" ") + el.dataset.title + " " + + el.dataset.kind + " " + + el.dataset.year + " " + + el.dataset.topics + " " + + el.dataset.authors ).toLowerCase(); + return text.includes(state.search); } -// Render results +// Change display of divs based on filtering/searching function render() { - const container = document.getElementById("results"); - container.innerHTML = ""; - const filtered = publications.filter(p => - matchesFilters(p) && matchesSearch(p) - ); - if (filtered.length === 0) { - container.innerHTML = "

No results

"; - return; - } - filtered.forEach(p => { - const div = document.createElement("div"); - div.className = "pub"; - div.innerHTML = ` -

${p.title} ${p.year ? ' ('+p.year+')' : ''}

-

Authors: ${p.authors.join(", ")}

-

Kind: ${formatValue("kind", p.kind)}

-

Topics: ${p.topics.join(", ")}

- `; - container.appendChild(div); + let count = 0; + publications.forEach(el => { + const visible = + matchesFilters(el) && + matchesSearch(el); + if (visible) count+=1; + renderCount(count) + el.style.display = visible ? "" : "none"; }); } +// Count of searched+filtered publications +function renderCount(count) { + const countEl = document.getElementById('pub-count'); + countEl.innerHTML = `${count}` ; +} + +// Set checkbox if user clicks on topic pill +function selectFilter(field, value) { + const checkbox = document.querySelector( + `input[type="checkbox"][data-field="${field}"][value="${CSS.escape(value)}"]` + ); + if (!checkbox) return; + if (!checkbox.checked) { + checkbox.checked = true; + state[field].add(value); + render(); + } +} + +// Clear all +function clearAllFilters() { + // Reset state + state.kind.clear(); + state.topic.clear(); + state.year.clear(); + state.search = ""; + // Uncheck all checkboxes + document.querySelectorAll('input[type="checkbox"]').forEach(cb => { + cb.checked = false; + }); + // Clear search input + const searchInput = document.getElementById("search"); + if (searchInput) { + searchInput.value = ""; + } + // Re-render results + render(); +} + // On startup: -// Build filters +// 1) Build filters renderFilter("filter-kind", "kind", getUniqueValues("kind")); -renderFilter("filter-topic", "topic", getUniqueValues("topics")); +renderFilter("filter-topic", "topic", getUniqueValues("topic")); renderFilter("filter-year", "year", getUniqueValues("year").map(String)); -// Search input +// 2) Register search input document.getElementById("search").addEventListener("input", e => { state.search = e.target.value.toLowerCase(); render(); }); -// Initial render +// 3) Add topic click handlers +document.querySelectorAll(".topic-chip").forEach(el => { + el.addEventListener("click", () => { + const topic = el.dataset.topic; + selectFilter("topic", topic); + }); +}); +// 4) Initial render render(); \ No newline at end of file -- 2.52.0