Build stamp, live listening strip, OG cards, closer shiki ground

- footer: 'built <ts> · <hash> · <n> notes', hash linking to Gitea
  (GARDEN_COMMIT passed in by build.sh; '+' marks dirty-tree builds)
- homepage header: live strip fed by api.c0smere.net/spotify/summary
  (weekly aggregate + monthly top artist/genre), fail-closed like the
  odometer and header quote
- /og/<slug>.png satori+resvg cards for every note + the homepage;
  og:/twitter: meta in Base
- shiki github-dark-default (#0d1117 sits closer to the page ground)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wes
2026-07-21 22:08:53 -04:00
co-authored by Claude Fable 5
parent 3d70e23471
commit 8f48656e91
8 changed files with 722 additions and 4 deletions
+115 -2
View File
@@ -2,6 +2,7 @@
import 'katex/dist/katex.min.css';
import { getCollection } from 'astro:content';
import { buildNavTree } from '../lib/nav.mjs';
import { COMMIT, BUILT_AT } from '../lib/build-info.mjs';
import NavTree from '../components/NavTree.astro';
import Toc from '../components/Toc.astro';
@@ -9,11 +10,18 @@ interface Props {
title: string;
description?: string;
headings?: { depth: number; slug: string; text: string }[];
// which /og/<slug>.png card this page advertises; synthetic listing
// pages fall back to the site card
ogSlug?: string;
}
const { title, description, headings = [] } = Astro.props;
const { title, description, headings = [], ogSlug = 'index' } = Astro.props;
const isHome = Astro.url.pathname === '/';
const navTree = buildNavTree(await getCollection('garden'));
const entries = await getCollection('garden');
const navTree = buildNavTree(entries);
const noteCount = entries.length;
const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
const ogImage = new URL(`/og/${ogSlug}.png`, Astro.site);
const pageUrl = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
@@ -24,6 +32,14 @@ const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
<title>{title}{isHome ? '' : ' // Wesley Ray'}</title>
<meta name="theme-color" content="#0b0d12" />
{description && <meta name="description" content={description} />}
<meta property="og:title" content={title} />
<meta property="og:type" content={isHome ? 'website' : 'article'} />
<meta property="og:url" content={pageUrl} />
{description && <meta property="og:description" content={description} />}
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<link
rel="preload"
href="/fonts/jetbrains-mono-v13-latin-regular.woff2"
@@ -61,6 +77,14 @@ const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
<span id="hq-text"></span>
<span id="hq-attr" class="hq-attr"></span>
</div>
{
isHome && (
<div id="live-stats" class="live-stats" hidden>
<span class="live-dot" aria-hidden="true" />
<span id="live-spotify" />
</div>
)
}
</header>
<main>
<slot />
@@ -85,6 +109,19 @@ const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
ticking at the lifetime average
</p>
</div>
{
COMMIT && (
<p class="build-stamp">
built {BUILT_AT} ET ·{' '}
<a
href={`https://git.c0smere.net/wes/garden-astro/commit/${COMMIT.replace('+', '')}`}
>
{COMMIT}
</a>{' '}
· {noteCount} notes
</p>
)
}
</footer>
</div>
<aside class="sidebar">
@@ -119,6 +156,39 @@ const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
}
</script>
<script>
// Homepage live strip: weekly listening aggregates from the API.
// Fail-closed: any error (or non-home page) leaves the slot hidden.
const strip = document.getElementById('live-stats');
if (strip) {
try {
const res = await fetch('https://api.c0smere.net/spotify/summary');
if (!res.ok) throw new Error(String(res.status));
const s = await res.json();
const slot = document.getElementById('live-spotify')!;
const put = (text: string, strong = false) => {
const el = document.createElement(strong ? 'span' : 'span');
if (strong) el.className = 'v';
el.textContent = text;
slot.append(el);
};
put(String(s.week_minutes), true);
put(' min of listening this week');
if (s.top_artist_month) {
put(' · lately: ');
put(s.top_artist_month, true);
}
if (s.top_genre_month) {
put(' · ');
put(s.top_genre_month, true);
}
strip.hidden = false;
} catch {
/* API down or blocked: strip stays hidden */
}
}
</script>
<script>
// Odometer: fetch totals once, then animate forward at the lifetime
// average rate. Looks live, but deliberately never exposes current
@@ -390,6 +460,38 @@ const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
margin-left: 0.4rem;
font-size: 0.75rem;
}
.live-stats {
display: flex;
align-items: baseline;
gap: 0.55rem;
margin: -0.75rem 0 1.75rem;
padding-bottom: 0.85rem;
border-bottom: 1px solid var(--hair);
font-size: 0.78rem;
color: var(--muted);
}
.live-stats .v {
color: var(--fg);
}
.live-dot {
flex: none;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--link);
align-self: center;
animation: live-pulse 2.4s ease-in-out infinite;
}
@keyframes live-pulse {
50% {
opacity: 0.35;
}
}
@media (prefers-reduced-motion: reduce) {
.live-dot {
animation: none;
}
}
/* ---- headings ---- */
h1, h2, h3, h4, h5, h6 {
@@ -637,6 +739,17 @@ const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
margin-top: 0.35rem;
font-size: 0.72rem;
}
.build-stamp {
margin-top: 1.25rem;
font-size: 0.72rem;
color: hsla(0, 0%, 100%, 0.4);
}
.build-stamp a {
color: inherit;
}
.build-stamp a:hover {
color: var(--link-bright);
}
/* ---- homepage: typographic feed, like the blog's index ---- */
.home main h1 {
+35
View File
@@ -0,0 +1,35 @@
// Build provenance for the footer stamp. Evaluated once per build (module
// scope). The containerized build has no git binary, so deploy/build.sh
// passes GARDEN_COMMIT in; local builds fall back to asking git directly.
// A trailing "+" marks a build from a dirty tree.
import { execSync } from 'node:child_process';
function detectCommit() {
if (process.env.GARDEN_COMMIT) return process.env.GARDEN_COMMIT;
try {
const h = execSync('git rev-parse --short HEAD', {
stdio: ['ignore', 'pipe', 'ignore'],
})
.toString()
.trim();
let dirty = '';
try {
execSync('git diff --quiet', { stdio: 'ignore' });
} catch {
dirty = '+';
}
return h + dirty;
} catch {
return '';
}
}
export const COMMIT = detectCommit();
const fmt = new Intl.DateTimeFormat('en-CA', {
dateStyle: 'short',
timeStyle: 'short',
hour12: false,
timeZone: 'America/New_York',
});
export const BUILT_AT = fmt.format(new Date()).replace(', ', ' ');
+1
View File
@@ -72,6 +72,7 @@ const contentLeadsWithTitle =
title={title}
description={entry?.data.description}
headings={rendered?.headings ?? []}
ogSlug={entry ? slugForId(entry.id) : undefined}
>
{!contentLeadsWithTitle && <h1>{title}</h1>}
{Content && <Content />}
+128
View File
@@ -0,0 +1,128 @@
// Build-time OG images: 1200x630 dark cards matching the site design,
// one per note (plus /og/index.png for the homepage). satori renders an
// element tree to SVG, resvg rasterizes to PNG. Base.astro points
// og:image here via the ogSlug prop.
import fs from 'node:fs';
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import { slugForId } from '../../lib/quartz-compat.mjs';
const FONT_DIR = 'node_modules/@fontsource/jetbrains-mono/files';
const mono400 = fs.readFileSync(`${FONT_DIR}/jetbrains-mono-latin-400-normal.woff`);
const mono800 = fs.readFileSync(`${FONT_DIR}/jetbrains-mono-latin-800-normal.woff`);
export async function getStaticPaths() {
const entries = await getCollection('garden', (e: any) => !e.data.draft);
return entries.map((e: any) => {
const isIndex = e.id === 'index';
const title = isIndex
? 'Wesley Ray'
: (e.data.title ?? e.id.split('/').pop());
const dir = e.id.includes('/') ? e.id.slice(0, e.id.lastIndexOf('/')) : '';
return {
params: { slug: isIndex ? 'index' : slugForId(e.id) },
props: {
title,
dir,
subtitle: isIndex ? 'projects, notes, and experiments' : dir,
},
};
});
}
export const GET: APIRoute = async ({ props }) => {
const { title, subtitle } = props as { title: string; subtitle: string };
const size = title.length > 55 ? 44 : title.length > 32 ? 56 : 68;
const svg = await satori(
{
type: 'div',
props: {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#0b0d12',
borderLeft: '14px solid #218107',
padding: '64px 72px',
fontFamily: 'JetBrains Mono',
color: '#ffffff',
},
children: [
{
type: 'div',
props: {
style: {
display: 'flex',
fontSize: '26px',
color: 'rgba(255,255,255,0.64)',
letterSpacing: '2px',
},
children: 'the garden // Wesley Ray',
},
},
{
type: 'div',
props: {
style: {
display: 'flex',
fontSize: `${size}px`,
fontWeight: 800,
lineHeight: 1.25,
letterSpacing: '-1px',
},
children: title,
},
},
{
type: 'div',
props: {
style: {
display: 'flex',
justifyContent: 'space-between',
fontSize: '26px',
},
children: [
{
type: 'div',
props: {
style: {
display: 'flex',
color: 'rgba(255,255,255,0.64)',
},
children: subtitle || ' ',
},
},
{
type: 'div',
props: {
style: { display: 'flex', color: '#2fa50f' },
children: 'garden.c0smere.net',
},
},
],
},
},
],
},
},
{
width: 1200,
height: 630,
fonts: [
{ name: 'JetBrains Mono', data: mono400, weight: 400, style: 'normal' },
{ name: 'JetBrains Mono', data: mono800, weight: 800, style: 'normal' },
],
},
);
const png = new Resvg(svg, {
fitTo: { mode: 'width', value: 1200 },
}).render().asPng();
return new Response(png, { headers: { 'Content-Type': 'image/png' } });
};