feat: extracts some repeated logic out into lib/ files
This commit is contained in:
parent
38653f2aa1
commit
99c1539aad
22 changed files with 200 additions and 336 deletions
|
|
@ -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'))
|
||||
: [];
|
||||
|
|
|
|||
|
|
@ -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
19
www/src/lib/api.ts
Normal 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
15
www/src/lib/format.ts
Normal 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
35
www/src/lib/posts.ts
Normal 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
53
www/src/lib/txt.ts
Normal 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'));
|
||||
}
|
||||
|
|
@ -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}>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.">
|
||||
|
||||
|
|
|
|||
|
|
@ -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.`}>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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.">
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.`}>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.">
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue