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
+248 -31
View File
@@ -1,33 +1,136 @@
---
import { getSiteStats } from '../lib/entries';
import SEO from '../components/SEO.astro';
import type { OgImage } from '../components/SEO.astro';
interface Props {
title: string;
description?: string;
wide?: boolean;
subheader?: string;
lastUpdated?: Date;
canonicalPath?: string;
canonicalOverride?: string;
ogType?: 'website' | 'article';
ogImage?: OgImage;
noindex?: boolean;
publishedTime?: Date;
modifiedTime?: Date;
articleTags?: string[];
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
}
const { title, description = 'Unique — a blog about delightful things.', wide = false } = Astro.props;
const {
title,
description = 'A small log of delightful, unique things — products, apps, phenomena, oddities.',
subheader,
lastUpdated,
canonicalPath,
canonicalOverride,
ogType,
ogImage,
noindex,
publishedTime,
modifiedTime,
articleTags,
jsonLd,
} = Astro.props;
const siteName = 'Unique';
const SITE_FOUNDED = new Date(2026, 4, 3);
const stats = await getSiteStats();
const fmtLong = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const fmtShort = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
const updatedDate = lastUpdated ?? stats.lastUpdated;
const year = new Date().getFullYear();
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>{title === siteName ? siteName : `${title} — ${siteName}`}</title>
<SEO
title={title}
description={description}
canonicalPath={canonicalPath}
canonicalOverride={canonicalOverride}
ogType={ogType}
ogImage={ogImage}
noindex={noindex}
publishedTime={publishedTime}
modifiedTime={modifiedTime}
articleTags={articleTags}
jsonLd={jsonLd}
/>
</head>
<body class:list={[{ wide }]}>
<header>
<a href="/" class="brand">{siteName}</a>
<p class="tagline">A small log of delightful things.</p>
<body>
<header class="masthead">
<div class="masthead-row">
<div class="masthead-stats">
<span>{stats.total} entries</span>
<span>founded {fmtShort.format(SITE_FOUNDED)}</span>
</div>
<a href="/" class="masthead-brand">{siteName}</a>
<nav class="masthead-nav" aria-label="Primary">
<a href="/about/">About</a>
<a href="/blog/">Blog</a>
</nav>
</div>
{subheader && (
<div class="subheader">
<p class="subheader-line">{subheader}</p>
{updatedDate && (
<p class="subheader-date">
Updated <time datetime={updatedDate.toISOString()}>{fmtLong.format(updatedDate)}</time>
</p>
)}
</div>
)}
</header>
<main>
<slot />
</main>
<footer>
<p>© {new Date().getFullYear()} Unique</p>
<footer class="site-footer">
<div class="site-footer-cols">
<section>
<h4>Browse</h4>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/grid/">Grid</a></li>
<li><a href="/sources/">Sources</a></li>
</ul>
</section>
<section>
<h4>Collections</h4>
<ul>
<li><a href="/grid/">Reviews ({stats.reviews})</a></li>
<li><a href="/find/">Finds ({stats.finds})</a></li>
<li><a href="/finds/">Lists ({stats.lists})</a></li>
<li><a href="/blog/">Posts ({stats.posts})</a></li>
</ul>
</section>
<section>
<h4>About</h4>
<ul>
<li><a href="/about/">About</a></li>
<li><a href="/blog/">Blog</a></li>
</ul>
</section>
</div>
<div class="site-footer-legal">
<p>
© {year} Unique
<span aria-hidden="true">·</span>
<a href="/legal/">Legal</a>
</p>
</div>
</footer>
<style is:global>
:root {
@@ -61,38 +164,152 @@ const siteName = 'Unique';
body {
max-width: var(--max);
margin: 0 auto;
padding: 2.5rem 1.25rem 4rem;
padding: 1.5rem 1.25rem 2rem;
}
body.wide { max-width: 76rem; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
header {
margin-bottom: 2.5rem;
/* Masthead */
.masthead {
margin-bottom: 2rem;
border-top: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
padding-bottom: 1.25rem;
}
header .brand {
.masthead-row {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 1.5rem;
padding: 1.1rem 0;
}
.masthead-stats {
display: flex;
flex-direction: column;
gap: 0.15rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1.5rem;
color: var(--fg);
letter-spacing: -0.01em;
}
header .tagline {
margin: 0.25rem 0 0;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-size: 0.95rem;
font-style: italic;
}
.masthead-brand {
font-family: ui-serif, Georgia, 'Iowan Old Style', 'Apple Garamond', 'Palatino Linotype', serif;
font-weight: 700;
font-size: clamp(2.4rem, 6vw, 4rem);
line-height: 1;
letter-spacing: -0.02em;
color: var(--fg);
text-align: center;
white-space: nowrap;
}
.masthead-brand:hover { text-decoration: none; color: var(--accent); }
.masthead-nav {
justify-self: end;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.15rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.masthead-nav a { color: var(--muted); }
.masthead-nav a:hover { color: var(--accent); }
.subheader {
text-align: center;
padding: 0.85rem 0 1rem;
border-top: 1px solid var(--rule);
}
.subheader-line {
margin: 0;
font-style: italic;
font-size: 1rem;
color: var(--fg);
}
.subheader-date {
margin: 0.25rem 0 0;
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);
}
@media (max-width: 640px) {
.masthead-row {
grid-template-columns: 1fr;
gap: 0.6rem;
padding: 1rem 0;
text-align: center;
}
.masthead-stats {
flex-direction: row;
justify-content: center;
gap: 0.5rem;
order: 2;
}
.masthead-brand { order: 1; }
.masthead-nav {
justify-self: center;
align-items: center;
order: 3;
}
}
main { min-height: 60vh; }
footer {
/* Site footer */
.site-footer {
margin-top: 4rem;
padding-top: 1.25rem;
padding-top: 1.5rem;
border-top: 1px solid var(--rule);
color: var(--muted);
font-size: 0.85rem;
}
footer p { margin: 0; }
.site-footer-cols {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2.5rem;
margin-bottom: 1.5rem;
text-align: center;
}
.site-footer-cols section {
min-width: 7rem;
}
.site-footer ul { align-items: center; }
.site-footer h4 {
margin: 0 0 0.5rem;
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(--fg);
font-weight: 700;
}
.site-footer ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.site-footer ul a { color: var(--muted); }
.site-footer ul a:hover { color: var(--accent); }
.site-footer-legal {
padding-top: 1rem;
border-top: 1px solid var(--rule);
text-align: center;
}
.site-footer-legal p { margin: 0; }
.site-footer-legal a { color: var(--muted); }
.site-footer-legal a:hover { color: var(--accent); }
.site-footer-legal span { margin: 0 0.4rem; }
h1, h2, h3 {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
letter-spacing: -0.015em;