From 0fa86f4a815f03495d3092289db0ab8caebdbcf4 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 23 Jan 2026 18:35:57 +0000 Subject: [PATCH] feat: stats page --- apps/blog/public/txt/stats.txt | 11 +++ apps/blog/scripts/generate-stats.js | 85 +++++++++++++++++++++ apps/blog/src/pages/guestbook-count.json.ts | 17 +++++ 3 files changed, 113 insertions(+) create mode 100644 apps/blog/public/txt/stats.txt create mode 100644 apps/blog/scripts/generate-stats.js create mode 100644 apps/blog/src/pages/guestbook-count.json.ts diff --git a/apps/blog/public/txt/stats.txt b/apps/blog/public/txt/stats.txt new file mode 100644 index 0000000..cb70939 --- /dev/null +++ b/apps/blog/public/txt/stats.txt @@ -0,0 +1,11 @@ +this site consists of [WORDS] words across [PAGES] pages + +there are [POSTS] blog posts +[TXT] txt files +and [BOOKMARKS] bookmarks + +[GUESTBOOK] people have signed the guestbook + +this file was generated automatically on deploy +cheers, +lewis diff --git a/apps/blog/scripts/generate-stats.js b/apps/blog/scripts/generate-stats.js new file mode 100644 index 0000000..1e61928 --- /dev/null +++ b/apps/blog/scripts/generate-stats.js @@ -0,0 +1,85 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import yaml from 'js-yaml'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(__dirname, '..'); + +function countWords(text) { + return text.split(/\s+/).filter(w => w.length > 0).length; +} + +// Count blog posts and their words +const postsDir = path.join(root, 'src/content/posts'); +const posts = fs.existsSync(postsDir) + ? fs.readdirSync(postsDir).filter(f => f.endsWith('.md')) + : []; +let postWords = 0; +for (const post of posts) { + const content = fs.readFileSync(path.join(postsDir, post), 'utf-8'); + // Remove frontmatter + const body = content.replace(/^---[\s\S]*?---/, ''); + postWords += countWords(body); +} + +// Count txt files and their words (excluding stats.txt which we're generating) +const txtDir = path.join(root, 'public/txt'); +const txtFiles = fs.existsSync(txtDir) + ? fs.readdirSync(txtDir).filter(f => f.endsWith('.txt') && f !== 'stats.txt') + : []; +let txtWords = 0; +for (const txt of txtFiles) { + const content = fs.readFileSync(path.join(txtDir, txt), 'utf-8'); + txtWords += countWords(content); +} + +// Count bookmarks +const bookmarksFile = path.join(root, 'src/data/bookmarks.yaml'); +const bookmarks = fs.existsSync(bookmarksFile) + ? yaml.load(fs.readFileSync(bookmarksFile, 'utf-8')) || [] + : []; + +// Guestbook count - read from built JSON file +const guestbookJsonFile = path.join(root, '.vercel/output/static/guestbook-count.json'); +let guestbookCount = 0; +if (fs.existsSync(guestbookJsonFile)) { + const data = JSON.parse(fs.readFileSync(guestbookJsonFile, 'utf-8')); + guestbookCount = data.count; +} + +// Calculate totals (excluding stats.txt words for now, we'll add them after generating) +const totalPages = 1 + 1 + posts.length + 1 + txtFiles.length + 1 + 1; // home, blog index, posts, txt index, txts, bookmarks, guestbook + +// Read template from public/txt/stats.txt and replace placeholders +const template = fs.readFileSync(path.join(root, 'public/txt/stats.txt'), 'utf-8'); + +// First pass: generate stats without stats.txt word count +let stats = template + .replace('[PAGES]', totalPages.toString()) + .replace('[POSTS]', posts.length.toString()) + .replace('[TXT]', txtFiles.length.toString()) + .replace('[BOOKMARKS]', bookmarks.length.toString()) + .replace('[GUESTBOOK]', guestbookCount.toString()); + +// Count words in the stats file itself (before adding [WORDS]) +const statsWords = countWords(stats.replace('[WORDS]', '0')); +const totalWords = postWords + txtWords + statsWords; + +// Final pass: replace [WORDS] with actual total +stats = stats.replace('[WORDS]', totalWords.toString()); + +// Write to Vercel output +const outputDir = path.join(root, '.vercel/output/static/txt'); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} +fs.writeFileSync(path.join(outputDir, 'stats.txt'), stats); + +console.log('Generated stats.txt'); +console.log(` Words: ${totalWords}`); +console.log(` Pages: ${totalPages}`); +console.log(` Posts: ${posts.length}`); +console.log(` Txt files: ${txtFiles.length}`); +console.log(` Bookmarks: ${bookmarks.length}`); +console.log(` Guestbook: ${guestbookCount}`); diff --git a/apps/blog/src/pages/guestbook-count.json.ts b/apps/blog/src/pages/guestbook-count.json.ts new file mode 100644 index 0000000..96b22d2 --- /dev/null +++ b/apps/blog/src/pages/guestbook-count.json.ts @@ -0,0 +1,17 @@ +import type { APIRoute } from 'astro'; +import { getApprovedEntries } from '../lib/db'; + +export const prerender = true; + +export const GET: APIRoute = async () => { + let count = 0; + try { + const entries = await getApprovedEntries(); + count = entries.length; + } catch { + // DB not available + } + return new Response(JSON.stringify({ count }), { + headers: { 'Content-Type': 'application/json' }, + }); +};