diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..3535d88
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,25 @@
+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 6ebe20f..c870fd5 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"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"
+ "build:www": "pnpm --filter @ily/www build",
+ "validate:www": "pnpm --filter @ily/www validate"
}
}
diff --git a/www/content/hello.md b/www/content/hello.md
index 72cdeaf..4ef35ee 100644
--- a/www/content/hello.md
+++ b/www/content/hello.md
@@ -1,6 +1,7 @@
---
title: hello
-date: 2026-02-26
+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/package.json b/www/package.json
index cfa3cc7..5c7ad4d 100644
--- a/www/package.json
+++ b/www/package.json
@@ -5,7 +5,8 @@
"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"
+ "serve": "pnpm build && npx serve .vercel/output/static -l 4322",
+ "validate": "node scripts/validate-content.js"
},
"dependencies": {
"@astrojs/db": "^0.19.0",
diff --git a/www/public/changelog.txt b/www/public/changelog.txt
index 5882854..a1b4dbe 100644
--- a/www/public/changelog.txt
+++ b/www/public/changelog.txt
@@ -1,4 +1,3 @@
-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 c1c9ffa..2b7aa3a 100644
--- a/www/public/config.yaml
+++ b/www/public/config.yaml
@@ -1,3 +1,4 @@
+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 01fbf1b..c466e18 100644
--- a/www/public/js/params.js
+++ b/www/public/js/params.js
@@ -4,7 +4,8 @@
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}';
+ 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}';
document.head.appendChild(Object.assign(document.createElement('style'), { textContent: css }));
}
@@ -29,10 +30,7 @@
if (has) {
document.documentElement.dataset.has = has;
has = has.toLowerCase();
- }
-
- document.addEventListener('DOMContentLoaded', function() {
- if (has) {
+ document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('section[data-section] .entry').forEach(function(entry) {
if (entry.textContent.toLowerCase().indexOf(has) === -1) {
entry.style.display = 'none';
@@ -43,22 +41,6 @@
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
new file mode 100644
index 0000000..3cce1f2
--- /dev/null
+++ b/www/scripts/validate-content.js
@@ -0,0 +1,43 @@
+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 6efc211..6499a0a 100644
--- a/www/src/content.config.ts
+++ b/www/src/content.config.ts
@@ -3,12 +3,13 @@ import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
import yaml from 'js-yaml';
-const posts = defineCollection({
+const md = 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(),
})
@@ -28,4 +29,4 @@ const bookmarks = defineCollection({
})
});
-export const collections = { posts, bookmarks };
+export const collections = { md, bookmarks };
diff --git a/www/src/layouts/Layout.astro b/www/src/layouts/Layout.astro
index ee442c2..4f59fe1 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 ' + title, showHeader = true, isHome = false, urls = [] } = Astro.props;
+const { title, description = 'personal website of lewis m.w.', showHeader = true, isHome = false, urls = [] } = Astro.props;
---
@@ -28,18 +28,7 @@ const { title, description = 'personal website of ' + title, showHeader = true,
{showHeader && (
)}
diff --git a/www/src/lib/api.ts b/www/src/lib/api.ts
index 6e4622e..1961a01 100644
--- a/www/src/lib/api.ts
+++ b/www/src/lib/api.ts
@@ -1,3 +1,5 @@
+import { isAdmin } from './auth';
+
export function jsonResponse(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
@@ -8,3 +10,10 @@ 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 eaac841..28b0d8b 100644
--- a/www/src/lib/auth.ts
+++ b/www/src/lib/auth.ts
@@ -1,26 +1,29 @@
import { getSession } from 'auth-astro/server';
-export type Session = { user?: { id?: string; name?: string | null } };
+type Session = { user?: { id?: string; name?: string | null } };
-export type AuthResult =
- | { status: 'admin'; session: Session }
- | { status: 'unauthenticated' }
- | { status: 'forbidden' }
- | { status: 'error' };
+export function isAdmin(userId: string | undefined): boolean {
+ return userId === import.meta.env.ADMIN_GITHUB_ID;
+}
-export async function getAdminSession(request: Request): Promise {
+export async function requireAdminSession(request: Request): Promise<
+ | { session: Session; error: null }
+ | { session: null; error: Response | null }
+> {
let session: Session | null;
try {
session = await getSession(request);
} catch {
- return { status: 'error' };
+ return { session: null, error: new Response('Auth not configured', { status: 500 }) };
}
- if (!session) return { status: 'unauthenticated' };
-
- if (session.user?.id !== import.meta.env.ADMIN_GITHUB_ID) {
- return { status: 'forbidden' };
+ if (!session) {
+ return { session: null, error: null };
}
- return { status: 'admin', session };
+ if (!isAdmin(session.user?.id)) {
+ return { session: null, error: new Response('Forbidden', { status: 403 }) };
+ }
+
+ return { session, error: null };
}
diff --git a/www/src/lib/consts.ts b/www/src/lib/consts.ts
index a2e8d87..a19834c 100644
--- a/www/src/lib/consts.ts
+++ b/www/src/lib/consts.ts
@@ -6,7 +6,7 @@ export const SUBDOMAINS = [
];
export const SECTIONS = {
- files: 'files',
+ plaintext: 'plaintext',
bookmarks: 'bookmarks',
guestbook: 'guestbook',
} as const;
diff --git a/www/src/lib/format.ts b/www/src/lib/format.ts
index 9257bde..cd07266 100644
--- a/www/src/lib/format.ts
+++ b/www/src/lib/format.ts
@@ -26,45 +26,25 @@ 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?: { suffix?: string }
+ options?: { pinned?: boolean }
): string {
- const suffixHtml = options?.suffix ? `${options.suffix}` : '';
- return `${formatDate(date)}${title}${suffixHtml}`;
+ const pinnedBadge = options?.pinned ? ' [pinned]' : '';
+ return `${formatDate(date)}${title}${pinnedBadge}`;
}
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) => get(b).date.getTime() - get(a).date.getTime());
+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();
+ });
}
diff --git a/www/src/lib/posts.ts b/www/src/lib/md.ts
similarity index 63%
rename from www/src/lib/posts.ts
rename to www/src/lib/md.ts
index 6b44477..378c2c6 100644
--- a/www/src/lib/posts.ts
+++ b/www/src/lib/md.ts
@@ -1,14 +1,22 @@
import type { CollectionEntry } from 'astro:content';
import { DEFAULT_CATEGORY } from './consts';
-import { sortEntries } from './format';
-export type Post = CollectionEntry<'posts'> & { body?: string };
+type Post = CollectionEntry<'md'>;
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[],
@@ -17,7 +25,7 @@ export function resolveRelatedPosts(
return slugs.flatMap(s => bySlug.get(s) ?? []);
}
-export function organizePostsByCategory(posts: Post[]): {
+export function organizePostsByCategory(posts: Post[], { sortAlphabetically = false } = {}): {
grouped: Record;
categories: string[];
} {
@@ -35,7 +43,7 @@ export function organizePostsByCategory(posts: Post[]): {
});
for (const category of categories) {
- grouped[category] = sortEntries(grouped[category], p => p.data);
+ grouped[category] = sortPosts(grouped[category], { alphabetically: sortAlphabetically });
}
return { grouped, categories };
diff --git a/www/src/lib/txt.ts b/www/src/lib/txt.ts
index ad13389..ee03984 100644
--- a/www/src/lib/txt.ts
+++ b/www/src/lib/txt.ts
@@ -1,15 +1,17 @@
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
-import { sortEntries } from './format';
+import { sortByPinnedThenDate } from './format';
export interface TxtFile {
name: string;
date: Date;
+ pinned: boolean;
description?: string;
}
export interface TxtConfig {
+ pinned?: string[];
descriptions?: Record;
dates?: Record;
}
@@ -30,6 +32,7 @@ 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 || {};
@@ -38,8 +41,9 @@ 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);
+ return sortByPinnedThenDate(files);
}
diff --git a/www/src/pages/[slug].astro b/www/src/pages/[slug].astro
index 5f92dde..c2a81a8 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, wordCount } from '../lib/format';
-import { getSlug, resolveRelatedPosts, type Post } from '../lib/posts';
+import { formatDate, formatListItem, excerpt } from '../lib/format';
+import { getSlug, resolveRelatedPosts } from '../lib/md';
export async function getStaticPaths() {
- const allPosts = await getCollection('posts');
+ const allPosts = await getCollection('md');
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 Post).body) || undefined;
+const description = excerpt((post as any).body) || undefined;
---
{post.data.title}
-{formatDate(post.data.date)}{post.data.updated && ` (updated ${formatDate(post.data.updated)})`} · {wordCount((post as Post).body)}{post.data.category && ` · ${post.data.category}`}
+{formatDate(post.data.date)}{post.data.updated && ` (updated ${formatDate(post.data.updated)})`}
{related.length > 0 && (
diff --git a/www/src/pages/admin.astro b/www/src/pages/admin.astro
index 66ff49c..f540c6c 100644
--- a/www/src/pages/admin.astro
+++ b/www/src/pages/admin.astro
@@ -2,15 +2,13 @@
export const prerender = false;
import { getPendingEntries, type GuestbookEntry } from '../lib/db';
-import { getAdminSession } from '../lib/auth';
+import { requireAdminSession } from '../lib/auth';
import Layout from '../layouts/Layout.astro';
import { formatDate } from '../lib/format';
-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;
+const { session, error } = await requireAdminSession(Astro.request);
+if (error) return error;
+if (!session) return Astro.redirect('/api/auth/signin');
let entries: GuestbookEntry[] = [];
try {
diff --git a/www/src/pages/api/deploy.ts b/www/src/pages/api/deploy.ts
index c6f7ab4..57ad7ec 100644
--- a/www/src/pages/api/deploy.ts
+++ b/www/src/pages/api/deploy.ts
@@ -1,18 +1,23 @@
import type { APIRoute } from 'astro';
-import { jsonResponse, errorResponse } from '../../lib/api';
-import { getAdminSession } from '../../lib/auth';
+import { getSession } from 'auth-astro/server';
+import { jsonResponse, errorResponse, requireAdmin } from '../../lib/api';
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
- const auth = await getAdminSession(request);
- if (auth.status !== 'admin') return errorResponse('Unauthorized', 403);
+ const session = await getSession(request);
+ const authError = requireAdmin(session);
+ if (authError) return authError;
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 32d92d9..a8a91a6 100644
--- a/www/src/pages/api/guestbook/[id].ts
+++ b/www/src/pages/api/guestbook/[id].ts
@@ -1,27 +1,33 @@
import type { APIRoute } from 'astro';
+import { getSession } from 'auth-astro/server';
import { approveEntry, deleteEntry } from '../../../lib/db';
-import { jsonResponse, errorResponse } from '../../../lib/api';
-import { getAdminSession } from '../../../lib/auth';
+import { jsonResponse, errorResponse, requireAdmin } from '../../../lib/api';
export const prerender = false;
export const PATCH: APIRoute = async ({ params, request }) => {
- const auth = await getAdminSession(request);
- if (auth.status !== 'admin') return errorResponse('Unauthorized', 403);
+ const session = await getSession(request);
+ const authError = requireAdmin(session);
+ if (authError) return authError;
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 auth = await getAdminSession(request);
- if (auth.status !== 'admin') return errorResponse('Unauthorized', 403);
+ const session = await getSession(request);
+ const authError = requireAdmin(session);
+ if (authError) return authError;
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 1b26c9b..d350661 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/posts';
+import { getSlug } from '../lib/md';
import { getTxtFiles } from '../lib/txt';
import { excerpt } from '../lib/format';
export async function GET(context: APIContext) {
- const posts = await getCollection('posts');
+ const posts = await getCollection('md');
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 Post).body) || post.data.title,
+ description: excerpt((post as any).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 9f6f8ef..75f592f 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, extractDomain, wordCount, escapeHtml } from '../lib/format';
-import { organizePostsByCategory, getSlug } from '../lib/posts';
+import { formatDate, formatListItem, escapeHtml } from '../lib/format';
+import { organizePostsByCategory, getSlug } from '../lib/md';
import { getTxtFiles } from '../lib/txt';
import { DEFAULT_CATEGORY, SECTIONS, SUBDOMAINS } from '../lib/consts';
-const posts = await getCollection('posts');
+const posts = await getCollection('md');
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
const bookmarksCollection = await getCollection('bookmarks');
@@ -37,24 +37,24 @@ const urls = [
{!isDefault && {category}}
- `
${formatListItem(post.data.date, `/${getSlug(post.id)}`, post.data.title, { suffix: wordCount(post.body) })}`
+ `
${formatListItem(post.data.date, `/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })}`
).join('')} />
);
})}
-
-{SECTIONS.files}
+
+{SECTIONS.plaintext}
{
const name = f.name.replace(/\.txt$/, '');
- return `
${formatListItem(f.date, `/${f.name}`, name, { suffix: f.description })}`;
+ return `
${formatListItem(f.date, `/${f.name}`, name, { pinned: f.pinned })}`;
}).join('')} />
{SECTIONS.bookmarks}
- `${formatListItem(b.data.date, b.data.url, b.data.title, { suffix: extractDomain(b.data.url) })}`
+ `${formatListItem(b.data.date, b.data.url, b.data.title)}`
).join('')} />
diff --git a/www/src/pages/sitemap.txt.ts b/www/src/pages/sitemap.txt.ts
index 634b568..2121a21 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/posts';
+import { getSlug } from '../lib/md';
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('posts');
+ const posts = await getCollection('md');
const txtFiles = getTxtFiles().map(f => f.name);
const urls = [
diff --git a/www/src/styles/global.css b/www/src/styles/global.css
index 4e595c6..4332b9c 100644
--- a/www/src/styles/global.css
+++ b/www/src/styles/global.css
@@ -1,10 +1,9 @@
body {
box-sizing: border-box;
- max-width: 34rem;
+ max-width: 48rem;
margin: 0 auto;
padding: 1rem;
text-align: justify;
- font-family: 'Times New Roman', serif;
}
img {
@@ -18,7 +17,6 @@ h1, h2, h3, h4, h5, h6 {
.muted {
color: #888;
- font-size: 0.9rem;
}
.left, .right {
@@ -26,7 +24,7 @@ h1, h2, h3, h4, h5, h6 {
font-size: 0.9rem;
}
-@media (min-width: 58rem) {
+@media (min-width: 63rem) {
.left, .right {
display: inline;
position: relative;
@@ -80,24 +78,24 @@ section {
margin: 1rem 0;
}
+section .section-label {
+ font-family: monospace;
+}
-html[data-has] .guestbook-form {
+.home-name-link {
display: none;
}
-header {
- display: flex;
- justify-content: space-between;
- align-items: baseline;
- gap: 2rem;
+html[data-just] .home-name {
+ display: none;
}
-.header-name {
- white-space: nowrap;
+html[data-just] .home-name-link {
+ display: inline;
}
-.header-links {
- text-align: right;
+html[data-has] .guestbook-form {
+ display: none;
}
section pre {
@@ -105,52 +103,42 @@ section pre {
}
.entry-list {
+ columns: 2 24ch;
+ column-gap: 3ch;
+ font-family: monospace;
margin: 0;
}
.entry {
display: grid;
- grid-template-columns: 4rem 1fr;
- align-items: baseline;
+ grid-template-columns: 10ch 1fr;
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 {
- margin: 0;
+ font-family: monospace;
+ white-space: pre;
}
.guestbook-entry {
display: grid;
- grid-template-columns: 4rem 1fr;
- align-items: baseline;
- break-inside: avoid;
+ grid-template-columns: 10ch 1fr;
+}
+
+.guestbook-entry > span:last-child {
+ white-space: normal;
}
.guestbook-form {
margin-top: 0.5rem;
- margin-left: 4rem;
+ margin-left: 10ch;
+ font-family: monospace;
}
html[data-compact] .list-meta {