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