Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>

This commit is contained in:
OpenClaw Bot 2026-02-24 16:33:29 -06:00
parent bc3282212f
commit dc060d2b1a
8 changed files with 266 additions and 159 deletions

70
docs/TITLE_FORMAT.md Normal file
View File

@ -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`

14
package-lock.json generated
View File

@ -22,6 +22,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"dotenv": "^17.3.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"tailwindcss": "^4", "tailwindcss": "^4",
@ -3078,6 +3079,19 @@
"node": ">=0.10.0" "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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@ -26,6 +26,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"dotenv": "^17.3.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"tailwindcss": "^4", "tailwindcss": "^4",

179
scripts/migrate-titles.js Normal file
View File

@ -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);
});

View File

@ -37,8 +37,8 @@ export default function BlogPage() {
function BlogPageContent() { function BlogPageContent() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const selectedTag = searchParams.get("tag"); const selectedTag = searchParams?.get("tag") ?? null;
const selectedPostId = searchParams.get("post"); const selectedPostId = searchParams?.get("post") ?? null;
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);

View File

@ -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 (
<div className="text-red-500 p-4">
{error}
</div>
);
}
return (
<div className="space-y-4">
{digests.map((digest) => (
<div key={digest.date} className="border p-4 rounded">
<h3 className="text-lg font-bold">{digest.date}</h3>
<div className="mt-2 text-gray-700">{digest.content}</div>
{digest.audio_url && (
<div className="mt-4">
<a
href={digest.audio_url}
className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
target="_blank"
rel="noopener noreferrer"
>
Listen to Podcast
</a>
</div>
)}
</div>
))}
</div>
);
}

View File

@ -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" });
}
}

View File

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel>
<title>Daily Digest Podcast</title>
<description>Audio version of the daily digest blog</description>
<link>https://blog-backup-two.vercel.app</link>
<itunes:subtitle>Audio version of the daily digest blog</itunes:subtitle>
<itunes:author>Daily Digest Team</itunes:author>
<itunes:category text="Technology"/>
${digests
.map(digest => {
return `
<item>
<title>Digest for ${digest.date}</title>
<description>${digest.content}</description>
<pubDate>${new Date(digest.date).toUTCString()}</pubDate>
<enclosure url="${digest.audio_url}" type="audio/mpeg"/>
<itunes:duration>5:00</itunes:duration>
</item>`;
})
.join("")}
</channel>
</rss>`;
res.setHeader("Content-Type", "application/xml");
res.status(200).send(xml);
} catch (error) {
console.error(error);
res.status(500).send("Error generating RSS feed");
}
}