feat: enhance performance and accessibility with non-blocking font loading, lazy-loading images, and mobile navbar toggle

This commit is contained in:
interlope committed 2026-06-01 23:48:36 +02:00
1 parent c0e40c7362
commit 57bb59e768
6 files changed
+220 -9

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
# ============================================================
# BLOWDIT — static asset delivery (Apache)
# Scoped to the theme directory: only affects files served from
# this folder (css/js/img/fonts). Bludit's routing lives in the
# site-root .htaccess and is untouched.
# No-op on nginx / PHP's built-in server.
# ============================================================
# ---- Compression ----
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/css application/javascript application/json image/svg+xml application/xml
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/css application/javascript application/json image/svg+xml application/xml text/plain
</IfModule>
# ---- Far-future caching ----
# Safe because every asset is requested with a ?v=<mtime> query string
# (see head.php / index.php), so a changed file gets a fresh URL.
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|png|jpe?g|gif|webp|svg|ico|woff2?)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
+89
View File
@@ -1726,6 +1726,95 @@ footer.footer-modern a:hover {
.back-to-top.is-visible { transform: none; }
}
/* ========================================
SYNTAX HIGHLIGHTING (highlight.js)
Use only the theme's token colours; keep the code block's own
background/padding (set on pre / --code-bg) rather than hljs's.
======================================== */
pre code.hljs {
padding: 0 !important;
background: transparent !important;
}
/* ========================================
KEYBOARD FOCUS — visible ring for keyboard users only
======================================== */
a:focus-visible,
button:focus-visible,
.swatch:focus-visible,
.tab-btn:focus-visible,
.carousel-arrow:focus-visible,
.carousel-dot:focus-visible,
.lightbox-arrow:focus-visible,
.code-copy:focus-visible,
.back-to-top:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 2px;
}
/* Don't show the ring for mouse clicks */
a:focus:not(:focus-visible),
button:focus:not(:focus-visible) {
outline: none;
}
/* ========================================
PRINT — clean, ink-friendly article output
======================================== */
@media print {
.navbar-modern,
.footer,
.sidebar-container,
.toc-sidebar,
.skip-link,
.reading-progress,
.back-to-top,
.code-copy,
.code-copy-toast,
.theme-picker-wrap {
display: none !important;
}
html, body {
background: #ffffff !important;
color: #000000 !important;
}
/* Use the full page width once the chrome is gone */
.container,
.container-wide {
max-width: 100% !important;
width: 100% !important;
}
.card-modern {
box-shadow: none !important;
border: none !important;
}
a {
color: #000000 !important;
text-decoration: underline;
}
/* Keep code blocks readable on paper */
pre, code {
background: #f5f5f5 !important;
color: #000000 !important;
border-color: #cccccc !important;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
/* Avoid awkward page breaks */
h1, h2, h3, h4 { page-break-after: avoid; }
pre, blockquote, table, figure, img { page-break-inside: avoid; }
}
/* ========================================
RESPONSIVE
======================================== */
+6 -5
View File
@@ -153,11 +153,12 @@
<!-- Javascript -->
<?php
// Include Jquery file from Bludit Core
echo Theme::jquery();
// Include javascript Bootstrap file from Bludit Core
echo Theme::jsBootstrap();
// 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
// ~115 KB. Re-add Theme::jquery() / Theme::jsBootstrap() here if you
// install a plugin that depends on them.
?>
<!-- Theme behaviour: deferred, cached external bundle.
+72
View File
@@ -25,6 +25,33 @@
imageViewer: I18N.imageViewer || 'Image viewer'
};
/* --------------------------------------------------------
Mobile navbar toggle (replaces Bootstrap's collapse JS)
Toggles the .show class that Bootstrap's CSS already styles.
-------------------------------------------------------- */
(function () {
var toggler = document.querySelector('.navbar-toggler');
var target = document.getElementById('navbarResponsive');
if (!toggler || !target) return;
function setOpen(open) {
target.classList.toggle('show', open);
toggler.classList.toggle('collapsed', !open);
toggler.setAttribute('aria-expanded', open ? 'true' : 'false');
}
toggler.addEventListener('click', function () {
setOpen(!target.classList.contains('show'));
});
// Collapse the menu after tapping a link (mobile)
Array.prototype.forEach.call(target.querySelectorAll('.nav-link'), function (link) {
link.addEventListener('click', function () {
if (target.classList.contains('show')) setOpen(false);
});
});
})();
/* --------------------------------------------------------
Theme picker
-------------------------------------------------------- */
@@ -619,6 +646,51 @@
Array.prototype.forEach.call(pres, enhance);
})();
/* --------------------------------------------------------
Syntax highlighting (highlight.js, from CDN)
Loaded ONLY when the page actually contains code. Just the token
colours are used — backgrounds/padding stay on the theme's --code-bg
(see style.css). The colour theme swaps with data-theme.
-------------------------------------------------------- */
(function () {
var codes = document.querySelectorAll('.content pre code, .tab-panel pre code');
if (!codes.length) return;
var BASE = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/';
function makeThemeLink(name) {
var l = document.createElement('link');
l.rel = 'stylesheet';
l.href = BASE + 'styles/' + name + '.min.css';
l.crossOrigin = 'anonymous';
document.head.appendChild(l);
return l;
}
var lightCss = makeThemeLink('github');
var darkCss = makeThemeLink('github-dark');
function syncTheme() {
var isLight = (document.documentElement.getAttribute('data-theme') === 'light');
lightCss.disabled = !isLight;
darkCss.disabled = isLight;
}
syncTheme();
new MutationObserver(syncTheme).observe(document.documentElement, {
attributes: true, attributeFilter: ['data-theme']
});
var s = document.createElement('script');
s.src = BASE + 'highlight.min.js';
s.crossOrigin = 'anonymous';
s.onload = function () {
if (!window.hljs) return;
Array.prototype.forEach.call(codes, function (code) {
try { window.hljs.highlightElement(code); } catch (e) {}
});
};
document.head.appendChild(s);
})();
/* --------------------------------------------------------
Reading progress bar — tracks scroll position over the page.
Only shown when there's article content to read.
+4 -2
View File
@@ -160,10 +160,12 @@
<!-- Include Favicon -->
<?php echo Theme::favicon('img/favicon.png'); ?>
<!-- Inter typeface (Blowfish-like typography) -->
<!-- Inter typeface (Blowfish-like typography) — loaded non-render-blocking:
fetched as print media, then flipped to all once it arrives. -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"></noscript>
<!-- Include Bootstrap CSS file bootstrap.css -->
<?php echo Theme::cssBootstrap(); ?>
+11 -2
View File
@@ -7,7 +7,7 @@
<!-- Cover image -->
<?php if ($page->coverImage()): ?>
<div class="cover-image-wrapper">
<img class="card-img-top" alt="<?php echo htmlspecialchars($page->title()); ?>" src="<?php echo $page->coverImage(); ?>"/>
<img class="card-img-top" alt="<?php echo htmlspecialchars($page->title(), ENT_QUOTES, 'UTF-8'); ?>" src="<?php echo $page->coverImage(); ?>" fetchpriority="high" decoding="async"/>
</div>
<?php endif ?>
@@ -25,7 +25,16 @@
<!-- Full content -->
<div class="content">
<?php echo $page->content(); ?>
<?php
// Lazy-load + async-decode in-content images that don't already
// set a loading attribute. Operates on the rendered HTML, so
// carousel/tabs fences (still raw text inside <pre>) are untouched.
echo preg_replace(
'/<img(?![^>]*\bloading=)/i',
'<img loading="lazy" decoding="async"',
$page->content()
);
?>
</div>
<!-- Tags and Category -->