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.
125 lines
3.1 KiB
Plaintext
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>
|