diff --git a/README.md b/README.md index 81dd536..4f041cb 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/blog_generator.js b/blog_generator.js index a4c3b0b..103d740 100644 --- a/blog_generator.js +++ b/blog_generator.js @@ -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(//gi, " ") + .replace(//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 = ``; if (dataNote.getContent() !== html) { diff --git a/probe.txt b/probe.txt new file mode 100644 index 0000000..e873aea --- /dev/null +++ b/probe.txt @@ -0,0 +1,130 @@ + + + + Richard's Blog + https://richard.familie-zink.org/blog + + Project documentation & technical blog by Richard Zink + en + Thu, 10 Sep 2026 07:25:48 GMT + + Inside this blog: a share-notes engine with a themable frontend + https://richard.familie-zink.org/blog/inside-this-blog-a-share-notes-engine-with-a-themable-frontend + inDtoyyz9thb + Tue, 08 Sep 2026 00:00:00 GMT + 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. + + + FAST-LIO with loop closing on IGNIS-8 + https://richard.familie-zink.org/blog/ignis8-fast-lio-loop-closing + v778JORAnpij + Wed, 02 Sep 2026 00:00:00 GMT + 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. + + + Arming the drive: a fail-safe by default + https://richard.familie-zink.org/blog/ignis8-drive-arm-gate + 7EyHklclcGVO + Sun, 30 Aug 2026 00:00:00 GMT + 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. + + + Emergency stop: a latch that never releases itself + https://richard.familie-zink.org/blog/ignis8-emergency-stop + kRTOgbiwjhfI + Sat, 29 Aug 2026 00:00:00 GMT + 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. + + + Cutting the thermal stream and SLAM load on IGNIS-8 + https://richard.familie-zink.org/blog/cutting-thermal-stream-and-slam-load + uzmCVAsFjcJB + Thu, 27 Aug 2026 00:00:00 GMT + 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. + + + Battery rethink: prismatic PHEV1 vs cylindrical M50L + https://richard.familie-zink.org/blog/ignis8-battery-phev1-m50l + S3wsaj19dS5L + Thu, 27 Aug 2026 00:00:00 GMT + 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. + + + The technical hurdles between the drawing board and the first drive + https://richard.familie-zink.org/blog/ignis8-technical-hurdles + ozevCW5vvOtE + Sun, 16 Aug 2026 00:00:00 GMT + 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. + + + Project start + https://richard.familie-zink.org/blog/ignis8-project-start + 0ixEFpiXl6Lg + Sat, 08 Aug 2026 00:00:00 GMT + 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. + + + Leather Notebook Cover + https://richard.familie-zink.org/blog/leather-notebook-cover + k6aslDxBHulY + Sat, 21 Feb 2026 00:00:00 GMT + Hand sewn notebook cover made from goat skin leather + + + !NEC MPi400 Namechange + https://richard.familie-zink.org/blog/nec-mpi400-namechange + QaHi3lULbcum + Tue, 28 Jan 2025 00:00:00 GMT + To directly go out of harms way with legal problems, I changed the name of the project. + + + !NEC MPi400: Reverse Engineering the Keyboard Matrix + https://richard.familie-zink.org/blog/nec-mpi400-reverse-engineering-the-keyboard-matrix + Bx7hKarVZlGf + Thu, 25 Jul 2024 00:00:00 GMT + Create PCB; Do measurements; Use QMK; Have Keyboard! 😎 + + + !NEC MPi400: Disassembly + https://richard.familie-zink.org/blog/nec-mpi400-project-start + 9mVAVwtlNCTb + Tue, 16 Jul 2024 00:00:00 GMT + Complete disassembly of the MobilePro 900 + + + SoC/OCV Calculation + https://richard.familie-zink.org/blog/soc-ocv-calculation + TxlNPtkaHjjA + Sun, 07 Jul 2024 07:01:00 GMT + C source for SoC calculation based on open circuit voltage + + + DLG INR18650-320 cell data + https://richard.familie-zink.org/blog/dlg-inr18650-320 + 9zxBjebNKoVJ + Sun, 25 Feb 2024 08:31:00 GMT + SoC/OCV liion cell data + + + Open Strainwave Mount (OSWM) planning + https://richard.familie-zink.org/blog/oswm-planning + 3nwNkCrqAMS9 + Sat, 02 Dec 2023 08:51:00 GMT + Planning a mount with strainwave gears for use with OnStep + + + NGC 281: Pacman Nebula + https://richard.familie-zink.org/blog/ngc-281-pacman-nebula + obZwmWDJdzK6 + Thu, 23 Mar 2023 07:43:00 GMT + Image of Pacman Nebula taken with Star Adventurer + + + Revisiting old astrophotographies + https://richard.familie-zink.org/blog/revisiting-astrophotographies + 7Mevqxav5uXv + Wed, 22 Jun 2022 11:28:00 GMT + Revisiting old photographies of M31 and M42 with a refreshed software process + + + \ No newline at end of file diff --git a/scripts/3d_model_viewer.js b/scripts/3d_model_viewer.js index 27334ab..183e4f9 100644 --- a/scripts/3d_model_viewer.js +++ b/scripts/3d_model_viewer.js @@ -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"; diff --git a/scripts/related_posts.js b/scripts/related_posts.js new file mode 100644 index 0000000..38bf4f8 --- /dev/null +++ b/scripts/related_posts.js @@ -0,0 +1,31 @@ + diff --git a/scripts/search_header.js b/scripts/search_header.js index ef2e30f..44627fc 100644 --- a/scripts/search_header.js +++ b/scripts/search_header.js @@ -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 = '
No results.
'; - } else { - overlay.innerHTML = hits.slice(0, 8).map(function (a) { - return '' + a.title + '' + - (a.summary ? '' + a.summary + '' : '') + ''; - }).join(""); + function esc(s){return String(s).replace(/[&<>"]/g,function(c){return({"&":"&","<":"<",">":">",'"':"""})[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=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='
No results.
';} + 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 ''+esc(a.title)+''+(sub?''+esc(sub)+'':'')+'';}).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");}); })(); - \ No newline at end of file + diff --git a/theme/blog_share_base.css b/theme/blog_share_base.css index cd63141..d2916bb 100644 --- a/theme/blog_share_base.css +++ b/theme/blog_share_base.css @@ -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}