From dc060d2b1abc7db582abe3e3e39766ea3aa7790d Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Tue, 24 Feb 2026 16:33:29 -0600 Subject: [PATCH] Signed-off-by: OpenClaw Bot --- docs/TITLE_FORMAT.md | 70 ++++++++++++ package-lock.json | 14 +++ package.json | 1 + scripts/migrate-titles.js | 179 ++++++++++++++++++++++++++++++ src/app/page.tsx | 4 +- src/components/DigestList.tsx | 53 --------- src/pages/api/podcast/generate.ts | 54 --------- src/pages/api/podcast/rss.ts | 50 --------- 8 files changed, 266 insertions(+), 159 deletions(-) create mode 100644 docs/TITLE_FORMAT.md create mode 100644 scripts/migrate-titles.js delete mode 100644 src/components/DigestList.tsx delete mode 100644 src/pages/api/podcast/generate.ts delete mode 100644 src/pages/api/podcast/rss.ts diff --git a/docs/TITLE_FORMAT.md b/docs/TITLE_FORMAT.md new file mode 100644 index 0000000..cffb98f --- /dev/null +++ b/docs/TITLE_FORMAT.md @@ -0,0 +1,70 @@ +# Daily Digest Title Format Standard + +## Standard Format + +All Daily Digest titles must follow this exact format: + +``` +## Daily Digest - {DayName}, {Month} {Day}, {Year} +``` + +### Examples + +āœ… **Correct:** +- `## Daily Digest - Monday, February 24, 2026` +- `## Daily Digest - Tuesday, March 3, 2026` +- `## Daily Digest - Sunday, December 31, 2026` + +āŒ **Incorrect:** +- `## Daily Digest - February 24, 2026` (missing day name) +- `## Daily Digest - Monday, February 24th, 2026` (uses ordinal "24th") +- `# Daily Digest - Monday, February 24, 2026` (wrong header level) + +## Rules + +1. **Header Level:** Always use `##` (H2), not `#` (H1) or `###` (H3) +2. **Day Name:** Always include the full day name (Monday, Tuesday, etc.) +3. **Month:** Full month name (January, February, etc.) +4. **Day:** Plain number without ordinal suffix (24, not 24th) +5. **Year:** Full 4-digit year (2026, not 26) +6. **Separators:** Comma after day name, comma after day number + +## Code Reference + +If you need to generate a title programmatically, use this function: + +```typescript +function getDayName(dateStr: string): string { + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const date = new Date(dateStr + 'T12:00:00'); // Use noon to avoid timezone issues + return days[date.getDay()]; +} + +function formatDateForTitle(dateStr: string): string { + const months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + const [year, month, day] = dateStr.split('-'); + const monthName = months[parseInt(month, 10) - 1]; + return `${monthName} ${parseInt(day, 10)}, ${year}`; +} + +function generateStandardTitle(dateStr: string): string { + const dayName = getDayName(dateStr); + const datePart = formatDateForTitle(dateStr); + return `## Daily Digest - ${dayName}, ${datePart}`; +} + +// Example usage: +const title = generateStandardTitle('2026-02-24'); +// Returns: "## Daily Digest - Tuesday, February 24, 2026" +``` + +## Migration History + +**Last Migration:** 2026-02-24 +- Migrated 9 existing posts to the new standard format +- Script: `scripts/migrate-titles.js` +- Run with: `node scripts/migrate-titles.js` +- Dry run: `node scripts/migrate-titles.js --dry-run` diff --git a/package-lock.json b/package-lock.json index a5f4b8b..2ce0d76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "dotenv": "^17.3.1", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", @@ -3078,6 +3079,19 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index 7dcc45e..5741538 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "dotenv": "^17.3.1", "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", diff --git a/scripts/migrate-titles.js b/scripts/migrate-titles.js new file mode 100644 index 0000000..5c1dbbe --- /dev/null +++ b/scripts/migrate-titles.js @@ -0,0 +1,179 @@ +#!/usr/bin/env node +/** + * Migration Script: Standardize Blog Titles + * + * This script standardizes all Daily Digest titles to the format: + * "## Daily Digest - Monday, February 24, 2026" + * + * Run: node migrate-titles.js [--dry-run] + */ + +require('dotenv').config({ path: require('path').join(__dirname, '..', '.env.local') }); +const { createClient } = require('@supabase/supabase-js'); + +const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://qnatchrjlpehiijwtreh.supabase.co'; +const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +if (!SUPABASE_SERVICE_KEY) { + console.error('Error: SUPABASE_SERVICE_ROLE_KEY or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variable required'); + process.exit(1); +} + +const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY); + +/** + * Get day name from date string (YYYY-MM-DD) + */ +function getDayName(dateStr) { + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const date = new Date(dateStr + 'T12:00:00'); // Use noon to avoid timezone issues + return days[date.getDay()]; +} + +/** + * Format date for title (e.g., "February 24, 2026") + */ +function formatDateForTitle(dateStr) { + const months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + const [year, month, day] = dateStr.split('-'); + const monthName = months[parseInt(month, 10) - 1]; + return `${monthName} ${parseInt(day, 10)}, ${year}`; +} + +/** + * Generate standardized title + */ +function generateStandardTitle(dateStr) { + const dayName = getDayName(dateStr); + const datePart = formatDateForTitle(dateStr); + return `## Daily Digest - ${dayName}, ${datePart}`; +} + +/** + * Extract current title from content + */ +function extractCurrentTitle(content) { + const lines = content.split('\n'); + const titleLine = lines.find(l => l.startsWith('# ') || l.startsWith('## ')); + return titleLine || null; +} + +/** + * Replace title in content + */ +function replaceTitle(content, newTitle) { + const lines = content.split('\n'); + const titleIndex = lines.findIndex(l => l.startsWith('# ') || l.startsWith('## ')); + + if (titleIndex === -1) { + // No title found, prepend new title + return newTitle + '\n\n' + content; + } + + // Replace existing title line + lines[titleIndex] = newTitle; + return lines.join('\n'); +} + +/** + * Process all messages and update titles + */ +async function migrateTitles(dryRun = false) { + console.log('šŸ”„ Fetching all blog messages...\n'); + + const { data: messages, error } = await supabase + .from('blog_messages') + .select('id, date, content') + .order('date', { ascending: false }); + + if (error) { + console.error('Error fetching messages:', error); + process.exit(1); + } + + console.log(`Found ${messages.length} messages\n`); + + const updates = []; + const skipped = []; + + for (const message of messages) { + const currentTitle = extractCurrentTitle(message.content); + const newTitle = generateStandardTitle(message.date); + + // Check if update is needed + if (currentTitle === newTitle) { + skipped.push({ + id: message.id, + date: message.date, + title: currentTitle, + reason: 'Already in standard format' + }); + continue; + } + + updates.push({ + id: message.id, + date: message.date, + oldTitle: currentTitle, + newTitle: newTitle + }); + + if (!dryRun) { + const newContent = replaceTitle(message.content, newTitle); + + const { error: updateError } = await supabase + .from('blog_messages') + .update({ content: newContent }) + .eq('id', message.id); + + if (updateError) { + console.error(`āŒ Error updating message ${message.id}:`, updateError); + } else { + console.log(`āœ… Updated: ${message.date}`); + console.log(` Old: ${currentTitle}`); + console.log(` New: ${newTitle}\n`); + } + } + } + + // Summary + console.log('\n' + '='.repeat(60)); + console.log('MIGRATION SUMMARY'); + console.log('='.repeat(60)); + console.log(`Total messages: ${messages.length}`); + console.log(`Updates needed: ${updates.length}`); + console.log(`Already correct: ${skipped.length}`); + + if (dryRun) { + console.log('\nšŸ“‹ DRY RUN - No changes made\n'); + if (updates.length > 0) { + console.log('Proposed changes:'); + updates.forEach(u => { + console.log(`\n${u.date}:`); + console.log(` From: ${u.oldTitle}`); + console.log(` To: ${u.newTitle}`); + }); + } + } else { + console.log(`\nāœ… Migration complete!`); + } + + return { updates, skipped }; +} + +// Main execution +const dryRun = process.argv.includes('--dry-run'); + +if (dryRun) { + console.log('šŸ” DRY RUN MODE - No changes will be made\n'); +} + +migrateTitles(dryRun) + .then(() => process.exit(0)) + .catch(err => { + console.error('Migration failed:', err); + process.exit(1); + }); diff --git a/src/app/page.tsx b/src/app/page.tsx index b3453fd..acd3ffd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -37,8 +37,8 @@ export default function BlogPage() { function BlogPageContent() { const router = useRouter(); const searchParams = useSearchParams(); - const selectedTag = searchParams.get("tag"); - const selectedPostId = searchParams.get("post"); + const selectedTag = searchParams?.get("tag") ?? null; + const selectedPostId = searchParams?.get("post") ?? null; const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); diff --git a/src/components/DigestList.tsx b/src/components/DigestList.tsx deleted file mode 100644 index 923afb2..0000000 --- a/src/components/DigestList.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useState, useEffect } from 'react'; -import { getDigests } from '../../lib/supabase'; - -export default function DigestList() { - const [digests, setDigests] = useState([]); - const [error, setError] = useState(null); - - useEffect(() => { - async function loadDigests() { - try { - const data = await getDigests(); - setDigests(data); - } catch (err) { - setError('Failed to load digests'); - console.error(err); - } - } - - loadDigests(); - }, []); - - if (error) { - return ( -
- {error} -
- ); - } - - return ( -
- {digests.map((digest) => ( -
-

{digest.date}

-
{digest.content}
- - {digest.audio_url && ( - - )} -
- ))} -
- ); -} diff --git a/src/pages/api/podcast/generate.ts b/src/pages/api/podcast/generate.ts deleted file mode 100644 index d69ad55..0000000 --- a/src/pages/api/podcast/generate.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { create } from "apisauce"; -import { ElevenLabs } from "elevenlabs"; - -const supabase = new SupabaseClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); -const elevenLabs = new ElevenLabs(process.env.ELEVENLABS_API_KEY); - -export default async function handler(req, res) { - if (req.method !== "POST") return res.status(405).end(); - - const { date } = req.body; - - try { - // Fetch digest content - const { data: digest, error: fetchError } = await supabase - .from("digests") - .select("content") - .eq("date", date) - .single(); - - if (fetchError) throw fetchError; - if (!digest) return res.status(404).json({ error: "Digest not found" }); - - // Generate audio - const audioResponse = await elevenLabs.textToSpeech({ - text: digest.content, - voice: "nova", - model: "eleven_multilingual_v2" - }); - - // Save to Supabase storage - const audioBuffer = await audioResponse.arrayBuffer(); - const { data: { publicUrl }, error: storageError } = await supabase.storage - .from("podcasts") - .upload(`digest-${date}.mp3`, Buffer.from(audioBuffer), { - upsert: true - }); - - if (storageError) throw storageError; - - // Update digest record - const { error: updateError } = await supabase - .from("digests") - .update({ audio_url: publicUrl }) - .eq("date", date); - - if (updateError) throw updateError; - - res.status(200).json({ audioUrl: publicUrl }); - } catch (error) { - console.error(error); - res.status(500).json({ error: "Failed to generate podcast" }); - } -} diff --git a/src/pages/api/podcast/rss.ts b/src/pages/api/podcast/rss.ts deleted file mode 100644 index 3578ed6..0000000 --- a/src/pages/api/podcast/rss.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { create } from "apisauce"; - -const supabase = new SupabaseClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); - -export default async function handler(req, res) { - if (req.method !== "GET") return res.status(405).end(); - - try { - // Fetch all digests ordered by date - const { data: digests, error } = await supabase - .from("digests") - .select("date, content, audio_url") - .order("date", { ascending: false }); - - if (error) throw error; - - // Generate RSS XML - const xml = ` - - - Daily Digest Podcast - Audio version of the daily digest blog - https://blog-backup-two.vercel.app - Audio version of the daily digest blog - Daily Digest Team - - - ${digests - .map(digest => { - return ` - - Digest for ${digest.date} - ${digest.content} - ${new Date(digest.date).toUTCString()} - - 5:00 - `; - }) - .join("")} - -`; - - res.setHeader("Content-Type", "application/xml"); - res.status(200).send(xml); - } catch (error) { - console.error(error); - res.status(500).send("Error generating RSS feed"); - } -}