site: rename "finds" superpost collection to "bundles"
Disambiguates the one-letter `find` (item) vs `finds` (roundup) split that
kept causing confusion. Audience-facing now: `/bundles/<slug>/`, "Bundle"
labels in home/footer/search.
Internals: `getCollection('finds')` → `('bundles')`, `Entry.type 'finds'`
→ `'bundle'`, `OgFallback 'finds'` → `'bundles'`, `findListMosaic` →
`bundleMosaic`, `FindsPost` → `Bundle`, `findsReferencing` →
`bundlesReferencing`. Build script + generated mosaic dir follow
(`scripts/build-bundle-mosaics.mjs`, `src/generated/bundle-mosaics/`).
Skills `build-finds` → `build-bundle`, `cluster-finds` →
`cluster-bundles`. The `find` collection (individual items) is unchanged.
No URL redirects: low external-link surface area.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import Masonry from '../../components/Masonry.astro';
|
||||
import FindCard from '../../components/FindCard.astro';
|
||||
import { resolveFinds } from '../../lib/find-items';
|
||||
import {
|
||||
buildOgImage,
|
||||
buildBreadcrumbs,
|
||||
buildItemList,
|
||||
PERSON_AUTHOR,
|
||||
absoluteUrl,
|
||||
} from '../../lib/seo';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const bundles = await getCollection('bundles');
|
||||
return bundles.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
const items = await resolveFinds(post.data.items);
|
||||
|
||||
const description =
|
||||
post.data.description ?? post.data.blurb ?? `A collection: ${post.data.title}.`;
|
||||
|
||||
const ogImage = buildOgImage(
|
||||
items[0]?.data.promotedTo,
|
||||
'bundles',
|
||||
`${post.data.title} cover image`
|
||||
);
|
||||
|
||||
const articleSchema: Record<string, unknown> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
'@id': absoluteUrl(`/bundles/${post.id}/#article`),
|
||||
headline: post.data.title,
|
||||
description,
|
||||
url: absoluteUrl(`/bundles/${post.id}/`),
|
||||
datePublished: post.data.date.toISOString(),
|
||||
author: PERSON_AUTHOR,
|
||||
image: ogImage.url,
|
||||
inLanguage: 'en-US',
|
||||
};
|
||||
|
||||
const itemList = buildItemList(
|
||||
items.map((it) => ({ url: `/find/${it.id}/`, name: it.data.name })),
|
||||
post.data.title
|
||||
);
|
||||
|
||||
const breadcrumbs = buildBreadcrumbs([
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: post.data.title, url: `/bundles/${post.id}/` },
|
||||
]);
|
||||
---
|
||||
|
||||
<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}
|
||||
articleTags={post.data.tags}
|
||||
jsonLd={[articleSchema, itemList, breadcrumbs]}
|
||||
>
|
||||
<article>
|
||||
<div class="body">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
<Masonry>
|
||||
{items.map((item) => <FindCard item={item} />)}
|
||||
</Masonry>
|
||||
|
||||
{(post.data.tags.length > 0 ||
|
||||
(post.data.category && post.data.category !== 'bundles')) && (
|
||||
<aside class="colophon" aria-label="Colophon">
|
||||
<dl>
|
||||
{post.data.category && post.data.category !== 'bundles' && (
|
||||
<>
|
||||
<dt>Filed under</dt>
|
||||
<dd>
|
||||
<a href={`/categories/${post.data.category}/`}>
|
||||
{post.data.category.charAt(0).toUpperCase() +
|
||||
post.data.category.slice(1)}
|
||||
</a>
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
{post.data.tags.length > 0 && (
|
||||
<>
|
||||
<dt>Tagged</dt>
|
||||
<dd class="colophon-tags">
|
||||
{post.data.tags.map((tag) => (
|
||||
<a href={`/tags/${tag}/`}>#{tag}</a>
|
||||
))}
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<p class="back"><a href="/">← back</a></p>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.body {
|
||||
margin-bottom: 1.75rem;
|
||||
max-width: 42rem;
|
||||
}
|
||||
.body :global(p) { margin: 0 0 1rem; }
|
||||
.body :global(blockquote) {
|
||||
margin: 1.25rem 0;
|
||||
padding: 0.15rem 0 0.15rem 1rem;
|
||||
border-left: 3px solid var(--accent);
|
||||
color: var(--fg);
|
||||
font-style: normal;
|
||||
}
|
||||
.body :global(blockquote p) { margin: 0; }
|
||||
.back { margin-top: 2rem; font-size: 0.9rem; }
|
||||
</style>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import Masonry from '../../components/Masonry.astro';
|
||||
import { Image } from 'astro:assets';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { bundleMosaic } from '../../lib/bundle-mosaic';
|
||||
|
||||
const bundles = (await getCollection('bundles')).sort(
|
||||
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
|
||||
);
|
||||
const lastUpdated = bundles[0]?.data.date;
|
||||
|
||||
const fmt = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Bundles"
|
||||
description={`All ${bundles.length} themed bundles on Unique.`}
|
||||
subheader={`All ${bundles.length} themed bundles.`}
|
||||
lastUpdated={lastUpdated}
|
||||
noindex
|
||||
>
|
||||
<Masonry>
|
||||
{bundles.map((b) => {
|
||||
const mosaic = bundleMosaic(b.id);
|
||||
return (
|
||||
<article>
|
||||
<a href={`/bundles/${b.id}/`} class="bundle-tile">
|
||||
{mosaic && (
|
||||
<div class="mosaic">
|
||||
<Image
|
||||
src={mosaic}
|
||||
widths={[320, 480, 640, 960]}
|
||||
sizes="(max-width: 520px) 92vw, (max-width: 880px) 45vw, 320px"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div class="body">
|
||||
<h2 class="title">{b.data.title}</h2>
|
||||
{b.data.blurb && <p class="blurb">{b.data.blurb}</p>}
|
||||
<p class="meta">
|
||||
<time datetime={b.data.date.toISOString()}>{fmt.format(b.data.date)}</time>
|
||||
<span class="dot" aria-hidden="true">·</span>
|
||||
<span>{b.data.items.length} picks</span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</Masonry>
|
||||
|
||||
<style>
|
||||
.bundle-tile {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--rule);
|
||||
color: inherit;
|
||||
transition: box-shadow 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
.bundle-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);
|
||||
}
|
||||
.bundle-tile:hover .title { color: var(--accent); }
|
||||
|
||||
.mosaic {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.mosaic :global(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 1rem 1.15rem 1.1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
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;
|
||||
}
|
||||
.meta {
|
||||
margin: 0.25rem 0 0;
|
||||
display: flex;
|
||||
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);
|
||||
}
|
||||
.meta .dot { color: var(--rule); }
|
||||
</style>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user