lazy-loading done
This commit is contained in:
2
site/.gitignore
vendored
2
site/.gitignore
vendored
@@ -22,3 +22,5 @@ pnpm-debug.log*
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
||||
nul
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user