diff --git a/morya-techcoder/CLAUDE.md b/morya-techcoder/CLAUDE.md index 427cca9..de2e460 100644 --- a/morya-techcoder/CLAUDE.md +++ b/morya-techcoder/CLAUDE.md @@ -48,10 +48,12 @@ morya-techcoder/ │ ├── layout.tsx # Root layout (fonts + theme provider) │ ├── (site)/ # 🆕 Route group for public site │ │ ├── layout.tsx # Navbar + Footer + ambient background -│ │ ├── page.tsx # Home page (Hero + featured content) -│ │ └── blog/ -│ │ ├── page.tsx # Blog list with search & filters -│ │ └── [slug]/page.tsx # Dynamic blog detail pages +│ │ ├── page.tsx # Home page (Hero + topic grid + rails) +│ │ ├── blog/ +│ │ │ ├── page.tsx # Blog list with search & filters +│ │ │ └── [slug]/page.tsx # Dynamic blog detail pages +│ │ └── topics/ +│ │ └── [category]/page.tsx # 🆕 Per-topic landing pages (SSG) │ ├── keystatic/ # 🆕 Keystatic admin UI │ │ └── [[...params]]/page.tsx # Visual CMS editor at /keystatic │ └── api/keystatic/ # 🆕 Keystatic file operations API @@ -63,16 +65,22 @@ morya-techcoder/ │ │ └── Footer.tsx # Multi-column footer with social links │ ├── sections/ │ │ ├── HeroSection.tsx # Animated hero with 3D code preview +│ │ ├── TopicGrid.tsx # 🆕 On-page topic discovery (reusable category treatment) │ │ ├── HomeContent.tsx # Category-grouped post grids │ │ ├── BlogListContent.tsx # Client-side search & filtering │ │ └── TrustedBy, Capabilities, Testimonials, FAQ, etc. │ ├── ui/ -│ │ ├── MagicBorderCard.tsx # Blog card with hover animations +│ │ ├── MagicBorderCard.tsx # Compact, elegant article card +│ │ ├── CategoryBadge.tsx # 🆕 Reusable color-coded category pill +│ │ ├── CardCarousel.tsx # 🆕 Responsive snapping slider (featured rail) │ │ ├── CodeBlock.tsx # Copy-to-clipboard code blocks -│ │ ├── TableOfContents.tsx # Auto-generated TOC (uses useActiveHeading hook) -│ │ ├── ReadingProgress.tsx # Scroll progress bar (uses useReadingProgress hook) +│ │ ├── TableOfContents.tsx # Desktop TOC — sliding indicator + completion (useReadingState) +│ │ ├── ReadingProgress.tsx # Compositor-only top progress bar (uses useScrollProgress) +│ │ ├── FadeInImage.tsx # next/image drop-in: eases from opacity/scale on load +│ │ ├── ScrollToTop.tsx # 🆕 Global fade-in scroll-to-top affordance +│ │ ├── SpotlightCursor.tsx # 🆕 Subtle warm cursor glow (desktop, reduced-motion aware) │ │ ├── NewsletterBox.tsx # Email subscription form -│ │ ├── ThemeToggle.tsx # Light/dark mode switcher +│ │ ├── ThemeToggle.tsx # Light/dark switch (View Transitions cross-fade) │ │ ├── DifficultyBadge.tsx # Visual difficulty indicators │ │ ├── HeadingLink.tsx # Anchor links for headings │ │ └── SectionHeading, Reveal, etc. @@ -96,16 +104,30 @@ morya-techcoder/ │ └── posts/*.mdx # Blog posts (MDX only, no .md duplicates) │ ├── lib/ # 🔄 Pure logic (framework-agnostic) -│ ├── categories.ts # 🔄 SINGLE SOURCE OF TRUTH for categories +│ ├── categories.ts # 🔄 SINGLE SOURCE OF TRUTH for categories (6 topics) +│ ├── category-icons.ts # 🆕 Maps category icon names → lucide components +│ ├── featureFlags.ts # 🆕 Progressive section rollout (Testimonials, FAQ, …) │ ├── posts.ts # 🆕 Business logic (filtering, featured, related) │ └── utils.ts # Helpers (cn, formatDate, calcReadingTime) │ ├── types/ # 🆕 Dedicated types directory │ └── blog.ts # 🔄 Moved from content/blogs.ts │ +├── components/layout/ +│ ├── Navbar.tsx # Glass navbar → mobile reading toolbar on articles +│ └── NavLinks.tsx # 🆕 Desktop nav: shared pill (layoutId) + magnetic + underline +│ +├── components/reading/ # 🆕 Immersive reading experience +│ ├── ReadingChromeProvider.tsx # Context: article chrome + Contents sheet state +│ ├── SetReadingChrome.tsx # Registers the current article (client, on the page) +│ ├── ArticleActions.tsx # 🆕 Sidebar companion: share/copy/bookmark/print/focus + progress +│ └── ReadingLayer.tsx # Top progress bar + floating circular indicator + TOC bottom sheet +│ ├── hooks/ # 🆕 Reusable client hooks -│ ├── useReadingProgress.ts # Extracted scroll percentage logic -│ └── useActiveHeading.ts # Extracted intersection observer logic +│ ├── useReadingProgress.ts # Scroll percentage (state-based) +│ ├── useScrollProgress.ts # 🆕 Imperative rAF scroll progress (no re-renders) +│ ├── useReadingState.ts # 🆕 Active heading + completed sections scroll-spy +│ └── useActiveHeading.ts # Intersection observer active-heading (desktop TOC) │ ├── docs/ # 🆕 Comprehensive documentation (8 files) │ ├── README.md # Documentation index @@ -126,11 +148,14 @@ morya-techcoder/ ### Routes -- `/` - Home page with hero, featured posts, category sections -- `/blog` - Blog listing with search and category filters +- `/` - Home page (hero, topic grid, featured carousel + editorial rails) +- `/blog` - Full article library with search and category filters - `/blog/[slug]` - Individual blog post pages (SSG via generateStaticParams) -- `/blog?category=AI` - Filtered views via URL params -- `/keystatic` - 🆕 Visual CMS admin interface (full-screen, no site chrome) +- `/topics/[category]` - 🆕 Per-topic landing pages (SSG for all 6 topics; empty + and "coming soon" topics render a graceful state). Slug via `categorySlug()`, + e.g. `/topics/ai`, `/topics/dev-tools`. This is the primary category route the + nav/footer/topic-grid link to; `/blog?category=` still works for direct links. +- `/keystatic` - Visual CMS admin interface (full-screen, no site chrome) ### Route Group Pattern @@ -193,16 +218,16 @@ export interface BlogPost { slug: string; excerpt: string; date: string; // ISO format (YYYY-MM-DD) - category: BlogCategory; // "AI" | "WebDev" | "Tricks" + category: BlogCategory; // "Programming" | "AI" | "Technology" | "Reviews" | "Guides" | "DevTools" tags: string[]; readingTime: string; // Auto-calculated (238 wpm) - coverImage: string; thumbnail: string; ogImage: string; difficulty?: Difficulty; updated?: string; // Optional last updated date prerequisites?: string[]; draft?: boolean; // 🆕 Visibility toggle (dev vs prod) + featured?: boolean; // 🆕 Homepage “Featured articles” eligibility seo?: { // 🆕 SEO overrides title?: string; description?: string; @@ -213,6 +238,7 @@ export interface BlogPost { **New Fields:** - `draft: boolean` - Filters posts in production (visible in dev only) +- `featured: boolean` - Show in homepage Featured section (Keystatic checkbox or frontmatter) - `seo: { title?, description? }` - Override default title/description for SEO ### Content Loader Architecture @@ -272,10 +298,13 @@ compileMDX({ - Primary: Orange gradient (`#F97316` → `#FDBA74`) - Accent: Rose/pink for "New" badges (`#F43F5E`) -**Category Colors (from `lib/categories.ts`):** -- AI: Orange tones -- WebDev: Blue tones -- Tricks: Green/emerald tones +**Category Colors (from `lib/categories.ts`), one accent per topic:** +- Programming: Blue tones +- AI (Artificial Intelligence): Orange tones (the brand accent) +- Technology: Cyan/teal tones +- Reviews: Violet tones +- Guides (Buying Guides): Emerald tones +- DevTools (Developer Tools): Slate tones — flagged `comingSoon` **Theme:** 40+ CSS custom properties (`--tc-*` tokens) with light/dark mode support @@ -287,20 +316,32 @@ compileMDX({ **After Refactor:** Everything derives from ONE object: ```typescript -export const CATEGORIES = { - AI: { - label: "AI & Machine Learning", - description: "Latest breakthroughs and practical AI engineering", - badge: "bg-tc-cat-ai-bg text-tc-cat-ai-text border border-tc-cat-ai-border", - dot: "bg-tc-cat-ai-text", +// Rich metadata per topic. Icons are string keys (lucide names) so this module +// stays framework-agnostic; lib/category-icons.ts resolves them to components. +const CATEGORY_DEFS = { + Programming: { + label: "Programming", + short: "Programming", + description: "Languages, frameworks, and the craft of writing great software.", + icon: "Code2", + badge: "bg-tc-cat-programming-bg text-tc-cat-programming-text border border-tc-cat-programming-border", + dot: "bg-tc-cat-programming-text", + accent: "text-tc-cat-programming-text", }, - WebDev: { /* ... */ }, - Tricks: { /* ... */ }, -} as const; - -// Derived exports (automatic from CATEGORIES) -export type BlogCategory = keyof typeof CATEGORIES; + AI: { /* Artificial Intelligence */ }, + Technology: { /* ... */ }, + Reviews: { /* ... */ }, + Guides: { /* Buying Guides */ }, + DevTools: { /* Developer Tools, comingSoon: true */ }, +} as const satisfies Record; + +// Derived exports (automatic from the definitions) +export type BlogCategory = keyof typeof CATEGORY_DEFS; +// Widened view so optional fields (e.g. comingSoon) are accessible everywhere +export const CATEGORIES: Record = CATEGORY_DEFS; export const CATEGORY_KEYS = Object.keys(CATEGORIES) as BlogCategory[]; +// Keys that accept content today (excludes "coming soon" topics) +export const ACTIVE_CATEGORY_KEYS = CATEGORY_KEYS.filter((k) => !CATEGORIES[k].comingSoon); export const categoryColors = /* map from CATEGORIES */; export const categoryOptions = /* for Keystatic dropdown */; ``` @@ -468,13 +509,14 @@ import type { BlogPost } from "@/types/blog"; title: "Your Post Title" excerpt: "Brief description for SEO and cards" date: "2026-07-12" - category: "AI" # or "WebDev" or "Tricks" + category: "Programming" # or "AI" | "Technology" | "Reviews" | "Guides" tags: ["nextjs", "typescript"] thumbnail: "/content/blog/post-thumbnail.jpg" ogImage: "/content/blog/post-og.jpg" difficulty: "Intermediate" # optional prerequisites: ["Basic React", "TypeScript"] # optional draft: false # optional (defaults false) + featured: true # optional — show in homepage Featured articles --- ``` 3. Write content in markdown/MDX @@ -495,6 +537,20 @@ import type { BlogPost } from "@/types/blog"; **Never mix:** Server-only imports in client components will fail +### Feature flags + +Toggle homepage (and later site-wide) sections from `lib/featureFlags.ts`: + +```ts +import { isFeatureEnabled } from "@/lib/featureFlags"; + +{isFeatureEnabled("showTestimonials") && } +{isFeatureEnabled("showFAQ") && } +``` + +Current defaults: `showTestimonials: false`, `showFAQ: false`. Components stay in +the tree — they simply don't render when disabled. + ### Adding New Categories 1. Edit `lib/categories.ts` - add to `CATEGORIES` object @@ -509,6 +565,14 @@ No need to edit types, filter pills, section headers, or color mappings separate - Conditional classes with `cn()` helper (`clsx` + `tailwind-merge`) - Avoid inline styles except for dynamic values (e.g., `style={{ width: percent }}`) - Animation classes: `.animate-fade-up`, `.animate-float`, `.animate-pulse-slow` +- **Motion system** — use the shared tokens `--tc-dur-fast|--tc-dur|--tc-dur-slow` + and `--tc-ease*` for all transitions (never hardcode ms/easing). Reusable + interaction utilities: `.moving-border` (animated gradient edge on hover), + `.press` (tactile scale), `.link-underline`, layered `--tc-shadow-md|lg`. + Public routes paint immediately without a client route-transition wrapper. + Article prose CSS and JetBrains Mono are scoped to `/blog/[slug]`; deferred + homepage sections use `content-visibility: auto`. See + [docs/motion-system.md](docs/motion-system.md). --- @@ -524,14 +588,17 @@ No need to edit types, filter pills, section headers, or color mappings separate - [keystatic.config.ts](keystatic.config.ts) - Keystatic schema & collections - [types/blog.ts](types/blog.ts) - TypeScript type definitions - [lib/categories.ts](lib/categories.ts) - SINGLE SOURCE OF TRUTH for categories +- [lib/featureFlags.ts](lib/featureFlags.ts) - Progressive feature flags (`isFeatureEnabled`) - [lib/posts.ts](lib/posts.ts) - Business logic (filtering, featured, related) - [lib/utils.ts](lib/utils.ts) - Helper functions ### Key Components - [components/layout/Navbar.tsx](components/layout/Navbar.tsx) - Glassmorphic navbar - [components/sections/HeroSection.tsx](components/sections/HeroSection.tsx) - Animated hero +- [components/sections/HomeContent.tsx](components/sections/HomeContent.tsx) - Magazine-style editorial rails - [components/sections/BlogListContent.tsx](components/sections/BlogListContent.tsx) - Search & filters -- [components/ui/MagicBorderCard.tsx](components/ui/MagicBorderCard.tsx) - Blog cards +- [components/ui/MagicBorderCard.tsx](components/ui/MagicBorderCard.tsx) - Blog cards (`.card-premium` hover) +- [components/ui/CardCarousel.tsx](components/ui/CardCarousel.tsx) - Infinite premium article carousel - [components/ui/TableOfContents.tsx](components/ui/TableOfContents.tsx) - Auto TOC - [components/ui/CodeBlock.tsx](components/ui/CodeBlock.tsx) - Code blocks with copy - [components/mdx/](components/mdx/) - MDX component library (11 components) diff --git a/morya-techcoder/app/(site)/blog/[slug]/article.css b/morya-techcoder/app/(site)/blog/[slug]/article.css new file mode 100644 index 0000000..db1ca8b --- /dev/null +++ b/morya-techcoder/app/(site)/blog/[slug]/article.css @@ -0,0 +1,134 @@ +.prose-tc { + max-width: 48rem; + font-family: var(--font-body); +} + +.prose-tc h1, +.prose-tc h2, +.prose-tc h3, +.prose-tc h4 { + color: var(--tc-text); + font-family: var(--font-heading); + scroll-margin-top: 5.5rem; +} + +.prose-tc h1 { + margin-block: 2.5rem 1rem; + font-size: 1.75rem; + font-weight: 700; + line-height: 1.2; + letter-spacing: -0.02em; +} + +.prose-tc h2 { + margin-block: 2rem 0.75rem; + font-size: 1.5rem; + font-weight: 700; + line-height: 1.25; + letter-spacing: -0.015em; +} + +.prose-tc h3 { + margin-block: 1.75rem 0.5rem; + font-size: 1.25rem; + font-weight: 600; + line-height: 1.3; + letter-spacing: -0.01em; +} + +.prose-tc h4 { + margin-block: 1.5rem 0.5rem; + font-size: 1.125rem; + font-weight: 600; + line-height: 1.4; +} + +@media (min-width: 768px) { + .prose-tc h1 { font-size: 2.25rem; } + .prose-tc h2 { font-size: 1.75rem; } + .prose-tc h3 { font-size: 1.375rem; } + .prose-tc h4 { font-size: 1.25rem; } +} + +.prose-tc p, +.prose-tc li { + color: var(--tc-text-muted); + font-family: var(--font-body); + font-size: 1.0625rem; + line-height: 1.8; +} + +.prose-tc p { margin-bottom: 1.5rem; } +.prose-tc ul, +.prose-tc ol { margin-bottom: 1.5rem; padding-left: 1.5rem; } +.prose-tc li { margin-bottom: 0.5rem; } +.prose-tc ul li { list-style-type: disc; } +.prose-tc ol li { list-style-type: decimal; } + +.prose-tc blockquote { + margin: 1.5rem 0; + padding: 0.75rem 1.25rem; + border-left: 3px solid var(--tc-primary); + border-radius: 0 12px 12px 0; + background: var(--tc-bg-elevated); +} +.prose-tc blockquote p { margin-bottom: 0; color: var(--tc-text); font-style: italic; } + +.prose-tc code { + padding: 0.15rem 0.45rem; + border: 1px solid var(--tc-border); + border-radius: 6px; + background: var(--tc-bg-elevated); + color: var(--tc-text); + font-family: var(--font-mono); + font-size: 0.875rem; +} +.prose-tc pre { + overflow-x: auto; + margin-bottom: 1.5rem; + padding: 1.25rem; + border: 1px solid var(--tc-border-code); + border-radius: 14px; + background: var(--tc-bg-code); + color: var(--tc-text-code); + font-family: var(--font-mono); +} +.prose-tc pre code { + padding: 0; + border: 0; + background: none; + color: inherit; + font-size: 0.875rem; + line-height: 1.7; +} + +.prose-tc a { + background-image: linear-gradient(var(--tc-primary), var(--tc-primary)); + background-position: 0 100%; + background-repeat: no-repeat; + background-size: 0 1.5px; + color: var(--tc-primary-dark); + text-decoration: none; + transition: background-size var(--tc-dur) var(--tc-ease), color var(--tc-dur) var(--tc-ease); +} +.prose-tc a:hover { background-size: 100% 1.5px; color: var(--tc-primary); } +.prose-tc img { margin: 1.5rem 0; border-radius: 14px; } +.prose-tc hr { margin: 2.5rem 0; border: 0; border-top: 1px solid var(--tc-border); } +.prose-tc strong { color: var(--tc-text); font-weight: 600; } +.prose-tc table { + display: block; + width: 100%; + overflow-x: auto; + margin: 1.5rem 0; + border-collapse: collapse; + font-size: 0.9375rem; +} +.prose-tc th, +.prose-tc td { + padding: 0.6rem 0.9rem; + border: 1px solid var(--tc-border); + text-align: left; + vertical-align: top; +} +.prose-tc th { background: var(--tc-bg-elevated); color: var(--tc-text); font-weight: 600; } +.prose-tc td { color: var(--tc-text-muted); } diff --git a/morya-techcoder/app/(site)/blog/[slug]/layout.tsx b/morya-techcoder/app/(site)/blog/[slug]/layout.tsx new file mode 100644 index 0000000..379469f --- /dev/null +++ b/morya-techcoder/app/(site)/blog/[slug]/layout.tsx @@ -0,0 +1,31 @@ +import { JetBrains_Mono } from "next/font/google"; +import ReadingLayer from "@/components/reading/ReadingLayer"; +import "./article.css"; + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + variable: "--font-mono", + display: "swap", + preload: false, +}); + +/** + * Article route layout — scopes JetBrains Mono + immersive reading chrome + * (progress / floating TOC) so Framer Motion never ships on home or list pages. + */ +export default function ArticleLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ {children} + {/* + Outside the article content tree so fixed chrome isn't trapped in + main's stacking context. Self-gates on reading context. + */} + +
+ ); +} diff --git a/morya-techcoder/app/(site)/blog/[slug]/loading.tsx b/morya-techcoder/app/(site)/blog/[slug]/loading.tsx new file mode 100644 index 0000000..7547179 --- /dev/null +++ b/morya-techcoder/app/(site)/blog/[slug]/loading.tsx @@ -0,0 +1,49 @@ +/** + * Article skeleton — matches the reading layout (article column + sidebar) so + * the transition into content stays still. Shimmer lives in `globals.css`. + */ +export default function ArticleLoading() { + return ( +
+
+
+ +
+ {/* Article column */} +
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ {Array.from({ length: 9 }).map((_, i) => ( +
+ ))} +
+
+ + {/* Sidebar */} + +
+
+
+ ); +} diff --git a/morya-techcoder/app/(site)/blog/[slug]/page.tsx b/morya-techcoder/app/(site)/blog/[slug]/page.tsx index 3dd8ba4..6badf32 100644 --- a/morya-techcoder/app/(site)/blog/[slug]/page.tsx +++ b/morya-techcoder/app/(site)/blog/[slug]/page.tsx @@ -3,16 +3,18 @@ import Link from "next/link"; import Image from "next/image"; import type { Metadata } from "next"; import { ArrowLeft, Clock, Calendar, Tag, History, ListChecks, Check } from "lucide-react"; -import { blogPosts, getBlogBySlug, compileBlogContent } from "@/content/loader"; +import { blogPosts, getBlogBySlug } from "@/content/loader"; +import { compileBlogContent } from "@/content/compile"; import { formatDate, getHeadings } from "@/lib/utils"; import { getRelatedPosts } from "@/lib/posts"; -import { categoryColors } from "@/lib/categories"; import type { BlogPost, Difficulty } from "@/types/blog"; import NewsletterBox from "@/components/ui/NewsletterBox"; import MagicBorderCard from "@/components/ui/MagicBorderCard"; -import ReadingProgress from "@/components/ui/ReadingProgress"; import TableOfContents from "@/components/ui/TableOfContents"; +import ArticleActions from "@/components/reading/ArticleActions"; +import SetReadingChrome from "@/components/reading/SetReadingChrome"; import DifficultyBadge from "@/components/ui/DifficultyBadge"; +import CategoryBadge from "@/components/ui/CategoryBadge"; type Props = { params: Promise<{ slug: string }>; @@ -31,16 +33,30 @@ export async function generateMetadata({ params }: Props): Promise { return { title: `${post.seo?.title || post.title} | TechCoder`, description: post.seo?.description || post.excerpt, - openGraph: ogImage - ? { images: [{ url: ogImage, width: 800, height: 450 }] } - : undefined, + alternates: { canonical: `/blog/${post.slug}` }, + openGraph: { + type: "article", + url: `/blog/${post.slug}`, + siteName: "TechCoder", + title: post.seo?.title || post.title, + description: post.seo?.description || post.excerpt, + publishedTime: post.date, + modifiedTime: post.updated || post.date, + tags: post.tags, + images: ogImage ? [{ url: ogImage, width: 800, height: 450, alt: post.title }] : undefined, + }, + twitter: { + card: "summary_large_image", + title: post.seo?.title || post.title, + description: post.seo?.description || post.excerpt, + images: ogImage ? [ogImage] : undefined, + }, }; } /** Difficulty defaults derived from the article's category when not set in frontmatter. */ function resolveDifficulty(post: BlogPost): Difficulty { if (post.difficulty) return post.difficulty; - if (post.category === "Tricks") return "Beginner"; if (post.category === "AI") return "Advanced"; return "Intermediate"; } @@ -56,17 +72,45 @@ export default async function BlogDetailPage({ params }: Props) { const showUpdated = post.updated && post.updated !== post.date; const related = getRelatedPosts(blogPosts, post, 2); + const articleJsonLd = { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: post.title, + description: post.seo?.description || post.excerpt, + datePublished: post.date, + dateModified: post.updated || post.date, + mainEntityOfPage: `https://techcoder.tech/blog/${post.slug}`, + image: post.ogImage || post.thumbnail + ? `https://techcoder.tech${post.ogImage || post.thumbnail}` + : undefined, + publisher: { + "@type": "Organization", + name: "TechCoder", + url: "https://techcoder.tech", + logo: { + "@type": "ImageObject", + url: "https://techcoder.tech/icon.png", + }, + }, + }; return ( <> - +