site: ui/ux refresh, seo pass, find/lists indexes, 25 new finds

- newspaper-style masthead (stats | "Unique" | About/Blog) on every page
  with per-page italic subheader + last-updated date on listing pages;
  sitewide 42rem reading width
- pinterest home (hero + masonry, "load more"); old list view at /index3/
- new pages: /about, /blog, /legal, /find/, /finds/, custom 404
- footer collection links now clickable; "founded" stat replaces "updated"
  in the masthead
- centralised SEO component with per-page json-ld (Review, BlogPosting,
  Product, Article, CollectionPage, ItemList, BreadcrumbList, etc.);
  @astrojs/sitemap + @astrojs/rss; robots.txt; promoted finds canonical
  to their review
- memoize getSiteStats() so the masthead/footer counts don't multiply
  per-page build cost
- 25 new daily-finds captures
This commit is contained in:
2026-05-03 16:58:23 -04:00
parent c184f3f50a
commit c8095d2766
54 changed files with 2337 additions and 95 deletions
+82 -3
View File
@@ -2,6 +2,18 @@
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import SourceBadge from '../../components/SourceBadge.astro';
import {
resolveExternalLink,
AFFILIATE_REL,
AFFILIATE_DISCLOSURE,
} from '../../lib/affiliate';
import {
buildOgImage,
buildBreadcrumbs,
reviewedItemType,
PERSON_AUTHOR,
absoluteUrl,
} from '../../lib/seo';
export async function getStaticPaths() {
const reviews = await getCollection('reviews');
@@ -24,14 +36,53 @@ const linkLabel =
review.data.link &&
(review.data.linkText ?? new URL(review.data.link).hostname.replace(/^www\./, ''));
const external = review.data.link ? resolveExternalLink(review.data.link) : undefined;
const fromFindItem = review.data.fromFind
? (await getCollection('find')).find((f) => f.id === review.data.fromFind)
: undefined;
const description =
review.data.description ?? review.data.subtitle ?? `A review of ${review.data.name}.`;
const ogImage = buildOgImage(review.id, 'reviews', `${review.data.name} hero image`);
const itemReviewed: Record<string, unknown> = {
'@type': reviewedItemType(review.data.category),
name: review.data.name,
};
if (review.data.link) itemReviewed.url = review.data.link;
if (ogImage) itemReviewed.image = ogImage.url;
const reviewSchema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'Review',
'@id': absoluteUrl(`/reviews/${review.id}/#review`),
url: absoluteUrl(`/reviews/${review.id}/`),
name: review.data.name,
headline: review.data.subtitle || review.data.name,
description,
datePublished: review.data.date.toISOString(),
author: PERSON_AUTHOR,
inLanguage: 'en-US',
itemReviewed,
};
const breadcrumbs = buildBreadcrumbs([
{ name: 'Home', url: '/' },
{ name: review.data.category, url: `/categories/${review.data.category}/` },
{ name: review.data.name, url: `/reviews/${review.id}/` },
]);
---
<BaseLayout
title={review.data.name}
description={review.data.description ?? review.data.subtitle}
description={description}
ogType="article"
ogImage={ogImage}
publishedTime={review.data.date}
articleTags={review.data.tags}
jsonLd={[reviewSchema, breadcrumbs]}
>
<article>
{review.data.source && (
@@ -50,16 +101,24 @@ const fromFindItem = review.data.fromFind
{review.data.category}
</a>
{
review.data.link && (
external && (
<>
<span class="dot">·</span>
<a href={review.data.link} target="_blank" rel="noopener noreferrer">
<a
href={external.href}
target="_blank"
rel={external.isAffiliate ? AFFILIATE_REL : 'noopener noreferrer'}
>
{linkLabel} ↗
</a>
{external.isAffiliate && <span class="aff-badge">Affiliate</span>}
</>
)
}
</p>
{external?.isAffiliate && (
<p class="aff-disclosure">{AFFILIATE_DISCLOSURE}</p>
)}
</header>
{fromFindItem && (
@@ -120,6 +179,26 @@ const fromFindItem = review.data.fromFind
.meta .dot { margin: 0 0.4rem; }
.meta .category { color: var(--muted); }
.meta .category:hover { color: var(--accent); }
.aff-badge {
display: inline-block;
margin-left: 0.45rem;
padding: 0.12rem 0.45rem;
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
background: var(--rule);
border-radius: 999px;
vertical-align: middle;
}
.aff-disclosure {
margin: 0.6rem 0 0;
font-size: 0.78rem;
color: var(--muted);
font-style: italic;
}
.body :global(p) { margin: 0 0 1rem; }
.body :global(h2) { font-size: 1.3rem; margin: 2rem 0 0.75rem; }
.body :global(h3) { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; }