Initial commit: CHORUS PING! blog
- Next.js 14 blog application with theme support - Docker containerization with volume bindings - Traefik integration with Let's Encrypt SSL - MDX support for blog posts - Theme toggle with localStorage persistence - Scheduled posts directory structure - Brand guidelines compliance with CHORUS colors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
106
lib/blog.ts
Normal file
106
lib/blog.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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')
|
||||
|
||||
export function getSortedPostsData(): BlogPost[] {
|
||||
// Get file names under /content/posts
|
||||
const fileNames = fs.readdirSync(postsDirectory)
|
||||
const allPostsData = fileNames
|
||||
.filter((fileName) => fileName.endsWith('.md'))
|
||||
.map((fileName) => {
|
||||
// Remove ".md" from file name to get id
|
||||
const slug = fileName.replace(/\.md$/, '')
|
||||
|
||||
// Read markdown file as string
|
||||
const fullPath = path.join(postsDirectory, fileName)
|
||||
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
|
||||
|
||||
// Calculate reading time (average 200 words per minute)
|
||||
const wordCount = matterResult.content.split(/\s+/).length
|
||||
const readingTime = Math.ceil(wordCount / 200)
|
||||
|
||||
return {
|
||||
slug,
|
||||
content: matterResult.content,
|
||||
readingTime,
|
||||
...meta,
|
||||
} as BlogPost
|
||||
})
|
||||
|
||||
// Sort posts by date
|
||||
return allPostsData.sort((a, b) => {
|
||||
if (a.date < b.date) {
|
||||
return 1
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getAllPostSlugs() {
|
||||
const fileNames = fs.readdirSync(postsDirectory)
|
||||
return fileNames
|
||||
.filter((fileName) => fileName.endsWith('.md'))
|
||||
.map((fileName) => {
|
||||
return {
|
||||
params: {
|
||||
slug: fileName.replace(/\.md$/, ''),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getPostData(slug: string): BlogPost | null {
|
||||
try {
|
||||
const fullPath = path.join(postsDirectory, `${slug}.md`)
|
||||
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
|
||||
|
||||
// Calculate reading time (average 200 words per minute)
|
||||
const wordCount = matterResult.content.split(/\s+/).length
|
||||
const readingTime = Math.ceil(wordCount / 200)
|
||||
|
||||
return {
|
||||
slug,
|
||||
content: matterResult.content,
|
||||
readingTime,
|
||||
...meta,
|
||||
} as BlogPost
|
||||
} catch (error) {
|
||||
console.error(`Error reading post ${slug}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user