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:
116
app/globals.css
Normal file
116
app/globals.css
Normal file
@@ -0,0 +1,116 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&family=Exo:ital,wght@0,100..900;1,100..900&family=Inconsolata:wdth,wght@50..200,200..900&display=swap');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--font-inter: 'Inter Tight', ui-sans-serif, system-ui;
|
||||
--font-exo: 'Exo', 'Inter Tight', ui-sans-serif, system-ui;
|
||||
--font-mono: 'Inconsolata', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
html {
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.blog-meta {
|
||||
@apply text-sm text-carbon-600 dark:text-carbon-500 flex items-center space-x-4;
|
||||
}
|
||||
|
||||
.blog-tag {
|
||||
@apply inline-block bg-carbon-200 dark:bg-mulberry-800 text-carbon-700 dark:text-mulberry-200 px-3 py-1 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
/* Reading progress indicator */
|
||||
.reading-progress {
|
||||
@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;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.line-clamp-3 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
113
app/layout.tsx
Normal file
113
app/layout.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter, Exo } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const exo = Exo({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-exo',
|
||||
display: 'swap',
|
||||
weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'CHORUS PING! - Insights on Contextual AI',
|
||||
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'],
|
||||
authors: [{ name: 'Anthony Lewis Rawlins', url: 'https://deepblack.cloud' }],
|
||||
creator: 'Deep Black Cloud',
|
||||
publisher: 'CHORUS Services',
|
||||
metadataBase: new URL('https://blog.chorus.services'),
|
||||
alternates: {
|
||||
canonical: 'https://blog.chorus.services',
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: 'https://blog.chorus.services',
|
||||
siteName: 'CHORUS PING!',
|
||||
title: 'CHORUS PING! - Insights on Contextual AI',
|
||||
description: 'Deep dives into contextual AI orchestration, agent coordination, and the future of intelligent systems.',
|
||||
images: [
|
||||
{
|
||||
url: '/logos/logo-ring-only.png',
|
||||
width: 256,
|
||||
height: 256,
|
||||
alt: 'CHORUS Services Logo',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'CHORUS PING! - Insights on Contextual AI',
|
||||
description: 'Deep dives into contextual AI orchestration, agent coordination, and the future of intelligent systems.',
|
||||
images: ['/logos/chorus-landscape-on-blue.png'],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
{/* Google tag (gtag.js) */}
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-WTFF8JL9SF"></script>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-WTFF8JL9SF');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* Theme initialization script */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
var theme = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var shouldBeDark = theme === 'dark' || (!theme && prefersDark);
|
||||
|
||||
if (shouldBeDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
} catch (e) {
|
||||
// If localStorage is not available, default to dark
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className={`${inter.variable} ${exo.variable} font-sans bg-white dark:bg-carbon-950 text-carbon-950 dark:text-carbon-100 transition-colors duration-300`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
68
app/page.tsx
Normal file
68
app/page.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getSortedPostsData, getFeaturedPosts } from '@/lib/blog'
|
||||
import BlogHeader from '@/components/BlogHeader'
|
||||
import BlogFooter from '@/components/BlogFooter'
|
||||
import PostCard from '@/components/PostCard'
|
||||
|
||||
export default function HomePage() {
|
||||
const allPosts = getSortedPostsData()
|
||||
const featuredPosts = getFeaturedPosts()
|
||||
const recentPosts = allPosts.slice(0, 6)
|
||||
|
||||
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>
|
||||
</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} />
|
||||
))}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
174
app/posts/[slug]/page.tsx
Normal file
174
app/posts/[slug]/page.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||
import { getPostData, getAllPostSlugs } from '@/lib/blog'
|
||||
import BlogHeader from '@/components/BlogHeader'
|
||||
import BlogFooter from '@/components/BlogFooter'
|
||||
import Link from 'next/link'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
|
||||
interface PostPageProps {
|
||||
params: Promise<{
|
||||
slug: string
|
||||
}>
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const slugs = getAllPostSlugs()
|
||||
return slugs.map(({ params }) => ({ slug: params.slug }))
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PostPageProps) {
|
||||
const { slug } = await params
|
||||
const post = getPostData(slug)
|
||||
|
||||
if (!post) {
|
||||
return {
|
||||
title: 'Post Not Found - CHORUS PING!'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${post.title} - CHORUS PING!`,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: 'article',
|
||||
publishedTime: post.date,
|
||||
authors: [post.author?.name || 'CHORUS Team'],
|
||||
tags: post.tags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: PostPageProps) {
|
||||
const { slug } = await params
|
||||
const post = getPostData(slug)
|
||||
|
||||
if (!post) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
const mdxOptions = {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [
|
||||
rehypeHighlight,
|
||||
rehypeSlug,
|
||||
[rehypeAutolinkHeadings, { behavior: 'wrap' }]
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<BlogHeader />
|
||||
|
||||
<article className="blog-container py-12">
|
||||
{/* Back link */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-carbon-600 dark:text-carbon-400 hover:text-carbon-950 dark:hover:text-carbon-200 transition-colors text-sm group"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2 group-hover:-translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to all posts
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Article header */}
|
||||
<header className="mb-12 max-w-4xl">
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{post.tags.map((tag) => (
|
||||
<span key={tag} className="blog-tag">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h1 className="text-h1 font-logo text-carbon-950 dark:text-mulberry-100 mb-6">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<p className="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 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">
|
||||
{post.author.name.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-carbon-950 dark:text-carbon-200 font-medium">
|
||||
{post.author.name}
|
||||
</p>
|
||||
{post.author.role && (
|
||||
<p className="text-carbon-600 dark:text-carbon-500 text-sm">
|
||||
{post.author.role}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="blog-meta text-right">
|
||||
<time dateTime={post.date} className="block">
|
||||
{formattedDate}
|
||||
</time>
|
||||
<span className="text-carbon-600 dark:text-carbon-600">
|
||||
{post.readingTime} min read
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Article content */}
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<MDXRemote source={post.content} options={mdxOptions} />
|
||||
</div>
|
||||
|
||||
{/* Article footer */}
|
||||
<footer className="mt-16 pt-8 border-t border-carbon-300 dark:border-carbon-800">
|
||||
<div className="text-center">
|
||||
<h3 className="text-h5 font-logo text-carbon-950 dark:text-carbon-100 mb-4">
|
||||
Join the CHORUS Community
|
||||
</h3>
|
||||
<p className="text-carbon-600 dark:text-carbon-400 mb-6 max-w-2xl mx-auto">
|
||||
Stay updated with the latest insights on contextual AI and agent orchestration.
|
||||
Join our waitlist to get early access to the CHORUS platform.
|
||||
</p>
|
||||
<Link
|
||||
href="https://chorus.services"
|
||||
className="inline-flex items-center px-6 py-3 bg-carbon-900 dark:bg-mulberry-700 hover:bg-carbon-800 dark:hover:bg-mulberry-600 text-white dark:text-mulberry-100 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Join Waitlist
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<BlogFooter />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user