diff --git a/css/style.css b/css/style.css
index ee6e9a1..2317b46 100644
--- a/css/style.css
+++ b/css/style.css
@@ -3,6 +3,38 @@
Monochrome palette with light & dark modes
======================================== */
+/* ----------------------------------------
+ INTER — self-hosted (no third-party font request)
+ ---------------------------------------- */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('../fonts/inter-400.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('../fonts/inter-500.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url('../fonts/inter-600.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url('../fonts/inter-700.woff2') format('woff2');
+}
+
/* ----------------------------------------
THEME TOKENS — Light Mode (default)
---------------------------------------- */
@@ -2212,6 +2244,164 @@ button:focus:not(:focus-visible) {
.toc-drawer { display: none !important; }
}
+/* ========================================
+ SEARCH OVERLAY
+ ======================================== */
+.search-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 2500;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 12vh 1rem 1rem;
+ background: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.15s ease, visibility 0s linear 0.15s;
+}
+
+.search-overlay.is-open {
+ opacity: 1;
+ visibility: visible;
+ transition: opacity 0.15s ease;
+}
+
+.search-box {
+ width: 100%;
+ max-width: 560px;
+ overflow: hidden;
+ background: var(--bg-surface);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+}
+
+.search-input {
+ width: 100%;
+ padding: 1rem 1.2rem;
+ border: none;
+ border-bottom: 1px solid var(--border-color);
+ background: transparent;
+ color: var(--text-primary);
+ font-family: var(--font-sans);
+ font-size: 1.05rem;
+ outline: none;
+}
+
+.search-input::placeholder { color: var(--text-muted); }
+
+.search-results {
+ list-style: none;
+ margin: 0;
+ padding: 0.4rem;
+ max-height: 50vh;
+ overflow-y: auto;
+}
+
+.search-result a {
+ display: flex;
+ align-items: baseline;
+ gap: 0.6rem;
+ padding: 0.55rem 0.8rem;
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+}
+
+.search-result a:hover,
+.search-result.is-active a {
+ background: var(--bg-hover);
+ text-decoration: none;
+}
+
+.search-result-title { font-weight: 600; }
+.search-result-meta {
+ margin-left: auto;
+ font-size: var(--font-size-xs);
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+
+.search-empty { padding: 1rem 1.2rem; color: var(--text-muted); }
+
+/* ========================================
+ SERIES BOX
+ ======================================== */
+.series-box {
+ margin: 0 0 var(--space-lg);
+ padding: var(--space-sm) var(--space-md);
+ border: 1px solid var(--border-color);
+ border-left: 4px solid var(--accent);
+ border-radius: var(--radius-sm);
+ background: var(--bg-subtle);
+}
+
+.series-box-title {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ margin-bottom: 0.4rem;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+.series-box-title .bd-icon { color: var(--accent); flex: 0 0 auto; }
+.series-list { margin: 0; padding-left: 1.5rem; }
+.series-item { padding: 0.12rem 0; color: var(--text-secondary); }
+.series-item a { color: var(--text-secondary); }
+.series-item a:hover { color: var(--accent); }
+.series-item.is-current span { color: var(--accent); font-weight: 600; }
+
+/* ========================================
+ CONTENT EXTRAS (kbd, mark, task lists, footnotes, external links)
+ ======================================== */
+.content kbd {
+ display: inline-block;
+ padding: 0.1em 0.45em;
+ font-family: var(--font-sans);
+ font-size: 0.8em;
+ line-height: 1.4;
+ color: var(--text-primary);
+ background: var(--bg-subtle);
+ border: 1px solid var(--border-color);
+ border-bottom-width: 2px;
+ border-radius: 5px;
+ white-space: nowrap;
+}
+
+.content mark {
+ padding: 0.05em 0.2em;
+ border-radius: 3px;
+ background: rgba(245, 200, 66, 0.35);
+ color: inherit;
+}
+
+/* Small ↗ after outbound links (JS adds target=_blank to external links) */
+.content a[target="_blank"]::after {
+ content: "↗";
+ margin-left: 0.15em;
+ font-size: 0.8em;
+ color: var(--text-muted);
+}
+
+/* Task lists (- [ ] / - [x]) — rendered by JS into real checkboxes */
+.content ul.task-list { list-style: none; padding-left: 0.2rem; }
+.content .task-list-item { display: flex; align-items: flex-start; gap: 0.5rem; }
+.content .task-list-item input[type="checkbox"] { margin-top: 0.35em; flex: 0 0 auto; }
+
+/* Footnotes (when the parser emits them) */
+.content .footnotes {
+ margin-top: var(--space-lg);
+ padding-top: var(--space-md);
+ border-top: 1px solid var(--border-subtle);
+ font-size: var(--font-size-sm);
+ color: var(--text-secondary);
+}
+
+/* Definition lists */
+.content dt { font-weight: 700; }
+.content dd { margin: 0 0 0.6rem; color: var(--text-secondary); }
+
/* ========================================
RESPONSIVE
======================================== */
diff --git a/fonts/inter-400.woff2 b/fonts/inter-400.woff2
new file mode 100644
index 0000000..f15b025
Binary files /dev/null and b/fonts/inter-400.woff2 differ
diff --git a/fonts/inter-500.woff2 b/fonts/inter-500.woff2
new file mode 100644
index 0000000..54f0a59
Binary files /dev/null and b/fonts/inter-500.woff2 differ
diff --git a/fonts/inter-600.woff2 b/fonts/inter-600.woff2
new file mode 100644
index 0000000..d189794
Binary files /dev/null and b/fonts/inter-600.woff2 differ
diff --git a/fonts/inter-700.woff2 b/fonts/inter-700.woff2
new file mode 100644
index 0000000..a68fb10
Binary files /dev/null and b/fonts/inter-700.woff2 differ
diff --git a/index.php b/index.php
index 9eae6aa..84f6ea6 100644
--- a/index.php
+++ b/index.php
@@ -191,6 +191,7 @@
// Theme directory URL, so the deferred bundle can load self-hosted assets
// (highlight.js + its theme CSS) relative to the theme.
window.BLOWDIT_THEME_URL = ;
+ window.BLOWDIT_HOME = ;
// Localised UI strings for the deferred bundle. $L->get() falls back to
// the key itself when a translation is missing, so this is safe in any language.
window.BLOWDIT_I18N = {
@@ -203,7 +204,11 @@
goToSlide: get('Go to slide')); ?>,
prevImage: get('Previous image')); ?>,
nextImage: get('Next image')); ?>,
- imageViewer: get('Image viewer')); ?>
+ imageViewer: get('Image viewer')); ?>,
+ onThisPage: get('On this page')); ?>,
+ search: get('Search')); ?>,
+ searchPlaceholder: get('Search posts…')); ?>,
+ noResults: get('No results')); ?>
};
' +
+ '' +
+ '
' +
+ '' + T.noResults + '
' +
+ '';
+ document.body.appendChild(overlay);
+
+ var input = overlay.querySelector('.search-input');
+ var results = overlay.querySelector('.search-results');
+ var empty = overlay.querySelector('.search-empty');
+ var active = -1;
+ var current = [];
+
+ function open() {
+ overlay.classList.add('is-open');
+ document.body.style.overflow = 'hidden';
+ input.value = '';
+ render([]);
+ setTimeout(function () { input.focus(); }, 30);
+ }
+ function close() {
+ overlay.classList.remove('is-open');
+ document.body.style.overflow = '';
+ }
+ function isOpen() { return overlay.classList.contains('is-open'); }
+
+ function score(item, q) {
+ var hay = (item.t + ' ' + (item.c || '') + ' ' + (item.g || []).join(' ') + ' ' + (item.d || '')).toLowerCase();
+ return hay.indexOf(q) !== -1;
+ }
+
+ function render(list) {
+ current = list;
+ active = list.length ? 0 : -1;
+ results.innerHTML = '';
+ empty.hidden = !(input.value.trim() && list.length === 0);
+ list.forEach(function (item, i) {
+ var li = document.createElement('li');
+ li.className = 'search-result' + (i === 0 ? ' is-active' : '');
+ li.innerHTML = '' +
+ (item.c ? '' : '') + '';
+ li.querySelector('.search-result-title').textContent = item.t;
+ if (item.c) li.querySelector('.search-result-meta').textContent = item.c;
+ results.appendChild(li);
+ });
+ }
+
+ function update() {
+ var q = input.value.trim().toLowerCase();
+ if (!q) { render([]); return; }
+ render(data.filter(function (item) { return score(item, q); }).slice(0, 8));
+ }
+
+ function move(delta) {
+ var items = results.querySelectorAll('.search-result');
+ if (!items.length) return;
+ items[active] && items[active].classList.remove('is-active');
+ active = (active + delta + items.length) % items.length;
+ items[active].classList.add('is-active');
+ items[active].scrollIntoView({ block: 'nearest' });
+ }
+
+ function go() {
+ var items = results.querySelectorAll('.search-result');
+ if (items[active]) { var a = items[active].querySelector('a'); if (a) window.location.href = a.href; }
+ }
+
+ input.addEventListener('input', update);
+ input.addEventListener('keydown', function (e) {
+ if (e.key === 'ArrowDown') { e.preventDefault(); move(1); }
+ else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); }
+ else if (e.key === 'Enter') { e.preventDefault(); go(); }
+ });
+ overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); });
+
+ Array.prototype.forEach.call(document.querySelectorAll('.js-search-open'), function (b) {
+ b.addEventListener('click', function (e) { e.preventDefault(); open(); });
+ });
+
+ // Expose for the keyboard-shortcuts handler.
+ window.BLOWDIT_SEARCH = { open: open, close: close, isOpen: isOpen };
+ })();
+
+ /* --------------------------------------------------------
+ Keyboard shortcuts: / search · g h home · t top · Esc close.
+ -------------------------------------------------------- */
+ (function () {
+ var lastG = 0;
+ function typing(t) {
+ return t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable);
+ }
+ document.addEventListener('keydown', function (e) {
+ var s = window.BLOWDIT_SEARCH;
+ if (e.key === 'Escape' || e.key === 'Esc') {
+ if (s && s.isOpen()) s.close();
+ return;
+ }
+ if (typing(e.target) || e.metaKey || e.ctrlKey || e.altKey) return;
+
+ if (e.key === '/') { e.preventDefault(); if (s) s.open(); return; }
+ if (e.key === 't' || e.key === 'T') {
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ window.scrollTo({ top: 0, behavior: reduce ? 'auto' : 'smooth' });
+ return;
+ }
+ // "g" then "h" -> home
+ if (e.key === 'g' || e.key === 'G') { lastG = Date.now(); return; }
+ if ((e.key === 'h' || e.key === 'H') && Date.now() - lastG < 600) {
+ window.location.href = (window.BLOWDIT_HOME || '/');
+ }
+ });
+ })();
+
})();
diff --git a/languages/en.json b/languages/en.json
index 845050e..513ec11 100644
--- a/languages/en.json
+++ b/languages/en.json
@@ -18,6 +18,10 @@
"Copy link": "Copy link",
"Link copied": "Link copied",
"Related posts": "Related posts",
+ "Search": "Search",
+ "Search posts…": "Search posts…",
+ "No results": "No results",
+ "Part of a series": "Part of a series",
"Home": "Home",
"On this page": "On this page",
"Page not found": "Page not found",
diff --git a/metadata.json b/metadata.json
index f1c864d..c637f43 100644
--- a/metadata.json
+++ b/metadata.json
@@ -2,9 +2,9 @@
"author": "spleenftw",
"email": "",
"website": "https://github.com/Spleenftw/blowdit",
- "version": "1.5.0",
- "releaseDate": "2026-06-02",
+ "version": "1.6.0",
+ "releaseDate": "2026-06-03",
"license": "MIT",
"compatible": "3.22.0",
- "notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints."
+ "notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints. v1.6.0: self-hosted Inter (no Google Fonts), client-side search overlay (\"/\" key + navbar button), keyboard shortcuts (/ , g h, t, Esc), a tabbed-code copy button, series navigation (series-* tag), and content typography (keycaps, task-list checkboxes, , external-link icon, footnote/definition-list styling)."
}
diff --git a/php/footer.php b/php/footer.php
index 6f9a9b8..07be646 100644
--- a/php/footer.php
+++ b/php/footer.php
@@ -18,3 +18,28 @@
+
+getList(1, 1000, true) as $bdSk) {
+ $bdSp = new Page($bdSk);
+ if ($bdSp->isStatic()) { continue; } // posts only
+ $bdSearchIndex[] = array(
+ 't' => blowdit_plain($bdSp->title()),
+ 'u' => $bdSp->permalink(),
+ 'd' => blowdit_plain($bdSp->description()),
+ 'c' => blowdit_plain($bdSp->category()),
+ 'g' => array_values(array_map('blowdit_plain', $bdSp->tags(true))),
+ );
+ }
+ }
+ echo '';
+ } catch (\Throwable $e) {}
+?>
diff --git a/php/head.php b/php/head.php
index cf4ded3..9a4adfa 100644
--- a/php/head.php
+++ b/php/head.php
@@ -244,12 +244,9 @@
-
-
-
-
-
+
+
diff --git a/php/icons.php b/php/icons.php
index cbbb55e..f18cbb3 100644
--- a/php/icons.php
+++ b/php/icons.php
@@ -21,6 +21,7 @@ if (!function_exists('blowdit_icon')) {
'chevron-right' => '',
'house' => '',
'link' => '',
+ 'search' => '',
'share' => '',
// --- Theme picker (mirror in blowdit.js) ---
'sun' => '',
diff --git a/php/navbar.php b/php/navbar.php
index f2717ca..5c1c315 100644
--- a/php/navbar.php
+++ b/php/navbar.php
@@ -59,6 +59,13 @@
+
+
+
+
+