Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
This commit is contained in:
parent
bc3282212f
commit
dc060d2b1a
70
docs/TITLE_FORMAT.md
Normal file
70
docs/TITLE_FORMAT.md
Normal 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
14
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
179
scripts/migrate-titles.js
Normal file
179
scripts/migrate-titles.js
Normal 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);
|
||||
});
|
||||
@ -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<Message[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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" });
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user