A small Astro + MDX blog for delightful, daily-use finds. The architecture separates capture from publication and supports reuse: - find/ captured items, hidden from indexes - finds/ editorial superposts referencing find slugs in a grid - reviews/ full-length writeups; can graduate from a find via fromFind - posts/ regular essays Three project-local skills under .claude/skills/ drive the pipeline: daily-finds (capture-only sweep across sources.json), build-finds (draft a thematic superpost), build-review (graduate a find). sources.json carries 47 curated feeds with per-source fetch strategy (webfetch / curl / skip) so Cloudflare-blocked sites and Reddit JSON endpoints are handled deterministically. /sources renders a 3-column card stream with a "Recent finds" cross-reference per source. Seeds: 17 reviews and one superpost referencing 5 finds.
151 lines
3.6 KiB
Plaintext
151 lines
3.6 KiB
Plaintext
---
|
|
import { getCollection } from 'astro:content';
|
|
import BaseLayout from '../layouts/BaseLayout.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],
|
|
}));
|
|
---
|
|
|
|
<BaseLayout title="Grid" wide>
|
|
<div class="intro">
|
|
<h1>Grid view</h1>
|
|
<p><a href="/">← list view</a></p>
|
|
</div>
|
|
|
|
<ul class="grid">
|
|
{
|
|
tiles.map((tile) => (
|
|
<li>
|
|
<a href={tile.href} class:list={['tile', { 'text-only': !tile.hero }]}>
|
|
{tile.hero?.kind === 'image' && (
|
|
<img
|
|
class="media"
|
|
src={tile.hero.src.src}
|
|
width={tile.hero.src.width}
|
|
height={tile.hero.src.height}
|
|
loading="lazy"
|
|
decoding="async"
|
|
alt=""
|
|
/>
|
|
)}
|
|
{tile.hero?.kind === 'video' && (
|
|
<video
|
|
class="media"
|
|
src={tile.hero.src}
|
|
autoplay
|
|
loop
|
|
muted
|
|
playsinline
|
|
preload="metadata"
|
|
/>
|
|
)}
|
|
<div class="text">
|
|
<span class="name">{tile.name}</span>
|
|
<span class="subtitle">{tile.subtitle}</span>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
))
|
|
}
|
|
</ul>
|
|
|
|
<style>
|
|
.intro {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.intro h1 { margin: 0; font-size: 1.5rem; }
|
|
.intro p { margin: 0; font-size: 0.9rem; color: var(--muted); }
|
|
|
|
.grid {
|
|
column-count: 3;
|
|
column-gap: 1rem;
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
@media (max-width: 880px) {
|
|
.grid { column-count: 2; }
|
|
}
|
|
@media (max-width: 520px) {
|
|
.grid { column-count: 1; }
|
|
}
|
|
|
|
.grid > li {
|
|
break-inside: avoid;
|
|
display: block;
|
|
margin: 0 0 1rem;
|
|
}
|
|
|
|
.tile {
|
|
display: block;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--rule);
|
|
background: var(--bg);
|
|
color: inherit;
|
|
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
|
}
|
|
.tile:hover {
|
|
text-decoration: none;
|
|
transform: translateY(-2px);
|
|
border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
|
|
box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.18);
|
|
}
|
|
.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;
|
|
}
|
|
.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>
|