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; }
|
.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
|
RESPONSIVE
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|||||||
@@ -153,11 +153,12 @@
|
|||||||
|
|
||||||
<!-- Javascript -->
|
<!-- Javascript -->
|
||||||
<?php
|
<?php
|
||||||
// Include Jquery file from Bludit Core
|
// jQuery and Bootstrap's JS bundle are intentionally NOT loaded: every
|
||||||
echo Theme::jquery();
|
// interactive piece in this theme is vanilla JS (theme picker, tabs,
|
||||||
|
// carousel, lightbox), and the only Bootstrap-JS feature used — the
|
||||||
// Include javascript Bootstrap file from Bludit Core
|
// mobile navbar collapse — is reimplemented in blowdit.js. This drops
|
||||||
echo Theme::jsBootstrap();
|
// ~115 KB. Re-add Theme::jquery() / Theme::jsBootstrap() here if you
|
||||||
|
// install a plugin that depends on them.
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Theme behaviour: deferred, cached external bundle.
|
<!-- Theme behaviour: deferred, cached external bundle.
|
||||||
|
|||||||
@@ -25,6 +25,33 @@
|
|||||||
imageViewer: I18N.imageViewer || 'Image viewer'
|
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
|
Theme picker
|
||||||
-------------------------------------------------------- */
|
-------------------------------------------------------- */
|
||||||
@@ -619,6 +646,51 @@
|
|||||||
Array.prototype.forEach.call(pres, enhance);
|
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.
|
Reading progress bar — tracks scroll position over the page.
|
||||||
Only shown when there's article content to read.
|
Only shown when there's article content to read.
|
||||||
|
|||||||
+4
-2
@@ -160,10 +160,12 @@
|
|||||||
<!-- Include Favicon -->
|
<!-- Include Favicon -->
|
||||||
<?php echo Theme::favicon('img/favicon.png'); ?>
|
<?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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 -->
|
<!-- Include Bootstrap CSS file bootstrap.css -->
|
||||||
<?php echo Theme::cssBootstrap(); ?>
|
<?php echo Theme::cssBootstrap(); ?>
|
||||||
|
|||||||
+11
-2
@@ -7,7 +7,7 @@
|
|||||||
<!-- Cover image -->
|
<!-- Cover image -->
|
||||||
<?php if ($page->coverImage()): ?>
|
<?php if ($page->coverImage()): ?>
|
||||||
<div class="cover-image-wrapper">
|
<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>
|
</div>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
@@ -25,7 +25,16 @@
|
|||||||
|
|
||||||
<!-- Full content -->
|
<!-- Full content -->
|
||||||
<div class="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>
|
</div>
|
||||||
|
|
||||||
<!-- Tags and Category -->
|
<!-- Tags and Category -->
|
||||||
|
|||||||
Reference in new issue
Block a user