Search full-text (blogData.text), related posts snippet, minify tool, generator event guard; README base+related

This commit is contained in:
Richard Zink
2026-09-10 09:29:52 +02:00
parent 212a98e7f2
commit 65cbaaf6cc
7 changed files with 251 additions and 34 deletions
+5 -3
View File
@@ -29,9 +29,9 @@ pointing the `shareCss` relation at the matching note – see
|---|---|---|---|
| **Coffee** | `theme/blog_share_theme_coffee.css` | espresso `#1d150f` / latte-cream `#f0e2d0`, caramel `#e0a15c` · cream `#E8E3C3` / ink `#2D1A1D`, berry `#8e2d53` | [megabyte0x/omarchy-coffee-theme](https://github.com/megabyte0x/omarchy-coffee-theme) (MIT) |
| **Harbor** | `theme/blog_share_theme_harbor.css` | near-black `#1b1b1b` / cream `#efebdc`, coral `#e75a50` · paper `#dfe4c4` / ink `#1c2d28`, nord-blue `#5e81ac` | [HANCORE-linux/omarchy-harbor-theme](https://github.com/HANCORE-linux/omarchy-harbordark-theme) (MIT) |
| **Gruvbox** | `theme/blog_share_theme_gruvbox.css` | bg `#282828` / fg `#ebdbb2`, accents red/green/yellow/blue | [morhetz/gruvbox](https://github.com/morhetz/gruvbox) (MIT) |
| **Gruvbox** (active) | `theme/blog_share_theme_gruvbox.css` | bg `#282828` / fg `#ebdbb2`, accents red/green/yellow/blue | [morhetz/gruvbox](https://github.com/morhetz/gruvbox) (MIT) |
| **Solarized** | `theme/blog_share_theme_solarized.css` | bg `#002b36` / fg `#839496`, blue `#268bd2` cyan `#2aa198` · bg `#fdf6e3` / fg `#657b83` | [Solarized by Ethan Schoonover](https://ethanschoonover.com/solarized/) (MIT) |
| **Tokyo Night** (active) | `theme/blog_share_theme_tokyonight.css` | storm `#24283b` / fg `#c0caf5`, blue `#7aa2f7` · day `#e1e2e7` / fg `#3760bf` | [tokyonight.nvim](https://github.com/folke/tokyonight.nvim) (Apache-2.0) |
| **Tokyo Night** | `theme/blog_share_theme_tokyonight.css` | storm `#24283b` / fg `#c0caf5`, blue `#7aa2f7` · day `#e1e2e7` / fg `#3760bf` | [tokyonight.nvim](https://github.com/folke/tokyonight.nvim) (Apache-2.0) |
![Coffee palette](assets/palette-coffee.png)
![Harbor palette](assets/palette-harbor.png)
@@ -84,7 +84,8 @@ pointing the `shareCss` relation at the matching note – see
|---|---|---|
| `blog_generator.js` | backend script that generates index, feed, sitemap, search data, category & tag pages | code, `application/javascript;env=backend` |
| `blog_publish_manager.js` | backend script that mirrors notes between `Unpublished` and the blog | code, `application/javascript;env=backend` |
| `theme/blog_share_theme_coffee.css` | Coffee / Coffee Latte share theme (active by default) | code, `text/css` |
| `theme/blog_share_base.css` | **Base stylesheet** (layout/components, palette-independent, var()-driven) | code, `text/css` |
| `theme/blog_share_theme_coffee.css` | Coffee / Coffee Latte share theme (palette overrides) | code, `text/css` |
| `theme/blog_share_theme_harbor.css` | Harbor / Harbordark share theme | code, `text/css` |
| `theme/blog_share_theme_gruvbox.css` | Gruvbox light/dark share theme | code, `text/css` |
| `theme/blog_share_theme_solarized.css` | Solarized dark/light share theme | code, `text/css` |
@@ -168,6 +169,7 @@ Under **`.blog`** create one code note per file, using the file name as the note
| `prev next` | `scripts/prev_next.js` | `text/html` |
| `reading time` | `scripts/reading_time.js` | `text/html` |
| `search header` | `scripts/search_header.js` | `text/html` |
| `related posts` | `scripts/related_posts.js` | `text/html` |
| `note fallback` | `scripts/note_fallback.js` | `text/html` |
| `menu date sort` | `scripts/menu_date_sort.js` | `text/html` |
| `3d model viewer` | `scripts/3d_model_viewer.js` | `text/html` |
+47 -1
View File
@@ -47,6 +47,38 @@ if (api.originEntity && isGeneratedNoteId(api.originEntity.noteId)) {
return;
}
// ---- P2: only rebuild on events that can affect the generated pages ----
// (avoids full rebuild on every snippet/CSS edit, image upload, self-set shareAlias, ...)
const GEN_RELEVANT_LABELS = new Set([
"publish", "date", "category", "tags", "summary", "state",
"blogGarden", "blogCategory", "blogNoArticles", "iconClass"
]);
const GEN_SKIP_RELATIONS = new Set([
"shareHtml", "shareCss", "shareTemplate", "shareFavicon", "shareOpenGraphImage",
"runOnNoteContentChange", "runOnAttributeChange", "runOnAttributeCreation",
"runOnNoteTitleChange", "runOnNoteCreation", "runOnChildNoteCreation",
"runOnBranchDeletion", "runOnEntityChange"
]);
function shouldRebuild() {
const o = api.originEntity;
const ev = api.originEvent || "";
if (!o) return true;
if (/contentChange/i.test(ev)) {
// Article body edits don't change the index/feed/tag pages (only summary labels do).
// Keep the refresh only for thumbnail image replacements (index shows the thumb).
const title = o.title || "";
if (!/^(?:thumb|.+_thumb)\.[A-Za-z0-9]+$/i.test(title)) return false;
}
if (o.type === "label" && !GEN_RELEVANT_LABELS.has(o.name)) return false;
if (o.type === "relation" && GEN_SKIP_RELATIONS.has(o.name)) return false;
if (o.type === "branch") return false; // publish/unpublish covers clone+move via label events
return true;
}
if (!shouldRebuild()) {
api.log("blog_generator: event skipped (no index impact)");
return;
}
try {
run();
} catch (e) {
@@ -392,6 +424,19 @@ function run() {
})(blogRoot, 0);
}
function plainText(a) {
try {
return String(a.getContent() || "")
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/&[a-z#0-9]+;/gi, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 1500);
} catch (e) { return ""; }
}
function buildBlogData() {
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
if (!systemNote) return;
@@ -405,7 +450,8 @@ function run() {
tags: tagList(a),
category: a.getLabelValue("category") || "",
date: (a.getLabelValue("date") || a.dateCreated).slice(0, 10),
dateText: dateText(a.getLabelValue("date") || a.dateCreated)
dateText: dateText(a.getLabelValue("date") || a.dateCreated),
text: plainText(a)
}));
const html = `<script>window.blogData = ${JSON.stringify({ articles: list })};</script>`;
if (dataNote.getContent() !== html) {
+130
View File
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Richard's Blog</title>
<link>https://richard.familie-zink.org/blog</link>
<atom:link href="https://richard.familie-zink.org/feed" rel="self" type="application/rss+xml"/>
<description>Project documentation &amp; technical blog by Richard Zink</description>
<language>en</language>
<lastBuildDate>Thu, 10 Sep 2026 07:25:48 GMT</lastBuildDate>
<item>
<title>Inside this blog: a share-notes engine with a themable frontend</title>
<link>https://richard.familie-zink.org/blog/inside-this-blog-a-share-notes-engine-with-a-themable-frontend</link>
<guid isPermaLink="false">inDtoyyz9thb</guid>
<pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate>
<description>How this blog runs on Trilium's public share: a custom EJS template, five note-based theme families with matching palettes, and the same colors for the Trilium app and Gitea - all in two repos.</description>
</item>
<item>
<title>FAST-LIO with loop closing on IGNIS-8</title>
<link>https://richard.familie-zink.org/blog/ignis8-fast-lio-loop-closing</link>
<guid isPermaLink="false">v778JORAnpij</guid>
<pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
<description>A code-level tour of IGNIS-8's LiDAR-inertial odometry: a FAST-LIO-style error-state Kalman filter fusing the L1 PM's 250 Hz IMU with its 20 Hz point cloud into a 6-DOF pose, plus a keyframe-based loop closer that detects revisits, confirms them by scan match and rebuilds the four-level voxel map without drift.</description>
</item>
<item>
<title>Arming the drive: a fail-safe by default</title>
<link>https://richard.familie-zink.org/blog/ignis8-drive-arm-gate</link>
<guid isPermaLink="false">7EyHklclcGVO</guid>
<pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate>
<description>Why IGNIS-8's drive starts disarmed like a drone: a fail-safe-by-default arm gate that only lets the vehicle move after the operator deliberately enables it, and that re-disarms automatically on every watchdog timeout or emergency stop.</description>
</item>
<item>
<title>Emergency stop: a latch that never releases itself</title>
<link>https://richard.familie-zink.org/blog/ignis8-emergency-stop</link>
<guid isPermaLink="false">kRTOgbiwjhfI</guid>
<pubDate>Sat, 29 Aug 2026 00:00:00 GMT</pubDate>
<description>Why IGNIS-8's emergency stop is a latched, multi-source system that never resets itself: two independent latches, battery and operator triggers, a manual-only release, and a stop that is selective — the drives release while the camera gimbal and sensors stay live.</description>
</item>
<item>
<title>Cutting the thermal stream and SLAM load on IGNIS-8</title>
<link>https://richard.familie-zink.org/blog/cutting-thermal-stream-and-slam-load</link>
<guid isPermaLink="false">uzmCVAsFjcJB</guid>
<pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate>
<description>Why my reconnaissance UGV kept throwing OVERLOAD, and how a lookup-table palette, a frame-rate cap and an offscreen raster blit took the thermal stream and the SLAM map from a saturated CPU to a smooth ride.</description>
</item>
<item>
<title>Battery rethink: prismatic PHEV1 vs cylindrical M50L</title>
<link>https://richard.familie-zink.org/blog/ignis8-battery-phev1-m50l</link>
<guid isPermaLink="false">S3wsaj19dS5L</guid>
<pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate>
<description>Comparing the Samsung PHEV1 prismatic 6S2P pack against a 6S21P LG INR21700M50L cylindrical candidate for IGNIS-8: OCV and power-limit curves, weight and space, and how both cell models are selectable in the VCU config.</description>
</item>
<item>
<title>The technical hurdles between the drawing board and the first drive</title>
<link>https://richard.familie-zink.org/blog/ignis8-technical-hurdles</link>
<guid isPermaLink="false">ozevCW5vvOtE</guid>
<pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate>
<description>The problems between the drawing board and a working vehicle: a browser-fed watchdog that flipped the OVERLOAD badge, a palette filter that ate nine cores, a camera that refused to follow orders, and a map that stalled the handheld.</description>
</item>
<item>
<title>Project start</title>
<link>https://richard.familie-zink.org/blog/ignis8-project-start</link>
<guid isPermaLink="false">0ixEFpiXl6Lg</guid>
<pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate>
<description>How IGNIS-8 came to be: a swollen cell on a battery production floor, a phone call, and the idea of a remotely operated platform that could look at it from a safe distance.</description>
</item>
<item>
<title>Leather Notebook Cover</title>
<link>https://richard.familie-zink.org/blog/leather-notebook-cover</link>
<guid isPermaLink="false">k6aslDxBHulY</guid>
<pubDate>Sat, 21 Feb 2026 00:00:00 GMT</pubDate>
<description>Hand sewn notebook cover made from goat skin leather</description>
</item>
<item>
<title>!NEC MPi400 Namechange</title>
<link>https://richard.familie-zink.org/blog/nec-mpi400-namechange</link>
<guid isPermaLink="false">QaHi3lULbcum</guid>
<pubDate>Tue, 28 Jan 2025 00:00:00 GMT</pubDate>
<description>To directly go out of harms way with legal problems, I changed the name of the project.</description>
</item>
<item>
<title>!NEC MPi400: Reverse Engineering the Keyboard Matrix</title>
<link>https://richard.familie-zink.org/blog/nec-mpi400-reverse-engineering-the-keyboard-matrix</link>
<guid isPermaLink="false">Bx7hKarVZlGf</guid>
<pubDate>Thu, 25 Jul 2024 00:00:00 GMT</pubDate>
<description>Create PCB; Do measurements; Use QMK; Have Keyboard! 😎</description>
</item>
<item>
<title>!NEC MPi400: Disassembly</title>
<link>https://richard.familie-zink.org/blog/nec-mpi400-project-start</link>
<guid isPermaLink="false">9mVAVwtlNCTb</guid>
<pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate>
<description>Complete disassembly of the MobilePro 900</description>
</item>
<item>
<title>SoC/OCV Calculation</title>
<link>https://richard.familie-zink.org/blog/soc-ocv-calculation</link>
<guid isPermaLink="false">TxlNPtkaHjjA</guid>
<pubDate>Sun, 07 Jul 2024 07:01:00 GMT</pubDate>
<description>C source for SoC calculation based on open circuit voltage</description>
</item>
<item>
<title>DLG INR18650-320 cell data</title>
<link>https://richard.familie-zink.org/blog/dlg-inr18650-320</link>
<guid isPermaLink="false">9zxBjebNKoVJ</guid>
<pubDate>Sun, 25 Feb 2024 08:31:00 GMT</pubDate>
<description>SoC/OCV liion cell data</description>
</item>
<item>
<title>Open Strainwave Mount (OSWM) planning</title>
<link>https://richard.familie-zink.org/blog/oswm-planning</link>
<guid isPermaLink="false">3nwNkCrqAMS9</guid>
<pubDate>Sat, 02 Dec 2023 08:51:00 GMT</pubDate>
<description>Planning a mount with strainwave gears for use with OnStep</description>
</item>
<item>
<title>NGC 281: Pacman Nebula</title>
<link>https://richard.familie-zink.org/blog/ngc-281-pacman-nebula</link>
<guid isPermaLink="false">obZwmWDJdzK6</guid>
<pubDate>Thu, 23 Mar 2023 07:43:00 GMT</pubDate>
<description>Image of Pacman Nebula taken with Star Adventurer</description>
</item>
<item>
<title>Revisiting old astrophotographies</title>
<link>https://richard.familie-zink.org/blog/revisiting-astrophotographies</link>
<guid isPermaLink="false">7Mevqxav5uXv</guid>
<pubDate>Wed, 22 Jun 2022 11:28:00 GMT</pubDate>
<description>Revisiting old photographies of M31 and M42 with a refreshed software process</description>
</item>
</channel>
</rss>
+2
View File
@@ -323,6 +323,8 @@
}
function showCanvas() {
wrap.innerHTML = "";
renderer.domElement.setAttribute("role", "img");
renderer.domElement.setAttribute("aria-label", "Interactive 3D model preview");
wrap.appendChild(renderer.domElement);
var hint = document.createElement("div");
hint.className = "model-viewer-hint";
+31
View File
@@ -0,0 +1,31 @@
<script>
(function () {
var data = window.blogData;
if (!data || !data.articles) return;
var me = document.body.getAttribute("data-note-id");
var all = data.articles;
var cur = null;
for (var i = 0; i < all.length; i++) { if (all[i].id === me) { cur = all[i]; break; } }
if (!cur) return;
var tags = cur.tags || [], cat = cur.category || "", scored = [];
for (var j = 0; j < all.length; j++) {
var a = all[j];
if (a.id === me) continue;
var s = 0;
if (cat && a.category === cat) s += 2;
for (var t = 0; t < tags.length; t++) { if ((a.tags || []).indexOf(tags[t]) >= 0) s += 1; }
if (s > 0) scored.push({ a: a, score: s });
}
scored.sort(function (x, y) { return y.score - x.score; });
var rel = scored.slice(0, 4);
if (!rel.length) return;
var host = document.getElementById("content");
if (!host) return;
var sec = document.createElement("section");
sec.className = "blog-related";
var h = document.createElement("h2"); h.textContent = "Related posts"; sec.appendChild(h);
var ul = document.createElement("ul");
rel.forEach(function (r) { var li = document.createElement("li"); var a = document.createElement("a"); a.href = r.a.url; a.textContent = r.a.title; li.appendChild(a); ul.appendChild(li); });
sec.appendChild(ul); host.appendChild(sec);
})();
</script>
+30 -30
View File
@@ -3,46 +3,46 @@
var old = document.querySelector(".search-item .search-input");
if (!old) return;
var box = old.closest(".search-item") || old.parentNode;
var input = document.createElement("input");
input.type = "text";
input.className = "blog-search-header-input";
input.placeholder = old.placeholder || "Search...";
old.replaceWith(input);
var overlay = document.createElement("div");
overlay.className = "blog-search-overlay";
box.appendChild(overlay);
function render(q) {
q = (q || "").toLowerCase().trim();
if (!q) { overlay.innerHTML = ""; overlay.classList.remove("open"); return; }
var articles = (window.blogData && window.blogData.articles) || [];
var hits = articles.filter(function (a) {
var hay = (a.title + " " + (a.summary || "") + " " + (a.tags || []).join(" ")).toLowerCase();
return hay.indexOf(q) !== -1;
});
if (!hits.length) {
overlay.innerHTML = '<div class="blog-search-none">No results.</div>';
} else {
overlay.innerHTML = hits.slice(0, 8).map(function (a) {
return '<a class="blog-search-overlay-hit" href="' + a.url + '"><span class="t">' + a.title + '</span>' +
(a.summary ? '<span class="s">' + a.summary + '</span>' : '') + '</a>';
}).join("");
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[c];});}
function snippet(text,word){if(!text)return"";var i=text.toLowerCase().indexOf(word);if(i<0)return"";var s=Math.max(0,i-32),e=Math.min(text.length,i+word.length+48);return(s>0?"\u2026":"")+esc(text.slice(s,e))+(e<text.length?"\u2026":"");}
function render(q){
q=(q||"").trim();
if(!q){overlay.innerHTML="";overlay.classList.remove("open");return;}
var words=q.toLowerCase().split(/\s+/).filter(Boolean);
var articles=(window.blogData&&window.blogData.articles)||[];
var scored=[];
for(var i=0;i<articles.length;i++){
var a=articles[i];
var title=(a.title||"").toLowerCase(),sum=(a.summary||"").toLowerCase(),tag=(a.tags||[]).join(" ").toLowerCase(),body=(a.text||"").toLowerCase();
var ok=true,score=0;
for(var w=0;w<words.length;w++){
var word=words[w],hit=0;
if(title.indexOf(word)>=0){score+=6;hit++;}
if(tag.indexOf(word)>=0){score+=4;hit++;}
if(sum.indexOf(word)>=0){score+=3;hit++;}
if(body.indexOf(word)>=0){score+=1;hit++;}
if(!hit){ok=false;break;}
}
if(ok&&score>0)scored.push({a:a,score:score});
}
scored.sort(function(x,y){return y.score-x.score;});
if(!scored.length){overlay.innerHTML='<div class="blog-search-none">No results.</div>';}
else{var first=words[0];overlay.innerHTML=scored.slice(0,8).map(function(s){var a=s.a;var sub=snippet(a.text,first)||a.summary;return '<a class="blog-search-overlay-hit" href="'+esc(a.url)+'"><span class="t">'+esc(a.title)+'</span>'+(sub?'<span class="s">'+esc(sub)+'</span>':'')+'</a>';}).join("");}
overlay.classList.add("open");
}
var t;
input.addEventListener("input", function () {
clearTimeout(t);
t = setTimeout(function () { render(input.value); }, 150);
});
input.addEventListener("focus", function () { if (input.value.trim()) render(input.value); });
input.addEventListener("blur", function () { setTimeout(function () { overlay.classList.remove("open"); }, 150); });
input.addEventListener("keydown", function (e) { if (e.key === "Escape") { overlay.classList.remove("open"); } });
document.addEventListener("click", function (e) {
if (!box.contains(e.target)) overlay.classList.remove("open");
});
input.addEventListener("input",function(){clearTimeout(t);t=setTimeout(function(){render(input.value);},150);});
input.addEventListener("focus",function(){if(input.value.trim())render(input.value);});
input.addEventListener("blur",function(){setTimeout(function(){overlay.classList.remove("open");},150);});
input.addEventListener("keydown",function(e){if(e.key==="Escape")overlay.classList.remove("open");});
document.addEventListener("click",function(e){if(!box.contains(e.target))overlay.classList.remove("open");});
})();
</script>
</script>
+6
View File
@@ -505,3 +505,9 @@ pre.code-processed .code-copy-btn:hover,
/* ---------- a11y: skip link ---------- */
.skip-link{position:absolute;left:-9999px;top:0;z-index:2000;background:var(--background-active);color:var(--background-secondary);padding:8px 14px;border-radius:0 0 8px 0;font-size:.9em;text-decoration:none}
.skip-link:focus{left:0}
/* ---------- Related posts ---------- */
.blog-related{margin-top:2em;padding-top:1.25em;border-top:1px solid var(--background-highlight)}
.blog-related ul{list-style:none;padding:0;margin:.5em 0 0;display:flex;flex-wrap:wrap;gap:8px}
.blog-related a{display:inline-block;padding:4px 12px;border:1px solid var(--background-highlight);border-radius:8px;background:var(--blog-card-bg);color:var(--text-heading);text-decoration:none;font-size:.9em}
.blog-related a:hover{border-color:var(--text-link);color:var(--text-link);text-decoration:none}