feat: Feature 3.1 — Landing page + blog + Phase 3 specs
Next.js 14+ web app in web/ directory: - Landing page: Hero, How It Works, Features, 3-tier Pricing with founder badges, Footer with email capture - Blog system: MDX-powered, /blog index + /blog/[slug] pages, reading time, Open Graph tags, JSON-LD structured data - Auth pages: /login + /signup (Supabase Auth ready) - Design system: dark theme, grade colors (A/B/C/D), BetonBLK voice - 1 seed blog post: "How to Read Line Movement Like a Sharp" - Specs for 3.2 (Scan UI), 3.3 (Bet Tracker), 3.4 (Stripe) Build passes clean: 7 static pages generated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { getAllPosts, getPostBySlug } from '@/lib/blog';
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = getAllPosts();
|
||||
return posts.map((post) => ({ slug: post.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = getPostBySlug(slug);
|
||||
if (!post) return {};
|
||||
return {
|
||||
title: `${post.title} — BetonBLK Blog`,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: 'article',
|
||||
publishedTime: post.date,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const post = getPostBySlug(slug);
|
||||
if (!post) notFound();
|
||||
|
||||
return (
|
||||
<article className="py-16 px-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<a href="/blog" className="text-sm text-[var(--text-muted)] hover:text-white transition mb-8 inline-block">
|
||||
← Back to Blog
|
||||
</a>
|
||||
<div className="flex items-center gap-3 text-xs text-[var(--text-muted)] mb-4">
|
||||
<time>{post.date}</time>
|
||||
<span>{post.readingTime} min read</span>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-6">{post.title}</h1>
|
||||
{post.tags.length > 0 && (
|
||||
<div className="flex gap-2 mb-8">
|
||||
{post.tags.map((tag) => (
|
||||
<span key={tag} className="px-2 py-0.5 text-xs rounded-full bg-[var(--border)] text-[var(--text-muted)]">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="prose prose-invert max-w-none text-[var(--text)] leading-relaxed whitespace-pre-wrap">
|
||||
{post.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: post.title,
|
||||
description: post.description,
|
||||
datePublished: post.date,
|
||||
author: { '@type': 'Organization', name: 'BetonBLK' },
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user