feat: enhance performance and accessibility with non-blocking font loading, lazy-loading images, and mobile navbar toggle
This commit is contained in:
1 parent
c0e40c7362
commit
57bb59e768
6 files changed
+220
-9
No files matched your search
@@ -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>
|
||||
@@ -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
|
||||
======================================== */
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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 -->
|
||||
|
||||
Reference in new issue
Block a user