From 384ca71f899fd8669be9c947bbbf7d353df9d3d6 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 18:23:24 +0000 Subject: [PATCH 01/10] refactor: deduplicates our sorting function --- www/src/lib/format.ts | 15 +++++++++------ www/src/lib/md.ts | 14 +++----------- www/src/lib/txt.ts | 4 ++-- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/www/src/lib/format.ts b/www/src/lib/format.ts index cd07266..41468a2 100644 --- a/www/src/lib/format.ts +++ b/www/src/lib/format.ts @@ -30,10 +30,11 @@ export function formatListItem( date: Date, url: string, title: string, - options?: { pinned?: boolean } + options?: { pinned?: boolean; suffix?: string } ): string { const pinnedBadge = options?.pinned ? ' [pinned]' : ''; - return `${formatDate(date)}${title}${pinnedBadge}`; + const suffix = options?.suffix ? ` ${options.suffix}` : ''; + return `${formatDate(date)}${title}${pinnedBadge}${suffix}`; } interface Sortable { @@ -41,10 +42,12 @@ interface Sortable { pinned?: boolean; } -export function sortByPinnedThenDate(items: T[]): T[] { +export function sortEntries(items: T[], key?: (item: T) => Sortable): T[] { + const get = key ?? (item => item as unknown as Sortable); return items.slice().sort((a, b) => { - if (a.pinned && !b.pinned) return -1; - if (!a.pinned && b.pinned) return 1; - return b.date.getTime() - a.date.getTime(); + const ak = get(a), bk = get(b); + if (ak.pinned && !bk.pinned) return -1; + if (!ak.pinned && bk.pinned) return 1; + return bk.date.getTime() - ak.date.getTime(); }); } diff --git a/www/src/lib/md.ts b/www/src/lib/md.ts index 378c2c6..eb1a1e0 100644 --- a/www/src/lib/md.ts +++ b/www/src/lib/md.ts @@ -1,5 +1,6 @@ import type { CollectionEntry } from 'astro:content'; import { DEFAULT_CATEGORY } from './consts'; +import { sortEntries } from './format'; type Post = CollectionEntry<'md'>; @@ -8,15 +9,6 @@ export function getSlug(postId: string): string { return parts[parts.length - 1]; } -function sortPosts(posts: Post[], { alphabetically = false } = {}): Post[] { - return posts.slice().sort((a, b) => { - if (a.data.pinned && !b.data.pinned) return -1; - if (!a.data.pinned && b.data.pinned) return 1; - if (alphabetically) return a.data.title.localeCompare(b.data.title); - return b.data.date.getTime() - a.data.date.getTime(); - }); -} - export function resolveRelatedPosts( slugs: string[], allPosts: T[], @@ -25,7 +17,7 @@ export function resolveRelatedPosts( return slugs.flatMap(s => bySlug.get(s) ?? []); } -export function organizePostsByCategory(posts: Post[], { sortAlphabetically = false } = {}): { +export function organizePostsByCategory(posts: Post[]): { grouped: Record; categories: string[]; } { @@ -43,7 +35,7 @@ export function organizePostsByCategory(posts: Post[], { sortAlphabetically = fa }); for (const category of categories) { - grouped[category] = sortPosts(grouped[category], { alphabetically: sortAlphabetically }); + grouped[category] = sortEntries(grouped[category], p => p.data); } return { grouped, categories }; diff --git a/www/src/lib/txt.ts b/www/src/lib/txt.ts index ee03984..2f0dea4 100644 --- a/www/src/lib/txt.ts +++ b/www/src/lib/txt.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import yaml from 'js-yaml'; -import { sortByPinnedThenDate } from './format'; +import { sortEntries } from './format'; export interface TxtFile { name: string; @@ -44,6 +44,6 @@ export function getTxtFiles(): TxtFile[] { pinned: pinnedSet.has(name), description: descriptions[name], })); - return sortByPinnedThenDate(files); + return sortEntries(files); } From 5cc122bf391145f8d78360179553a4682ccabe28 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 18:26:40 +0000 Subject: [PATCH 02/10] refactor: cleans up authentication duplication between page and api routes --- www/src/lib/api.ts | 9 --------- www/src/lib/auth.ts | 29 +++++++++++++---------------- www/src/pages/admin.astro | 10 ++++++---- www/src/pages/api/deploy.ts | 17 ++++++----------- www/src/pages/api/guestbook/[id].ts | 22 ++++++++-------------- 5 files changed, 33 insertions(+), 54 deletions(-) diff --git a/www/src/lib/api.ts b/www/src/lib/api.ts index 1961a01..6e4622e 100644 --- a/www/src/lib/api.ts +++ b/www/src/lib/api.ts @@ -1,5 +1,3 @@ -import { isAdmin } from './auth'; - export function jsonResponse(data: unknown, status = 200): Response { return new Response(JSON.stringify(data), { status, @@ -10,10 +8,3 @@ export function jsonResponse(data: unknown, status = 200): Response { export function errorResponse(message: string, status: number): Response { return jsonResponse({ error: message }, status); } - -export function requireAdmin(session: { user?: { id?: string } } | null): Response | null { - if (!session?.user?.id || !isAdmin(session.user.id)) { - return errorResponse('Unauthorized', 403); - } - return null; -} diff --git a/www/src/lib/auth.ts b/www/src/lib/auth.ts index 28b0d8b..eaac841 100644 --- a/www/src/lib/auth.ts +++ b/www/src/lib/auth.ts @@ -1,29 +1,26 @@ import { getSession } from 'auth-astro/server'; -type Session = { user?: { id?: string; name?: string | null } }; +export type Session = { user?: { id?: string; name?: string | null } }; -export function isAdmin(userId: string | undefined): boolean { - return userId === import.meta.env.ADMIN_GITHUB_ID; -} +export type AuthResult = + | { status: 'admin'; session: Session } + | { status: 'unauthenticated' } + | { status: 'forbidden' } + | { status: 'error' }; -export async function requireAdminSession(request: Request): Promise< - | { session: Session; error: null } - | { session: null; error: Response | null } -> { +export async function getAdminSession(request: Request): Promise { let session: Session | null; try { session = await getSession(request); } catch { - return { session: null, error: new Response('Auth not configured', { status: 500 }) }; + return { status: 'error' }; } - if (!session) { - return { session: null, error: null }; + if (!session) return { status: 'unauthenticated' }; + + if (session.user?.id !== import.meta.env.ADMIN_GITHUB_ID) { + return { status: 'forbidden' }; } - if (!isAdmin(session.user?.id)) { - return { session: null, error: new Response('Forbidden', { status: 403 }) }; - } - - return { session, error: null }; + return { status: 'admin', session }; } diff --git a/www/src/pages/admin.astro b/www/src/pages/admin.astro index f540c6c..66ff49c 100644 --- a/www/src/pages/admin.astro +++ b/www/src/pages/admin.astro @@ -2,13 +2,15 @@ export const prerender = false; import { getPendingEntries, type GuestbookEntry } from '../lib/db'; -import { requireAdminSession } from '../lib/auth'; +import { getAdminSession } from '../lib/auth'; import Layout from '../layouts/Layout.astro'; import { formatDate } from '../lib/format'; -const { session, error } = await requireAdminSession(Astro.request); -if (error) return error; -if (!session) return Astro.redirect('/api/auth/signin'); +const auth = await getAdminSession(Astro.request); +if (auth.status === 'error') return new Response('Auth not configured', { status: 500 }); +if (auth.status === 'unauthenticated') return Astro.redirect('/api/auth/signin'); +if (auth.status !== 'admin') return new Response('Forbidden', { status: 403 }); +const { session } = auth; let entries: GuestbookEntry[] = []; try { diff --git a/www/src/pages/api/deploy.ts b/www/src/pages/api/deploy.ts index 57ad7ec..c6f7ab4 100644 --- a/www/src/pages/api/deploy.ts +++ b/www/src/pages/api/deploy.ts @@ -1,23 +1,18 @@ import type { APIRoute } from 'astro'; -import { getSession } from 'auth-astro/server'; -import { jsonResponse, errorResponse, requireAdmin } from '../../lib/api'; +import { jsonResponse, errorResponse } from '../../lib/api'; +import { getAdminSession } from '../../lib/auth'; export const prerender = false; export const POST: APIRoute = async ({ request }) => { - const session = await getSession(request); - const authError = requireAdmin(session); - if (authError) return authError; + const auth = await getAdminSession(request); + if (auth.status !== 'admin') return errorResponse('Unauthorized', 403); const hookUrl = import.meta.env.VERCEL_DEPLOY_HOOK; - if (!hookUrl) { - return errorResponse('Deploy hook not configured', 500); - } + if (!hookUrl) return errorResponse('Deploy hook not configured', 500); const res = await fetch(hookUrl, { method: 'POST' }); - if (!res.ok) { - return errorResponse('Failed to trigger deploy', 502); - } + if (!res.ok) return errorResponse('Failed to trigger deploy', 502); return jsonResponse({ success: true }); }; diff --git a/www/src/pages/api/guestbook/[id].ts b/www/src/pages/api/guestbook/[id].ts index a8a91a6..32d92d9 100644 --- a/www/src/pages/api/guestbook/[id].ts +++ b/www/src/pages/api/guestbook/[id].ts @@ -1,33 +1,27 @@ import type { APIRoute } from 'astro'; -import { getSession } from 'auth-astro/server'; import { approveEntry, deleteEntry } from '../../../lib/db'; -import { jsonResponse, errorResponse, requireAdmin } from '../../../lib/api'; +import { jsonResponse, errorResponse } from '../../../lib/api'; +import { getAdminSession } from '../../../lib/auth'; export const prerender = false; export const PATCH: APIRoute = async ({ params, request }) => { - const session = await getSession(request); - const authError = requireAdmin(session); - if (authError) return authError; + const auth = await getAdminSession(request); + if (auth.status !== 'admin') return errorResponse('Unauthorized', 403); const id = parseInt(params.id!, 10); - if (isNaN(id)) { - return errorResponse('Invalid ID', 400); - } + if (isNaN(id)) return errorResponse('Invalid ID', 400); await approveEntry(id); return jsonResponse({ success: true }); }; export const DELETE: APIRoute = async ({ params, request }) => { - const session = await getSession(request); - const authError = requireAdmin(session); - if (authError) return authError; + const auth = await getAdminSession(request); + if (auth.status !== 'admin') return errorResponse('Unauthorized', 403); const id = parseInt(params.id!, 10); - if (isNaN(id)) { - return errorResponse('Invalid ID', 400); - } + if (isNaN(id)) return errorResponse('Invalid ID', 400); await deleteEntry(id); return jsonResponse({ success: true }); From c647fd62c3faba14023e2163f1137d2d2bd36235 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 18:29:00 +0000 Subject: [PATCH 03/10] refactor: Post type --- www/src/lib/md.ts | 2 +- www/src/pages/[slug].astro | 4 ++-- www/src/pages/feed.xml.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/www/src/lib/md.ts b/www/src/lib/md.ts index eb1a1e0..4c5cb5b 100644 --- a/www/src/lib/md.ts +++ b/www/src/lib/md.ts @@ -2,7 +2,7 @@ import type { CollectionEntry } from 'astro:content'; import { DEFAULT_CATEGORY } from './consts'; import { sortEntries } from './format'; -type Post = CollectionEntry<'md'>; +export type Post = CollectionEntry<'md'> & { body?: string }; export function getSlug(postId: string): string { const parts = postId.split('/'); diff --git a/www/src/pages/[slug].astro b/www/src/pages/[slug].astro index c2a81a8..58fa84a 100644 --- a/www/src/pages/[slug].astro +++ b/www/src/pages/[slug].astro @@ -2,7 +2,7 @@ import { getCollection, render } from 'astro:content'; import Layout from '../layouts/Layout.astro'; import { formatDate, formatListItem, excerpt } from '../lib/format'; -import { getSlug, resolveRelatedPosts } from '../lib/md'; +import { getSlug, resolveRelatedPosts, type Post } from '../lib/md'; export async function getStaticPaths() { const allPosts = await getCollection('md'); @@ -15,7 +15,7 @@ export async function getStaticPaths() { const { post, allPosts } = Astro.props; const { Content } = await render(post); const related = post.data.related ? resolveRelatedPosts(post.data.related, allPosts) : []; -const description = excerpt((post as any).body) || undefined; +const description = excerpt((post as Post).body) || undefined; --- diff --git a/www/src/pages/feed.xml.ts b/www/src/pages/feed.xml.ts index d350661..417ebd6 100644 --- a/www/src/pages/feed.xml.ts +++ b/www/src/pages/feed.xml.ts @@ -1,7 +1,7 @@ import rss from '@astrojs/rss'; import { getCollection } from 'astro:content'; import type { APIContext } from 'astro'; -import { getSlug } from '../lib/md'; +import { getSlug, type Post } from '../lib/md'; import { getTxtFiles } from '../lib/txt'; import { excerpt } from '../lib/format'; @@ -14,7 +14,7 @@ export async function GET(context: APIContext) { title: post.data.title, pubDate: post.data.date, link: `/${getSlug(post.id)}`, - description: excerpt((post as any).body) || post.data.title, + description: excerpt((post as Post).body) || post.data.title, })), ...txtFiles.map(txt => ({ title: txt.name, From 20811f107ba31c61ee2bebafd1d2a3408f9f647c Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 19:09:39 +0000 Subject: [PATCH 04/10] feat: removed pins, and added right-aligned suffixes --- www/content/hello.md | 1 - www/public/config.yaml | 1 - www/src/content.config.ts | 3 +-- www/src/lib/format.ts | 39 +++++++++++++++++++++++++++----------- www/src/lib/txt.ts | 4 ---- www/src/pages/[slug].astro | 4 ++-- www/src/pages/index.astro | 8 ++++---- www/src/styles/global.css | 16 ++++++++++++++++ 8 files changed, 51 insertions(+), 25 deletions(-) diff --git a/www/content/hello.md b/www/content/hello.md index 4ef35ee..255e402 100644 --- a/www/content/hello.md +++ b/www/content/hello.md @@ -1,7 +1,6 @@ --- title: hello date: 2023-02-26 -pinned: true --- i've always had some sort of homepage. it was originally on bebo, then that died and i didn't ever get into other social media, so i made a website diff --git a/www/public/config.yaml b/www/public/config.yaml index 2b7aa3a..c1c9ffa 100644 --- a/www/public/config.yaml +++ b/www/public/config.yaml @@ -1,4 +1,3 @@ -pinned: [] descriptions: cv.txt: curriculum vitae now.txt: what i'm doing now diff --git a/www/src/content.config.ts b/www/src/content.config.ts index 6499a0a..914dbec 100644 --- a/www/src/content.config.ts +++ b/www/src/content.config.ts @@ -3,13 +3,12 @@ import { glob, file } from 'astro/loaders'; import { z } from 'astro/zod'; import yaml from 'js-yaml'; -const md = defineCollection({ +const posts = defineCollection({ loader: glob({ pattern: '**/*.md', base: './content' }), schema: z.object({ title: z.string(), date: z.coerce.date(), updated: z.coerce.date().optional(), - pinned: z.boolean().optional(), category: z.string().optional(), related: z.array(z.string()).optional(), }) diff --git a/www/src/lib/format.ts b/www/src/lib/format.ts index 41468a2..9257bde 100644 --- a/www/src/lib/format.ts +++ b/www/src/lib/format.ts @@ -26,28 +26,45 @@ export function formatDate(date: Date): string { return `${d}/${m}/${y}`; } +export function wordCount(markdown: string | undefined): string { + if (!markdown) return ''; + const words = markdown + .replace(/^---[\s\S]*?---/m, '') + .replace(/^#+\s+.*$/gm, '') + .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/[*_~`]/g, '') + .replace(/:[a-z]+\[([^\]]*)\]/g, '$1') + .trim() + .split(/\s+/) + .filter(Boolean).length; + if (words < 100) return `${words} words`; + const mins = Math.ceil(words / 200); + return `${mins} min`; +} + +export function extractDomain(url: string): string { + try { + return new URL(url).hostname.replace(/^www\./, ''); + } catch { + return ''; + } +} + export function formatListItem( date: Date, url: string, title: string, - options?: { pinned?: boolean; suffix?: string } + options?: { suffix?: string } ): string { - const pinnedBadge = options?.pinned ? ' [pinned]' : ''; - const suffix = options?.suffix ? ` ${options.suffix}` : ''; - return `${formatDate(date)}${title}${pinnedBadge}${suffix}`; + const suffixHtml = options?.suffix ? `${options.suffix}` : ''; + return `${formatDate(date)}${title}${suffixHtml}`; } interface Sortable { date: Date; - pinned?: boolean; } export function sortEntries(items: T[], key?: (item: T) => Sortable): T[] { const get = key ?? (item => item as unknown as Sortable); - return items.slice().sort((a, b) => { - const ak = get(a), bk = get(b); - if (ak.pinned && !bk.pinned) return -1; - if (!ak.pinned && bk.pinned) return 1; - return bk.date.getTime() - ak.date.getTime(); - }); + return items.slice().sort((a, b) => get(b).date.getTime() - get(a).date.getTime()); } diff --git a/www/src/lib/txt.ts b/www/src/lib/txt.ts index 2f0dea4..ad13389 100644 --- a/www/src/lib/txt.ts +++ b/www/src/lib/txt.ts @@ -6,12 +6,10 @@ import { sortEntries } from './format'; export interface TxtFile { name: string; date: Date; - pinned: boolean; description?: string; } export interface TxtConfig { - pinned?: string[]; descriptions?: Record; dates?: Record; } @@ -32,7 +30,6 @@ export function getTxtFiles(): TxtFile[] { if (!fs.existsSync(txtDir)) return []; const config = loadTxtConfig(); - const pinnedSet = new Set(config.pinned || []); const descriptions = config.descriptions || {}; const dates = config.dates || {}; @@ -41,7 +38,6 @@ export function getTxtFiles(): TxtFile[] { .map(name => ({ name, date: dates[name] ? new Date(dates[name]) : new Date(0), - pinned: pinnedSet.has(name), description: descriptions[name], })); return sortEntries(files); diff --git a/www/src/pages/[slug].astro b/www/src/pages/[slug].astro index 58fa84a..4f1c6a5 100644 --- a/www/src/pages/[slug].astro +++ b/www/src/pages/[slug].astro @@ -1,7 +1,7 @@ --- import { getCollection, render } from 'astro:content'; import Layout from '../layouts/Layout.astro'; -import { formatDate, formatListItem, excerpt } from '../lib/format'; +import { formatDate, formatListItem, excerpt, wordCount } from '../lib/format'; import { getSlug, resolveRelatedPosts, type Post } from '../lib/md'; export async function getStaticPaths() { @@ -21,7 +21,7 @@ const description = excerpt((post as Post).body) || undefined;

{post.data.title}

-

{formatDate(post.data.date)}{post.data.updated && ` (updated ${formatDate(post.data.updated)})`}

+

{formatDate(post.data.date)}{post.data.updated && ` (updated ${formatDate(post.data.updated)})`} · {wordCount((post as Post).body)}{post.data.category && ` · ${post.data.category}`}

{related.length > 0 && ( diff --git a/www/src/pages/index.astro b/www/src/pages/index.astro index 75f592f..59bf463 100644 --- a/www/src/pages/index.astro +++ b/www/src/pages/index.astro @@ -2,7 +2,7 @@ import { getCollection } from 'astro:content'; import Layout from '../layouts/Layout.astro'; import { getApprovedEntries, type GuestbookEntry } from '../lib/db'; -import { formatDate, formatListItem, escapeHtml } from '../lib/format'; +import { formatDate, formatListItem, extractDomain, wordCount, escapeHtml } from '../lib/format'; import { organizePostsByCategory, getSlug } from '../lib/md'; import { getTxtFiles } from '../lib/txt'; import { DEFAULT_CATEGORY, SECTIONS, SUBDOMAINS } from '../lib/consts'; @@ -37,7 +37,7 @@ const urls = [
{!isDefault && }
- `${formatListItem(post.data.date, `/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })}` + `${formatListItem(post.data.date, `/${getSlug(post.id)}`, post.data.title, { suffix: wordCount(post.body) })}` ).join('')} />
); @@ -47,14 +47,14 @@ const urls = [
{ const name = f.name.replace(/\.txt$/, ''); - return `${formatListItem(f.date, `/${f.name}`, name, { pinned: f.pinned })}`; + return `${formatListItem(f.date, `/${f.name}`, name, { suffix: f.description })}`; }).join('')} />
- `${formatListItem(b.data.date, b.data.url, b.data.title)}` + `${formatListItem(b.data.date, b.data.url, b.data.title, { suffix: extractDomain(b.data.url) })}` ).join('')} />
diff --git a/www/src/styles/global.css b/www/src/styles/global.css index 4332b9c..3a9212b 100644 --- a/www/src/styles/global.css +++ b/www/src/styles/global.css @@ -116,11 +116,27 @@ section pre { } .entry-content { + display: flex; overflow: hidden; white-space: nowrap; +} + +.entry-content > a { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; text-overflow: ellipsis; } +.entry-suffix { + flex: 1 10000 0%; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + text-align: right; + padding-left: 1ch; +} + .guestbook-entries { font-family: monospace; white-space: pre; From e4052fc14514dd75e290a07f67852315bfadaed4 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 19:11:57 +0000 Subject: [PATCH 05/10] refactor: renamed md to posts, and plaintext to files --- www/src/content.config.ts | 2 +- www/src/lib/consts.ts | 2 +- www/src/lib/{md.ts => posts.ts} | 2 +- www/src/pages/[slug].astro | 4 ++-- www/src/pages/feed.xml.ts | 4 ++-- www/src/pages/index.astro | 8 ++++---- www/src/pages/sitemap.txt.ts | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) rename www/src/lib/{md.ts => posts.ts} (94%) diff --git a/www/src/content.config.ts b/www/src/content.config.ts index 914dbec..6efc211 100644 --- a/www/src/content.config.ts +++ b/www/src/content.config.ts @@ -28,4 +28,4 @@ const bookmarks = defineCollection({ }) }); -export const collections = { md, bookmarks }; +export const collections = { posts, bookmarks }; diff --git a/www/src/lib/consts.ts b/www/src/lib/consts.ts index a19834c..a2e8d87 100644 --- a/www/src/lib/consts.ts +++ b/www/src/lib/consts.ts @@ -6,7 +6,7 @@ export const SUBDOMAINS = [ ]; export const SECTIONS = { - plaintext: 'plaintext', + files: 'files', bookmarks: 'bookmarks', guestbook: 'guestbook', } as const; diff --git a/www/src/lib/md.ts b/www/src/lib/posts.ts similarity index 94% rename from www/src/lib/md.ts rename to www/src/lib/posts.ts index 4c5cb5b..6b44477 100644 --- a/www/src/lib/md.ts +++ b/www/src/lib/posts.ts @@ -2,7 +2,7 @@ import type { CollectionEntry } from 'astro:content'; import { DEFAULT_CATEGORY } from './consts'; import { sortEntries } from './format'; -export type Post = CollectionEntry<'md'> & { body?: string }; +export type Post = CollectionEntry<'posts'> & { body?: string }; export function getSlug(postId: string): string { const parts = postId.split('/'); diff --git a/www/src/pages/[slug].astro b/www/src/pages/[slug].astro index 4f1c6a5..5f92dde 100644 --- a/www/src/pages/[slug].astro +++ b/www/src/pages/[slug].astro @@ -2,10 +2,10 @@ import { getCollection, render } from 'astro:content'; import Layout from '../layouts/Layout.astro'; import { formatDate, formatListItem, excerpt, wordCount } from '../lib/format'; -import { getSlug, resolveRelatedPosts, type Post } from '../lib/md'; +import { getSlug, resolveRelatedPosts, type Post } from '../lib/posts'; export async function getStaticPaths() { - const allPosts = await getCollection('md'); + const allPosts = await getCollection('posts'); return allPosts.map(post => ({ params: { slug: getSlug(post.id) }, props: { post, allPosts } diff --git a/www/src/pages/feed.xml.ts b/www/src/pages/feed.xml.ts index 417ebd6..1b26c9b 100644 --- a/www/src/pages/feed.xml.ts +++ b/www/src/pages/feed.xml.ts @@ -1,12 +1,12 @@ import rss from '@astrojs/rss'; import { getCollection } from 'astro:content'; import type { APIContext } from 'astro'; -import { getSlug, type Post } from '../lib/md'; +import { getSlug, type Post } from '../lib/posts'; import { getTxtFiles } from '../lib/txt'; import { excerpt } from '../lib/format'; export async function GET(context: APIContext) { - const posts = await getCollection('md'); + const posts = await getCollection('posts'); const txtFiles = getTxtFiles(); const items = [ diff --git a/www/src/pages/index.astro b/www/src/pages/index.astro index 59bf463..9f6f8ef 100644 --- a/www/src/pages/index.astro +++ b/www/src/pages/index.astro @@ -3,11 +3,11 @@ import { getCollection } from 'astro:content'; import Layout from '../layouts/Layout.astro'; import { getApprovedEntries, type GuestbookEntry } from '../lib/db'; import { formatDate, formatListItem, extractDomain, wordCount, escapeHtml } from '../lib/format'; -import { organizePostsByCategory, getSlug } from '../lib/md'; +import { organizePostsByCategory, getSlug } from '../lib/posts'; import { getTxtFiles } from '../lib/txt'; import { DEFAULT_CATEGORY, SECTIONS, SUBDOMAINS } from '../lib/consts'; -const posts = await getCollection('md'); +const posts = await getCollection('posts'); const { grouped, categories: sortedCategories } = organizePostsByCategory(posts); const bookmarksCollection = await getCollection('bookmarks'); @@ -43,8 +43,8 @@ const urls = [ ); })} -
- +
+
{ const name = f.name.replace(/\.txt$/, ''); return `${formatListItem(f.date, `/${f.name}`, name, { suffix: f.description })}`; diff --git a/www/src/pages/sitemap.txt.ts b/www/src/pages/sitemap.txt.ts index 2121a21..634b568 100644 --- a/www/src/pages/sitemap.txt.ts +++ b/www/src/pages/sitemap.txt.ts @@ -1,12 +1,12 @@ import { getCollection } from 'astro:content'; import type { APIContext } from 'astro'; -import { getSlug } from '../lib/md'; +import { getSlug } from '../lib/posts'; import { getTxtFiles } from '../lib/txt'; import { SUBDOMAINS } from '../lib/consts'; export async function GET(context: APIContext) { const site = context.site?.origin ?? 'https://wynne.rs'; - const posts = await getCollection('md'); + const posts = await getCollection('posts'); const txtFiles = getTxtFiles().map(f => f.name); const urls = [ From 3809e7c9ddce32d6ab6067577a20570d3b75ec12 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 19:37:53 +0000 Subject: [PATCH 06/10] refactor: header alignment --- www/src/layouts/Layout.astro | 4 ++-- www/src/styles/global.css | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/www/src/layouts/Layout.astro b/www/src/layouts/Layout.astro index 4f59fe1..5cea85d 100644 --- a/www/src/layouts/Layout.astro +++ b/www/src/layouts/Layout.astro @@ -9,7 +9,7 @@ interface Props { urls?: string[]; } -const { title, description = 'personal website of lewis m.w.', showHeader = true, isHome = false, urls = [] } = Astro.props; +const { title, description = 'personal website of ' + title, showHeader = true, isHome = false, urls = [] } = Astro.props; --- @@ -28,7 +28,7 @@ const { title, description = 'personal website of lewis m.w.', showHeader = true {showHeader && (
-
{isHome ? lewis m.w.lewis m.w. : lewis m.w.} mail gh rss sitemap random newest
+ {isHome ? {title}{title} : lewis m.w.}mail gh rss sitemap random newest
)} diff --git a/www/src/styles/global.css b/www/src/styles/global.css index 3a9212b..1da1ddc 100644 --- a/www/src/styles/global.css +++ b/www/src/styles/global.css @@ -98,6 +98,22 @@ html[data-has] .guestbook-form { display: none; } +header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 2ch; + font-family: monospace; +} + +.header-name { + white-space: nowrap; +} + +.header-links { + text-align: right; +} + section pre { margin: 0; } From 917ef06879ded97765ca615ed555390f648f7e9a Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 20:38:48 +0000 Subject: [PATCH 07/10] feat: style changes: the death of monospace --- www/content/hello.md | 2 +- www/src/styles/global.css | 34 +++++++++++++--------------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/www/content/hello.md b/www/content/hello.md index 255e402..72cdeaf 100644 --- a/www/content/hello.md +++ b/www/content/hello.md @@ -1,6 +1,6 @@ --- title: hello -date: 2023-02-26 +date: 2026-02-26 --- i've always had some sort of homepage. it was originally on bebo, then that died and i didn't ever get into other social media, so i made a website diff --git a/www/src/styles/global.css b/www/src/styles/global.css index 1da1ddc..00a1bec 100644 --- a/www/src/styles/global.css +++ b/www/src/styles/global.css @@ -1,9 +1,10 @@ body { box-sizing: border-box; - max-width: 48rem; + max-width: 34rem; margin: 0 auto; padding: 1rem; text-align: justify; + font-family: 'Times New Roman', serif; } img { @@ -17,6 +18,7 @@ h1, h2, h3, h4, h5, h6 { .muted { color: #888; + font-size: 0.9rem; } .left, .right { @@ -24,7 +26,7 @@ h1, h2, h3, h4, h5, h6 { font-size: 0.9rem; } -@media (min-width: 63rem) { +@media (min-width: 58rem) { .left, .right { display: inline; position: relative; @@ -78,9 +80,6 @@ section { margin: 1rem 0; } -section .section-label { - font-family: monospace; -} .home-name-link { display: none; @@ -102,8 +101,7 @@ header { display: flex; justify-content: space-between; align-items: baseline; - gap: 2ch; - font-family: monospace; + gap: 2rem; } .header-name { @@ -119,15 +117,13 @@ section pre { } .entry-list { - columns: 2 24ch; - column-gap: 3ch; - font-family: monospace; margin: 0; } .entry { display: grid; - grid-template-columns: 10ch 1fr; + grid-template-columns: 4rem 1fr; + align-items: baseline; break-inside: avoid; } @@ -150,27 +146,23 @@ section pre { overflow: hidden; text-overflow: ellipsis; text-align: right; - padding-left: 1ch; + padding-left: 0.5rem; } .guestbook-entries { - font-family: monospace; - white-space: pre; + margin: 0; } .guestbook-entry { display: grid; - grid-template-columns: 10ch 1fr; -} - -.guestbook-entry > span:last-child { - white-space: normal; + grid-template-columns: 4rem 1fr; + align-items: baseline; + break-inside: avoid; } .guestbook-form { margin-top: 0.5rem; - margin-left: 10ch; - font-family: monospace; + margin-left: 4rem; } html[data-compact] .list-meta { From 30212a2eaf6abc32059c402dfc2144ecab1a0b02 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 20:44:14 +0000 Subject: [PATCH 08/10] feat: adds to changelog --- www/public/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/www/public/changelog.txt b/www/public/changelog.txt index a1b4dbe..5882854 100644 --- a/www/public/changelog.txt +++ b/www/public/changelog.txt @@ -1,3 +1,4 @@ +2026-03-27 - narrower layout (34rem), single-column entries, serif font, smaller muted text 2026-03-26 - inline section labels, compact layout 2026-02-07 - related posts ! 2026-01-31 - text files now live at cleaner URLs (/*.txt instead of /txt/*.txt) From 66360b9c7a3fd2d85764f2ef3cbe386b7b97010c Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 21:16:18 +0000 Subject: [PATCH 09/10] feat: adds find button to header --- www/public/js/params.js | 26 ++++++++++++++++++++++---- www/src/layouts/Layout.astro | 13 ++++++++++++- www/src/styles/global.css | 12 ------------ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/www/public/js/params.js b/www/public/js/params.js index c466e18..01fbf1b 100644 --- a/www/public/js/params.js +++ b/www/public/js/params.js @@ -4,8 +4,7 @@ var just = p.get('just'); if (just && /^[a-z0-9-]+$/.test(just)) { document.documentElement.dataset.just = just; - var css = 'section[data-section]:not([data-section="' + just + '"]){display:none}' - + ' section[data-section="' + just + '"] .section-label{pointer-events:none;text-decoration:none;color:inherit}'; + var css = 'section[data-section]:not([data-section="' + just + '"]){display:none}'; document.head.appendChild(Object.assign(document.createElement('style'), { textContent: css })); } @@ -30,7 +29,10 @@ if (has) { document.documentElement.dataset.has = has; has = has.toLowerCase(); - document.addEventListener('DOMContentLoaded', function() { + } + + document.addEventListener('DOMContentLoaded', function() { + if (has) { document.querySelectorAll('section[data-section] .entry').forEach(function(entry) { if (entry.textContent.toLowerCase().indexOf(has) === -1) { entry.style.display = 'none'; @@ -41,6 +43,22 @@ entry.style.display = 'none'; } }); + } + + document.querySelectorAll('.section-label').forEach(function(a) { + var link = new URLSearchParams(a.search); + p.forEach(function(v, k) { if (!link.has(k)) link.set(k, v); }); + a.href = '?' + link.toString(); }); - } + + var find = document.getElementById('find'); + if (find) find.addEventListener('click', function(e) { + e.preventDefault(); + var term = prompt('find:'); + if (!term) return; + var q = new URLSearchParams(location.search); + q.set('has', term); + location.search = q.toString(); + }); + }); }(); diff --git a/www/src/layouts/Layout.astro b/www/src/layouts/Layout.astro index 5cea85d..ee442c2 100644 --- a/www/src/layouts/Layout.astro +++ b/www/src/layouts/Layout.astro @@ -28,7 +28,18 @@ const { title, description = 'personal website of ' + title, showHeader = true, {showHeader && (
- {isHome ? {title}{title} : lewis m.w.}mail gh rss sitemap random newest + + {isHome ? title : 'lewis m.w.'} + + + mail + gh + rss + sitemap + random + newest + find +
)} diff --git a/www/src/styles/global.css b/www/src/styles/global.css index 00a1bec..4e595c6 100644 --- a/www/src/styles/global.css +++ b/www/src/styles/global.css @@ -81,18 +81,6 @@ section { } -.home-name-link { - display: none; -} - -html[data-just] .home-name { - display: none; -} - -html[data-just] .home-name-link { - display: inline; -} - html[data-has] .guestbook-form { display: none; } From f2acf367848a6c04296579861c0dd8f096ba2567 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 27 Mar 2026 21:37:59 +0000 Subject: [PATCH 10/10] revert: gh workflow/validation - already covered by building --- .github/workflows/validate.yml | 25 ------------------- package.json | 3 +-- www/package.json | 3 +-- www/scripts/validate-content.js | 43 --------------------------------- 4 files changed, 2 insertions(+), 72 deletions(-) delete mode 100644 .github/workflows/validate.yml delete mode 100644 www/scripts/validate-content.js diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml deleted file mode 100644 index 3535d88..0000000 --- a/.github/workflows/validate.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Validate content -on: - push: - paths: - - 'www/content/**' - - 'www/public/*.txt' - - 'www/public/config.yaml' - pull_request: - paths: - - 'www/content/**' - - 'www/public/*.txt' - - 'www/public/config.yaml' - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm validate:www diff --git a/package.json b/package.json index c870fd5..6ebe20f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "dev:penfield": "pnpm --filter @ily/penfield dev", "dev:www": "pnpm --filter @ily/www dev", "build:penfield": "pnpm --filter @ily/penfield build", - "build:www": "pnpm --filter @ily/www build", - "validate:www": "pnpm --filter @ily/www validate" + "build:www": "pnpm --filter @ily/www build" } } diff --git a/www/package.json b/www/package.json index 5c7ad4d..cfa3cc7 100644 --- a/www/package.json +++ b/www/package.json @@ -5,8 +5,7 @@ "dev": "astro dev --port 4322", "build": "astro build --remote && node scripts/generate-stats.js", "preview": "astro preview", - "serve": "pnpm build && npx serve .vercel/output/static -l 4322", - "validate": "node scripts/validate-content.js" + "serve": "pnpm build && npx serve .vercel/output/static -l 4322" }, "dependencies": { "@astrojs/db": "^0.19.0", diff --git a/www/scripts/validate-content.js b/www/scripts/validate-content.js deleted file mode 100644 index 3cce1f2..0000000 --- a/www/scripts/validate-content.js +++ /dev/null @@ -1,43 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import yaml from 'js-yaml'; - -const root = path.resolve(import.meta.dirname, '..'); -const contentDir = path.join(root, 'content'); -const publicDir = path.join(root, 'public'); -const errors = []; - -const mdFiles = fs.readdirSync(contentDir).filter(f => f.endsWith('.md')); -for (const file of mdFiles) { - const content = fs.readFileSync(path.join(contentDir, file), 'utf8'); - const match = content.match(/^---\n([\s\S]*?)\n---/); - if (!match) { - errors.push(`${file}: missing frontmatter`); - continue; - } - const frontmatter = match[1]; - if (!/^date:\s*.+/m.test(frontmatter)) { - errors.push(`${file}: missing required 'date' field`); - } -} - -const configPath = path.join(publicDir, 'config.yaml'); -const config = fs.existsSync(configPath) - ? yaml.load(fs.readFileSync(configPath, 'utf8')) - : {}; -const configDates = config.dates || {}; -const txtFiles = fs.readdirSync(publicDir).filter(f => f.endsWith('.txt')); -for (const file of txtFiles) { - if (!configDates[file]) { - errors.push(`${file}: missing date in config.yaml`); - } -} - -if (errors.length) { - console.error('Content validation failed:\n'); - for (const err of errors) console.error(` - ${err}`); - console.error(''); - process.exit(1); -} else { - console.log(`Validated ${mdFiles.length} posts and ${txtFiles.length} txt files.`); -}