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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user