feat: removed pins, and added right-aligned suffixes

This commit is contained in:
Lewis Wynne 2026-03-27 19:09:39 +00:00
parent c647fd62c3
commit 20811f107b
8 changed files with 51 additions and 25 deletions

View file

@ -1,7 +1,6 @@
--- ---
title: hello title: hello
date: 2023-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 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

View file

@ -1,4 +1,3 @@
pinned: []
descriptions: descriptions:
cv.txt: curriculum vitae cv.txt: curriculum vitae
now.txt: what i'm doing now now.txt: what i'm doing now

View file

@ -3,13 +3,12 @@ import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod'; import { z } from 'astro/zod';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
const md = defineCollection({ const posts = defineCollection({
loader: glob({ pattern: '**/*.md', base: './content' }), loader: glob({ pattern: '**/*.md', base: './content' }),
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
date: z.coerce.date(), date: z.coerce.date(),
updated: z.coerce.date().optional(), updated: z.coerce.date().optional(),
pinned: z.boolean().optional(),
category: z.string().optional(), category: z.string().optional(),
related: z.array(z.string()).optional(), related: z.array(z.string()).optional(),
}) })

View file

@ -26,28 +26,45 @@ export function formatDate(date: Date): string {
return `${d}/${m}/${y}`; 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( export function formatListItem(
date: Date, date: Date,
url: string, url: string,
title: string, title: string,
options?: { pinned?: boolean; suffix?: string } options?: { suffix?: string }
): string { ): string {
const pinnedBadge = options?.pinned ? ' [pinned]' : ''; const suffixHtml = options?.suffix ? `<span class="entry-suffix muted">${options.suffix}</span>` : '';
const suffix = options?.suffix ? ` ${options.suffix}` : ''; return `<span class="list-meta"><span class="muted">${formatDate(date)}</span></span><span class="entry-content"><a href="${url}" title="${title}">${title}</a>${suffixHtml}</span>`;
return `<span class="list-meta"><span class="muted">${formatDate(date)}</span></span><span class="entry-content"><a href="${url}" title="${title}">${title}</a>${pinnedBadge}${suffix}</span>`;
} }
interface Sortable { interface Sortable {
date: Date; date: Date;
pinned?: boolean;
} }
export function sortEntries<T>(items: T[], key?: (item: T) => Sortable): T[] { export function sortEntries<T>(items: T[], key?: (item: T) => Sortable): T[] {
const get = key ?? (item => item as unknown as Sortable); const get = key ?? (item => item as unknown as Sortable);
return items.slice().sort((a, b) => { return items.slice().sort((a, b) => get(b).date.getTime() - get(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();
});
} }

View file

@ -6,12 +6,10 @@ import { sortEntries } from './format';
export interface TxtFile { export interface TxtFile {
name: string; name: string;
date: Date; date: Date;
pinned: boolean;
description?: string; description?: string;
} }
export interface TxtConfig { export interface TxtConfig {
pinned?: string[];
descriptions?: Record<string, string>; descriptions?: Record<string, string>;
dates?: Record<string, string>; dates?: Record<string, string>;
} }
@ -32,7 +30,6 @@ export function getTxtFiles(): TxtFile[] {
if (!fs.existsSync(txtDir)) return []; if (!fs.existsSync(txtDir)) return [];
const config = loadTxtConfig(); const config = loadTxtConfig();
const pinnedSet = new Set(config.pinned || []);
const descriptions = config.descriptions || {}; const descriptions = config.descriptions || {};
const dates = config.dates || {}; const dates = config.dates || {};
@ -41,7 +38,6 @@ export function getTxtFiles(): TxtFile[] {
.map(name => ({ .map(name => ({
name, name,
date: dates[name] ? new Date(dates[name]) : new Date(0), date: dates[name] ? new Date(dates[name]) : new Date(0),
pinned: pinnedSet.has(name),
description: descriptions[name], description: descriptions[name],
})); }));
return sortEntries(files); return sortEntries(files);

View file

@ -1,7 +1,7 @@
--- ---
import { getCollection, render } from 'astro:content'; import { getCollection, render } from 'astro:content';
import Layout from '../layouts/Layout.astro'; 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'; import { getSlug, resolveRelatedPosts, type Post } from '../lib/md';
export async function getStaticPaths() { export async function getStaticPaths() {
@ -21,7 +21,7 @@ const description = excerpt((post as Post).body) || undefined;
<article> <article>
<h1>{post.data.title}</h1> <h1>{post.data.title}</h1>
<p class="muted" style="margin-top: 0;">{formatDate(post.data.date)}{post.data.updated && ` (updated ${formatDate(post.data.updated)})`}</p> <p class="muted" style="margin-top: 0;">{formatDate(post.data.date)}{post.data.updated && ` (updated ${formatDate(post.data.updated)})`} · {wordCount((post as Post).body)}{post.data.category && ` · ${post.data.category}`}</p>
<Content /> <Content />
</article> </article>
{related.length > 0 && ( {related.length > 0 && (

View file

@ -2,7 +2,7 @@
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import { getApprovedEntries, type GuestbookEntry } from '../lib/db'; 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 { organizePostsByCategory, getSlug } from '../lib/md';
import { getTxtFiles } from '../lib/txt'; import { getTxtFiles } from '../lib/txt';
import { DEFAULT_CATEGORY, SECTIONS, SUBDOMAINS } from '../lib/consts'; import { DEFAULT_CATEGORY, SECTIONS, SUBDOMAINS } from '../lib/consts';
@ -37,7 +37,7 @@ const urls = [
<section data-section={category}> <section data-section={category}>
{!isDefault && <a class="section-label" href={`?just=${category}`}>{category}</a>} {!isDefault && <a class="section-label" href={`?just=${category}`}>{category}</a>}
<div class="entry-list" set:html={categoryPosts.map(post => <div class="entry-list" set:html={categoryPosts.map(post =>
`<span class="entry">${formatListItem(post.data.date, `/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })}</span>` `<span class="entry">${formatListItem(post.data.date, `/${getSlug(post.id)}`, post.data.title, { suffix: wordCount(post.body) })}</span>`
).join('')} /> ).join('')} />
</section> </section>
); );
@ -47,14 +47,14 @@ const urls = [
<a class="section-label" href={`?just=${SECTIONS.plaintext}`}>{SECTIONS.plaintext}</a> <a class="section-label" href={`?just=${SECTIONS.plaintext}`}>{SECTIONS.plaintext}</a>
<div class="entry-list" set:html={txtFiles.map(f => { <div class="entry-list" set:html={txtFiles.map(f => {
const name = f.name.replace(/\.txt$/, ''); const name = f.name.replace(/\.txt$/, '');
return `<span class="entry">${formatListItem(f.date, `/${f.name}`, name, { pinned: f.pinned })}</span>`; return `<span class="entry">${formatListItem(f.date, `/${f.name}`, name, { suffix: f.description })}</span>`;
}).join('')} /> }).join('')} />
</section> </section>
<section data-section={SECTIONS.bookmarks}> <section data-section={SECTIONS.bookmarks}>
<a class="section-label" href={`?just=${SECTIONS.bookmarks}`}>{SECTIONS.bookmarks}</a> <a class="section-label" href={`?just=${SECTIONS.bookmarks}`}>{SECTIONS.bookmarks}</a>
<div class="entry-list" set:html={bookmarks.map(b => <div class="entry-list" set:html={bookmarks.map(b =>
`<span class="entry">${formatListItem(b.data.date, b.data.url, b.data.title)}</span>` `<span class="entry">${formatListItem(b.data.date, b.data.url, b.data.title, { suffix: extractDomain(b.data.url) })}</span>`
).join('')} /> ).join('')} />
</section> </section>

View file

@ -116,11 +116,27 @@ section pre {
} }
.entry-content { .entry-content {
display: flex;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
}
.entry-content > a {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis; 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 { .guestbook-entries {
font-family: monospace; font-family: monospace;
white-space: pre; white-space: pre;