perf: address PageSpeed Insights issues (CLS, contrast, touch targets, JS minification)

- Reserve sidebar min-height on desktop to prevent CLS from Alpine.js hydration
- Defer lite-yt-embed.css with media="print" onload pattern
- Add terser JS minification in eleventy.after build hook
- Increase touch target sizing for category pills, facepile avatars, nav items
- Fix text-surface-400 contrast ratio (3.05:1 → 6.23:1) across 20 instances

Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
This commit is contained in:
Ricardo
2026-03-07 20:13:45 +01:00
parent 2c60bc2580
commit 6ff40c8317
14 changed files with 59 additions and 25 deletions

View File

@@ -27,7 +27,7 @@
<div class="space-y-2">
{% for artist in topArtists | head(5) %}
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<span class="w-6 h-6 flex items-center justify-center text-sm font-bold text-surface-400 bg-surface-100 dark:bg-surface-700 rounded-full">{{ loop.index }}</span>
<span class="w-6 h-6 flex items-center justify-center text-sm font-bold text-surface-600 dark:text-surface-400 bg-surface-100 dark:bg-surface-700 rounded-full">{{ loop.index }}</span>
<span class="flex-1 font-medium text-surface-900 dark:text-surface-100">{{ artist.name }}</span>
<span class="text-sm text-surface-600 dark:text-surface-400">{{ artist.playCount }} plays</span>
</div>
@@ -54,7 +54,7 @@
{% endif %}
<p class="text-sm font-medium text-surface-900 dark:text-surface-100 truncate">{{ album.title }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ album.artist }}</p>
<p class="text-xs text-surface-400">{{ album.playCount }} plays</p>
<p class="text-xs text-surface-600 dark:text-surface-400">{{ album.playCount }} plays</p>
</div>
{% endfor %}
</div>

View File

@@ -60,7 +60,7 @@
</time>
</div>
{{ likedUrl | unfurlCard | safe }}
<a class="u-like-of text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
<a class="u-like-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
{% if post.templateContent %}
@@ -93,7 +93,7 @@
</h3>
{% endif %}
{{ bookmarkedUrl | unfurlCard | safe }}
<a class="u-bookmark-of text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
<a class="u-bookmark-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
{% if post.templateContent %}
@@ -121,7 +121,7 @@
</time>
</div>
{{ repostedUrl | unfurlCard | safe }}
<a class="u-repost-of text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
<a class="u-repost-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
{% if post.templateContent %}
@@ -149,7 +149,7 @@
</time>
</div>
{{ replyToUrl | unfurlCard | safe }}
<a class="u-in-reply-to text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
<a class="u-in-reply-to text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
{{ replyToUrl }}
</a>
{% if post.templateContent %}

View File

@@ -57,7 +57,7 @@
</time>
</div>
{{ likedUrl | unfurlCard | safe }}
<a class="u-like-of text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
<a class="u-like-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
{% if post.templateContent %}
@@ -90,7 +90,7 @@
</h3>
{% endif %}
{{ bookmarkedUrl | unfurlCard | safe }}
<a class="u-bookmark-of text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
<a class="u-bookmark-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
{% if post.templateContent %}
@@ -118,7 +118,7 @@
</time>
</div>
{{ repostedUrl | unfurlCard | safe }}
<a class="u-repost-of text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
<a class="u-repost-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
{% if post.templateContent %}
@@ -146,7 +146,7 @@
</time>
</div>
{{ replyToUrl | unfurlCard | safe }}
<a class="u-in-reply-to text-xs text-surface-400 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
<a class="u-in-reply-to text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
{{ replyToUrl }}
</a>
{% if post.templateContent %}

View File

@@ -67,7 +67,8 @@
var _pfQueue = [];
function initPagefind(sel, opts) { _pfQueue.push([sel, opts]); }
</script>
<link rel="stylesheet" href="/css/lite-yt-embed.css?v={{ '/css/lite-yt-embed.css' | hash }}">
<link rel="stylesheet" href="/css/lite-yt-embed.css?v={{ '/css/lite-yt-embed.css' | hash }}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/lite-yt-embed.css?v={{ '/css/lite-yt-embed.css' | hash }}"></noscript>
<script src="/js/vendor/lite-yt-embed.js?v={{ '/js/vendor/lite-yt-embed.js' | hash }}" defer></script>
{# Alpine.js components — MUST load before Alpine core (Alpine.data() registration via alpine:init) #}
<script src="/js/comments.js?v={{ '/js/comments.js' | hash }}" defer></script>

View File

@@ -67,7 +67,7 @@ withSidebar: false
class="text-xs px-2 py-0.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400"
x-text="commit.repoName"></a>
<span class="text-xs text-surface-600 dark:text-surface-400 font-mono" x-text="formatDate(commit.date)"></span>
<span class="text-xs text-surface-400" x-text="'by ' + commit.author"></span>
<span class="text-xs text-surface-600 dark:text-surface-400" x-text="'by ' + commit.author"></span>
</div>
<template x-if="commit.body">
<details class="mt-2">
@@ -97,7 +97,7 @@ withSidebar: false
</div>
{# Summary #}
<div x-show="commits.length > 0" class="mt-6 text-center text-xs text-surface-400">
<div x-show="commits.length > 0" class="mt-6 text-center text-xs text-surface-600 dark:text-surface-400">
<span x-text="commits.length + ' commits'"></span>
<span x-show="currentDays !== 'all'"> from the last <span x-text="currentDays"></span> days</span>
<span x-show="currentDays === 'all'"> (all time)</span>

View File

@@ -45,6 +45,8 @@ main.container{padding-top:1.5rem;padding-bottom:1.5rem}
.layout-with-sidebar{display:grid;grid-template-columns:1fr;gap:1.5rem}
@media(min-width:1024px){.layout-with-sidebar{grid-template-columns:2fr 1fr;gap:2rem}}
.main-content{min-width:0;overflow-x:hidden}
/* Reserve sidebar space on desktop to prevent CLS when Alpine.js hydrates collapsible widgets */
@media(min-width:1024px){.sidebar{min-height:600px}}
/* Basic typography — prevent FOUT */
h1,h2,h3,h4{margin:0;line-height:1.25}

View File

@@ -187,7 +187,7 @@
}
.nav-dropdown-menu a {
@apply block px-4 py-2 text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-900 dark:hover:text-surface-100 no-underline;
@apply block px-4 py-2.5 text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-900 dark:hover:text-surface-100 no-underline;
}
.nav-dropdown-divider {
@@ -224,7 +224,7 @@
}
.mobile-nav-submenu a {
@apply pl-8 py-2 text-sm border-b-0;
@apply pl-8 py-3 text-sm border-b-0;
}
.mobile-nav-divider {
@@ -332,7 +332,7 @@
/* Category tags (post metadata pills) */
.p-category {
@apply inline-block px-2 py-0.5 text-xs bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 rounded border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors;
@apply inline-block px-3 py-1.5 text-xs bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 rounded border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors;
}
/* Inline hashtags in post content — styled as subtle links, not pills */
@@ -353,7 +353,7 @@
}
.facepile-avatar img {
@apply w-8 h-8 rounded-full;
@apply w-10 h-10 rounded-full;
}
/* GitHub components */

View File

@@ -7,6 +7,7 @@ import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor";
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
import { minify } from "html-minifier-terser";
import { minify as minifyJS } from "terser";
import registerUnfurlShortcode, { getCachedCard, prefetchUrl } from "./lib/unfurl-shortcode.js";
import matter from "gray-matter";
import { createHash, createHmac } from "crypto";
@@ -1267,6 +1268,36 @@ export default function (eleventyConfig) {
}
// JS minification — minify source JS files in output (skip vendor, already-minified)
if (runMode === "build" && !incremental) {
const jsOutputDir = directories?.output || dir.output;
const jsDir = resolve(jsOutputDir, "js");
if (existsSync(jsDir)) {
let jsMinified = 0;
let jsSaved = 0;
for (const file of readdirSync(jsDir).filter(f => f.endsWith(".js") && !f.endsWith(".min.js"))) {
const filePath = resolve(jsDir, file);
try {
const src = readFileSync(filePath, "utf-8");
const result = await minifyJS(src, { compress: true, mangle: true });
if (result.code) {
const saved = src.length - result.code.length;
if (saved > 0) {
writeFileSync(filePath, result.code);
jsSaved += saved;
jsMinified++;
}
}
} catch (err) {
console.error(`[js-minify] Failed to minify ${file}:`, err.message);
}
}
if (jsMinified > 0) {
console.log(`[js-minify] Minified ${jsMinified} JS files, saved ${(jsSaved / 1024).toFixed(1)} KiB`);
}
}
}
// Syndication webhook — trigger after incremental rebuilds (new posts are now live)
// Cuts syndication latency from ~2 min (poller) to ~5 sec (immediate trigger)
if (incremental) {

View File

@@ -220,7 +220,7 @@ withSidebar: true
<div class="text-right flex-shrink-0">
<span class="text-xs text-surface-600 dark:text-surface-400">{{ listening.relativeTime }}</span>
{% if listening.duration %}
<span class="text-xs text-surface-400 block">{{ listening.duration }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 block">{{ listening.duration }}</span>
{% endif %}
</div>
</div>

View File

@@ -25,7 +25,7 @@ permalink: /interactions/
role="tab" id="interactions-tab-outbound" aria-controls="interactions-panel-outbound"
class="px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors">
My Activity
<span class="ml-1 text-xs text-surface-400">(outbound)</span>
<span class="ml-1 text-xs text-surface-600 dark:text-surface-400">(outbound)</span>
</button>
<button
@click="activeTab = 'inbound'"
@@ -34,7 +34,7 @@ permalink: /interactions/
role="tab" id="interactions-tab-inbound" aria-controls="interactions-panel-inbound"
class="px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors">
Received
<span class="ml-1 text-xs text-surface-400">(inbound)</span>
<span class="ml-1 text-xs text-surface-600 dark:text-surface-400">(inbound)</span>
<span x-show="totalInbound > 0" x-text="totalInbound" class="ml-1 px-1.5 py-0.5 text-xs bg-rose-100 dark:bg-rose-900 text-rose-700 dark:text-rose-300 rounded-full"></span>
</button>
</div>

View File

@@ -110,7 +110,7 @@ export function renderCard(url, metadata) {
<div class="flex-1 p-3 sm:p-4 min-w-0">
<p class="font-semibold text-sm sm:text-base text-surface-900 dark:text-surface-100 truncate m-0">${escapeHtml(title)}</p>
${desc ? `<p class="text-xs sm:text-sm text-surface-600 dark:text-surface-400 mt-1 m-0 line-clamp-2">${escapeHtml(desc)}</p>` : ""}
<p class="text-xs text-surface-400 dark:text-surface-400 mt-2 m-0">${faviconHtml}${escapeHtml(domain)}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2 m-0">${faviconHtml}${escapeHtml(domain)}</p>
</div>
${imgHtml}
</a>

View File

@@ -87,7 +87,7 @@ permalink: /podroll/
<span x-text="episode.podcast?.title || 'Unknown'"></span>
</a>
<time class="font-mono text-sm" :datetime="episode.published" x-text="formatDate(episode.published)"></time>
<span x-show="episode.enclosure" class="text-surface-400">
<span x-show="episode.enclosure" class="text-surface-600 dark:text-surface-400">
<svg class="w-3 h-3 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15.536a5 5 0 001.414 1.414m2.828-9.9a9 9 0 012.828-2.828"/>
</svg>

View File

@@ -102,7 +102,7 @@ permalink: /readlater/
></span>
<time class="font-mono text-sm" :datetime="item.savedAt" x-text="formatDate(item.savedAt)"></time>
</div>
<p class="text-xs text-surface-400 mt-1 truncate" x-text="item.url"></p>
<p class="text-xs text-surface-600 dark:text-surface-400 mt-1 truncate" x-text="item.url"></p>
</div>
</div>

View File

@@ -58,7 +58,7 @@ pagefindIgnore: true
<div>{{ legacyUrl }}</div>
{% endfor %}
{% else %}
<span class="text-surface-400">-</span>
<span class="text-surface-600 dark:text-surface-400">-</span>
{% endif %}
</td>
<td class="p-2 text-right">
@@ -67,7 +67,7 @@ pagefindIgnore: true
{{ allMentions.length }}
</span>
{% else %}
<span class="text-surface-400">0</span>
<span class="text-surface-600 dark:text-surface-400">0</span>
{% endif %}
</td>
</tr>