diff --git a/www/scripts/generate-stats.js b/www/scripts/generate-stats.js index abbac65..6bf8b7e 100644 --- a/www/scripts/generate-stats.js +++ b/www/scripts/generate-stats.js @@ -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')) : []; diff --git a/www/src/content.config.ts b/www/src/content.config.ts index bea154a..6268bc5 100644 --- a/www/src/content.config.ts +++ b/www/src/content.config.ts @@ -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 }; diff --git a/www/src/lib/api.ts b/www/src/lib/api.ts new file mode 100644 index 0000000..1961a01 --- /dev/null +++ b/www/src/lib/api.ts @@ -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; +} diff --git a/www/src/lib/format.ts b/www/src/lib/format.ts new file mode 100644 index 0000000..2ed9219 --- /dev/null +++ b/www/src/lib/format.ts @@ -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; + } +} diff --git a/www/src/lib/posts.ts b/www/src/lib/posts.ts new file mode 100644 index 0000000..6ee9805 --- /dev/null +++ b/www/src/lib/posts.ts @@ -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; + 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); + + 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 }; +} diff --git a/www/src/lib/txt.ts b/www/src/lib/txt.ts new file mode 100644 index 0000000..c5e9afe --- /dev/null +++ b/www/src/lib/txt.ts @@ -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')); +} diff --git a/www/src/pages/admin.astro b/www/src/pages/admin.astro index 50bda50..d634e79 100644 --- a/www/src/pages/admin.astro +++ b/www/src/pages/admin.astro @@ -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}`; -} --- diff --git a/www/src/pages/api/deploy.ts b/www/src/pages/api/deploy.ts index 3ae85cb..57ad7ec 100644 --- a/www/src/pages/api/deploy.ts +++ b/www/src/pages/api/deploy.ts @@ -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 }); }; diff --git a/www/src/pages/api/guestbook.ts b/www/src/pages/api/guestbook.ts index 822e1d8..79a2d04 100644 --- a/www/src/pages/api/guestbook.ts +++ b/www/src/pages/api/guestbook.ts @@ -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); } }; diff --git a/www/src/pages/api/guestbook/[id].ts b/www/src/pages/api/guestbook/[id].ts index 286987e..a8a91a6 100644 --- a/www/src/pages/api/guestbook/[id].ts +++ b/www/src/pages/api/guestbook/[id].ts @@ -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 }); }; diff --git a/www/src/pages/bookmarks/index.astro b/www/src/pages/bookmarks/index.astro index e4e5415..4c37d9f 100644 --- a/www/src/pages/bookmarks/index.astro +++ b/www/src/pages/bookmarks/index.astro @@ -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; - } -} --- diff --git a/www/src/pages/draft/[slug].astro b/www/src/pages/draft/[slug].astro index f1bdf2c..7c6b454 100644 --- a/www/src/pages/draft/[slug].astro +++ b/www/src/pages/draft/[slug].astro @@ -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}`; -} --- diff --git a/www/src/pages/draft/index.astro b/www/src/pages/draft/index.astro index 636c8df..611eb3b 100644 --- a/www/src/pages/draft/index.astro +++ b/www/src/pages/draft/index.astro @@ -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); - -// 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); --- @@ -64,7 +37,7 @@ function formatDate(date: Date): string { sortedCategories.map(category => (
{category} -
 `${formatDate(post.data.date)}    ${post.data.title}${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
+      
 `${formatDate(post.data.date)}    ${post.data.title}${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
     
)) )} diff --git a/www/src/pages/feed.xml.ts b/www/src/pages/feed.xml.ts index 1f8b258..a95b65a 100644 --- a/www/src/pages/feed.xml.ts +++ b/www/src/pages/feed.xml.ts @@ -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 => ({ diff --git a/www/src/pages/guestbook/index.astro b/www/src/pages/guestbook/index.astro index e5c1c62..5e33851 100644 --- a/www/src/pages/guestbook/index.astro +++ b/www/src/pages/guestbook/index.astro @@ -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}`; -} --- diff --git a/www/src/pages/index.astro b/www/src/pages/index.astro index cd27787..fa25f03 100644 --- a/www/src/pages/index.astro +++ b/www/src/pages/index.astro @@ -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); - -// 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; - } -} --- @@ -100,7 +31,7 @@ function extractDomain(url: string): string {
{category}
 `${formatDate(post.data.date)}    ${post.data.title}${post.data.pinned ? ' [pinned]' : ''}`),
+  ...categoryPosts.slice(0, 10).map(post => `${formatDate(post.data.date)}    ${post.data.title}${post.data.pinned ? ' [pinned]' : ''}`),
   ...(categoryPosts.length > 10 ? [`+${categoryPosts.length - 10} more`] : [])
 ].join('\n')} />
 
diff --git a/www/src/pages/md/[slug].astro b/www/src/pages/md/[slug].astro index af1d28c..82c8db5 100644 --- a/www/src/pages/md/[slug].astro +++ b/www/src/pages/md/[slug].astro @@ -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}`; -} --- diff --git a/www/src/pages/md/index.astro b/www/src/pages/md/index.astro index 269eb22..9cd469b 100644 --- a/www/src/pages/md/index.astro +++ b/www/src/pages/md/index.astro @@ -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); - -// 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); --- {sortedCategories.map(category => (
{category} -
 `${formatDate(post.data.date)}    ${post.data.title}${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
+
 `${formatDate(post.data.date)}    ${post.data.title}${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
 
))}
diff --git a/www/src/pages/random.ts b/www/src/pages/random.ts index dea64e9..2cce017 100644 --- a/www/src/pages/random.ts +++ b/www/src/pages/random.ts @@ -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), ]; diff --git a/www/src/pages/sitemap.txt.ts b/www/src/pages/sitemap.txt.ts index 921e6c8..cf3f8ef 100644 --- a/www/src/pages/sitemap.txt.ts +++ b/www/src/pages/sitemap.txt.ts @@ -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', diff --git a/www/src/pages/txt/index.astro b/www/src/pages/txt/index.astro index c1bdc40..42d655c 100644 --- a/www/src/pages/txt/index.astro +++ b/www/src/pages/txt/index.astro @@ -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(); --- diff --git a/www/src/utils.ts b/www/src/utils.ts index 8f61965..f0a6e20 100644 --- a/www/src/utils.ts +++ b/www/src/utils.ts @@ -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();