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/content/hello.md b/www/content/hello.md index 4ef35ee..72cdeaf 100644 --- a/www/content/hello.md +++ b/www/content/hello.md @@ -1,7 +1,6 @@ --- title: hello -date: 2023-02-26 -pinned: true +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/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/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) 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/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/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.`); -} diff --git a/www/src/content.config.ts b/www/src/content.config.ts index 6499a0a..6efc211 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(), }) @@ -29,4 +28,4 @@ const bookmarks = defineCollection({ }) }); -export const collections = { md, bookmarks }; +export const collections = { posts, bookmarks }; diff --git a/www/src/layouts/Layout.astro b/www/src/layouts/Layout.astro index 4f59fe1..ee442c2 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,18 @@ 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 : 'lewis m.w.'} + + + mail + gh + rss + sitemap + random + newest + find +
)} 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/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/format.ts b/www/src/lib/format.ts index cd07266..9257bde 100644 --- a/www/src/lib/format.ts +++ b/www/src/lib/format.ts @@ -26,25 +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 } + options?: { suffix?: string } ): string { - const pinnedBadge = options?.pinned ? ' [pinned]' : ''; - return `${formatDate(date)}${title}${pinnedBadge}`; + const suffixHtml = options?.suffix ? `${options.suffix}` : ''; + return `${formatDate(date)}${title}${suffixHtml}`; } interface Sortable { date: Date; - pinned?: boolean; } -export function sortByPinnedThenDate(items: T[]): T[] { - 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(); - }); +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) => get(b).date.getTime() - get(a).date.getTime()); } diff --git a/www/src/lib/md.ts b/www/src/lib/posts.ts similarity index 63% rename from www/src/lib/md.ts rename to www/src/lib/posts.ts index 378c2c6..6b44477 100644 --- a/www/src/lib/md.ts +++ b/www/src/lib/posts.ts @@ -1,22 +1,14 @@ import type { CollectionEntry } from 'astro:content'; import { DEFAULT_CATEGORY } from './consts'; +import { sortEntries } from './format'; -type Post = CollectionEntry<'md'>; +export type Post = CollectionEntry<'posts'> & { body?: string }; export function getSlug(postId: string): string { const parts = postId.split('/'); 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..ad13389 100644 --- a/www/src/lib/txt.ts +++ b/www/src/lib/txt.ts @@ -1,17 +1,15 @@ 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; 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,9 +38,8 @@ 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 sortByPinnedThenDate(files); + return sortEntries(files); } diff --git a/www/src/pages/[slug].astro b/www/src/pages/[slug].astro index c2a81a8..5f92dde 100644 --- a/www/src/pages/[slug].astro +++ b/www/src/pages/[slug].astro @@ -1,11 +1,11 @@ --- 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 { formatDate, formatListItem, excerpt, wordCount } from '../lib/format'; +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 } @@ -15,13 +15,13 @@ 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; ---

{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/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 }); diff --git a/www/src/pages/feed.xml.ts b/www/src/pages/feed.xml.ts index d350661..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 } 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 = [ @@ -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, diff --git a/www/src/pages/index.astro b/www/src/pages/index.astro index 75f592f..9f6f8ef 100644 --- a/www/src/pages/index.astro +++ b/www/src/pages/index.astro @@ -2,12 +2,12 @@ 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 { organizePostsByCategory, getSlug } from '../lib/md'; +import { formatDate, formatListItem, extractDomain, wordCount, escapeHtml } from '../lib/format'; +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'); @@ -37,24 +37,24 @@ 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('')} />
); })} -
- +
+
{ 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/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 = [ diff --git a/www/src/styles/global.css b/www/src/styles/global.css index 4332b9c..4e595c6 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,67 +80,77 @@ section { margin: 1rem 0; } -section .section-label { - font-family: monospace; -} - -.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; } +header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 2rem; +} + +.header-name { + white-space: nowrap; +} + +.header-links { + text-align: right; +} + section pre { margin: 0; } .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; } .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: 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 {