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
+60
View File
@@ -0,0 +1,60 @@
// Amazon Associates tracking IDs, keyed by Amazon hostname.
// Add additional marketplaces here when applicable (each marketplace issues its own ID).
const AMAZON_TAGS: Record<string, string> = {
'amazon.com': 'unique.rzen.dev-20',
};
// Hostnames that always belong to the US marketplace (short links resolve there).
const AMAZON_US_SHORTLINKS = new Set(['amzn.to', 'a.co']);
function normalizeHost(host: string): string {
return host.toLowerCase().replace(/^www\./, '');
}
function amazonHostFor(host: string): string | null {
const h = normalizeHost(host);
if (AMAZON_US_SHORTLINKS.has(h)) return 'amazon.com';
if (h === 'smile.amazon.com') return 'amazon.com';
if (h === 'amazon.com' || h.endsWith('.amazon.com')) return 'amazon.com';
// Other regional marketplaces (amazon.co.uk, amazon.de, etc.). Only mark as
// affiliate when we have a tag configured for that marketplace.
const match = h.match(/^(?:[\w-]+\.)?(amazon\.(?:[a-z.]+))$/);
return match ? match[1] : null;
}
export function isAmazonUrl(url: string): boolean {
try {
return amazonHostFor(new URL(url).hostname) !== null;
} catch {
return false;
}
}
export interface ExternalLink {
href: string;
isAffiliate: boolean;
network?: 'amazon';
}
/**
* Returns the link to render externally. If the URL points at an Amazon
* marketplace we have a tag for, append the tag and flag it as affiliate.
* Otherwise return the URL unchanged.
*/
export function resolveExternalLink(rawUrl: string): ExternalLink {
try {
const u = new URL(rawUrl);
const marketplace = amazonHostFor(u.hostname);
if (!marketplace) return { href: rawUrl, isAffiliate: false };
const tag = AMAZON_TAGS[marketplace];
if (!tag) return { href: rawUrl, isAffiliate: false, network: 'amazon' };
u.searchParams.set('tag', tag);
return { href: u.toString(), isAffiliate: true, network: 'amazon' };
} catch {
return { href: rawUrl, isAffiliate: false };
}
}
export const AFFILIATE_REL = 'sponsored noopener noreferrer';
export const AFFILIATE_DISCLOSURE =
'Affiliate link — we may earn a commission from qualifying Amazon purchases at no extra cost to you.';
+37
View File
@@ -50,3 +50,40 @@ export async function getAllEntries(): Promise<Entry[]> {
return [...r, ...p, ...f].sort((a, b) => b.date.valueOf() - a.date.valueOf());
}
export type SiteStats = {
total: number;
lastUpdated: Date | null;
reviews: number;
finds: number;
posts: number;
lists: number;
};
let statsCache: Promise<SiteStats> | null = null;
export function getSiteStats(): Promise<SiteStats> {
if (!statsCache) statsCache = computeSiteStats();
return statsCache;
}
async function computeSiteStats(): Promise<SiteStats> {
const entries = await getAllEntries();
const findItems = await getCollection('find');
const reviews = entries.filter((e) => e.type === 'review').length;
const finds = findItems.length;
const posts = entries.filter((e) => e.type === 'post').length;
const lists = entries.filter((e) => e.type === 'finds').length;
const allDates = [
...entries.map((e) => e.date),
...findItems.map((f) => f.data.date),
].sort((a, b) => b.valueOf() - a.valueOf());
return {
total: reviews + finds + posts + lists,
lastUpdated: allDates[0] ?? null,
reviews,
finds,
posts,
lists,
};
}
+126
View File
@@ -0,0 +1,126 @@
import { heroes } from './hero';
import type { OgImage } from '../components/SEO.astro';
export type OgFallback = 'reviews' | 'finds' | 'posts' | 'site';
const SITE_URL = 'https://unique.rzen.dev';
const FALLBACK_PATHS: Record<OgFallback, string> = {
reviews: '/og/reviews.png',
finds: '/og/finds.png',
posts: '/og/posts.png',
site: '/og/default.png',
};
const FALLBACK_DIMS = { width: 1200, height: 630, type: 'image/png' };
export function buildOgImage(
heroId: string | undefined,
fallback: OgFallback,
alt: string
): OgImage {
const hero = heroId ? heroes[heroId] : undefined;
if (hero?.kind === 'image') {
const fmt = hero.src.format;
const mime = fmt
? `image/${fmt === 'jpg' ? 'jpeg' : fmt}`
: undefined;
return {
url: new URL(hero.src.src, SITE_URL).href,
width: hero.src.width,
height: hero.src.height,
alt,
type: mime,
};
}
return {
url: new URL(FALLBACK_PATHS[fallback], SITE_URL).href,
width: FALLBACK_DIMS.width,
height: FALLBACK_DIMS.height,
alt,
type: FALLBACK_DIMS.type,
};
}
export function absoluteUrl(path: string): string {
return new URL(path, SITE_URL).href;
}
export type Crumb = { name: string; url: string };
export function buildBreadcrumbs(crumbs: Crumb[]): Record<string, unknown> {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: crumbs.map((c, i) => ({
'@type': 'ListItem',
position: i + 1,
name: c.name,
item: absoluteUrl(c.url),
})),
};
}
export type ListItem = { url: string; name: string };
export function buildItemList(
items: ListItem[],
name?: string
): Record<string, unknown> {
return {
'@context': 'https://schema.org',
'@type': 'ItemList',
...(name ? { name } : {}),
numberOfItems: items.length,
itemListElement: items.map((it, i) => ({
'@type': 'ListItem',
position: i + 1,
name: it.name,
url: absoluteUrl(it.url),
})),
};
}
export const PERSON_AUTHOR = {
'@type': 'Person',
name: 'rzen',
url: `${SITE_URL}/about/`,
};
export const ORGANIZATION = {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': `${SITE_URL}/#org`,
name: 'Unique',
url: SITE_URL,
logo: `${SITE_URL}/favicon.svg`,
};
export const WEBSITE = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'@id': `${SITE_URL}/#site`,
name: 'Unique',
url: SITE_URL,
description:
'A small log of delightful, unique things — products, apps, phenomena, oddities.',
publisher: { '@id': `${SITE_URL}/#org` },
inLanguage: 'en-US',
};
const SOFTWARE_CATEGORIES = new Set([
'apps',
'app',
'software',
'macos-apps',
'macos',
'ios-apps',
'ios',
'tools',
]);
export function reviewedItemType(category: string): 'SoftwareApplication' | 'Product' {
return SOFTWARE_CATEGORIES.has(category.toLowerCase())
? 'SoftwareApplication'
: 'Product';
}