diff --git a/README.md b/README.md index 78107d5..12ef7a1 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,93 @@ -# Blowdit +# Blowfish A clean, minimal theme for [Bludit](https://www.bludit.com) inspired by the [Blowfish](https://blowfish.page/) Hugo theme. -   +   ## Features -- 🎨 **Five-theme swatch picker** — click the theme button to open a dropdown with coloured swatches for Light, Dark, Nord, Dracula and Catppuccin. Choice persists via `localStorage` + cookie (no flash on next load); falls back to OS preference. -- 🧱 **Monochrome base design** — restrained grayscale palette with crisp Inter typography. -- 👤 **Profile hero** — circular avatar, site title, slogan and social links on the homepage front page. -- 📖 **Sticky Table of Contents** — articles that contain headings (`h2`/`h3`/`h4`) get a left sidebar ToC that sticks while scrolling and highlights the active section. Layout automatically switches to 3-column with a wider container. -- 📚 **Smart right sidebar** — shows only About / Categories / Hit Counter on the homepage (navigation is redundant there); shows the full plugin set on article pages. -- 🔀 **Randomized navigation** — the Navigation/Pages plugin list is shuffled on each load. -- ⏱️ **Reading time & dates** shown on the post list and single posts. -- 🔗 **Social icons** in the navbar and hero (SVG files in `img/`). -- 🏷️ **Tags & categories** rendered as pill badges. -- 🖼️ **Image lightbox** — clicking any article image opens it full-screen. A dark overlay with a magnifier icon appears on hover to hint at the interaction. Closes on click or Esc. -- 🎠 **Image carousel** — use a ` ```carousel ` fenced block, one image per line (markdown image syntax or plain URL). -- 📑 **Tabbed code blocks** — use a ` ```tabs ` fenced block with `[Tab Name]` or `@tab Name` headers to show content from multiple servers/environments side by side. -- 📜 **Code block scrollbar** — always-visible horizontal scrollbar on `
` blocks with a custom slim style that respects the active theme.
-- 📱 Fully responsive; no flash of the wrong theme on load.
+- **Five-theme swatch picker** — Light, Dark, Nord, Dracula, Catppuccin. Choice persists via `localStorage` + cookie (no flash on next load); falls back to OS preference.
+- **Monochrome base design** — restrained grayscale palette with crisp Inter typography (self-hosted, no Google Fonts).
+- **Profile hero** — circular avatar, site title, slogan and social links on the homepage.
+- **Sticky Table of Contents** — articles with `h2`/`h3`/`h4` headings get a left sidebar ToC that sticks while scrolling and highlights the active section. Layout switches to 3-column with a wider container. A floating button opens a drawer on mobile.
+- **Smart right sidebar** — shows only About / Categories / Hit Counter on the homepage; full plugin set on article pages.
+- **Client-side search overlay** — `/` key or navbar button; searches titles, descriptions and tags client-side.
+- **Keyboard shortcuts** — `/` (search), `g h` (go home), `t` (back to top), `Esc` (close overlay).
+- **Randomized navigation** — the Navigation/Pages plugin list is shuffled on every load.
+- **Reading time & dates** shown on the post list and single posts.
+- **Social icons** in the navbar and hero (SVG files in `img/`).
+- **Tags & categories** rendered as pill badges.
+- **Image lightbox** — clicking any article image opens it full-screen with a dark overlay and zoom hint.
+- **Image carousel** — ` ```carousel ` fenced block, one image per line (markdown syntax or bare URL with optional `| caption`).
+- **Tabbed code blocks** — ` ```tabs ` fenced block with `[Tab Name]` or `@tab Name` headers.
+- **Code block header bar** — language label + copy-to-clipboard button on every code block.
+- **Callout/admonition boxes** — `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!DANGER]`, `> [!IMPORTANT]`.
+- **Series navigation** — posts tagged `series-*` display an ordered series box.
+- **SEO** — Open Graph, Twitter Card, JSON-LD (WebSite, Article, BreadcrumbList, sameAs), canonical URL, theme-color meta, cover-image preload.
+- **Accessibility** — skip link, ARIA labels, `:focus-visible`, heading anchor links, print stylesheet.
+- **PWA** — web app manifest included.
+- Fully responsive; no jQuery, no Bootstrap JS, no webfont requests.
## Installation
-1. Download or clone this repository into your Bludit `bl-themes` directory:
+1. Clone or copy this repository into your Bludit `bl-themes/` directory:
```
- /usr/share/nginx/html/bl-themes/
+ bl-themes/bludit-blowfish/
```
-2. In the Bludit admin panel go to **Settings → Themes** and activate **Blowdit**.
+2. In the Bludit admin panel go to **Settings → Themes** and activate **Blowfish**.
-3. Add your assets to `blowdit/img/`:
- - Your avatar as `spleenftw.jpeg` — used in the homepage hero and the article sidebar compact card.
+3. Add your assets to `img/`:
+ - Your avatar as `spleenftw.jpeg` — used in the homepage hero and the article sidebar card.
To change the filename, update `php/sidebar.php` and `php/home.php`.
## Layout
```
-blowdit/
-├── index.php Main template (layout, all inline JS)
+bludit-blowfish/
+├── index.php Main template (layout, sidebar logic, JS config)
├── metadata.json Theme metadata
+├── manifest.webmanifest
├── css/
│ └── style.css All styles (CSS custom properties, 5 themes)
+├── js/
+│ └── blowfish.js Deferred JS bundle (ToC, lightbox, carousel, search, …)
├── php/
│ ├── head.php , fonts, favicon, anti-FOUC theme script
-│ ├── navbar.php Navbar, social SVG icons, theme-picker dropdown
+│ ├── navbar.php Navbar, social icons, theme-picker dropdown
│ ├── home.php Profile hero + post listing
-│ ├── page.php Single page / post
+│ ├── page.php Single page/post
│ ├── sidebar.php Right sidebar plugin container
│ ├── toc.php Left ToC sidebar (article pages with headings)
-│ └── footer.php Footer
+│ ├── footer.php Footer + search-index JSON
+│ └── icons.php Inline-SVG icon helper + shared PHP functions
├── img/ Social SVG icons + avatar
└── languages/ Translations (10 languages)
```
## Themes
-Each theme is a set of CSS custom properties. Light lives in `:root`; the others
-override it under `[data-theme="dark"]`, `[data-theme="nord"]`,
-`[data-theme="dracula"]` and `[data-theme="catppuccin"]`.
+Each colour theme is a block of CSS custom properties. Light lives in `:root`; the others override it under `[data-theme="dark"]`, `[data-theme="nord"]`, `[data-theme="dracula"]` and `[data-theme="catppuccin"]`.
-The attribute is set on `` before the first paint (in `php/head.php`) to
-avoid a flash. The navbar swatch picker updates it at runtime and persists the
-choice to both `localStorage` and a cookie.
+The `data-theme` attribute is set on `` before the first paint (in `php/head.php`) to avoid a flash. The navbar swatch picker updates it at runtime and persists the choice to `localStorage` and a cookie.
-To add a new theme, add a variable block in `css/style.css` and add the theme
-name + swatch colours to the picker in `php/navbar.php` and the `THEMES` array
-in `index.php`.
+To add a new theme: add a variable block in `css/style.css`, register the theme name + background colour in `index.php` (`$blowfishThemeBg`), and add a swatch entry in `php/navbar.php`.
## Table of Contents
-When an article's rendered HTML contains any `h2`, `h3` or `h4` headings, the
-layout automatically switches from 2-column to 3-column:
+When an article's rendered HTML contains any `h2`, `h3` or `h4` headings, the layout automatically switches from 2-column to 3-column:
```
[ Left ToC (col-md-3) ] [ Article (col-md-6) ] [ Right sidebar (col-md-3) ]
```
-The ToC is hidden on mobile. The left column has no `align-self-start` so it
-stretches to the full article height, which is what `position: sticky` needs to
-function. The container switches to `.container-wide` (1400 px max-width) and
-the navbar inner container widens to match via `body.has-toc .navbar .container`.
+The sidebar ToC is hidden on mobile; a floating button opens a slide-up drawer instead. The container switches to `.container-wide` (1400 px max-width) and the navbar widens to match via `body.has-toc .navbar .container`.
## Tabbed code blocks
-Use a ` ```tabs ` fenced block. Delimit tabs with `[Tab Name]` (brackets) or
-`@tab Tab Name` (@ prefix):
+Use a ` ```tabs ` fenced block. Delimit tabs with `[Tab Name]` (brackets) or `@tab Tab Name` (@ prefix):
````
```tabs
@@ -101,8 +101,7 @@ root@pgs02:~# psql -c "SELECT …"
## Image carousel
-Use a ` ```carousel ` fenced block. Each non-empty line is one slide. Accepts
-standard markdown image syntax or a bare URL with an optional `| caption`:
+Use a ` ```carousel ` fenced block. Each non-empty line is one slide. Accepts standard markdown image syntax or a bare URL with an optional `| caption`:
````
```carousel
@@ -111,13 +110,19 @@ standard markdown image syntax or a bare URL with an optional `| caption`:
```
````
-Use `![]()` (empty alt) to show no caption. Supports swipe on touch devices.
+Supports swipe on touch devices.
-## Randomized navigation
+## Callout boxes
-If the **Navigation** (or **Pages**) plugin is in the sidebar, its list is
-shuffled on every page load. The plugin output is wrapped in `.js-random-nav`;
-adjust the detection in `index.php` if your plugin uses a different class.
+```markdown
+> [!NOTE]
+> Useful information.
+
+> [!WARNING]
+> Something to watch out for.
+```
+
+Supported types: `NOTE`, `TIP`, `WARNING`, `DANGER`, `IMPORTANT`.
## License
diff --git a/css/style.css b/css/style.css
index b439c36..21bdbe7 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1,5 +1,5 @@
/* ========================================
- BLOWDIT — a Bludit theme inspired by Blowfish
+ BLOWFISH — a Bludit theme inspired by Blowfish
Monochrome palette with light & dark modes
======================================== */
diff --git a/index.php b/index.php
index 84f6ea6..7ced1e2 100644
--- a/index.php
+++ b/index.php
@@ -1,54 +1,54 @@
Advanced ->
- // Email account settings). Leave it to auto-detect, or hard-set $blowditEmail
+ // Email account settings). Leave it to auto-detect, or hard-set $blowfishEmail
// below to override. Empty -> the icon is hidden.
- $blowditEmail = '';
+ $blowfishEmail = '';
if (isset($site)) {
// Try Bludit's site settings (accessor is getValue(); getField() as backup).
$bdGet = method_exists($site, 'getValue') ? 'getValue'
: (method_exists($site, 'getField') ? 'getField' : null);
if ($bdGet) {
- $blowditEmail = $site->$bdGet('emailFromAddress'); // Bludit's "Sender email"
- if (empty($blowditEmail)) { $blowditEmail = $site->$bdGet('email'); }
+ $blowfishEmail = $site->$bdGet('emailFromAddress'); // Bludit's "Sender email"
+ if (empty($blowfishEmail)) { $blowfishEmail = $site->$bdGet('email'); }
}
}
// Bludit's sender email isn't always exposed to themes (it's an SMTP/plugin
// setting), so fall back to a fixed address. Set to '' to hide the icon.
- if (empty($blowditEmail)) { $blowditEmail = 'contact@interlope.xyz'; }
- $blowditTwitterHandle = '@spleenftw'; // twitter:site / twitter:creator; '' to omit
+ if (empty($blowfishEmail)) { $blowfishEmail = 'contact@interlope.xyz'; }
+ $blowfishTwitterHandle = '@spleenftw'; // twitter:site / twitter:creator; '' to omit
// Read the theme from a cookie so the root element can be rendered
// pre-coloured. This paints the page in the theme colour from the first
// byte of every response, eliminating the white flash between navigations.
- $blowditThemeBg = array(
+ $blowfishThemeBg = array(
'light' => '#ffffff',
'dark' => '#171717',
'nord' => '#2e3440',
'dracula' => '#282a36',
'catppuccin' => '#1e1e2e',
);
- $blowditTheme = (isset($_COOKIE['blowdit-theme']) && isset($blowditThemeBg[$_COOKIE['blowdit-theme']]))
- ? $_COOKIE['blowdit-theme']
+ $blowfishTheme = (isset($_COOKIE['blowfish-theme']) && isset($blowfishThemeBg[$_COOKIE['blowfish-theme']]))
+ ? $_COOKIE['blowfish-theme']
: 'light';
- $blowditBg = $blowditThemeBg[$blowditTheme];
- $blowditScheme = ($blowditTheme === 'light') ? 'light' : 'dark';
+ $blowfishBg = $blowfishThemeBg[$blowfishTheme];
+ $blowfishScheme = ($blowfishTheme === 'light') ? 'light' : 'dark';
// Pre-compute before so we can stamp the has-toc class on it,
// which lets CSS align the navbar container with the wider article container.
- $blowditIsArticle = ($WHERE_AM_I === 'page' && isset($page) && !$page->isStatic() && !$url->notFound());
- $blowditHasHeadings = $blowditIsArticle && (bool) preg_match('/content());
+ $blowfishIsArticle = ($WHERE_AM_I === 'page' && isset($page) && !$page->isStatic() && !$url->notFound());
+ $blowfishHasHeadings = $blowfishIsArticle && (bool) preg_match('/content());
?>
-
+
->
+>
get('Skip to content'); ?>
@@ -126,8 +126,8 @@
// The hero (and the relocated search) appear on every paginated home page.
$heroVisible = ($WHERE_AM_I === 'home');
// Reuse the values pre-computed before (avoids calling $page->content() twice).
- $isArticlePage = $blowditIsArticle;
- $hasHeadings = $blowditHasHeadings;
+ $isArticlePage = $blowfishIsArticle;
+ $hasHeadings = $blowfishHasHeadings;
?>
@@ -179,7 +179,7 @@
// jQuery and Bootstrap's JS bundle are intentionally NOT loaded: every
// interactive piece in this theme is vanilla JS (theme picker, tabs,
// carousel, lightbox), and the only Bootstrap-JS feature used — the
- // mobile navbar collapse — is reimplemented in blowdit.js. This drops
+ // mobile navbar collapse — is reimplemented in blowfish.js. This drops
// ~115 KB. Re-add Theme::jquery() / Theme::jsBootstrap() here if you
// install a plugin that depends on them.
?>
@@ -190,11 +190,11 @@
diff --git a/js/blowdit.js b/js/blowfish.js
similarity index 98%
rename from js/blowdit.js
rename to js/blowfish.js
index 3e2d05d..51f872c 100644
--- a/js/blowdit.js
+++ b/js/blowfish.js
@@ -1,5 +1,5 @@
/* ============================================================
- BLOWDIT — theme behaviour
+ BLOWFISH — theme behaviour
Deferred, cacheable bundle of all client-side enhancements.
Loaded with ';
} catch (\Throwable $e) {}
diff --git a/php/head.php b/php/head.php
index 9a4adfa..7e4e79b 100644
--- a/php/head.php
+++ b/php/head.php
@@ -5,23 +5,23 @@
'#1f1f1f',
'dark' => '#1f1f1f',
'nord' => '#3b4252',
'dracula' => '#343746',
'catppuccin' => '#313244',
);
- $blowditChrome = isset($blowditChromeColors[$blowditTheme]) ? $blowditChromeColors[$blowditTheme] : '#1f1f1f';
+ $blowfishChrome = isset($blowfishChromeColors[$blowfishTheme]) ? $blowfishChromeColors[$blowfishTheme] : '#1f1f1f';
?>
-
+
-coverImage()): ?>
+coverImage()): ?>
-
+
@@ -32,7 +32,7 @@
Sets data-theme AND paints the html background/color-scheme inline, so the
very first frame (before style.css loads) already uses the theme colour. -->
@@ -64,13 +64,13 @@
getValue($field);
if (!$raw) { return ''; }
if (class_exists('Date')) {
@@ -86,7 +86,7 @@
$ogDesc = $page->description();
$ogUrl = $page->permalink(true);
$ogImage = $page->coverImage(true);
- $ogPublishedIso = blowdit_iso_date($page, 'date');
+ $ogPublishedIso = blowfish_iso_date($page, 'date');
} else {
$ogTitle = $site->title();
$ogDesc = '';
@@ -105,52 +105,52 @@
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
+
-
-
+
+
-
+
'
. json_encode($doc, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
. '' . "\n";
};
// Social profile URLs, used as schema.org "sameAs".
- $blowditSameAs = array();
+ $blowfishSameAs = array();
foreach (Theme::socialNetworks() as $snKey => $snLabel) {
$snUrl = $site->{$snKey}();
- if (!empty($snUrl)) { $blowditSameAs[] = $snUrl; }
+ if (!empty($snUrl)) { $blowfishSameAs[] = $snUrl; }
}
// WebSite (every page) — declares the site + a sitelinks search box.
$siteLd = array(
'@context' => 'https://schema.org',
'@type' => 'WebSite',
- 'name' => blowdit_plain($site->title()),
+ 'name' => blowfish_plain($site->title()),
'url' => Theme::siteUrl(),
);
$siteDesc = $site->description() ?: $site->slogan();
- if ($siteDesc) { $siteLd['description'] = blowdit_plain($siteDesc); }
+ if ($siteDesc) { $siteLd['description'] = blowfish_plain($siteDesc); }
$siteLd['potentialAction'] = array(
'@type' => 'SearchAction',
'target' => array(
@@ -159,7 +159,7 @@
),
'query-input' => 'required name=search_term_string',
);
- $blowditEmitLd($siteLd);
+ $blowfishEmitLd($siteLd);
// BlogPosting + BreadcrumbList — articles only.
if ($ogIsArticle) {
@@ -168,7 +168,7 @@
$ldPublished = $ogPublishedIso;
// dateModified: fall back to published date when not tracked.
- $ldModified = blowdit_iso_date($page, 'dateModified');
+ $ldModified = blowfish_iso_date($page, 'dateModified');
if (!$ldModified) { $ldModified = $ldPublished; }
// timeRequired as an ISO 8601 duration (e.g. "PT8M") parsed from readingTime().
@@ -179,23 +179,23 @@
$author = array(
'@type' => 'Person',
- 'name' => blowdit_plain($page->username()),
+ 'name' => blowfish_plain($page->username()),
'url' => Theme::siteUrl(),
);
- if (!empty($blowditSameAs)) { $author['sameAs'] = $blowditSameAs; }
+ if (!empty($blowfishSameAs)) { $author['sameAs'] = $blowfishSameAs; }
$jsonLd = array(
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
- 'headline' => blowdit_plain($page->title()),
- 'description' => blowdit_plain($ldDescription),
+ 'headline' => blowfish_plain($page->title()),
+ 'description' => blowfish_plain($ldDescription),
'image' => $ldImage,
'datePublished' => $ldPublished,
'dateModified' => $ldModified,
'author' => $author,
'publisher' => array(
'@type' => 'Organization',
- 'name' => blowdit_plain($site->title()),
+ 'name' => blowfish_plain($site->title()),
'logo' => array(
'@type' => 'ImageObject',
'url' => DOMAIN_THEME . 'img/favicon.png',
@@ -208,25 +208,25 @@
'wordCount' => str_word_count(strip_tags($page->content())),
);
if ($ldTimeRequired) { $jsonLd['timeRequired'] = $ldTimeRequired; }
- $blowditEmitLd($jsonLd);
+ $blowfishEmitLd($jsonLd);
// Breadcrumb: Home > [Category] > Article
$crumbs = array(array(
'@type' => 'ListItem', 'position' => 1,
- 'name' => blowdit_plain($site->title()), 'item' => Theme::siteUrl(),
+ 'name' => blowfish_plain($site->title()), 'item' => Theme::siteUrl(),
));
$pos = 2;
if ($page->categoryKey()) {
$crumbs[] = array(
'@type' => 'ListItem', 'position' => $pos++,
- 'name' => blowdit_plain($page->category()), 'item' => $page->categoryPermalink(),
+ 'name' => blowfish_plain($page->category()), 'item' => $page->categoryPermalink(),
);
}
$crumbs[] = array(
'@type' => 'ListItem', 'position' => $pos,
- 'name' => blowdit_plain($page->title()), 'item' => $page->permalink(true),
+ 'name' => blowfish_plain($page->title()), 'item' => $page->permalink(true),
);
- $blowditEmitLd(array(
+ $blowfishEmitLd(array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $crumbs,
@@ -235,7 +235,7 @@
?>
-
+
@@ -252,7 +252,7 @@
+ (see php/icons.php + blowfish.js), saving ~120 KB font + ~80 KB CSS. -->
-
- title()); ?>
+
+ title()); ?>
slogan()) : ?>
- slogan()); ?>
+ slogan()); ?>
description()) : ?>
- description()); ?>
+ description()); ?>
@@ -59,12 +59,12 @@
@@ -75,11 +75,11 @@
@@ -120,7 +120,7 @@
-
+
@@ -140,7 +140,7 @@
-
+
diff --git a/php/icons.php b/php/icons.php
index f18cbb3..862064d 100644
--- a/php/icons.php
+++ b/php/icons.php
@@ -7,10 +7,10 @@
* font-size rules (.navbar .nav-link .bd-icon, .metadata .bd-icon, …) scale it.
*
* Theme-picker icons (sun/moon/snow/droplet/cup/half) are duplicated in
- * blowdit.js so the toggle can swap them client-side; keep the two in sync.
+ * blowfish.js so the toggle can swap them client-side; keep the two in sync.
*/
-if (!function_exists('blowdit_icon')) {
- function blowdit_icon($name) {
+if (!function_exists('blowfish_icon')) {
+ function blowfish_icon($name) {
static $paths = array(
// --- UI ---
'calendar' => ' ',
@@ -23,7 +23,7 @@ if (!function_exists('blowdit_icon')) {
'link' => ' ',
'search' => ' ',
'share' => ' ',
- // --- Theme picker (mirror in blowdit.js) ---
+ // --- Theme picker (mirror in blowfish.js) ---
'sun' => ' ',
'moon' => ' ',
'snow' => ' ',
@@ -39,24 +39,24 @@ if (!function_exists('blowdit_icon')) {
// Bludit already HTML-escapes titles/names on save (Sanitize::html), so a
// plain htmlspecialchars() in the theme double-encodes ("&" -> "&",
- // shown as "&"). blowdit_text() decodes any existing entities first, then
+ // shown as "&"). blowfish_text() decodes any existing entities first, then
// encodes exactly once — correct whether the value is pre-encoded or raw, and
- // always output-safe. blowdit_plain() returns decoded text (for JSON-LD etc.).
- function blowdit_plain($s) {
+ // always output-safe. blowfish_plain() returns decoded text (for JSON-LD etc.).
+ function blowfish_plain($s) {
return html_entity_decode((string) $s, ENT_QUOTES, 'UTF-8');
}
- function blowdit_text($s) {
- return htmlspecialchars(blowdit_plain($s), ENT_QUOTES, 'UTF-8');
+ function blowfish_text($s) {
+ return htmlspecialchars(blowfish_plain($s), ENT_QUOTES, 'UTF-8');
}
// Theme asset URL with an mtime cache-buster (so edited images/SVGs refetch).
- function blowdit_asset($rel) {
+ function blowfish_asset($rel) {
$v = @filemtime(THEME_DIR . $rel);
return DOMAIN_THEME . $rel . ($v ? '?v=' . $v : '');
}
// Map a theme key to its toggle icon name.
- function blowdit_theme_icon($theme) {
+ function blowfish_theme_icon($theme) {
$map = array(
'light' => 'sun',
'dark' => 'moon',
diff --git a/php/navbar.php b/php/navbar.php
index ead0ff4..1fe3d57 100644
--- a/php/navbar.php
+++ b/php/navbar.php
@@ -1,7 +1,7 @@