feat: extracts some repeated logic out into lib/ files

This commit is contained in:
Lewis Wynne 2026-01-31 22:10:03 +00:00
parent 38653f2aa1
commit 99c1539aad
22 changed files with 200 additions and 336 deletions

View file

@ -11,7 +11,7 @@ function countWords(text) {
}
// Count blog posts and their words
const postsDir = path.join(root, 'src/content/posts');
const postsDir = path.join(root, 'src/content/md');
const posts = fs.existsSync(postsDir)
? fs.readdirSync(postsDir).filter(f => f.endsWith('.md'))
: [];

View file

@ -3,15 +3,14 @@ import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
import yaml from 'js-yaml';
const posts = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
const md = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/md' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
pinned: z.boolean().optional(),
category: z.string().optional(),
draft: z.boolean().optional(),
slug: z.string(),
})
});
@ -29,4 +28,4 @@ const bookmarks = defineCollection({
})
});
export const collections = { posts, bookmarks };
export const collections = { md, bookmarks };

19
www/src/lib/api.ts Normal file
View file

@ -0,0 +1,19 @@
import { isAdmin } from './auth';
export function jsonResponse(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
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;
}

15
www/src/lib/format.ts Normal file
View file

@ -0,0 +1,15 @@
export function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
export function extractDomain(url: string): string {
try {
const parsed = new URL(url);
return parsed.hostname.replace(/^www\./, '');
} catch {
return url;
}
}

35
www/src/lib/posts.ts Normal file
View file

@ -0,0 +1,35 @@
import type { CollectionEntry } from 'astro:content';
type Post = CollectionEntry<'md'>;
export function sortPosts(posts: Post[]): 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;
return b.data.date.getTime() - a.data.date.getTime();
});
}
export function organizePostsByCategory(posts: Post[]): {
grouped: Record<string, Post[]>;
categories: string[];
} {
const grouped = posts.reduce((acc, post) => {
const category = post.data.category ?? 'md';
if (!acc[category]) acc[category] = [];
acc[category].push(post);
return acc;
}, {} as Record<string, Post[]>);
const categories = Object.keys(grouped).sort((a, b) => {
if (a === 'md') return -1;
if (b === 'md') return 1;
return a.localeCompare(b);
});
for (const category of categories) {
grouped[category] = sortPosts(grouped[category]);
}
return { grouped, categories };
}

53
www/src/lib/txt.ts Normal file
View file

@ -0,0 +1,53 @@
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
import { getGitDate } from '../utils';
export interface TxtFile {
name: string;
date: Date;
pinned: boolean;
}
export interface TxtConfig {
pinned?: string[];
}
export function getTxtDir(): string {
return path.join(process.cwd(), 'public/txt');
}
export function loadTxtConfig(): TxtConfig {
const configPath = path.join(getTxtDir(), 'config.yaml');
return fs.existsSync(configPath)
? yaml.load(fs.readFileSync(configPath, 'utf8')) as TxtConfig
: {};
}
export function getTxtFiles(): TxtFile[] {
const txtDir = getTxtDir();
if (!fs.existsSync(txtDir)) return [];
const config = loadTxtConfig();
const pinnedSet = new Set(config.pinned || []);
return fs.readdirSync(txtDir)
.filter(file => file.endsWith('.txt'))
.map(name => ({
name,
date: getGitDate(path.join(txtDir, name)),
pinned: pinnedSet.has(name),
}))
.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 getTxtFileNames(): string[] {
const txtDir = getTxtDir();
if (!fs.existsSync(txtDir)) return [];
return fs.readdirSync(txtDir).filter(file => file.endsWith('.txt'));
}

View file

@ -5,6 +5,7 @@ import { getSession } from 'auth-astro/server';
import { getPendingEntries, type GuestbookEntry } from '../lib/db';
import { isAdmin } from '../lib/auth';
import Layout from '../layouts/Layout.astro';
import { formatDate } from '../lib/format';
let session;
try {
@ -27,13 +28,6 @@ try {
} catch {
// handle error
}
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
---
<Layout title="admin - guestbook" showHeader={false}>

View file

@ -1,36 +1,23 @@
import type { APIRoute } from 'astro';
import { getSession } from 'auth-astro/server';
import { isAdmin } from '../../lib/auth';
import { jsonResponse, errorResponse, requireAdmin } from '../../lib/api';
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
const session = await getSession(request);
if (!session?.user?.id || !isAdmin(session.user.id)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 403,
headers: { 'Content-Type': 'application/json' },
});
}
const authError = requireAdmin(session);
if (authError) return authError;
const hookUrl = import.meta.env.VERCEL_DEPLOY_HOOK;
if (!hookUrl) {
return new Response(JSON.stringify({ error: 'Deploy hook not configured' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
return errorResponse('Deploy hook not configured', 500);
}
const res = await fetch(hookUrl, { method: 'POST' });
if (!res.ok) {
return new Response(JSON.stringify({ error: 'Failed to trigger deploy' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
return errorResponse('Failed to trigger deploy', 502);
}
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
return jsonResponse({ success: true });
};

View file

@ -1,5 +1,6 @@
import type { APIRoute } from 'astro';
import { createEntry } from '../../lib/db';
import { jsonResponse, errorResponse } from '../../lib/api';
export const prerender = false;
@ -9,22 +10,13 @@ export const POST: APIRoute = async ({ request }) => {
const { name, message, url } = data;
if (!name || !message) {
return new Response(JSON.stringify({ error: 'Name and message are required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
return errorResponse('Name and message are required', 400);
}
await createEntry(name.slice(0, 100), message.slice(0, 500), url?.slice(0, 200) || null);
return new Response(JSON.stringify({ success: true }), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Failed to create entry' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
return jsonResponse({ success: true }, 201);
} catch {
return errorResponse('Failed to create entry', 500);
}
};

View file

@ -1,54 +1,34 @@
import type { APIRoute } from 'astro';
import { getSession } from 'auth-astro/server';
import { approveEntry, deleteEntry } from '../../../lib/db';
import { isAdmin } from '../../../lib/auth';
import { jsonResponse, errorResponse, requireAdmin } from '../../../lib/api';
export const prerender = false;
export const PATCH: APIRoute = async ({ params, request }) => {
const session = await getSession(request);
if (!session?.user?.id || !isAdmin(session.user.id)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 403,
headers: { 'Content-Type': 'application/json' },
});
}
const authError = requireAdmin(session);
if (authError) return authError;
const id = parseInt(params.id!, 10);
if (isNaN(id)) {
return new Response(JSON.stringify({ error: 'Invalid ID' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
return errorResponse('Invalid ID', 400);
}
await approveEntry(id);
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
return jsonResponse({ success: true });
};
export const DELETE: APIRoute = async ({ params, request }) => {
const session = await getSession(request);
if (!session?.user?.id || !isAdmin(session.user.id)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 403,
headers: { 'Content-Type': 'application/json' },
});
}
const authError = requireAdmin(session);
if (authError) return authError;
const id = parseInt(params.id!, 10);
if (isNaN(id)) {
return new Response(JSON.stringify({ error: 'Invalid ID' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
return errorResponse('Invalid ID', 400);
}
await deleteEntry(id);
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
return jsonResponse({ success: true });
};

View file

@ -1,26 +1,11 @@
---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatDate, extractDomain } from '../../lib/format';
const bookmarksCollection = await getCollection('bookmarks');
const bookmarks = bookmarksCollection
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
function extractDomain(url: string): string {
try {
const parsed = new URL(url);
return parsed.hostname.replace(/^www\./, '');
} catch {
return url;
}
}
---
<Layout title="bookmarks - lewis m.w.">

View file

@ -5,6 +5,8 @@ import { getSession } from 'auth-astro/server';
import { getCollection, render } from 'astro:content';
import { isAdmin } from '../../lib/auth';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { getSlug } from '../../utils';
let session;
try {
@ -22,21 +24,14 @@ if (!isAdmin(session.user?.id)) {
}
const slug = Astro.params.slug;
const posts = await getCollection('posts', ({ data }) => data.draft === true);
const post = posts.find(p => p.id === slug);
const posts = await getCollection('md', ({ data }) => data.draft === true);
const post = posts.find(p => getSlug(p.id) === slug);
if (!post) {
return new Response('Not found', { status: 404 });
}
const { Content } = await render(post);
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
---
<Layout title={`${post.data.title} - lewis m.w.`}>

View file

@ -5,6 +5,9 @@ import { getSession } from 'auth-astro/server';
import { getCollection } from 'astro:content';
import { isAdmin } from '../../lib/auth';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { organizePostsByCategory } from '../../lib/posts';
import { getSlug } from '../../utils';
let session;
try {
@ -21,38 +24,8 @@ if (!isAdmin(session.user?.id)) {
return new Response('Forbidden', { status: 403 });
}
const posts = await getCollection('posts', ({ data }) => data.draft === true);
// Group by category (default: "posts")
const grouped = posts.reduce((acc, post) => {
const category = post.data.category ?? 'posts';
if (!acc[category]) acc[category] = [];
acc[category].push(post);
return acc;
}, {} as Record<string, typeof posts>);
// Sort categories: "posts" first, then alphabetically
const sortedCategories = Object.keys(grouped).sort((a, b) => {
if (a === 'posts') return -1;
if (b === 'posts') return 1;
return a.localeCompare(b);
});
// Sort posts within each category: pinned first, then by date descending
for (const category of sortedCategories) {
grouped[category].sort((a, b) => {
if (a.data.pinned && !b.data.pinned) return -1;
if (!a.data.pinned && b.data.pinned) return 1;
return b.data.date.getTime() - a.data.date.getTime();
});
}
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
const posts = await getCollection('md', ({ data }) => data.draft === true);
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
---
<Layout title="drafts - lewis m.w.">
@ -64,7 +37,7 @@ function formatDate(date: Date): string {
sortedCategories.map(category => (
<details open>
<summary>{category}</summary>
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/draft/${post.id}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/draft/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
</details>
))
)}

View file

@ -1,34 +1,19 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import fs from 'node:fs';
import path from 'node:path';
import type { APIContext } from 'astro';
import { getGitDate } from '../utils';
interface TxtFile {
name: string;
date: Date;
}
import { getTxtFiles } from '../lib/txt';
import { getSlug } from '../utils';
export async function GET(context: APIContext) {
const posts = await getCollection('posts', ({ data }) => data.draft !== true);
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const bookmarks = await getCollection('bookmarks');
const txtDir = path.join(process.cwd(), 'public/txt');
const txtFiles: TxtFile[] = fs.existsSync(txtDir)
? fs.readdirSync(txtDir)
.filter(file => file.endsWith('.txt'))
.map(name => ({
name,
date: getGitDate(path.join(txtDir, name)),
}))
: [];
const txtFiles = getTxtFiles();
const items = [
...posts.map(post => ({
title: post.data.title,
pubDate: post.data.date,
link: `/md/${post.id}`,
link: `/md/${getSlug(post.id)}`,
description: post.data.title,
})),
...txtFiles.map(txt => ({

View file

@ -1,6 +1,7 @@
---
import Layout from '../../layouts/Layout.astro';
import { getApprovedEntries, type GuestbookEntry } from '../../lib/db';
import { formatDate } from '../../lib/format';
let guestbookEntries: GuestbookEntry[] = [];
try {
@ -8,13 +9,6 @@ try {
} catch {
// DB not available during dev without env vars
}
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
---
<Layout title="guestbook - lewis m.w.">

View file

@ -1,73 +1,20 @@
---
import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro';
import yaml from 'js-yaml';
import fs from 'node:fs';
import path from 'node:path';
import { getApprovedEntries, type GuestbookEntry } from '../lib/db';
import { getGitDate } from '../utils';
import { formatDate, extractDomain } from '../lib/format';
import { organizePostsByCategory } from '../lib/posts';
import { getTxtFiles } from '../lib/txt';
import { getSlug } from '../utils';
interface TxtFile {
name: string;
date: Date;
pinned: boolean;
}
interface TxtConfig {
pinned?: string[];
}
const posts = await getCollection('posts', ({ data }) => data.draft !== true);
// Group by category (default: "posts")
const grouped = posts.reduce((acc, post) => {
const category = post.data.category ?? 'posts';
if (!acc[category]) acc[category] = [];
acc[category].push(post);
return acc;
}, {} as Record<string, typeof posts>);
// Sort categories: "posts" first, then alphabetically
const sortedCategories = Object.keys(grouped).sort((a, b) => {
if (a === 'posts') return -1;
if (b === 'posts') return 1;
return a.localeCompare(b);
});
// Sort posts within each category: pinned first, then by date descending
for (const category of sortedCategories) {
grouped[category].sort((a, b) => {
if (a.data.pinned && !b.data.pinned) return -1;
if (!a.data.pinned && b.data.pinned) return 1;
return b.data.date.getTime() - a.data.date.getTime();
});
}
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
const bookmarksCollection = await getCollection('bookmarks');
const bookmarks = bookmarksCollection
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
// Auto-discover txt files from public/txt/
const txtDir = path.join(process.cwd(), 'public/txt');
const txtConfigPath = path.join(txtDir, 'config.yaml');
const txtConfig: TxtConfig = fs.existsSync(txtConfigPath)
? yaml.load(fs.readFileSync(txtConfigPath, 'utf8')) as TxtConfig
: {};
const pinnedSet = new Set(txtConfig.pinned || []);
const txtFiles: TxtFile[] = fs.existsSync(txtDir)
? fs.readdirSync(txtDir)
.filter(file => file.endsWith('.txt'))
.map(name => {
const filePath = path.join(txtDir, name);
return { name, date: getGitDate(filePath), pinned: pinnedSet.has(name) };
})
.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 txtFiles = getTxtFiles();
let guestbookEntries: GuestbookEntry[] = [];
try {
@ -75,22 +22,6 @@ try {
} catch {
// DB not available during dev without env vars
}
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
function extractDomain(url: string): string {
try {
const parsed = new URL(url);
return parsed.hostname.replace(/^www\./, '');
} catch {
return url;
}
}
---
<Layout title="lewis m.w." isHome>
@ -100,7 +31,7 @@ function extractDomain(url: string): string {
<details open>
<summary>{category}</summary>
<pre set:html={[
...categoryPosts.slice(0, 10).map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${post.id}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`),
...categoryPosts.slice(0, 10).map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`),
...(categoryPosts.length > 10 ? [`<a href="/md/">+${categoryPosts.length - 10} more</a>`] : [])
].join('\n')} />
</details>

View file

@ -1,24 +1,19 @@
---
import { getCollection, render } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { getSlug } from '../../utils';
export async function getStaticPaths() {
const posts = await getCollection('posts', ({ data }) => data.draft !== true);
const posts = await getCollection('md', ({ data }) => data.draft !== true);
return posts.map(post => ({
params: { slug: post.data.slug },
params: { slug: getSlug(post.id) },
props: { post }
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
---
<Layout title={`${post.data.title} - lewis m.w.`}>

View file

@ -1,46 +1,19 @@
---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { organizePostsByCategory } from '../../lib/posts';
import { getSlug } from '../../utils';
const posts = await getCollection('posts', ({ data }) => data.draft !== true);
// Group by category (default: "posts")
const grouped = posts.reduce((acc, post) => {
const category = post.data.category ?? 'posts';
if (!acc[category]) acc[category] = [];
acc[category].push(post);
return acc;
}, {} as Record<string, typeof posts>);
// Sort categories: "posts" first, then alphabetically
const sortedCategories = Object.keys(grouped).sort((a, b) => {
if (a === 'posts') return -1;
if (b === 'posts') return 1;
return a.localeCompare(b);
});
// Sort posts within each category: pinned first, then by date descending
for (const category of sortedCategories) {
grouped[category].sort((a, b) => {
if (a.data.pinned && !b.data.pinned) return -1;
if (!a.data.pinned && b.data.pinned) return 1;
return b.data.date.getTime() - a.data.date.getTime();
});
}
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
---
<Layout title="md - lewis m.w.">
{sortedCategories.map(category => (
<details open>
<summary>{category}</summary>
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${post.data.slug}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
</details>
))}
</Layout>

View file

@ -1,22 +1,18 @@
import { getCollection } from 'astro:content';
import fs from 'node:fs';
import path from 'node:path';
import type { APIContext } from 'astro';
import { getTxtFileNames } from '../lib/txt';
import { getSlug } from '../utils';
export const prerender = false;
export async function GET(context: APIContext) {
const site = context.site?.origin ?? 'https://wynne.rs';
const posts = await getCollection('posts', ({ data }) => data.draft !== true);
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const bookmarks = await getCollection('bookmarks');
const txtDir = path.join(process.cwd(), 'public/txt');
const txtFiles = fs.existsSync(txtDir)
? fs.readdirSync(txtDir).filter(file => file.endsWith('.txt'))
: [];
const txtFiles = getTxtFileNames();
const urls = [
...posts.map(post => `/md/${post.id}`),
...posts.map(post => `/md/${getSlug(post.id)}`),
...txtFiles.map(txt => `/txt/${txt}`),
...bookmarks.map(b => b.data.url),
];

View file

@ -1,7 +1,7 @@
import { getCollection } from 'astro:content';
import fs from 'node:fs';
import path from 'node:path';
import type { APIContext } from 'astro';
import { getTxtFileNames } from '../lib/txt';
import { getSlug } from '../utils';
const SUBDOMAINS = [
'https://penfield.wynne.rs/',
@ -9,17 +9,13 @@ const SUBDOMAINS = [
export async function GET(context: APIContext) {
const site = context.site?.origin ?? 'https://wynne.rs';
const posts = await getCollection('posts', ({ data }) => data.draft !== true);
const txtDir = path.join(process.cwd(), 'public/txt');
const txtFiles = fs.existsSync(txtDir)
? fs.readdirSync(txtDir).filter(file => file.endsWith('.txt'))
: [];
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const txtFiles = getTxtFileNames();
const urls = [
'/',
'/md',
...posts.map(post => `/md/${post.id}`),
...posts.map(post => `/md/${getSlug(post.id)}`),
'/txt',
...txtFiles.map(txt => `/txt/${txt}`),
'/bookmarks',

View file

@ -1,47 +1,9 @@
---
import Layout from '../../layouts/Layout.astro';
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
import { getGitDate } from '../../utils';
import { formatDate } from '../../lib/format';
import { getTxtFiles } from '../../lib/txt';
interface TxtFile {
name: string;
date: Date;
pinned: boolean;
}
interface TxtConfig {
pinned?: string[];
}
const txtDir = path.join(process.cwd(), 'public/txt');
const configPath = path.join(txtDir, 'config.yaml');
const config: TxtConfig = fs.existsSync(configPath)
? yaml.load(fs.readFileSync(configPath, 'utf8')) as TxtConfig
: {};
const pinnedSet = new Set(config.pinned || []);
const txtFiles: TxtFile[] = fs.existsSync(txtDir)
? fs.readdirSync(txtDir)
.filter(file => file.endsWith('.txt'))
.map(name => {
const filePath = path.join(txtDir, name);
return { name, date: getGitDate(filePath), pinned: pinnedSet.has(name) };
})
.sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return b.date.getTime() - a.date.getTime();
})
: [];
function formatDate(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = String(date.getFullYear()).slice(-2);
return `${d}/${m}/${y}`;
}
const txtFiles = getTxtFiles();
---
<Layout title="txt - lewis m.w.">

View file

@ -1,5 +1,10 @@
import { execSync } from 'node:child_process';
export function getSlug(postId: string): string {
const parts = postId.split('/');
return parts[parts.length - 1];
}
export function getGitDate(filePath: string): Date {
try {
const timestamp = execSync(`git log -1 --format=%cI -- "${filePath}"`, { encoding: 'utf8' }).trim();