site: media + colocation — chunk 1/4 (code, configs, docs)

Code, configs, README, CHANGELOG, sources/log files, .claude.
Subsequent chunks land the renamed media files.
This commit is contained in:
2026-05-04 17:54:14 -04:00
parent e873460465
commit 5dcd99361f
25 changed files with 1138 additions and 243 deletions
+47 -29
View File
@@ -1,9 +1,12 @@
---
import { getCollection, render } from 'astro:content';
import { Image } from 'astro:assets';
import BaseLayout from '../../layouts/BaseLayout.astro';
import SourceBadge from '../../components/SourceBadge.astro';
import Backlinks from '../../components/Backlinks.astro';
import { findsReferencing } from '../../lib/backlinks';
import { findHero } from '../../lib/find-hero';
import EditAction from '../../components/EditAction.astro';
import {
resolveExternalLink,
AFFILIATE_REL,
@@ -22,12 +25,7 @@ export async function getStaticPaths() {
const { item } = Astro.props;
const { Content } = await render(item);
const featuringPosts = await findsReferencing(item.id);
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const hero = findHero(item.id);
const linkLabel =
item.data.linkText ?? new URL(item.data.link).hostname.replace(/^www\./, '');
@@ -72,6 +70,10 @@ const breadcrumbs = buildBreadcrumbs([
<BaseLayout
title={item.data.name}
description={description}
subheader={item.data.name}
subheaderSubtitle={item.data.subtitle}
subheaderVariant="headline"
lastUpdated={item.data.date}
ogType="article"
ogImage={ogImage}
canonicalOverride={canonicalOverride}
@@ -80,18 +82,22 @@ const breadcrumbs = buildBreadcrumbs([
jsonLd={[productSchema, breadcrumbs]}
>
<article>
<p class="source-line"><SourceBadge source={item.data.source} /></p>
<header class="find-header">
<h1>{item.data.name}</h1>
{item.data.subtitle && <p class="subtitle">{item.data.subtitle}</p>}
<p class="meta">
<time datetime={item.data.date.toISOString()}>
{fmt.format(item.data.date)}
</time>
</p>
<p class="source-line"><SourceBadge source={item.data.source} /></p>
</header>
{hero && (
<Image
class="hero"
src={hero}
widths={[640, 960, 1280]}
sizes="(max-width: 720px) 92vw, 42rem"
loading="eager"
decoding="async"
alt={item.data.name}
/>
)}
<div class="body">
<Content />
</div>
@@ -120,6 +126,16 @@ const breadcrumbs = buildBreadcrumbs([
)
}
{import.meta.env.DEV && !item.data.promotedTo && (
<div class="edit-toolbar">
<EditAction
label="Graduate to review"
command={`/build-review ${item.id}`}
confirm={`This will draft src/content/reviews/${item.id}.mdx, expand the find body into a full review, propose a hero asset, link the find via promotedTo, and append a graduation note to daily-finds.log.md.`}
/>
</div>
)}
<Backlinks posts={featuringPosts} />
{
@@ -136,23 +152,20 @@ const breadcrumbs = buildBreadcrumbs([
</article>
<style>
.source-line { margin: 0 0 0.5rem; }
.find-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--rule);
}
h1 { margin: 0 0 0.35rem; }
.subtitle {
margin: 0 0 0.75rem;
color: var(--muted);
font-size: 1.1rem;
font-style: italic;
}
.meta {
margin: 0;
color: var(--muted);
font-size: 0.9rem;
.source-line { margin: 0; }
.hero {
display: block;
width: 100%;
height: auto;
max-height: 28rem;
object-fit: cover;
border-radius: 8px;
border: 1px solid var(--rule);
background: var(--card-fill);
margin: 0 0 1.5rem;
}
.body :global(p) { margin: 0 0 1rem; }
.external {
@@ -208,5 +221,10 @@ const breadcrumbs = buildBreadcrumbs([
.tag { font-size: 0.85rem; color: var(--muted); }
.tag:hover { color: var(--accent); }
.back { margin-top: 2rem; font-size: 0.9rem; }
.edit-toolbar {
margin: 2rem 0 0;
padding-top: 1rem;
border-top: 1px dashed color-mix(in srgb, var(--accent) 30%, var(--rule));
}
</style>
</BaseLayout>
+59 -30
View File
@@ -1,15 +1,15 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Masonry from '../../components/Masonry.astro';
import FindCard from '../../components/FindCard.astro';
import EditAction from '../../components/EditAction.astro';
import { getAllFinds } from '../../lib/find-items';
const INITIAL_GRID = 18;
const BATCH_SIZE = 18;
const finds = await getAllFinds();
const lastUpdated = finds[0]?.data.date;
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
---
<BaseLayout
@@ -19,35 +19,64 @@ const fmt = new Intl.DateTimeFormat('en-US', {
lastUpdated={lastUpdated}
noindex
>
<ul class="find-index">
{finds.map((f) => (
<li>
<a href={`/find/${f.id}/`}>{f.data.name}</a>
<time datetime={f.data.date.toISOString()}>{fmt.format(f.data.date)}</time>
</li>
))}
</ul>
<Masonry initial={INITIAL_GRID} batch={BATCH_SIZE}>
{finds.map((f) => <FindCard item={f} />)}
</Masonry>
{import.meta.env.DEV && (
<div class="edit-floating-toolbar" data-find-toolbar>
<span class="edit-toolbar-count">
<span data-find-selected-count>0</span> selected
</span>
<EditAction
action="finds-collection"
label="Make a finds collection"
confirm="This will draft a themed finds list (.mdx in src/content/finds/) from the selected items."
/>
</div>
)}
<script>
// Update the selected-count badge as boxes are toggled.
const counter = document.querySelector('[data-find-selected-count]');
if (counter) {
const refresh = () => {
const n = document.querySelectorAll('[data-find-checkbox]:checked').length;
counter.textContent = String(n);
};
document.addEventListener('change', (e) => {
const t = e.target as HTMLElement;
if (t.matches('[data-find-checkbox]')) refresh();
});
refresh();
}
</script>
<style>
.find-index {
list-style: none;
margin: 0;
padding: 0;
}
.find-index li {
.edit-floating-toolbar {
position: sticky;
bottom: 1rem;
margin-top: 2rem;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
padding: 0.5rem 0;
border-bottom: 1px solid var(--rule);
align-items: center;
gap: 0.85rem;
justify-content: flex-end;
padding: 0.65rem 0.85rem;
background: var(--bg);
border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--rule));
border-radius: 8px;
box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.18);
}
.find-index li:last-child { border-bottom: 0; }
.find-index time {
.edit-toolbar-count {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.edit-toolbar-count [data-find-selected-count] {
color: var(--fg);
font-weight: 700;
}
</style>
</BaseLayout>
+8 -26
View File
@@ -1,7 +1,8 @@
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import FindsGrid from '../../components/FindsGrid.astro';
import Masonry from '../../components/Masonry.astro';
import FindCard from '../../components/FindCard.astro';
import { resolveFinds } from '../../lib/find-items';
import {
buildOgImage,
@@ -23,12 +24,6 @@ const { post } = Astro.props;
const { Content } = await render(post);
const items = await resolveFinds(post.data.items);
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const description =
post.data.description ?? post.data.blurb ?? `A collection: ${post.data.title}.`;
@@ -65,6 +60,9 @@ const breadcrumbs = buildBreadcrumbs([
<BaseLayout
title={post.data.title}
description={description}
subheader={post.data.title}
subheaderVariant="headline"
lastUpdated={post.data.date}
ogType="article"
ogImage={ogImage}
publishedTime={post.data.date}
@@ -72,20 +70,13 @@ const breadcrumbs = buildBreadcrumbs([
jsonLd={[articleSchema, itemList, breadcrumbs]}
>
<article>
<header class="finds-header">
<h1>{post.data.title}</h1>
<p class="meta">
<time datetime={post.data.date.toISOString()}>
{fmt.format(post.data.date)}
</time>
</p>
</header>
<div class="body">
<Content />
</div>
<FindsGrid items={items} />
<Masonry>
{items.map((item) => <FindCard item={item} />)}
</Masonry>
{
post.data.tags.length > 0 && (
@@ -101,15 +92,6 @@ const breadcrumbs = buildBreadcrumbs([
</article>
<style>
.finds-header {
margin-bottom: 1.5rem;
}
h1 { margin: 0 0 0.35rem; font-size: 1.85rem; }
.meta {
margin: 0;
color: var(--muted);
font-size: 0.9rem;
}
.body {
margin-bottom: 1.75rem;
max-width: 42rem;
+56 -22
View File
@@ -1,5 +1,6 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Masonry from '../../components/Masonry.astro';
import { getCollection } from 'astro:content';
const lists = (await getCollection('finds')).sort(
@@ -11,6 +12,7 @@ const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
});
---
@@ -21,35 +23,67 @@ const fmt = new Intl.DateTimeFormat('en-US', {
lastUpdated={lastUpdated}
noindex
>
<ul class="lists-index">
<Masonry>
{lists.map((l) => (
<li>
<a href={`/finds/${l.id}/`}>{l.data.title}</a>
<time datetime={l.data.date.toISOString()}>{fmt.format(l.data.date)}</time>
</li>
<article>
<a href={`/finds/${l.id}/`} class="list-tile">
<p class="eyebrow">
<time datetime={l.data.date.toISOString()}>{fmt.format(l.data.date)}</time>
<span class="dot" aria-hidden="true">·</span>
<span>{l.data.items.length} picks</span>
</p>
<h2 class="title">{l.data.title}</h2>
{l.data.blurb && <p class="blurb">{l.data.blurb}</p>}
</a>
</article>
))}
</ul>
</Masonry>
<style>
.lists-index {
list-style: none;
margin: 0;
padding: 0;
.list-tile {
display: block;
padding: 1.25rem 1.35rem 1.4rem;
border-radius: 8px;
border: 2px solid var(--rule);
background: linear-gradient(155deg, var(--rule), transparent);
color: inherit;
transition: box-shadow 0.18s ease, border-color 0.18s ease;
}
.lists-index li {
.list-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);
}
.list-tile:hover .title { color: var(--accent); }
.eyebrow {
margin: 0 0 0.5rem;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
padding: 0.5rem 0;
border-bottom: 1px solid var(--rule);
}
.lists-index li:last-child { border-bottom: 0; }
.lists-index time {
flex-wrap: wrap;
gap: 0.4rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.eyebrow .dot { color: var(--rule); }
.title {
margin: 0 0 0.5rem;
font-family: ui-serif, Georgia, 'Iowan Old Style', 'Apple Garamond', 'Palatino Linotype', serif;
font-weight: 700;
font-size: 1.35rem;
line-height: 1.2;
letter-spacing: -0.01em;
color: var(--fg);
transition: color 0.18s ease;
}
.blurb {
margin: 0;
color: var(--muted);
font-size: 0.95rem;
line-height: 1.45;
}
</style>
</BaseLayout>
+1 -1
View File
@@ -48,7 +48,7 @@ const lastUpdated = reviews[0]?.data.date;
loop
muted
playsinline
preload="metadata"
preload="none"
aria-label={tile.name}
/>
)}
+1 -1
View File
@@ -100,7 +100,7 @@ const itemList = buildItemList(
loop
muted
playsinline
preload="metadata"
preload="none"
aria-label={tile.name}
/>
)}
+26 -21
View File
@@ -2,6 +2,7 @@
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import SourceBadge from '../../components/SourceBadge.astro';
import EditAction from '../../components/EditAction.astro';
import {
resolveExternalLink,
AFFILIATE_REL,
@@ -30,6 +31,7 @@ const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
});
const linkLabel =
@@ -78,6 +80,10 @@ const breadcrumbs = buildBreadcrumbs([
<BaseLayout
title={review.data.name}
description={description}
subheader={review.data.name}
subheaderSubtitle={review.data.subtitle}
subheaderVariant="headline"
lastUpdated={review.data.date}
ogType="article"
ogImage={ogImage}
publishedTime={review.data.date}
@@ -85,18 +91,11 @@ const breadcrumbs = buildBreadcrumbs([
jsonLd={[reviewSchema, breadcrumbs]}
>
<article>
{review.data.source && (
<p class="source-line"><SourceBadge source={review.data.source} /></p>
)}
<header class="review-header">
<h1>{review.data.name}</h1>
<p class="subtitle">{review.data.subtitle}</p>
{review.data.source && (
<p class="source-line"><SourceBadge source={review.data.source} /></p>
)}
<p class="meta">
<time datetime={review.data.date.toISOString()}>
{fmt.format(review.data.date)}
</time>
<span class="dot">·</span>
<a href={`/categories/${review.data.category}/`} class="category">
{review.data.category}
</a>
@@ -135,6 +134,16 @@ const breadcrumbs = buildBreadcrumbs([
<Content />
</div>
{import.meta.env.DEV && (
<div class="edit-toolbar">
<EditAction
label="Make today's pick"
command={`/set-pick ${review.id}`}
confirm={`This will set ${review.data.name} as the homepage hero (updates HERO_SLUG in src/pages/index.astro).`}
/>
</div>
)}
{
review.data.tags.length > 0 && (
<footer class="tags">
@@ -160,19 +169,10 @@ const breadcrumbs = buildBreadcrumbs([
}
.from-find time { color: var(--muted); }
.review-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--rule);
}
h1 { margin: 0 0 0.35rem; }
.subtitle {
margin: 0 0 0.75rem;
color: var(--muted);
font-size: 1.1rem;
font-style: italic;
margin-bottom: 1.5rem;
}
.meta {
margin: 0;
margin: 0.5rem 0 0;
color: var(--muted);
font-size: 0.9rem;
}
@@ -309,5 +309,10 @@ const breadcrumbs = buildBreadcrumbs([
.tag { font-size: 0.85rem; color: var(--muted); }
.tag:hover { color: var(--accent); }
.back { margin-top: 2rem; font-size: 0.9rem; }
.edit-toolbar {
margin: 2rem 0 0;
padding-top: 1rem;
border-top: 1px dashed color-mix(in srgb, var(--accent) 30%, var(--rule));
}
</style>
</BaseLayout>
+313
View File
@@ -0,0 +1,313 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout
title="Search"
description="Search reviews, finds, lists, and posts across Unique."
subheader="Search"
noindex
>
<form class="search-form" role="search" autocomplete="off">
<input
id="q"
type="search"
name="q"
placeholder="Search reviews, finds, lists, posts…"
aria-label="Search"
autofocus
/>
</form>
<p id="status" class="status" aria-live="polite"></p>
<div id="results" class="masonry" role="list"></div>
<script>
const input = document.getElementById('q') as HTMLInputElement;
const status = document.getElementById('status') as HTMLParagraphElement;
const results = document.getElementById('results') as HTMLDivElement;
type Entry = {
type: 'review' | 'find' | 'list' | 'post';
url: string;
title: string;
subtitle: string;
date: string;
tags: string[];
blurb: string;
source?: string;
};
const typeLabels: Record<Entry['type'], string> = {
review: 'Review',
find: 'Find',
list: 'List',
post: 'Post',
};
let entries: Entry[] = [];
let ready = false;
let lastTiles: HTMLElement[] = [];
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
});
function escapeHtml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function score(entry: Entry, terms: string[]): number {
const title = entry.title.toLowerCase();
const subtitle = entry.subtitle.toLowerCase();
const blurb = entry.blurb.toLowerCase();
const tags = entry.tags.join(' ').toLowerCase();
const source = (entry.source ?? '').toLowerCase();
let total = 0;
for (const term of terms) {
let s = 0;
if (title.includes(term)) s += 10;
if (title.startsWith(term)) s += 5;
if (subtitle.includes(term)) s += 4;
if (tags.includes(term)) s += 3;
if (blurb.includes(term)) s += 2;
if (source.includes(term)) s += 2;
if (s === 0) return 0;
total += s;
}
return total;
}
function colCount(): number {
const w = window.innerWidth;
if (w <= 520) return 1;
if (w <= 880) return 2;
return 3;
}
function tileHtml(entry: Entry): string {
const date = fmt.format(new Date(entry.date));
const blurb = entry.blurb || entry.subtitle;
const blurbEl = blurb
? `<p class="r-blurb">${escapeHtml(blurb)}</p>`
: '';
const source = entry.source
? `<p class="r-source">via ${escapeHtml(entry.source)}</p>`
: '';
return `<a href="${entry.url}" class="r-tile" role="listitem">
<p class="r-eyebrow"><span class="r-type">${typeLabels[entry.type]}</span><span class="r-dot" aria-hidden="true">·</span><time datetime="${entry.date}">${date}</time></p>
<p class="r-title">${escapeHtml(entry.title)}</p>
${source}${blurbEl}
</a>`;
}
function layout(tiles: HTMLElement[]) {
results.replaceChildren();
const cols: HTMLDivElement[] = [];
const n = colCount();
for (let i = 0; i < n; i++) {
const c = document.createElement('div');
c.className = 'masonry-col';
results.appendChild(c);
cols.push(c);
}
for (const tile of tiles) {
let min = Infinity;
let idx = 0;
for (let i = 0; i < cols.length; i++) {
const h = cols[i].offsetHeight;
if (h < min) { min = h; idx = i; }
}
cols[idx].appendChild(tile);
}
}
function buildTiles(items: Entry[]): HTMLElement[] {
const wrap = document.createElement('div');
wrap.innerHTML = items.map(tileHtml).join('');
return Array.from(wrap.children) as HTMLElement[];
}
function render(query: string) {
const q = query.trim().toLowerCase();
if (!q) {
lastTiles = [];
results.replaceChildren();
status.textContent = ready
? `${entries.length} entries indexed.`
: 'Loading index…';
return;
}
if (!ready) {
status.textContent = 'Loading index…';
return;
}
const terms = q.split(/\s+/).filter(Boolean);
const matches: Array<{ entry: Entry; score: number }> = [];
for (const entry of entries) {
const s = score(entry, terms);
if (s > 0) matches.push({ entry, score: s });
}
matches.sort((a, b) => b.score - a.score || b.entry.date.localeCompare(a.entry.date));
const top = matches.slice(0, 80);
status.textContent = matches.length
? `${matches.length} match${matches.length === 1 ? '' : 'es'}`
: 'No matches.';
lastTiles = buildTiles(top.map((m) => m.entry));
layout(lastTiles);
}
function syncFromUrl() {
const params = new URLSearchParams(window.location.search);
const q = params.get('q') ?? '';
input.value = q;
render(q);
}
let urlTimer: number | undefined;
input.addEventListener('input', () => {
render(input.value);
if (urlTimer) window.clearTimeout(urlTimer);
urlTimer = window.setTimeout(() => {
const url = new URL(window.location.href);
if (input.value) url.searchParams.set('q', input.value);
else url.searchParams.delete('q');
window.history.replaceState(null, '', url.toString());
}, 300);
});
let resizeTimer: number | undefined;
let currentCols = colCount();
window.addEventListener('resize', () => {
if (resizeTimer) window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(() => {
const next = colCount();
if (next !== currentCols) {
currentCols = next;
if (lastTiles.length) layout(lastTiles);
}
}, 150);
});
fetch('/search.json')
.then((r) => r.json())
.then((data: { entries: Entry[] }) => {
entries = data.entries;
ready = true;
syncFromUrl();
})
.catch(() => {
status.textContent = 'Could not load search index.';
});
syncFromUrl();
</script>
<style>
.search-form {
margin: 0 0 1.25rem;
}
#q {
width: 100%;
padding: 0.7rem 0.9rem;
font-family: ui-serif, Georgia, 'Iowan Old Style', 'Apple Garamond', 'Palatino Linotype', serif;
font-size: 1.05rem;
color: var(--fg);
background: var(--bg);
border: 1px solid var(--rule);
border-radius: 8px;
outline: none;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
#q:focus {
border-color: color-mix(in srgb, var(--accent) 55%, var(--rule));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
}
.status {
margin: 0 0 1rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.masonry {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.masonry :global(.masonry-col) {
flex: 1 1 0;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.masonry :global(.r-tile) {
display: block;
padding: 1rem 1.1rem 1.1rem;
border-radius: 8px;
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;
}
.masonry :global(.r-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);
}
.masonry :global(.r-tile:hover .r-title) { color: var(--accent); }
.masonry :global(.r-eyebrow) {
margin: 0 0 0.4rem;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.masonry :global(.r-type) { color: var(--accent); }
.masonry :global(.r-dot) { color: var(--rule); }
.masonry :global(.r-title) {
margin: 0 0 0.35rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1.05rem;
letter-spacing: -0.01em;
color: var(--fg);
transition: color 0.18s ease;
}
.masonry :global(.r-source) {
margin: 0 0 0.45rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.masonry :global(.r-blurb) {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
line-height: 1.4;
}
</style>
</BaseLayout>
+56
View File
@@ -0,0 +1,56 @@
import { getCollection } from 'astro:content';
export async function GET() {
const [reviews, finds, lists, posts] = await Promise.all([
getCollection('reviews'),
getCollection('find'),
getCollection('finds'),
getCollection('posts'),
]);
const entries = [
...reviews.map((r) => ({
type: 'review',
url: `/reviews/${r.id}/`,
title: r.data.name,
subtitle: r.data.subtitle ?? '',
date: r.data.date.toISOString(),
tags: r.data.tags ?? [],
blurb: r.data.description ?? '',
})),
...finds.map((f) => ({
type: 'find',
url: `/find/${f.id}/`,
title: f.data.name,
subtitle: f.data.subtitle ?? '',
date: f.data.date.toISOString(),
tags: f.data.tags ?? [],
blurb: f.data.description ?? '',
source: f.data.source,
})),
...lists.map((l) => ({
type: 'list',
url: `/finds/${l.id}/`,
title: l.data.title,
subtitle: l.data.blurb ?? '',
date: l.data.date.toISOString(),
tags: l.data.tags ?? [],
blurb: l.data.description ?? '',
})),
...posts.map((p) => ({
type: 'post',
url: `/posts/${p.id}/`,
title: p.data.title,
subtitle: '',
date: p.data.date.toISOString(),
tags: p.data.tags ?? [],
blurb: p.data.description ?? '',
})),
];
entries.sort((a, b) => b.date.localeCompare(a.date));
return new Response(JSON.stringify({ entries }), {
headers: { 'Content-Type': 'application/json' },
});
}
+23 -7
View File
@@ -20,16 +20,15 @@ type Source = {
notes: string;
};
const sources: Source[] = [...sourcesData.sources].sort((a, b) =>
a.name.localeCompare(b.name)
);
const topicLabels: Record<string, string> = {
'macos-apps': 'macOS apps',
'apple-tips': 'apple tips',
'tools': 'tools',
'gifts': 'gifts',
'clothing': 'clothing',
'travel': 'travel',
'quotes': 'quotes',
'dad-jokes': 'dad jokes',
'general-delight': 'general delight',
};
@@ -48,10 +47,27 @@ for (const f of allFinds) {
arr.push(f);
findsBySource.set(f.data.source, arr);
}
for (const arr of findsBySource.values()) {
arr.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
}
const lastUpdated = allFinds.sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
)[0]?.data.date;
const sources: Source[] = [...sourcesData.sources].sort((a, b) => {
const af = findsBySource.get(a.name) ?? [];
const bf = findsBySource.get(b.name) ?? [];
const aLatest = af[0]?.data.date.valueOf() ?? -Infinity;
const bLatest = bf[0]?.data.date.valueOf() ?? -Infinity;
if (aLatest !== bLatest) return bLatest - aLatest;
if (af.length !== bf.length) return bf.length - af.length;
return a.name.localeCompare(b.name);
});
const lastUpdated = findsBySource.size
? new Date(
Math.max(
...[...findsBySource.values()].map((arr) => arr[0].data.date.valueOf())
)
)
: undefined;
const description = `The ${sources.length} feeds, shops, and forums Unique draws daily picks from.`;
const ogImage = buildOgImage(undefined, 'site', 'Unique sources catalog');