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

@@ -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>
)
}
}