lazy-loading done
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled

This commit is contained in:
2026-02-10 15:59:03 -05:00
parent 7bd51837de
commit ac3de3e142
24 changed files with 923 additions and 16 deletions

2
site/.gitignore vendored
View File

@@ -22,3 +22,5 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
nul

View File

@@ -395,6 +395,12 @@ textarea:focus-visible {
background: rgba(255, 255, 255, 0.06);
}
.card-media .img-shimmer-wrap {
width: 100%;
height: 180px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.card-media img {
width: 100%;
height: 180px;
@@ -410,6 +416,63 @@ textarea:focus-visible {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
/* --- Image shimmer / lazy-load placeholder --- */
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.img-shimmer-wrap {
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.08);
}
.img-shimmer-wrap::before {
content: "";
position: absolute;
inset: 0;
z-index: 1;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.12) 35%,
rgba(255, 255, 255, 0.22) 50%,
rgba(255, 255, 255, 0.12) 65%,
transparent 100%
);
animation: shimmer 1.6s ease-in-out infinite;
}
.img-shimmer-wrap img {
position: relative;
z-index: 2;
}
.img-shimmer-wrap img.img-loading {
opacity: 0 !important;
}
.img-shimmer-wrap img.img-loaded {
opacity: 1;
transition: opacity 250ms ease;
}
.img-shimmer-wrap.img-error::before {
animation: none;
}
.img-shimmer-wrap.img-loaded::before {
display: none;
}
/* --- End image shimmer --- */
.card-body {
display: flex;
flex: 1;
@@ -505,7 +568,8 @@ textarea:focus-visible {
grid-template-columns: 1fr;
}
.card-media img,
.card-placeholder {
.card-placeholder,
.card-media .img-shimmer-wrap {
height: 200px;
}
}

View File

@@ -5,7 +5,7 @@
*/
// Bump this value on deploy to invalidate caches.
const CACHE_VERSION = "v1";
const CACHE_VERSION = "v2";
const CACHE_SHELL = `shell-${CACHE_VERSION}`;
const CACHE_PAGES = `pages-${CACHE_VERSION}`;
@@ -123,6 +123,25 @@ self.addEventListener("fetch", (event) => {
return;
}
// Network-first for global CSS to avoid serving stale styling after deploy.
// (The SW already caches styles, but network-first prevents a "stuck" old CSS experience.)
if (url.origin === self.location.origin && url.pathname === "/styles/global.css") {
event.respondWith(
(async () => {
try {
const fresh = await fetch(request);
await cachePutSafe(CACHE_SHELL, request, fresh.clone());
return fresh;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
return fetch(request);
}
})(),
);
return;
}
// Stale-while-revalidate for styles/scripts/fonts from same-origin.
if (
url.origin === self.location.origin &&

View File

@@ -43,7 +43,13 @@ const summaryText = truncate(summary || "", 180);
{...(linkAttrs || {})}
>
<div class="card-media">
{imageUrl ? <img src={imageUrl} alt="" loading="lazy" /> : <div class="card-placeholder" />}
{imageUrl ? (
<div class="img-shimmer-wrap">
<img src={imageUrl} alt="" loading="lazy" class="img-loading" />
</div>
) : (
<div class="card-placeholder" />
)}
</div>
<div class="card-body">

View File

@@ -204,5 +204,30 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
mql.addEventListener("change", () => setOpen(!mql.matches));
})();
</script>
<script is:inline>
(() => {
function reveal(img) {
img.classList.remove("img-loading");
img.classList.add("img-loaded");
var wrap = img.closest(".img-shimmer-wrap");
if (wrap) wrap.classList.add("img-loaded");
}
var imgs = document.querySelectorAll("img.img-loading");
for (var i = 0; i < imgs.length; i++) {
(function(img) {
if (img.complete && img.naturalWidth > 0) {
reveal(img);
return;
}
img.addEventListener("load", function() { reveal(img); });
img.addEventListener("error", function() {
var wrap = img.closest(".img-shimmer-wrap");
if (wrap) wrap.classList.add("img-error");
});
})(imgs[i]);
}
})();
</script>
</body>
</html>

View File

@@ -45,12 +45,15 @@ const metaDescription = (page.excerpt || "").slice(0, 160);
</a>
</div>
{page.featuredImageUrl ? (
<img
src={page.featuredImageUrl}
alt=""
loading="lazy"
style="width: 100%; max-height: 420px; object-fit: cover; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.12);"
/>
<div class="img-shimmer-wrap" style="width: 100%; max-height: 420px; border-radius: 16px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.12);">
<img
src={page.featuredImageUrl}
alt=""
loading="lazy"
class="img-loading"
style="width: 100%; max-height: 420px; object-fit: cover; display: block;"
/>
</div>
) : null}
<div class="prose" set:html={page.contentHtml} />
</section>

View File

@@ -48,12 +48,15 @@ const metaDescription = (post.excerpt || "").slice(0, 160);
{new Date(post.publishedAt).toLocaleDateString()}
</p>
{post.featuredImageUrl ? (
<img
src={post.featuredImageUrl}
alt=""
loading="lazy"
style="width: 100%; max-height: 420px; object-fit: cover; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.12);"
/>
<div class="img-shimmer-wrap" style="width: 100%; max-height: 420px; border-radius: 16px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.12);">
<img
src={post.featuredImageUrl}
alt=""
loading="lazy"
class="img-loading"
style="width: 100%; max-height: 420px; object-fit: cover; display: block;"
/>
</div>
) : null}
<div class="prose" set:html={post.contentHtml} />
</section>