Release v1.2.0: Newspaper-style layout with major UI refinements

This release transforms PING into a sophisticated newspaper-style digital
publication with enhanced readability and professional presentation.

Major Features:
- New FeaturedPostHero component with full-width newspaper design
- Completely redesigned homepage with responsive newspaper grid layout
- Enhanced PostCard component with refined typography and spacing
- Improved mobile-first responsive design (mobile → tablet → desktop → 2XL)
- Archive section with multi-column layout for deeper content discovery

Technical Improvements:
- Enhanced blog post validation and error handling in lib/blog.ts
- Better date handling and normalization for scheduled posts
- Improved Dockerfile with correct content volume mount paths
- Fixed port configuration (3025 throughout stack)
- Updated Tailwind config with refined typography and newspaper aesthetics
- Added getFeaturedPost() function for hero selection

UI/UX Enhancements:
- Professional newspaper-style borders and dividers
- Improved dark mode styling throughout
- Better content hierarchy and visual flow
- Enhanced author bylines and metadata presentation
- Refined color palette with newspaper sophistication

Documentation:
- Added DESIGN_BRIEF_NEWSPAPER_LAYOUT.md detailing design principles
- Added TESTING_RESULTS_25_POSTS.md with test scenarios

This release establishes PING as a premium publication platform for
AI orchestration and contextual intelligence thought leadership.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-10-19 00:23:51 +11:00
parent 796924499d
commit 5e0be60c30
40 changed files with 1865 additions and 324 deletions

View File

@@ -194,78 +194,14 @@ body {
}
/* CHORUS Blog Typography */
.prose {
@apply max-w-none text-carbon-950 dark:text-carbon-100;
}
.prose h1 {
@apply text-h2 font-sans font-thin text-carbon-950 dark:text-mulberry-100 mb-8;
}
.prose h2 {
@apply text-h3 font-logo text-carbon-950 dark:text-mulberry-200 mb-6 mt-12;
}
.prose h3 {
@apply text-h4 font-sans text-carbon-950 dark:text-mulberry-300 mb-4 mt-8;
}
.prose h4 {
@apply text-h5 font-sans text-carbon-800 dark:text-mulberry-400 mb-3 mt-6;
}
.prose h5 {
@apply text-h6 font-sans text-carbon-700 dark:text-mulberry-500 mb-3 mt-6;
}
.prose h6 {
@apply text-h7 font-sans text-carbon-600 dark:text-mulberry-600 mb-2 mt-4;
}
.prose p {
@apply text-base leading-relaxed mb-6 text-carbon-800 dark:text-carbon-200;
}
.prose a {
@apply text-ocean-600 dark:text-ocean-400 hover:text-ocean-800 dark:hover:text-ocean-300 transition-colors underline decoration-ocean-500/30 hover:decoration-ocean-600/50 dark:hover:decoration-ocean-400/50;
}
.prose strong {
@apply text-carbon-950 dark:text-carbon-100 font-semibold;
}
.prose code {
@apply bg-carbon-200 dark:bg-carbon-800 text-eucalyptus-700 dark:text-eucalyptus-400 px-2 py-1 rounded-sm font-mono text-sm;
}
.prose pre {
@apply bg-carbon-100 dark:bg-carbon-900 border border-carbon-300 dark:border-carbon-700 rounded-lg overflow-x-auto p-4 mb-6;
}
.prose pre code {
@apply bg-transparent text-carbon-950 dark:text-carbon-200 p-0;
}
.prose blockquote {
@apply border-l-4 border-carbon-400 dark:border-mulberry-600 pl-6 italic text-carbon-600 dark:text-carbon-300 my-6;
}
.prose ul {
@apply list-disc pl-6 mb-6 space-y-2;
}
.prose ol {
@apply list-decimal pl-6 mb-6 space-y-2;
}
.prose li {
@apply text-carbon-800 dark:text-carbon-200;
}
/* Blog-specific utilities */
.blog-container {
@apply max-w-4xl mx-auto px-4 sm:px-6 lg:px-8;
padding-bottom: 0;
margin-bottom: 0;
border-bottom: none;
}
.blog-meta {

View File

@@ -1,68 +1,253 @@
import { getSortedPostsData, getFeaturedPosts } from '@/lib/blog'
import { getSortedPostsData, getFeaturedPost } from '@/lib/blog'
import BlogHeader from '@/components/BlogHeader'
import BlogFooter from '@/components/BlogFooter'
import PostCard from '@/components/PostCard'
import FeaturedPostHero from '@/components/FeaturedPostHero'
export default function HomePage() {
// Force dynamic rendering to pick up volume-mounted content
export const dynamic = 'force-dynamic'
export default async function HomePage() {
const allPosts = getSortedPostsData()
const featuredPosts = getFeaturedPosts()
const recentPosts = allPosts.slice(0, 6)
const featuredPost = getFeaturedPost()
const recentPosts = allPosts.slice(1, 7) // Skip the first post since it's featured
const additionalPosts = allPosts.slice(8, 20) // Additional posts for newspaper columns
return (
<main className="min-h-screen">
<BlogHeader />
{/* Hero Section */}
<section className="blog-container py-16">
<div className="max-w-3xl mx-auto text-center">
<h1 className="text-h1 font-logo text-carbon-950 dark:text-mulberry-100 mb-6">
Informed Agentic AI
</h1>
<p className="text-xl text-carbon-700 dark:text-carbon-300 leading-relaxed">
Deep dives into AI orchestration, agent coordination, and the future of
intelligent systems from the team building CHORUS.
</p>
{/* Featured Post - Full Width Hero */}
{featuredPost && (
<FeaturedPostHero post={featuredPost} />
)}
{/* Recent Posts - Responsive Newspaper Layout */}
<section className="w-full px-4 sm:px-8 py-16 border-t border-carbon-200 dark:border-carbon-800">
{/* Section Header - Newspaper Style */}
<div className="max-w-6xl mx-auto">
{recentPosts.length > 0 ? (
<div className="flex flex-col 2xl:flex-row gap-8">
{/* Main Content Area - Primary Articles */}
<div className="flex-1 max-w-6xl mx-auto 2xl:mx-0">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-12 gap-y-8">
{recentPosts.slice(0, 4).map((post) => (
<PostCard key={post.slug} post={post} />
))}
</div>
</div>
{/* Sidebar Articles - Only on 2XL+ screens if more articles available */}
{/* {recentPosts.length > 4 && (
<aside className="hidden 2xl:block w-80 space-y-6">
<div className="bg-sand-50/50 dark:bg-carbon-900/30 border border-carbon-200 dark:border-carbon-800 p-6 rounded-lg">
<h3 className="font-bold text-carbon-950 dark:text-carbon-100 mb-4 text-lg border-b border-carbon-300 dark:border-carbon-700 pb-2">
More Analysis
</h3>
<div className="space-y-4">
{recentPosts.slice(4, 7).map((post) => (
<article key={post.slug} className="group">
<a href={`/posts/${post.slug}`} className="block">
<div className="mb-1">
<span className="text-mulberry-600 dark:text-mulberry-400 text-xs font-bold uppercase tracking-wide">
{post.tags?.[0] || 'Analysis'}
</span>
</div>
<h4 className="text-sm font-bold text-carbon-950 dark:text-carbon-100 leading-tight mb-2 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors">
{post.title}
</h4>
<div className="flex items-center text-xxs text-carbon-600 dark:text-carbon-500">
<span>{post.author?.name || 'CHORUS Team'}</span>
<span className="mx-1">•</span>
<span>{post.readingTime} min read</span>
</div>
</a>
</article>
))}
</div>
</div>
</aside>
)} */}
</div>
) : (
<div className="text-center py-12">
<div className="w-16 h-16 bg-carbon-200 dark:bg-carbon-800 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-carbon-600 dark:text-carbon-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</div>
<h3 className="text-h5 font-semibold text-carbon-950 dark:text-carbon-200 mb-2">Coming Soon</h3>
<p className="text-carbon-600 dark:text-carbon-400 max-w-md mx-auto">
We're preparing some excellent content about contextual AI and agent orchestration.
Check back soon for our first posts!
</p>
</div>
)}
</div>
</section>
{/* Featured Posts */}
{featuredPosts.length > 0 && (
<section className="blog-container py-8">
<h2 className="text-h3 font-logo text-carbon-950 dark:text-carbon-100 mb-8">Featured Posts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{featuredPosts.map((post) => (
<PostCard key={post.slug} post={post} featured={true} />
))}
{/* Newspaper Column Layout - Additional Articles */}
{additionalPosts.length > 0 && (
<section className="w-full px-4 sm:px-8 py-16 border-t border-carbon-200 dark:border-carbon-800 bg-sand-25/20 dark:bg-carbon-950/50">
<div className="max-w-6xl mx-auto">
{/* Section Header */}
<div className="flex items-center mb-12">
<div className="flex-1 h-px bg-carbon-300 dark:bg-carbon-700"></div>
<div className="px-6">
<h2 className="text-2xl md:text-3xl font-bold text-carbon-950 dark:text-carbon-100 text-center">
Archive
</h2>
<p className="text-center text-carbon-600 dark:text-carbon-500 text-sm mt-1 italic">
Deeper insights and analysis
</p>
</div>
<div className="flex-1 h-px bg-carbon-300 dark:bg-carbon-700"></div>
</div>
{/* Flexbox Newspaper Columns - 4 Narrow Equal Columns */}
<div className="hidden lg:flex gap-6">
{/* Column 1 */}
<div className="flex-1 space-y-4">
{additionalPosts.slice(0, 3).map((post) => (
<article key={post.slug} className="group border-b border-carbon-200 dark:border-carbon-700 pb-4 mb-4">
<a href={`/posts/${post.slug}`} className="block">
{/* <div className="mb-2">
<span className="text-mulberry-600 dark:text-mulberry-400 text-xs font-bold uppercase tracking-wide">
{post.tags?.[0] || 'Analysis'}
</span>
</div> */}
<h3 className="text-sm font-bold text-carbon-950 dark:text-carbon-100 leading-tight mb-2 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors">
{post.title}
</h3>
<div className="mb-chorus-sm flex items-center text-xxs text-carbon-500 dark:text-carbon-600">
<span>{post.author?.name || 'CHORUS Team'}</span>
<span className="mx-1">•</span>
<span>{post.readingTime} min</span>
</div>
<p className="text-xs text-carbon-600 dark:text-carbon-400 leading-relaxed mb-2 line-clamp-3">
{post.description}
</p>
</a>
</article>
))}
</div>
{/* Column 2 */}
<div className="flex-1 space-y-4">
{additionalPosts.slice(3, 6).map((post) => (
<article key={post.slug} className="group border-b border-carbon-200 dark:border-carbon-700 pb-4 mb-4">
<a href={`/posts/${post.slug}`} className="block">
{/* <div className="mb-2">
<span className="text-mulberry-600 dark:text-mulberry-400 text-xs font-bold uppercase tracking-wide">
{post.tags?.[0] || 'Analysis'}
</span>
</div> */}
<h3 className="text-sm font-bold text-carbon-950 dark:text-carbon-100 leading-tight mb-2 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors">
{post.title}
</h3>
<div className="mb-chorus-sm flex items-center text-xxs text-carbon-500 dark:text-carbon-600">
<span>{post.author?.name || 'CHORUS Team'}</span>
<span className="mx-1">•</span>
<span>{post.readingTime} min</span>
</div>
<p className="text-xs text-carbon-600 dark:text-carbon-400 leading-relaxed mb-2 line-clamp-3">
{post.description}
</p>
</a>
</article>
))}
</div>
{/* Column 3 */}
<div className="flex-1 space-y-4">
{additionalPosts.slice(6, 9).map((post) => (
<article key={post.slug} className="group border-b border-carbon-200 dark:border-carbon-700 pb-4 mb-4">
<a href={`/posts/${post.slug}`} className="block">
{/* <div className="mb-2">
<span className="text-mulberry-600 dark:text-mulberry-400 text-xs font-bold uppercase tracking-wide">
{post.tags?.[0] || 'Analysis'}
</span>
</div> */}
<h3 className="text-sm font-bold text-carbon-950 dark:text-carbon-100 leading-tight mb-2 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors">
{post.title}
</h3>
<div className="mb-chorus-sm flex items-center text-xxs text-carbon-500 dark:text-carbon-600">
<span>{post.author?.name || 'CHORUS Team'}</span>
<span className="mx-1">•</span>
<span>{post.readingTime} min</span>
</div>
<p className="text-xs text-carbon-600 dark:text-carbon-400 leading-relaxed mb-2 line-clamp-3">
{post.description}
</p>
</a>
</article>
))}
</div>
{/* Column 4 */}
<div className="flex-1 space-y-4">
{additionalPosts.slice(9, 12).map((post) => (
<article key={post.slug} className="group border-b border-carbon-200 dark:border-carbon-700 pb-4 mb-4">
<a href={`/posts/${post.slug}`} className="block">
{/* <div className="mb-2">
<span className="text-mulberry-600 dark:text-mulberry-400 text-xs font-bold uppercase tracking-wide">
{post.tags?.[0] || 'Analysis'}
</span>
</div> */}
<h3 className="text-sm font-bold text-carbon-950 dark:text-carbon-100 leading-tight mb-2 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors">
{post.title}
</h3>
<div className="mb-chorus-sm flex items-center text-xxs text-carbon-500 dark:text-carbon-600">
<span>{post.author?.name || 'CHORUS Team'}</span>
<span className="mx-1">•</span>
<span>{post.readingTime} min</span>
</div>
<p className="text-xs text-carbon-600 dark:text-carbon-400 leading-relaxed mb-2 line-clamp-3">
{post.description}
</p>
</a>
</article>
))}
</div>
</div>
{/* Mobile/Tablet Fallback - Single Column */}
<div className="lg:hidden space-y-6">
{additionalPosts.slice(0, 8).map((post) => (
<article key={post.slug} className="group border-b border-carbon-200 dark:border-carbon-700 pb-4 mb-4">
<a href={`/posts/${post.slug}`} className="block">
<div className="mb-2">
<span className="text-mulberry-600 dark:text-mulberry-400 text-xs font-bold uppercase tracking-wide">
{post.tags?.[0] || 'Analysis'}
</span>
</div>
<h3 className="text-lg font-bold text-carbon-950 dark:text-carbon-100 leading-tight mb-3 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors">
{post.title}
</h3>
<p className="text-sm text-carbon-600 dark:text-carbon-400 leading-relaxed mb-3">
{post.description}
</p>
<div className="flex items-center text-xxs text-carbon-500 dark:text-carbon-600">
<span>{post.author?.name || 'CHORUS Team'}</span>
<span className="mx-2">•</span>
<time dateTime={post.date}>
{new Date(post.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</time>
<span className="mx-2"></span>
<span>{post.readingTime} min read</span>
</div>
</a>
</article>
))}
</div>
</div>
</section>
)}
{/* Recent Posts */}
<section className="blog-container py-12">
<h2 className="text-h3 font-logo text-carbon-950 dark:text-carbon-100 mb-8">Recent Posts</h2>
{recentPosts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{recentPosts.map((post) => (
<PostCard key={post.slug} post={post} />
))}
</div>
) : (
<div className="text-center py-12">
<div className="w-16 h-16 bg-carbon-200 dark:bg-carbon-800 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-carbon-600 dark:text-carbon-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</div>
<h3 className="text-h5 font-semibold text-carbon-950 dark:text-carbon-200 mb-2">Coming Soon</h3>
<p className="text-carbon-600 dark:text-carbon-400 max-w-md mx-auto">
We're preparing some excellent content about contextual AI and agent orchestration.
Check back soon for our first posts!
</p>
</div>
)}
</section>
<BlogFooter />
</main>
)
}
}

View File

@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { getPostData, getAllPostSlugs } from '@/lib/blog'
import { getPostData } from '@/lib/blog'
import BlogHeader from '@/components/BlogHeader'
import BlogFooter from '@/components/BlogFooter'
import Link from 'next/link'
@@ -15,10 +15,8 @@ interface PostPageProps {
}>
}
export async function generateStaticParams() {
const slugs = getAllPostSlugs()
return slugs.map(({ params }) => ({ slug: params.slug }))
}
// Force dynamic rendering - no static generation
export const dynamic = 'force-dynamic'
export async function generateMetadata({ params }: PostPageProps) {
const { slug } = await params
@@ -102,38 +100,38 @@ export default async function PostPage({ params }: PostPageProps) {
))}
</div>
<h1 className="text-h1 font-logo text-carbon-950 dark:text-mulberry-100 mb-6">
<h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-h1 font-bold text-carbon-950 dark:text-mulberry-100 mb-6 leading-tight">
{post.title}
</h1>
<p className="text-xl text-carbon-700 dark:text-carbon-300 leading-relaxed mb-8">
<p className="text-base sm:text-lg md:text-xl text-carbon-700 dark:text-carbon-300 leading-relaxed mb-8">
{post.description}
</p>
<div className="flex items-center justify-between border-b border-carbon-300 dark:border-carbon-800 pb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between border-b border-carbon-300 dark:border-carbon-800 pb-8 gap-4">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gradient-to-br from-mulberry-400 to-ocean-500 rounded-full flex items-center justify-center">
<span className="text-carbon-950 font-semibold">
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-mulberry-400 to-ocean-500 rounded-full flex items-center justify-center">
<span className="text-carbon-950 font-semibold text-sm sm:text-base">
{post.author.name.charAt(0)}
</span>
</div>
<div>
<p className="text-carbon-950 dark:text-carbon-200 font-medium">
<p className="text-carbon-950 dark:text-carbon-200 font-medium text-sm sm:text-base">
{post.author.name}
</p>
{post.author.role && (
<p className="text-carbon-600 dark:text-carbon-500 text-sm">
<p className="text-carbon-600 dark:text-carbon-500 text-xs sm:text-sm">
{post.author.role}
</p>
)}
</div>
</div>
<div className="blog-meta text-right">
<time dateTime={post.date} className="block">
<div className="blog-meta sm:text-right">
<time dateTime={post.date} className="block text-sm sm:text-base">
{formattedDate}
</time>
<span className="text-carbon-600 dark:text-carbon-600">
<span className="text-carbon-600 dark:text-carbon-600 text-xs sm:text-sm">
{post.readingTime} min read
</span>
</div>
@@ -141,7 +139,7 @@ export default async function PostPage({ params }: PostPageProps) {
</header>
{/* Article content */}
<div className="prose prose-lg max-w-none">
<div className="prose prose-lg max-w-none dark:prose-invert">
<MDXRemote source={post.content} options={mdxOptions} />
</div>

View File

@@ -1,6 +1,9 @@
import { getSortedPostsData } from '@/lib/blog'
import { NextResponse } from 'next/server'
// Force dynamic rendering to pick up volume-mounted content
export const dynamic = 'force-dynamic'
// RSS feed configuration
const SITE_URL = 'https://blog.chorus.services'
const SITE_TITLE = 'CHORUS Services Blog'