Files
unique/src/pages/grid.astro
T
rzen ed1d484ccb site: home + grid tiles use <Image> instead of raw <img>
Eliminates the byte-dedup hazard that just bit timelined and apex: every
image consumer now goes through Astro's image pipeline, so duplicate
bytes share their webp variants instead of one consumer's strategy
silently winning and dropping the original asset.

Side-benefit is significant: tiles and the home hero now ship responsive
webp variants via srcset/sizes instead of the original PNG/JPG. The
superkey hero alone drops from 192 KB to ~29 KB at the largest size.

Video branches stay raw — astro:assets is image-only and videos don't
have this bug.
2026-05-04 20:58:18 -04:00

125 lines
3.1 KiB
Plaintext

---
import { getCollection } from 'astro:content';
import { Image } from 'astro:assets';
import BaseLayout from '../layouts/BaseLayout.astro';
import Masonry from '../components/Masonry.astro';
import { heroes } from '../lib/hero';
const reviews = (await getCollection('reviews')).sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
);
const tiles = reviews.map((review) => ({
href: `/reviews/${review.id}/`,
name: review.data.name,
subtitle: review.data.subtitle,
hero: heroes[review.id],
}));
const lastUpdated = reviews[0]?.data.date;
---
<BaseLayout
title="Grid"
description="Every review on Unique, tiled in a single grid."
subheader="Every review as a tile."
lastUpdated={lastUpdated}
noindex
>
<Masonry>
{tiles.map((tile) => (
<article>
<a href={tile.href} class:list={['tile', { 'text-only': !tile.hero }]}>
{tile.hero?.kind === 'image' && (
<Image
class="media"
src={tile.hero.src}
widths={[320, 480, 640]}
sizes="(max-width: 520px) 92vw, (max-width: 880px) 45vw, 320px"
loading="lazy"
decoding="async"
alt={tile.name}
/>
)}
{tile.hero?.kind === 'video' && (
<video
class="media"
src={tile.hero.src}
autoplay
loop
muted
playsinline
preload="none"
aria-label={tile.name}
/>
)}
<div class="text">
<span class="name">{tile.name}</span>
<span class="subtitle">{tile.subtitle}</span>
</div>
</a>
</article>
))}
</Masonry>
<style>
.tile {
display: block;
border-radius: 8px;
overflow: hidden;
border: 2px solid var(--rule);
background: var(--bg);
color: inherit;
transition: box-shadow 0.18s ease, border-color 0.18s ease;
}
.tile:hover {
text-decoration: none;
border-color: color-mix(in srgb, var(--accent) 50%, var(--rule));
box-shadow: 0 0 20px -4px color-mix(in srgb, var(--accent) 30%, transparent);
}
.tile:hover .name { color: var(--accent); }
.media {
display: block;
width: 100%;
height: auto;
background: var(--rule);
}
.text {
padding: 0.85rem 1rem 1rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
background: var(--card-fill);
}
.name {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1rem;
letter-spacing: -0.01em;
color: var(--fg);
transition: color 0.18s ease;
}
.subtitle {
color: var(--muted);
font-size: 0.92rem;
line-height: 1.4;
}
.tile.text-only {
background: linear-gradient(155deg, var(--rule), transparent);
}
.tile.text-only .text {
padding: 1.5rem 1.25rem;
}
.tile.text-only .name {
font-size: 1.25rem;
margin-bottom: 0.3rem;
}
.tile.text-only .subtitle {
font-size: 1rem;
}
</style>
</BaseLayout>