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>
This commit is contained in:
anthonyrawlins
2025-08-28 02:09:38 +10:00
parent 91c1cb9e5b
commit 5e53840371
12 changed files with 347 additions and 130 deletions

View File

@@ -15,12 +15,13 @@ RUN npm ci --only=production
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY package.json package-lock.json* ./
RUN npm ci
COPY . . COPY . .
# Set build-time environment variables # Set build-time environment variables
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV production ENV NODE_ENV=production
# Build the application # Build the application
RUN npm run build RUN npm run build
@@ -29,8 +30,8 @@ RUN npm run build
FROM base AS runner FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED=1
# Create non-root user # Create non-root user
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
@@ -47,9 +48,9 @@ RUN chown -R nextjs:nodejs /app/posts /app/scheduled
USER nextjs USER nextjs
EXPOSE 3000 EXPOSE 3025
ENV PORT 3000 ENV PORT=3025
ENV HOSTNAME "0.0.0.0" ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"] CMD ["node", "server.js"]

View File

@@ -7,8 +7,181 @@
--font-inter: 'Inter Tight', ui-sans-serif, system-ui; --font-inter: 'Inter Tight', ui-sans-serif, system-ui;
--font-exo: 'Exo', 'Inter Tight', ui-sans-serif, system-ui; --font-exo: 'Exo', 'Inter Tight', ui-sans-serif, system-ui;
--font-mono: 'Inconsolata', ui-monospace, monospace; --font-mono: 'Inconsolata', ui-monospace, monospace;
/* CHORUS Brand CSS Variables - Ultra-Minimalist Implementation */
/* Core Brand Colors */
--color-carbon: #000000;
--color-mulberry: #0b0213;
--color-walnut: #403730;
--color-nickel: #c1bfb1;
/* CHORUS Semantic Color Tokens - 8 Color System */
--chorus-primary: #0b0213; /* carbon-950 */
--chorus-secondary: #0b0213; /* mulberry-950 */
--chorus-accent: #403730; /* walnut-900 */
--chorus-neutral: #c1bfb1; /* nickel-500 */
--chorus-info: #3a4654; /* ocean-900 */
--chorus-success: #3a4540; /* eucalyptus-900 */
--chorus-warning: #6a5c46; /* sand-900 */
--chorus-danger: #3e2d2c; /* coral-900 */
/* Light Theme Variables */
--bg-primary: #FFFFFF;
--bg-secondary: #CCCCCC;
--bg-tertiary: #AAAAAA;
--bg-accent: #F7F7E2;
--text-primary: #000000;
--text-secondary: #1A1A1A;
--text-tertiary: #333333;
--text-subtle: #666666;
--text-ghost: #999999;
--border-invisible: #FAFAFA;
--border-subtle: #F0F0F0;
--border-defined: #E5E5E5;
--border-emphasis: #CCCCCC;
--accent-primary: #0b0213; /* Mulberry */
--accent-secondary: #403730; /* Walnut */
--accent-system: #5a6c80; /* Ocean */
/* Spacing System */
--space-micro: 0.25rem; /* 8px */
--space-sm: 0.5rem; /* 16px */
--space-md: 1rem; /* 32px */
--space-lg: 2rem; /* 64px */
--space-xl: 4rem; /* 128px */
--space-xxl: 8rem; /* 256px */
} }
/* Dark Theme Variables */
.dark {
--bg-primary: #000000;
--bg-secondary: #333333;
--bg-tertiary: #555555; /* Mulberry */
--bg-accent: #1a1426;
--text-primary: #FFFFFF;
--text-secondary: #E0E0E0;
--text-tertiary: #C0C0C0;
--text-subtle: #A0A0A0;
--text-ghost: #808080;
--border-invisible: #1a1a1a;
--border-subtle: #2a2a2a;
--border-defined: #3a3a3a;
--border-emphasis: #4a4a4a;
--accent-primary: #F5F5DC; /* Sand */
--accent-secondary: #c1bfb1; /* Nickel */
}
/* CHORUS Accessibility Theme System - Complete Color Adaptations */
/* Protanopia (Red-blind) - Ocean/Sand adaptations for maximum clarity */
[data-theme="protanopia"] {
/* Logo material override */
--color-ring-primary: #1e40af; /* Blue-800 */
/* Primary brand colors adapted for red-blindness */
--chorus-primary: #3a4654; /* ocean-900 replaces carbon */
--chorus-secondary: #3a4654; /* ocean-900 replaces mulberry */
--chorus-accent: #6a5c46; /* sand-900 warm neutral */
--chorus-neutral: #c1bfb1; /* nickel-500 unchanged */
--chorus-info: #1e40af; /* ocean-800 enhanced */
--chorus-success: #6a5c46; /* sand-900 replaces eucalyptus */
--chorus-warning: #D0C9BF; /* sand-400 enhanced */
--chorus-danger: #4a5867; /* ocean-800 replaces coral */
/* Semantic color reassignments */
--accent-primary: #3a4654; /* Ocean replaces Mulberry */
--accent-secondary: #6a5c46; /* Sand replaces Walnut */
--accent-system: #1e40af; /* Enhanced Ocean blue */
}
/* Deuteranopia (Green-blind) - Blue/Yellow enhanced contrast */
[data-theme="deuteranopia"] {
/* Logo material override */
--color-ring-primary: #6b21a8; /* Purple-800 */
/* Primary brand colors adapted for green-blindness */
--chorus-primary: #0b0213; /* mulberry-950 enhanced */
--chorus-secondary: #6b21a8; /* purple-800 distinct */
--chorus-accent: #1e40af; /* blue-800 replaces walnut */
--chorus-neutral: #c1bfb1; /* nickel-500 unchanged */
--chorus-info: #1e40af; /* blue-800 enhanced */
--chorus-success: #6b21a8; /* purple-800 replaces eucalyptus */
--chorus-warning: #F1F0EF; /* sand-100 high contrast */
--chorus-danger: #991b1b; /* red-800 enhanced */
/* Semantic color reassignments */
--accent-primary: #6b21a8; /* Purple primary */
--accent-secondary: #1e40af; /* Blue secondary */
--accent-system: #991b1b; /* Red system alerts */
}
/* Tritanopia (Blue-blind) - Coral/Eucalyptus substitutions */
[data-theme="tritanopia"] {
/* Logo material override */
--color-ring-primary: #991b1b; /* Red-800 */
/* Primary brand colors adapted for blue-blindness */
--chorus-primary: #000000; /* carbon-950 unchanged */
--chorus-secondary: #403730; /* walnut-900 enhanced */
--chorus-accent: #991b1b; /* red-800 replaces blue elements */
--chorus-neutral: #c1bfb1; /* nickel-500 unchanged */
--chorus-info: #991b1b; /* red-800 replaces ocean */
--chorus-success: #6a5c46; /* sand-900 natural green replacement */
--chorus-warning: #D0C9BF; /* sand-400 warm warning */
--chorus-danger: #403730; /* walnut-900 dark contrast */
/* Semantic color reassignments */
--accent-primary: #991b1b; /* Red replaces blue elements */
--accent-secondary: #6a5c46; /* Sand warm accent */
--accent-system: #403730; /* Walnut system elements */
}
/* Achromatopsia (Complete color blindness) - High contrast grayscale only */
[data-theme="achromatopsia"] {
/* Logo material override */
--color-ring-primary: #374151; /* Gray-700 */
/* Complete grayscale adaptation */
--chorus-primary: #000000; /* Pure black */
--chorus-secondary: #1a1a1a; /* Carbon-800 */
--chorus-accent: #666666; /* Mid gray */
--chorus-neutral: #808080; /* Neutral mid-gray */
--chorus-info: #374151; /* Gray-700 */
--chorus-success: #2a2a2a; /* Dark gray for success */
--chorus-warning: #a0a0a0; /* Light gray for warnings */
--chorus-danger: #1a1a1a; /* Very dark gray for danger */
/* High contrast grayscale system */
--accent-primary: #000000; /* Pure black */
--accent-secondary: #666666; /* Mid-gray */
--accent-system: #374151; /* System gray */
/* Enhanced contrast overrides for all text and UI elements */
--bg-primary: #ffffff;
--bg-secondary: #f0f0f0;
--bg-tertiary: #e0e0e0;
--bg-accent: #c0c0c0;
--text-primary: #000000;
--text-secondary: #1a1a1a;
--text-tertiary: #333333;
--text-subtle: #666666;
--text-ghost: #999999;
--border-invisible: #f8f8f8;
--border-subtle: #e0e0e0;
--border-defined: #c0c0c0;
--border-emphasis: #808080;
}
html { html {
font-feature-settings: "cv02", "cv03", "cv04", "cv11"; font-feature-settings: "cv02", "cv03", "cv04", "cv11";
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -20,6 +193,7 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
/* CHORUS Blog Typography */ /* CHORUS Blog Typography */
.prose { .prose {
@apply max-w-none text-carbon-950 dark:text-carbon-100; @apply max-w-none text-carbon-950 dark:text-carbon-100;
@@ -107,6 +281,11 @@ body {
@apply fixed top-0 left-0 w-full h-1 bg-ocean-600 dark:bg-mulberry-600 transform origin-left scale-x-0 transition-transform z-50; @apply fixed top-0 left-0 w-full h-1 bg-ocean-600 dark:bg-mulberry-600 transform origin-left scale-x-0 transition-transform z-50;
} }
a {
text-decoration: none;
text-decoration-line: none;
}
/* Utilities */ /* Utilities */
.line-clamp-3 { .line-clamp-3 {
overflow: hidden; overflow: hidden;

View File

@@ -19,8 +19,8 @@ export const metadata: Metadata = {
title: 'CHORUS PING! - Insights on Contextual AI', title: 'CHORUS PING! - Insights on Contextual AI',
description: 'Deep dives into contextual AI orchestration, agent coordination, and the future of intelligent systems.', description: 'Deep dives into contextual AI orchestration, agent coordination, and the future of intelligent systems.',
keywords: ['contextual AI', 'agent orchestration', 'enterprise AI', 'AI insights', 'technology blog'], keywords: ['contextual AI', 'agent orchestration', 'enterprise AI', 'AI insights', 'technology blog'],
authors: [{ name: 'Anthony Lewis Rawlins', url: 'https://deepblack.cloud' }], authors: [{ name: 'Anthony Rawlins', url: 'https://chorus.services' }],
creator: 'Deep Black Cloud', creator: 'Anthony Rawlins',
publisher: 'CHORUS Services', publisher: 'CHORUS Services',
metadataBase: new URL('https://blog.chorus.services'), metadataBase: new URL('https://blog.chorus.services'),
alternates: { alternates: {
@@ -70,14 +70,14 @@ export default function RootLayout({
<html lang="en"> <html lang="en">
<head> <head>
{/* Google tag (gtag.js) */} {/* Google tag (gtag.js) */}
<script async src="https://www.googletagmanager.com/gtag/js?id=G-WTFF8JL9SF"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-RTYKD3GJ44"></script>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'G-WTFF8JL9SF'); gtag('config', 'G-RTYKD3GJ44);
`, `,
}} }}
/> />

View File

@@ -64,8 +64,8 @@ export default async function PostPage({ params }: PostPageProps) {
rehypePlugins: [ rehypePlugins: [
rehypeHighlight, rehypeHighlight,
rehypeSlug, rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }] [rehypeAutolinkHeadings, { behavior: 'wrap' } as any]
], ] as any,
} }
} }

View File

@@ -17,7 +17,7 @@ export default function BlogFooter() {
</div> </div>
</div> </div>
<p className="text-carbon-600 dark:text-carbon-400 text-sm leading-relaxed mb-6 max-w-md"> <p className="text-carbon-600 dark:text-carbon-400 text-sm leading-relaxed mb-6 max-w-md">
Insights and deep dives into contextual AI orchestration, agent coordination, Insights and deep dives into Context Engineering for Agentic AI and Orchestration, Agent Collaboration,
and the future of intelligent systems. and the future of intelligent systems.
</p> </p>
<div className="flex space-x-4"> <div className="flex space-x-4">
@@ -62,16 +62,6 @@ export default function BlogFooter() {
Platform Platform
</Link> </Link>
</li> </li>
<li>
<Link href="https://chorus.services/about" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
About
</Link>
</li>
<li>
<Link href="https://chorus.services/contact" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
Contact
</Link>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -23,7 +23,7 @@ export default function BlogHeader() {
? 'bg-white/80 dark:bg-carbon-950/80 backdrop-blur-md border-b border-carbon-200/50 dark:border-carbon-800/50' ? 'bg-white/80 dark:bg-carbon-950/80 backdrop-blur-md border-b border-carbon-200/50 dark:border-carbon-800/50'
: 'bg-white/20 dark:bg-carbon-950/20 backdrop-blur-sm' : 'bg-white/20 dark:bg-carbon-950/20 backdrop-blur-sm'
}`}> }`}>
<nav className="blog-container py-4"> <nav className="blog-container py-chorus-lg">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Link href="/" className="flex items-center space-x-3 group"> <Link href="/" className="flex items-center space-x-3 group">
<div className="w-14 h-14 group-hover:scale-110 transition-transform"> <div className="w-14 h-14 group-hover:scale-110 transition-transform">
@@ -35,25 +35,29 @@ export default function BlogHeader() {
</div> </div>
</Link> </Link>
<div className="hidden md:flex items-center space-x-8"> <div className="hidden md:flex items-center space-x-8">
<Link
href="https://chorus.services"
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-semibold"
>
Home
</Link>
<Link <Link
href="/" href="/"
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-medium" className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-semibold"
> >
All Posts All Posts
</Link> </Link>
<Link {/* <Link
href="/tags" href="/tags"
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-medium" className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-semibold"
> >
Topics Topics
</Link> </Link> */}
<Link
href="https://chorus.services"
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-medium"
>
About CHORUS
</Link>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">

View File

@@ -16,12 +16,12 @@ export default function PostCard({ post, featured = false }: PostCardProps) {
return ( return (
<article className={`group ${ <article className={`group ${
featured featured
? 'bg-carbon-50 dark:bg-carbon-900 border border-carbon-200 dark:border-carbon-800 rounded-xl p-8 hover:border-carbon-400 dark:hover:border-mulberry-700 transition-all duration-300' ? 'bg-gradient-to-b from-sand-200 to-sand-100 dark:from-mulberry-900 to-mulberry-700 border border-carbon-200 dark:border-carbon-800 p-chorus-lg hover:border-carbon-400 dark:hover:border-mulberry-700 transition-all duration-300'
: 'bg-carbon-100/50 dark:bg-carbon-900/50 border border-carbon-200/50 dark:border-carbon-800/50 rounded-lg p-6 hover:bg-carbon-50 dark:hover:bg-carbon-900 hover:border-carbon-300 dark:hover:border-carbon-700 transition-all duration-300' : 'bg-gradient-to-b from-sand-100 to-sand-50 dark:from-mulberry-800 to-mulberry-800 border border-carbon-200/50 dark:border-carbon-800/50 p-chorus-lg hover:bg-carbon-50 dark:hover:bg-carbon-900 hover:border-carbon-300 dark:hover:border-carbon-700 transition-all duration-300'
}`}> }`}>
<Link href={`/posts/${post.slug}`} className="block"> <Link href={`/posts/${post.slug}`} className="block">
<div className="mb-4"> <div className="mb-chorus-md">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-chorus-sm">
<div className="blog-meta"> <div className="blog-meta">
<time dateTime={post.date} className="text-carbon-600 dark:text-carbon-500"> <time dateTime={post.date} className="text-carbon-600 dark:text-carbon-500">
{formattedDate} {formattedDate}
@@ -40,7 +40,7 @@ export default function PostCard({ post, featured = false }: PostCardProps) {
<h2 className={`${ <h2 className={`${
featured ? 'text-h3' : 'text-h4' featured ? 'text-h3' : 'text-h4'
} font-logo text-carbon-950 dark:text-carbon-100 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors mb-3`}> } font-logo text-carbon-950 dark:text-carbon-100 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors mb-chorus-lg`}>
{post.title} {post.title}
</h2> </h2>

View File

@@ -20,8 +20,8 @@ if [ ! -d "node_modules" ]; then
fi fi
# Start the development server # Start the development server
echo "🚀 Starting Next.js development server on port 3002..." echo "🚀 Starting Next.js development server on port 3025..."
echo "📝 Blog will be available at: http://localhost:3002" echo "📝 Blog will be available at: http://localhost:3025"
echo "🌍 Production URL will be: https://blog.chorus.services" echo "🌍 Production URL will be: https://blog.chorus.services"
echo "" echo ""
echo "Press Ctrl+C to stop the server" echo "Press Ctrl+C to stop the server"

View File

@@ -2,29 +2,32 @@ version: '3.8'
services: services:
chorus-blog: chorus-blog:
build: image: registry.home.deepblack.cloud/tony/chorus-blog:latest
context: .
dockerfile: Dockerfile
ports: ports:
- "3002:3000" - "3025:3025"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1 - NEXT_TELEMETRY_DISABLED=1
volumes: volumes:
- ./posts:/app/posts:ro - /rust/containers/PING/content/posts:/app/content/posts:ro
- ./scheduled:/app/scheduled:ro - /rust/containers/PING/content/scheduled:/app/content/scheduled:ro
- blog_logs:/app/.next/cache - blog_logs:/app/.next/cache
labels: deploy:
- "traefik.enable=true" labels:
- "traefik.docker.network=tengig" - "traefik.enable=true"
- "traefik.http.routers.chorus-blog.rule=Host(`blog.chorus.services`)" - "traefik.docker.network=tengig"
- "traefik.http.routers.chorus-blog.entrypoints=web-secured" - "traefik.http.routers.chorus-blog.rule=Host(`blog.chorus.services`)"
- "traefik.http.routers.chorus-blog.tls.certresolver=letsencryptresolver" - "traefik.http.routers.chorus-blog.entrypoints=web-secured"
- "traefik.http.services.chorus-blog.loadbalancer.server.port=3000" - "traefik.http.routers.chorus-blog.tls.certresolver=letsencryptresolver"
- "traefik.http.services.chorus-blog.loadbalancer.passhostheader=true" - "traefik.http.services.chorus-blog.loadbalancer.server.port=3025"
- "traefik.http.services.chorus-blog.loadbalancer.passhostheader=true"
placement:
constraints:
- node.hostname == walnut
networks: networks:
- tengig - tengig
restart: unless-stopped - blog
volumes: volumes:
blog_logs: blog_logs:
@@ -32,3 +35,5 @@ volumes:
networks: networks:
tengig: tengig:
external: true external: true
blog:
driver: overlay

View File

@@ -4,82 +4,116 @@ import matter from 'gray-matter'
import { BlogPost, BlogMeta } from '@/types/blog' import { BlogPost, BlogMeta } from '@/types/blog'
const postsDirectory = path.join(process.cwd(), 'content/posts') 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[] { export function getSortedPostsData(): BlogPost[] {
// Get file names under /content/posts // Read posts from both published and scheduled directories
const fileNames = fs.readdirSync(postsDirectory) const publishedPosts = readMarkdownFiles(postsDirectory)
const allPostsData = fileNames const scheduledPosts = readMarkdownFiles(scheduledDirectory)
.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 // Combine all posts
const fullPath = path.join(postsDirectory, fileName) const allPosts = [...publishedPosts, ...scheduledPosts]
const fileContents = fs.readFileSync(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section // Filter posts that should be published
const matterResult = matter(fileContents) const publishablePosts = allPosts.filter(post => shouldPublishPost(post))
const meta = matterResult.data as BlogMeta
// Calculate reading time (average 200 words per minute) // Sort posts by date (newest first)
const wordCount = matterResult.content.split(/\s+/).length return publishablePosts.sort((a, b) => {
const readingTime = Math.ceil(wordCount / 200) const dateA = new Date(a.date)
const dateB = new Date(b.date)
return { return dateB.getTime() - dateA.getTime()
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() { export function getAllPostSlugs() {
const fileNames = fs.readdirSync(postsDirectory) // Get all publishable posts and extract their slugs
return fileNames const posts = getSortedPostsData()
.filter((fileName) => fileName.endsWith('.md')) return posts.map((post) => {
.map((fileName) => { return {
return { params: {
params: { slug: post.slug,
slug: fileName.replace(/\.md$/, ''), },
}, }
} })
})
} }
export function getPostData(slug: string): BlogPost | null { export function getPostData(slug: string): BlogPost | null {
try { // Find the post in our sorted posts data
const fullPath = path.join(postsDirectory, `${slug}.md`) const posts = getSortedPostsData()
const fileContents = fs.readFileSync(fullPath, 'utf8') const post = posts.find(p => p.slug === slug)
// Use gray-matter to parse the post metadata section if (!post) {
const matterResult = matter(fileContents) console.error(`Post with slug '${slug}' not found or not yet publishable`)
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 return null
} }
return post
} }
export function getFeaturedPosts(): BlogPost[] { export function getFeaturedPosts(): BlogPost[] {

View File

@@ -1,16 +1,16 @@
{ {
"name": "chorus-ping", "name": "chorus-ping",
"version": "1.0.0", "version": "1.0.1",
"description": "CHORUS PING! - Static blog posts using Next.js", "description": "CHORUS PING! - Static blog posts using Next.js",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3002", "dev": "next dev -p 3024",
"build": "next build", "build": "next build",
"start": "next start -p 3002", "start": "next start -p 3025",
"lint": "next lint", "lint": "next lint",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"docker:build": "docker build -t chorus-blog .", "docker:build": "docker build -t chorus-blog .",
"docker:run": "docker run -p 3002:3000 chorus-blog", "docker:run": "docker run -p 3025:3025 chorus-blog",
"docker:compose": "docker-compose up -d" "docker:compose": "docker-compose up -d"
}, },
"dependencies": { "dependencies": {

View File

@@ -4,6 +4,8 @@ export interface BlogPost {
description: string description: string
content: string content: string
date: string date: string
publishDate?: string
draft?: boolean
author: { author: {
name: string name: string
avatar?: string avatar?: string
@@ -19,6 +21,8 @@ export interface BlogMeta {
title: string title: string
description: string description: string
date: string date: string
publishDate?: string
draft?: boolean
author: { author: {
name: string name: string
avatar?: string avatar?: string