Files
chorus-ping-blog/lib/blog.ts
anthonyrawlins 5e53840371 Implement automated scheduled publishing for blog
- Enhanced blog library to read from both posts/ and scheduled/ directories
- Added publishDate filtering with real-time checking (no cron jobs needed)
- Support for draft posts and recursive directory scanning
- Posts automatically appear when publishDate is reached
- Containerized solution that works without external scheduling
- Added publishDate field to blog types and updated existing scheduled post

Tested and verified:
 Past-dated posts appear automatically
 Future-dated posts remain hidden until publish time
 Draft posts are excluded regardless of date
 Maintains existing functionality for regular posts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 02:09:38 +10:00

140 lines
4.0 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { BlogPost, BlogMeta } from '@/types/blog'
const postsDirectory = path.join(process.cwd(), 'content/posts')
const scheduledDirectory = path.join(process.cwd(), 'content/scheduled')
// Helper function to check if a post should be published
function shouldPublishPost(post: BlogMeta): boolean {
// If no publishDate is set, use the date field
const publishDate = post.publishDate || post.date
if (!publishDate) return true
// Don't publish draft posts
if (post.draft === true) return false
// Check if publish date has passed
const publishDateTime = new Date(publishDate)
const now = new Date()
return publishDateTime <= now
}
// Helper function to recursively read markdown files from a directory
function readMarkdownFiles(directory: string, basePath: string = ''): BlogPost[] {
if (!fs.existsSync(directory)) {
return []
}
const items = fs.readdirSync(directory)
const posts: BlogPost[] = []
items.forEach((item) => {
const fullPath = path.join(directory, item)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
// Recursively read subdirectories
const subPosts = readMarkdownFiles(fullPath, path.join(basePath, item))
posts.push(...subPosts)
} else if (item.endsWith('.md')) {
try {
// Read markdown file as string
const fileContents = fs.readFileSync(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents)
const meta = matterResult.data as BlogMeta
// Create slug from filename (remove .md extension)
const slug = item.replace(/\.md$/, '')
// Calculate reading time (average 200 words per minute)
const wordCount = matterResult.content.split(/\s+/).length
const readingTime = Math.ceil(wordCount / 200)
const post: BlogPost = {
slug,
content: matterResult.content,
readingTime,
...meta,
}
posts.push(post)
} catch (error) {
console.error(`Error reading markdown file ${fullPath}:`, error)
}
}
})
return posts
}
export function getSortedPostsData(): BlogPost[] {
// Read posts from both published and scheduled directories
const publishedPosts = readMarkdownFiles(postsDirectory)
const scheduledPosts = readMarkdownFiles(scheduledDirectory)
// Combine all posts
const allPosts = [...publishedPosts, ...scheduledPosts]
// Filter posts that should be published
const publishablePosts = allPosts.filter(post => shouldPublishPost(post))
// Sort posts by date (newest first)
return publishablePosts.sort((a, b) => {
const dateA = new Date(a.date)
const dateB = new Date(b.date)
return dateB.getTime() - dateA.getTime()
})
}
export function getAllPostSlugs() {
// Get all publishable posts and extract their slugs
const posts = getSortedPostsData()
return posts.map((post) => {
return {
params: {
slug: post.slug,
},
}
})
}
export function getPostData(slug: string): BlogPost | null {
// Find the post in our sorted posts data
const posts = getSortedPostsData()
const post = posts.find(p => p.slug === slug)
if (!post) {
console.error(`Post with slug '${slug}' not found or not yet publishable`)
return null
}
return post
}
export function getFeaturedPosts(): BlogPost[] {
const allPosts = getSortedPostsData()
return allPosts.filter(post => post.featured)
}
export function getPostsByTag(tag: string): BlogPost[] {
const allPosts = getSortedPostsData()
return allPosts.filter(post =>
post.tags.some(t => t.toLowerCase() === tag.toLowerCase())
)
}
export function getAllTags(): string[] {
const allPosts = getSortedPostsData()
const tags = new Set<string>()
allPosts.forEach(post => {
post.tags.forEach(tag => tags.add(tag))
})
return Array.from(tags).sort()
}