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:
Kev
2026-03-22 09:43:38 -04:00
parent ed6502a880
commit bfa8345ebf
26 changed files with 5142 additions and 31 deletions
+46
View File
@@ -0,0 +1,46 @@
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const BLOG_DIR = path.join(process.cwd(), 'content', 'blog');
export interface BlogPost {
slug: string;
title: string;
date: string;
description: string;
tags: string[];
content: string;
readingTime: number;
}
export function getAllPosts(): BlogPost[] {
if (!fs.existsSync(BLOG_DIR)) return [];
const files = fs.readdirSync(BLOG_DIR).filter((f) => f.endsWith('.mdx') || f.endsWith('.md'));
const posts = files.map((file) => {
const raw = fs.readFileSync(path.join(BLOG_DIR, file), 'utf-8');
const { data, content } = matter(raw);
const slug = file.replace(/\.mdx?$/, '');
const wordCount = content.split(/\s+/).length;
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
return {
slug,
title: data.title || slug,
date: data.date || '',
description: data.description || '',
tags: data.tags || [],
content,
readingTime,
};
});
return posts.sort((a, b) => (a.date > b.date ? -1 : 1));
}
export function getPostBySlug(slug: string): BlogPost | null {
const posts = getAllPosts();
return posts.find((p) => p.slug === slug) || null;
}