From 2c75761593110edfdf627058579ecf34297e9a4a Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Sat, 24 Oct 2026 00:07:56 -0400 Subject: [PATCH 01/14] feat(site): 2026 Duolingo Chalkboard & Excalidraw redesign + partner link tracker --- DESIGN.md | 109 +++++ apps/sanity/schemas/documents/sponsor.ts | 64 ++- .../schemas/singletons/sponsorConfig.ts | 37 ++ apps/site/astro.config.mjs | 9 + apps/site/src/components/AjMascot.astro | 85 ++++ apps/site/src/components/AjYarnTrail.astro | 53 +++ apps/site/src/components/AnimatedHero.astro | 129 ++++-- apps/site/src/components/ContentCard.astro | 68 +-- .../components/ExcalidrawBlueprintCard.astro | 100 +++++ .../src/components/ExcalidrawDiagram.astro | 402 ++++++++++++++++++ apps/site/src/components/Footer.astro | 27 +- apps/site/src/components/Header.astro | 58 ++- apps/site/src/components/HomeSection.astro | 91 +++- apps/site/src/components/SponsorCard.astro | 10 +- apps/site/src/components/ThemeScript.astro | 13 +- .../src/components/icons/AjHeadphones.astro | 33 +- .../src/components/icons/AjPrimaryAlt.astro | 15 +- .../src/components/portable-text/PtCode.astro | 82 ++-- .../src/components/portable-text/PtLink.astro | 4 +- apps/site/src/layouts/BaseLayout.astro | 3 +- apps/site/src/lib/og-mascots.ts | 8 +- apps/site/src/lib/og-utils.ts | 37 +- apps/site/src/lib/partner-links.ts | 203 +++++++++ apps/site/src/lib/portable-text-html.ts | 5 +- apps/site/src/lib/sanity/queries/pages.ts | 35 +- apps/site/src/lib/sanity/resolve-href.ts | 2 + apps/site/src/pages/book.astro | 34 ++ apps/site/src/pages/course/[slug].astro | 124 ++++++ apps/site/src/pages/courses/index.astro | 277 ++++++++++++ apps/site/src/pages/index.astro | 104 ++++- apps/site/src/pages/podcast/[slug].astro | 7 + apps/site/src/pages/post/[slug].astro | 6 + apps/site/src/pages/sponsorships.astro | 34 ++ apps/site/src/styles/global.css | 267 +++++++++--- apps/site/tests/tier1-features.test.mjs | 47 ++ scripts/unify-covers-and-youtube.mjs | 168 ++++++++ 36 files changed, 2486 insertions(+), 264 deletions(-) create mode 100644 DESIGN.md create mode 100644 apps/site/src/components/AjMascot.astro create mode 100644 apps/site/src/components/AjYarnTrail.astro create mode 100644 apps/site/src/components/ExcalidrawBlueprintCard.astro create mode 100644 apps/site/src/components/ExcalidrawDiagram.astro create mode 100644 apps/site/src/lib/partner-links.ts create mode 100644 apps/site/src/pages/course/[slug].astro create mode 100644 apps/site/src/pages/courses/index.astro create mode 100644 scripts/unify-covers-and-youtube.mjs diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..6f81914b6 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,109 @@ +--- +name: CodingCat.dev 2026 — Duolingo Chalkboard & Excalidraw +colors: + background: '#131F24' + surface: '#131F24' + surface-card: '#202F36' + surface-elevated: '#283B44' + surface-border: '#37464F' + chalk-white: '#FFFFFF' + chalk-muted: '#DCE6EC' + chalk-subtle: '#8FA6B2' + primary: '#58CC02' + primary-lip: '#46A302' + primary-hover: '#89E219' + secondary: '#1CB0F6' + secondary-lip: '#1899D6' + tertiary: '#FF9600' + tertiary-lip: '#E58700' + highlight-yellow: '#FFC800' + highlight-yellow-lip: '#E5B400' + danger-coral: '#FF4B4B' + danger-coral-lip: '#EA2B2B' + accent-purple: '#CE82FF' + accent-purple-lip: '#A560E8' + canvas-diagram: '#0D1519' +typography: + display-xl: + fontFamily: Nunito + fontSize: 56px + fontWeight: '900' + lineHeight: 62px + letterSpacing: '-0.02em' + display-lg: + fontFamily: Nunito + fontSize: 44px + fontWeight: '900' + lineHeight: 50px + letterSpacing: '-0.02em' + headline-lg: + fontFamily: Nunito + fontSize: 32px + fontWeight: '800' + lineHeight: 38px + headline-md: + fontFamily: Nunito + fontSize: 24px + fontWeight: '800' + lineHeight: 32px + sketch-label: + fontFamily: Caveat + fontSize: 24px + fontWeight: '700' + lineHeight: 28px + body-lg: + fontFamily: Plus Jakarta Sans + fontSize: 18px + fontWeight: '500' + lineHeight: 28px + body-md: + fontFamily: Plus Jakarta Sans + fontSize: 16px + fontWeight: '500' + lineHeight: 24px + code-inline: + fontFamily: Space Mono + fontSize: 14px + fontWeight: '400' + lineHeight: 20px +rounded: + sm: 0.5rem + DEFAULT: 0.75rem + md: 1rem + lg: 1.25rem + xl: 1.5rem + full: 9999px +--- + +# CodingCat.dev 2026 — Duolingo Chalkboard & Hand-Drawn Excalidraw Design System + +## 1. Core Brand Philosophy ("free. fun. purrfect.") +CodingCat.dev 2026 combines **Duolingo's joyful, high-saturation gamified learning UX** with **authentic hand-drawn Excalidraw chalkboard diagrams** so learners from Day-1 beginners to staff-level engineers feel welcomed, energized, and never intimidated. + +- **No Generic Corporate Slop:** Never use muted slate-purple SaaS dashboards, tiny low-contrast text, or cramped 4-column bento grids. +- **Spacious Scrollytelling Rhythm:** Each scroll section focuses on **one big idea**, pairing a giant, friendly rounded lowercase headline in Feather Green (`#58CC02`) with a short scannable explanation and a playful hand-drawn Excalidraw chalkboard diagram. + +## 2. Color Palette & Tactile 3D Tokens + +| Token Role | Name | Main Hex | 3D Lip / Shadow Hex | Usage | +| :--- | :--- | :--- | :--- | :--- | +| **Chalkboard Canvas** | Deep Teal-Charcoal | `#131F24` | `#0D1519` (Diagram Well) | Primary background across all pages | +| **Elevated Card Surface** | Chalk Slate | `#202F36` | `#37464F` (`2px–3px` border + `4px` bottom lip) | Topic cards, podcast cards, interactive modules | +| **Primary Brand & Action** | Feather Green | `#58CC02` | `#46A302` (`4px` bottom lip) | Display headlines (`free. fun. purrfect.`), primary CTAs, AJ's eyes, `✓` pass states | +| **Secondary / Builder** | Macaw Sky Blue | `#1CB0F6` | `#1899D6` (`4px` bottom lip) | Inline highlighted copy, AJ's headphones, Excalidraw diagram boxes, Builder track | +| **Yarn & Energy Accent** | Fox Flame Orange | `#FF9600` | `#E58700` (`4px` bottom lip) | AJ's glowing yarn ball & scroll-unwinding string, cross-hatched Excalidraw nodes | +| **Spark & Tag Accent** | Bee Sunny Yellow | `#FFC800` | `#E5B400` (`4px` bottom lip) | AJ's headphone ear-cups & collar tag, star doodles, annotation callouts | +| **Play / Anti-Pattern** | Cardinal Coral Red | `#FF4B4B` | `#EA2B2B` (`4px` bottom lip) | YouTube/Podcast play buttons & Excalidraw `✗` anti-pattern strokes | +| **Pro / Agent Accent** | Beetle Candy Purple | `#CE82FF` | `#A560E8` (`4px` bottom lip) | Advanced AI Agent nodes & pro badges | +| **Chalk Ink** | Crisp White Chalk | `#FFFFFF` | `#DCE6EC` (Body) | Hand-drawn Excalidraw box strokes, sketchy arrows, and primary copy | + +## 3. AJ the Mascot (2026 Feather Green & Macaw Blue Edition) +- **Silhouette & Sticker Rim-Light:** Friendly black cat (`#131F24` / `#0B1114`) with a bold, crisp white (`#FFFFFF`) die-cut sticker outline so AJ pops off every dark chalkboard surface. +- **Eyes:** Huge, expressive **Feather Green (`#58CC02`)** eyes with white catchlights. +- **Headphones & Collar:** Chunky **Macaw Sky Blue (`#1CB0F6`)** headband and outer cups accented with **Sunny Yellow (`#FFC800`)** ear-cup cushions and collar + golden `"AJ"` tag. +- **Interactive Prop:** A glowing **Flame Orange (`#FF9600`)** ball of yarn that unwinds into a scroll-driven SVG string winding down the page across Excalidraw diagrams and section dividers. + +## 4. Hand-Drawn Excalidraw Diagram & Card Conventions +- **Sketchy Double Borders & Cross-Hatching:** Diagrams use crisp white (`#FFFFFF`) hand-drawn double-stroked boxes filled with diagonal cross-hatched marker strokes in `#1CB0F6` (Sky Blue), `#FF9600` (Flame Orange), `#58CC02` (Feather Green), `#FFC800` (Sunny Yellow), and `#CE82FF` (Candy Purple). +- **Tactile 3D Buttons:** All primary and secondary buttons use `16px` border-radius, bold uppercase/lowercase tracking, and a `4px` solid bottom lip (`border-bottom-width: 4px`) that physically depresses (`translateY(2px)`) on active click. +- **Podcast & YouTube Cover Unification:** Every 16:9 episode thumbnail & cover uses the `#131F24` / `#0D1519` blackboard canvas, a bold hand-drawn + chunky display title on top, an episode-specific Excalidraw cross-hatched diagram on the left/center, and a die-cut outlined cutout of host Alex Patterson + guest on the right with AJ & his orange yarn ball. diff --git a/apps/sanity/schemas/documents/sponsor.ts b/apps/sanity/schemas/documents/sponsor.ts index 2fadd917f..3b39d7393 100644 --- a/apps/sanity/schemas/documents/sponsor.ts +++ b/apps/sanity/schemas/documents/sponsor.ts @@ -11,11 +11,71 @@ export default defineType({ fields: [ ...baseType.fields, defineField({ - title: "Link", + title: "Partner Landing / Tracking Link", name: "url", type: "url", - validation: (Rule) => Rule.required(), + description: + "Canonical sponsor URL including contracted coupon or UTM parameters (e.g. https://www.sanity.io/manage?coupon=CodingCatDev)", + validation: (Rule) => + Rule.required().custom((val) => { + if (!val) return true; + try { + const parsed = new URL(val); + if ( + parsed.hostname.endsWith("sanity.io") && + !parsed.searchParams.has("coupon") + ) { + return "Sanity partner links must include ?coupon=CodingCatDev"; + } + return true; + } catch { + return "Must be a valid URL"; + } + }), + }), + defineField({ + title: "Match Domains", + name: "matchDomains", + type: "array", + of: [{ type: "string" }], + description: + "Bare domains (e.g. sanity.io, algolia.com) that automatically rewrite to use this partner's coupon or UTM parameters across the site.", + }), + defineField({ + title: "Coupon Code", + name: "couponCode", + type: "string", + description: "Partner coupon code (e.g. CodingCatDev)", + }), + defineField({ + title: "Rewrite Mode", + name: "rewriteMode", + type: "string", + initialValue: "root_or_append", + options: { + list: [ + { + title: "Rewrite root /manage to Landing URL & append params to deep links", + value: "root_or_append", + }, + { + title: "Always preserve path & append coupon/UTM query params", + value: "append_params", + }, + { + title: "Replace bare root domain only (for shortlinks like cld.media / mule.to)", + value: "replace_root_only", + }, + ], + }, + }), + defineField({ + title: "Active Partner", + name: "active", + type: "boolean", + initialValue: true, }), ], }); + diff --git a/apps/sanity/schemas/singletons/sponsorConfig.ts b/apps/sanity/schemas/singletons/sponsorConfig.ts index 33b7260f3..5090f7122 100644 --- a/apps/sanity/schemas/singletons/sponsorConfig.ts +++ b/apps/sanity/schemas/singletons/sponsorConfig.ts @@ -105,6 +105,43 @@ export default defineType({ description: "Maximum sponsor emails to send per cron run. Keeps volume manageable and avoids spam flags", initialValue: 10, }), + defineField({ + name: "partnerTrackers", + title: "Partner Link & UTM/Coupon Trackers", + type: "array", + description: + "Automatic URL rewrites applied whenever a post, podcast, or sponsor card links to a partner domain (e.g. sanity.io -> https://www.sanity.io/manage?coupon=CodingCatDev).", + of: [ + { + type: "object", + fields: [ + defineField({ name: "name", title: "Partner Name", type: "string" }), + defineField({ + name: "matchDomains", + title: "Match Domains", + type: "array", + of: [{ type: "string" }], + }), + defineField({ + name: "landingUrl", + title: "Canonical Partner URL", + type: "url", + }), + defineField({ + name: "couponCode", + title: "Coupon Code", + type: "string", + }), + defineField({ + name: "utmSource", + title: "UTM Source", + type: "string", + initialValue: "codingcatdev", + }), + ], + }, + ], + }), ], preview: { prepare() { diff --git a/apps/site/astro.config.mjs b/apps/site/astro.config.mjs index c2c8320ca..18feb9d22 100644 --- a/apps/site/astro.config.mjs +++ b/apps/site/astro.config.mjs @@ -69,6 +69,15 @@ export default defineConfig({ subsets: ["latin"], fallbacks: ["system-ui", "sans-serif"], }, + { + provider: fontProviders.google(), + name: "Caveat", + cssVariable: "--font-caveat", + weights: ["400 700"], + styles: ["normal"], + subsets: ["latin"], + fallbacks: ["cursive", "sans-serif"], + }, ], // Legacy RSS feed redirects. Listing stubs (/blog/page, /podcasts/page, // /authors, /guests, /sponsors) are implemented as dedicated SSR redirect diff --git a/apps/site/src/components/AjMascot.astro b/apps/site/src/components/AjMascot.astro new file mode 100644 index 000000000..88f763f18 --- /dev/null +++ b/apps/site/src/components/AjMascot.astro @@ -0,0 +1,85 @@ +--- +/** + * AJ the CodingCat.dev Mascot — 2026 Feather Green & Macaw Blue Edition (Column 1). + * + * Features AJ with a bold white die-cut sticker rim-light, Feather Green (#58CC02) + * eyes, Macaw Sky Blue (#1CB0F6) & Sunny Yellow (#FFC800) headphones, and a + * glowing Flame Orange (#FF9600) yarn ball + hand-drawn Excalidraw speech/action badge. + */ +import AjHeadphones from "@/components/icons/AjHeadphones.astro"; +import AjPrimaryAlt from "@/components/icons/AjPrimaryAlt.astro"; + +interface Props { + pose?: "yarn-chase" | "code-peek" | "lever-pull" | "headphones"; + caption?: string; + cls?: string; + showYarnBall?: boolean; +} + +const { + pose = "yarn-chase", + caption, + cls = "w-44 h-44", + showYarnBall = true, +} = Astro.props; +--- + +
+ { + caption && ( +
+ {caption} +
+ ) + } +
+ { + pose === "code-peek" || pose === "lever-pull" ? ( + + ) : ( + + ) + } + + { + showYarnBall && ( + + ) + } +
+
diff --git a/apps/site/src/components/AjYarnTrail.astro b/apps/site/src/components/AjYarnTrail.astro new file mode 100644 index 000000000..f52f446cc --- /dev/null +++ b/apps/site/src/components/AjYarnTrail.astro @@ -0,0 +1,53 @@ +--- +/** + * Scroll-driven Flame Orange (#FF9600) unwinding yarn string that connects + * sections down the page, inspired by Duolingo's playful path progression. + */ +interface Props { + variant?: "left-to-right" | "right-to-left" | "center-wave"; +} + +const { variant = "left-to-right" } = Astro.props; + +const pathD = + variant === "left-to-right" + ? "M 180 0 C 180 70, 820 50, 820 120" + : variant === "right-to-left" + ? "M 820 0 C 820 70, 180 50, 180 120" + : "M 500 0 C 360 40, 640 80, 500 120"; +--- + + diff --git a/apps/site/src/components/AnimatedHero.astro b/apps/site/src/components/AnimatedHero.astro index 1e2d79fd3..610967efb 100644 --- a/apps/site/src/components/AnimatedHero.astro +++ b/apps/site/src/components/AnimatedHero.astro @@ -1,24 +1,27 @@ --- -import AjHeadphones from "@/components/icons/AjHeadphones.astro"; -import AjPrimaryAlt from "@/components/icons/AjPrimaryAlt.astro"; -import { HERO_ICONS } from "@/lib/hero-icons"; - /** - * Ported from the Next `animated-hero.tsx`. Its only state was `isHovered`, - * driving a transform on the two paw marks — expressed here with - * `group-hover:`, so the hero ships no JavaScript at all. + * CodingCat.dev 2026 — Duolingo Chalkboard & Hand-Drawn Excalidraw Hero. + * + * Combines Duolingo's giant lowercase Feather Green ("free. fun. purrfect.") + * scrollytelling energy and tactile 4px-lip 3D buttons with AJ the Mascot + * chasing his glowing Flame Orange (#FF9600) yarn ball across a hand-drawn + * Excalidraw chalkboard diagram. */ +import AjMascot from "@/components/AjMascot.astro"; +import ExcalidrawDiagram from "@/components/ExcalidrawDiagram.astro"; +import { HERO_ICONS } from "@/lib/hero-icons"; --- -
- diff --git a/apps/site/src/components/ExcalidrawBlueprintCard.astro b/apps/site/src/components/ExcalidrawBlueprintCard.astro new file mode 100644 index 000000000..eb6b9f254 --- /dev/null +++ b/apps/site/src/components/ExcalidrawBlueprintCard.astro @@ -0,0 +1,100 @@ +--- +/** + * Auto-generated Hand-Drawn Excalidraw "Visual Blueprint" Card for every + * Podcast (`/podcast/[slug]`) and Blog Post (`/post/[slug]`) detail page. + * + * Extracts the core workflow from the post/episode title and tags so every + * single lesson on CodingCat.dev includes an approachable Excalidraw diagram + * guided by AJ & his Flame Orange yarn ball. + */ +import ExcalidrawDiagram, { + type ExcalidrawNode, +} from "@/components/ExcalidrawDiagram.astro"; +import AjMascot from "@/components/AjMascot.astro"; + +interface Props { + title: string; + kind?: "podcast" | "post" | "course"; + tags?: Array; +} + +const { title, kind = "post", tags = [] } = Astro.props; + +const cleanTags = tags + .filter((t): t is string => Boolean(t && t.trim())) + .slice(0, 2); + +const topicHook = + cleanTags[0] || + title + .replace(/^(CodingCat\.dev|Purrfect\.dev)\s*[-:]?\s*/i, "") + .split(/[:\-–|]/)[0] + .trim() + .slice(0, 22); + +const secondHook = + cleanTags[1] || + (kind === "podcast" ? "Architecture & Patterns" : "Hands-On Implementation"); + +const nodes: ExcalidrawNode[] = [ + { + id: "fundamentals", + badge: "Step 1", + label: topicHook || "Core Concept", + sublabel: "Mental model & why it matters", + color: "green", + }, + { + id: "build", + badge: "Step 2", + label: secondHook, + sublabel: "Under the hood & live code", + color: "blue", + }, + { + id: "production", + badge: "Step 3", + label: "Ship It Purrfectly", + sublabel: "Real-world trade-offs", + color: "orange", + }, +]; + +const edges = [ + { from: "fundamentals", to: "build", label: "unroll the yarn" }, + { from: "build", to: "production", label: "production ready!" }, +]; +--- + +
+
+
+ + ✏️ Hand-Drawn Visual Blueprint + +

+ how this {kind === "podcast" ? "episode" : "lesson"} connects together. +

+

+ Follow AJ’s orange yarn trail from first principles to production code. +

+
+ +
+ + 48 ? "…" : ""}`} + nodes={nodes} + edges={edges} + compact={true} + /> +
diff --git a/apps/site/src/components/ExcalidrawDiagram.astro b/apps/site/src/components/ExcalidrawDiagram.astro new file mode 100644 index 000000000..30f3c31c1 --- /dev/null +++ b/apps/site/src/components/ExcalidrawDiagram.astro @@ -0,0 +1,402 @@ +--- +/** + * Hand-Drawn Excalidraw Chalkboard Diagram Engine (`ExcalidrawDiagram.astro`). + * + * Renders authentic Excalidraw-style double-stroked sketchy white chalk boxes, + * diagonal cross-hatched Duolingo color fills (#58CC02, #1CB0F6, #FF9600, + * #FFC800, #CE82FF, #FF4B4B), curved chalk arrows, and Caveat hand-written + * annotations on a deep #0D1519 chalkboard well. + * + * Can be driven either by structured `nodes` + `edges` props OR by raw + * `mermaid` / `excalidraw` text blocks from Portable Text. + */ + +export type ExcalidrawColor = + | "green" + | "blue" + | "orange" + | "yellow" + | "purple" + | "red"; + +export interface ExcalidrawNode { + id: string; + label: string; + sublabel?: string; + color?: ExcalidrawColor; + badge?: string; +} + +export interface ExcalidrawEdge { + from: string; + to: string; + label?: string; +} + +interface Props { + title?: string; + caption?: string; + nodes?: ExcalidrawNode[]; + edges?: ExcalidrawEdge[]; + rawCode?: string; + compact?: boolean; +} + +const { + title = "Architecture Sketch", + caption, + nodes: propNodes, + edges: propEdges, + rawCode, + compact = false, +} = Astro.props; + +const COLOR_MAP: Record< + ExcalidrawColor, + { hex: string; patternId: string; border: string } +> = { + green: { + hex: "#58CC02", + patternId: "hatch-green", + border: "#58CC02", + }, + blue: { + hex: "#1CB0F6", + patternId: "hatch-blue", + border: "#1CB0F6", + }, + orange: { + hex: "#FF9600", + patternId: "hatch-orange", + border: "#FF9600", + }, + yellow: { + hex: "#FFC800", + patternId: "hatch-yellow", + border: "#FFC800", + }, + purple: { + hex: "#CE82FF", + patternId: "hatch-purple", + border: "#CE82FF", + }, + red: { + hex: "#FF4B4B", + patternId: "hatch-red", + border: "#FF4B4B", + }, +}; + +const PALETTE_CYCLE: ExcalidrawColor[] = [ + "green", + "blue", + "orange", + "purple", + "yellow", + "red", +]; + +function parseMermaidOrText(raw: string): { + nodes: ExcalidrawNode[]; + edges: ExcalidrawEdge[]; +} { + const nodeMap = new Map(); + const edges: ExcalidrawEdge[] = []; + const lines = raw + .split(/\r?\n/) + .map((l) => l.trim()) + .filter( + (l) => + l.length > 0 && + !l.startsWith("%%") && + !l.match(/^(graph|flowchart|sequenceDiagram|stateDiagram)/i), + ); + + const cleanLabel = (str: string) => + str + .replace(/^[\[\(\{\>]+|[\]\)\}]+$/g, "") + .replace(/["']/g, "") + .trim(); + + for (const line of lines) { + const arrowMatch = line.match( + /^([A-Za-z0-9_-]+)(?:\[([^\]]+)\]|(?:\(([^\)]+)\))|\{([^\}]+)\})?\s*(?:--+>|==+>|-\.+->|--\s*([^-]+)\s*-->)\s*([A-Za-z0-9_-]+)(?:\[([^\]]+)\]|(?:\(([^\)]+)\))|\{([^\}]+)\})?/, + ); + if (arrowMatch) { + const [, fromId, f1, f2, f3, edgeLbl, toId, t1, t2, t3] = arrowMatch; + const fromLabel = cleanLabel(f1 || f2 || f3 || fromId); + const toLabel = cleanLabel(t1 || t2 || t3 || toId); + if (!nodeMap.has(fromId)) { + nodeMap.set(fromId, { + id: fromId, + label: fromLabel, + color: PALETTE_CYCLE[nodeMap.size % PALETTE_CYCLE.length], + }); + } + if (!nodeMap.has(toId)) { + nodeMap.set(toId, { + id: toId, + label: toLabel, + color: PALETTE_CYCLE[nodeMap.size % PALETTE_CYCLE.length], + }); + } + edges.push({ + from: fromId, + to: toId, + label: edgeLbl ? cleanLabel(edgeLbl) : undefined, + }); + } + } + + if (nodeMap.size === 0) { + const fallbackSteps = lines.slice(0, 4); + fallbackSteps.forEach((step, idx) => { + const id = `step-${idx}`; + nodeMap.set(id, { + id, + label: step.slice(0, 32), + color: PALETTE_CYCLE[idx % PALETTE_CYCLE.length], + }); + if (idx > 0) { + edges.push({ from: `step-${idx - 1}`, to: id }); + } + }); + } + + return { nodes: Array.from(nodeMap.values()).slice(0, 5), edges }; +} + +const resolved = + propNodes && propNodes.length > 0 + ? { nodes: propNodes, edges: propEdges ?? [] } + : rawCode + ? parseMermaidOrText(rawCode) + : { + nodes: [ + { + id: "idea", + label: "1. Curiosity", + sublabel: "Ask how it works", + color: "green" as ExcalidrawColor, + }, + { + id: "code", + label: "2. Sketch & Build", + sublabel: "Write real code", + color: "blue" as ExcalidrawColor, + }, + { + id: "ship", + label: "3. Ship to Prod!", + sublabel: "Share with the world", + color: "orange" as ExcalidrawColor, + }, + ], + edges: [ + { from: "idea", to: "code", label: "chase the yarn" }, + { from: "code", to: "ship", label: "purrfect!" }, + ], + }; + +const nodes = resolved.nodes; +const edges = resolved.edges; +const count = Math.max(nodes.length, 1); +const svgWidth = Math.max(count * 240, 720); +const svgHeight = compact ? 200 : 230; +const boxW = 184; +const boxH = 92; +const boxY = Math.round((svgHeight - boxH) / 2) + 8; +--- + +
+ {/* Top Chalkboard Header Bar */} +
+
+ + + + + ✏️ {title} + +
+ + excalidraw.chalkboard + +
+ + {/* Responsive Hand-Drawn SVG Flowchart */} +
+ + + {/* Diagonal Cross-Hatch Marker Patterns (Excalidraw signature fill) */} + + + + + + + + + + + + + + + + + + + + + { + nodes.map((node, index) => { + const stepSpan = svgWidth / count; + const x = Math.round(index * stepSpan + (stepSpan - boxW) / 2); + const y = boxY + (index % 2 === 0 ? -4 : 6); + const colorKey = node.color ?? PALETTE_CYCLE[index % PALETTE_CYCLE.length]; + const palette = COLOR_MAP[colorKey]; + const nextX = Math.round((index + 1) * stepSpan + (stepSpan - boxW) / 2); + const nextY = boxY + ((index + 1) % 2 === 0 ? -4 : 6); + const edge = edges[index]; + + return ( + + {/* Sketchy Excalidraw Arrow to Next Node */} + {index < nodes.length - 1 && ( + + + + {edge?.label && ( + + {edge.label} + + )} + + )} + + {/* Excalidraw Cross-Hatched Marker Fill */} + + + {/* Outer Sketchy Chalk Stroke #1 */} + + + {/* Offset Double-Stroke Chalk Line #2 (Excalidraw Roughness Effect) */} + + + {/* Optional Step Pill Badge */} + {node.badge && ( + + + + {node.badge} + + + )} + + {/* Node Primary Label */} + + {node.label.length > 21 ? `${node.label.slice(0, 19)}…` : node.label} + + + {/* Node Handwritten Sub-label */} + {node.sublabel && ( + + {node.sublabel.length > 26 + ? `${node.sublabel.slice(0, 24)}…` + : node.sublabel} + + )} + + ); + }) + } + +
+ + { + caption && ( +
+ 🧶 {caption} +
+ ) + } +
diff --git a/apps/site/src/components/Footer.astro b/apps/site/src/components/Footer.astro index c7755957f..72cf5a699 100644 --- a/apps/site/src/components/Footer.astro +++ b/apps/site/src/components/Footer.astro @@ -4,6 +4,7 @@ import NavLink from "./NavLink.astro"; const year = new Date().getUTCFullYear(); const legalLinks = [ + { href: "/courses", label: "Courses" }, { href: "/about", label: "About" }, { href: "/contact", label: "Contact" }, { href: "/developers", label: "Developers" }, @@ -21,8 +22,6 @@ const llmResourceLinks = [ { href: "/.well-known/mcp/server-card.json", label: "MCP Card" }, ]; -// Inlined rather than pulled from react-icons — four glyphs do not justify -// shipping an icon library (and its React runtime) to every page. const socialLinks = [ { href: "https://twitter.com/codingcatdev", @@ -48,11 +47,11 @@ const socialLinks = [ ---
diff --git a/apps/site/src/components/Header.astro b/apps/site/src/components/Header.astro index c6d8aa898..fc21243ac 100644 --- a/apps/site/src/components/Header.astro +++ b/apps/site/src/components/Header.astro @@ -12,26 +12,27 @@ const desktopLinks = navLinks.filter((link) => link?.sideOnly !== true); const hasCoursesLink = desktopLinks.some((link) => (link?.path ?? "").startsWith("/course"), ); +const hasHomeLink = navLinks.some((link) => (link?.path ?? "") === "/"); --- -
-

Discovery Manifests

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
StandardEndpointDescription
RFC 9727 API Catalog/.well-known/api-catalogIANA linkset discovery for all public APIs
OpenAPI 3.1/openapi.jsonComplete machine-readable OpenAPI schema
MCP Server Card/.well-known/mcp/server-card.jsonModel Context Protocol server capability card
ARD AI Catalog/.well-known/ai-catalog.jsonAgentic Resource Discovery manifest
Agent Skills/.well-known/agent-skills/index.jsonCloudflare Agent Skills Discovery index
LLM Resources/llms.txt, /llms-full.txt, /blog/llms.txt, /podcasts/llms.txtContext-window-friendly LLM directories
RFC 9728 OAuth PRM/.well-known/oauth-protected-resourceOAuth 2.0 Protected Resource Metadata
Web Bot Auth/.well-known/http-message-signatures-directoryHTTP Message Signatures JWKS directory
Auth.md/auth.mdAgent registration & authentication contracts
Pricing.md/pricing.mdAgent pricing and usage constraints
Agent Guide/AGENTS.mdRoot-level agent capability guide
-
-
+
+
+

Discovery Manifests

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StandardEndpointDescription
RFC 9727 API Catalog/.well-known/api-catalogIANA linkset discovery for all public APIs
OpenAPI 3.1/openapi.jsonComplete machine-readable OpenAPI schema
MCP Server Card/.well-known/mcp/server-card.jsonModel Context Protocol server capability card
ARD AI Catalog/.well-known/ai-catalog.jsonAgentic Resource Discovery manifest
Agent Skills/.well-known/agent-skills/index.jsonCloudflare Agent Skills Discovery index
LLM Directories/llms.txt · /llms-full.txtContext-window-friendly LLM directories
Agent Guide/AGENTS.mdRoot-level agent capability guide
+
+
-
-

Search API

-

Perform keyword search across tutorials, blog posts, podcasts, authors, and sponsors:

-
GET {origin}/api/search?q=astro HTTP/1.1
+				
+

Search API

+

Perform semantic and keyword search across courses, blog posts, podcasts, authors, and guests:

+
GET {origin}/api/search?q=sanity HTTP/1.1
 Accept: application/json
-
+
-
-

Streamable Model Context Protocol (MCP)

-

Agents can invoke MCP tools over HTTP POST:

-
POST {origin}/api/mcp HTTP/1.1
+				
+

Streamable Model Context Protocol (MCP)

+

Agents can invoke MCP tools over HTTP POST:

+
POST {origin}/api/mcp HTTP/1.1
 Content-Type: application/json
 
 {
@@ -112,18 +96,12 @@ Content-Type: application/json
   "method": "tools/call",
   "params": {
     "name": "search_content",
-    "arguments": { "query": "typescript" }
+    "arguments": { "query": "sanity content releases" }
   },
   "id": 1
 }
-
- -
-

Markdown Content Negotiation

-

- Any page on CodingCat.dev can be retrieved in clean Markdown by appending /index.md or .md to the URL (e.g., /blog/index.md or /blog.md) or sending the HTTP header Accept: text/markdown. -

-
+
+
- + diff --git a/apps/site/src/pages/guest/[slug].astro b/apps/site/src/pages/guest/[slug].astro index 0d398f35c..2b7597ef4 100644 --- a/apps/site/src/pages/guest/[slug].astro +++ b/apps/site/src/pages/guest/[slug].astro @@ -42,8 +42,8 @@ const relatedPodcasts = (person.related?.podcast ?? []) as ContentListItem[]; --- -
-
-
+
+
+
-

+ + 🎙️ Podcast Guest + +

{person.title}

-
- -
+ { + person.content && ( +
+ +
+ ) + } { relatedPodcasts.length > 0 && ( ) @@ -106,8 +118,8 @@ const relatedPodcasts = (person.related?.podcast ?? []) as ContentListItem[]; { relatedPosts.length > 0 && ( - @@ -144,9 +148,11 @@ const totalPosts = counts.posts ?? relatedPosts.length; 4} + searchPlaceholder={`Filter ${stegaClean(person.title)} blog posts...`} priorityCount={0} nounPlural="blog posts" /> diff --git a/apps/site/src/styles/global.css b/apps/site/src/styles/global.css index 9786279a4..c62fa1fc6 100644 --- a/apps/site/src/styles/global.css +++ b/apps/site/src/styles/global.css @@ -532,5 +532,5 @@ html.aj-drawer-open #aj-agent-drawer { */ .infinite-card-deferred { content-visibility: auto; - contain-intrinsic-size: auto none auto 460px; + contain-intrinsic-size: auto 520px; } From a46fb9369785d3ed7d5c782105ebb785532bc7be Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Mon, 26 Oct 2026 22:18:35 -0400 Subject: [PATCH 13/14] feat(mascot): integrate 57-path articulated AJ SVG rig across site & add /mascot-lab --- .../src/components/AjAnimatedMascot.astro | 948 ++++++++++++++ apps/site/src/components/AjMascot.astro | 118 +- apps/site/src/components/AnimatedHero.astro | 28 +- apps/site/src/components/ChatModal.astro | 21 +- .../components/ExcalidrawBlueprintCard.astro | 7 +- apps/site/src/components/Footer.astro | 6 +- .../src/components/aj-rig/AjFxLayer.astro | 627 ++++++++++ .../src/components/aj-rig/AjHeadBones.astro | 239 ++++ .../components/aj-rig/AjHexBadgeDefs.astro | 24 + .../components/aj-rig/AjLowerBodyBones.astro | 273 ++++ .../src/components/aj-rig/ajLimbRenderers.ts | 210 ++++ apps/site/src/components/aj-rig/ajRigTypes.ts | 171 +++ .../src/components/aj-rig/ajTracedPaths.ts | 121 ++ apps/site/src/pages/404.astro | 3 +- apps/site/src/pages/about.astro | 3 +- apps/site/src/pages/api/mascot-lab.ts | 395 ++++++ apps/site/src/pages/blog/index.astro | 34 +- apps/site/src/pages/courses/index.astro | 9 +- apps/site/src/pages/mascot-lab.astro | 1105 +++++++++++++++++ apps/site/src/pages/podcasts/index.astro | 34 +- apps/site/src/pages/search.astro | 32 +- apps/site/src/pages/sign-in.astro | 132 +- apps/site/src/pages/sponsorships.astro | 32 +- apps/site/tests/tier1-features.test.mjs | 146 +++ assets/AJ-3D-Turnaround-Sheet.jpg | Bin 0 -> 286332 bytes .../alex-likeness/alex-avatar-sheet-6up.jpg | Bin 0 -> 1256827 bytes .../references/09-assets.md | 10 + skills/codingcat-media-studio/SKILL.md | 1 + .../references/aj-mascot-prompts.md | 38 + .../references/google-flow-veo.md | 53 + .../references/youtube-thumbnails.md | 2 +- 31 files changed, 4645 insertions(+), 177 deletions(-) create mode 100644 apps/site/src/components/AjAnimatedMascot.astro create mode 100644 apps/site/src/components/aj-rig/AjFxLayer.astro create mode 100644 apps/site/src/components/aj-rig/AjHeadBones.astro create mode 100644 apps/site/src/components/aj-rig/AjHexBadgeDefs.astro create mode 100644 apps/site/src/components/aj-rig/AjLowerBodyBones.astro create mode 100644 apps/site/src/components/aj-rig/ajLimbRenderers.ts create mode 100644 apps/site/src/components/aj-rig/ajRigTypes.ts create mode 100644 apps/site/src/components/aj-rig/ajTracedPaths.ts create mode 100644 apps/site/src/pages/api/mascot-lab.ts create mode 100644 apps/site/src/pages/mascot-lab.astro create mode 100644 assets/AJ-3D-Turnaround-Sheet.jpg create mode 100644 assets/alex-likeness/alex-avatar-sheet-6up.jpg create mode 100644 skills/codingcat-media-studio/references/aj-mascot-prompts.md create mode 100644 skills/codingcat-media-studio/references/google-flow-veo.md diff --git a/apps/site/src/components/AjAnimatedMascot.astro b/apps/site/src/components/AjAnimatedMascot.astro new file mode 100644 index 000000000..bea60c260 --- /dev/null +++ b/apps/site/src/components/AjAnimatedMascot.astro @@ -0,0 +1,948 @@ +--- +/** + * `AjAnimatedMascot.astro` + * + * Production-ready, multi-layered, articulated Interactive SVG + CSS Keyframes + + * Web Animations API (WAAPI) & Scroll/Cursor State Machine Component for AJ the Cat. + * + * Supports: + * - `variant="full-body" | "head"` + * - `angle="three-quarter-right" | "three-quarter-left"` (with counter-flipped readable text) + * - `state="idle-wave" | "podcast-groove" | "point-diagram" | "celebrate" | "search-curious" | "404-confused" | "peek-corner" | "bored-nap" | "chase-yarn" | "ship-it-laptop" | "coffee-sip" | "mind-blown" | "cool-shades" | "canonical-rest"` + * - `mouth="smile-open" | "grin" | "surprised-o" | "talking"` + * - Viewport-gated `requestAnimationFrame` cursor pupil tracking + * - Randomized natural double-blink state machine every 3.2s to 6.0s + * - Scroll-driven section glance & head tilt + * - Click / keyboard (`Enter` / `Space`) `celebrate` burst + * - `@media (prefers-reduced-motion: reduce)` safety overrides + */ +import AjFxLayer from "@/components/aj-rig/AjFxLayer.astro"; +import AjHeadBones from "@/components/aj-rig/AjHeadBones.astro"; +import AjHexBadgeDefs from "@/components/aj-rig/AjHexBadgeDefs.astro"; +import AjLowerBodyBones from "@/components/aj-rig/AjLowerBodyBones.astro"; +import { AJ_TRACED_PATHS as P } from "@/components/aj-rig/ajTracedPaths"; +import type { + AjAnimationState, + AjMascotVariant, + AjMouthViseme, + AjTurnaroundAngle, +} from "@/components/aj-rig/ajRigTypes"; + +export type { + AjAnimationState, + AjMascotVariant, + AjMouthViseme, + AjTurnaroundAngle, +}; + +export interface Props { + state?: AjAnimationState; + variant?: AjMascotVariant; + angle?: AjTurnaroundAngle; + mouth?: AjMouthViseme; + caption?: string; + cls?: string; + showYarnBall?: boolean; + showPivots?: boolean; + interactive?: boolean; + id?: string; +} + +const { + state = "idle-wave", + variant = "full-body", + angle = "three-quarter-right", + mouth, + caption, + cls = variant === "head" ? "w-20 h-20" : "w-44 h-auto", + showYarnBall = false, + showPivots = false, + interactive = true, + id = `aj-${Math.random().toString(36).slice(2, 9)}`, +} = Astro.props; + +const defaultMouthByState: Partial> = { + "podcast-groove": "grin", + "search-curious": "surprised-o", + "404-confused": "surprised-o", + "coffee-sip": "grin", + "mind-blown": "surprised-o", + "cool-shades": "grin", +}; + +const activeMouth: AjMouthViseme = + mouth ?? defaultMouthByState[state] ?? "smile-open"; + +const effectiveVariant: AjMascotVariant = + state === "peek-corner" ? "head" : variant; + +const viewBox = + state === "bored-nap" + ? "-55 75 660 595" + : state === "peek-corner" + ? "0 40 560 620" + : effectiveVariant === "head" + ? "10 12 540 676" + : "0 0 560 1030"; + +const isLeftAngle = angle === "three-quarter-left"; +--- + +
+ { + caption && ( +
+ {caption} +
+ ) + } + +
+ + + + { + state === "bored-nap" ? ( + /* Horizontal Cat Loaf Pose (state="bored-nap") */ + + + {/* Die-Cut Sticker Underlay for Loaf */} + + + + + + + + {/* Foreground Loaf Body & Tail */} + + + + + + + + {/* Scaled Head Resting on Front Paws */} + + + + + + + + + + + + + + + z + + + Z + + + z + + + + ) : ( + /* Standard Articulated Full-Body & Head-Only Bone Rig */ + <> + {effectiveVariant === "full-body" && ( + + )} + + + + {/* 1. Die-Cut White Sticker Underlay (#F8FAFC + 1px #94A3B8 Rim) */} + + + {/* 2-7. Lower Body Bones (Tail, Hind Legs, Left Arm, Torso, Right Arm) */} + + + {/* 8. Head, Headphones, Ears, Eyes, Nose, Whiskers & Switchable Mouth */} + + + {/* 9. Soft Lavender Collar & Pastel-Rainbow Hexagonal "AJ" Tag */} + + + + {/* 10 & 11. State-Specific Vector Props, Particles & Pivot Inspector Overlay */} + + + + ) + } + + + { + showYarnBall && ( + + ) + } +
+
+ + + + diff --git a/apps/site/src/components/AjMascot.astro b/apps/site/src/components/AjMascot.astro index ca90c980e..cb1a9b2b8 100644 --- a/apps/site/src/components/AjMascot.astro +++ b/apps/site/src/components/AjMascot.astro @@ -1,15 +1,24 @@ --- /** - * AJ the CodingCat.dev Mascot - Canonical 2D Upright Bipedal Vector Edition. + * AJ the CodingCat.dev Mascot (`AjMascot.astro`) * - * Styled for both Chalkboard Paper Light Mode (`8d1319903d504a79ae56110486455e17`) - * and Obsidian Chalkboard Dark Mode (`b9e00f99c0d34e9884bf7766a014577b`). + * Delegates directly to `AjAnimatedMascot.astro` so every existing caller + * across Courses, Podcasts, Blog Posts, and Diagrams automatically gets the + * 57-path articulated 2D vector bone rig. */ -import AjHeadphones from "@/components/icons/AjHeadphones.astro"; -import AjPrimaryAlt from "@/components/icons/AjPrimaryAlt.astro"; +import AjAnimatedMascot, { + type AjAnimationState, + type AjMascotVariant, + type AjMouthViseme, + type AjTurnaroundAngle, +} from "@/components/AjAnimatedMascot.astro"; interface Props { pose?: "yarn-chase" | "code-peek" | "lever-pull" | "headphones"; + state?: AjAnimationState; + variant?: AjMascotVariant; + angle?: AjTurnaroundAngle; + mouth?: AjMouthViseme; caption?: string; cls?: string; showYarnBall?: boolean; @@ -18,85 +27,34 @@ interface Props { const { pose = "yarn-chase", + state, + variant, + angle = "three-quarter-right", + mouth, caption, cls = "w-44 h-auto", showYarnBall = true, fullBody = true, } = Astro.props; ---- -
- { - caption && ( -
- {caption} -
- ) - } -
- { - fullBody ? ( - - - AJ the CodingCat.dev mascot standing upright wearing purple podcast headphones and a hexagonal AJ collar tag - - ) : pose === "code-peek" || pose === "lever-pull" ? ( - - ) : ( - - ) - } +const poseToState: Record, AjAnimationState> = { + "yarn-chase": "idle-wave", + "code-peek": "point-diagram", + "lever-pull": "point-diagram", + headphones: "podcast-groove", +}; + +const resolvedState: AjAnimationState = state ?? poseToState[pose] ?? "idle-wave"; +const resolvedVariant: AjMascotVariant = + variant ?? (fullBody ? "full-body" : "head"); +--- - { - showYarnBall && ( - - ) - } -
-
+ diff --git a/apps/site/src/components/AnimatedHero.astro b/apps/site/src/components/AnimatedHero.astro index 4b1a88dbc..484180869 100644 --- a/apps/site/src/components/AnimatedHero.astro +++ b/apps/site/src/components/AnimatedHero.astro @@ -8,6 +8,7 @@ * * Uses canonical terminology: Courses, Podcasts, Blog, and Diagrams. */ +import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro"; ---
@@ -253,23 +254,16 @@
- {/* Bobbling AJ Mascot Character in Bottom-Right Corner */} -
-
- “Purrfect code!” 🐾 -
- - - AJ the CodingCat.dev mascot standing upright wearing purple podcast headphones - + {/* Articulated Interactive 2D Vector AJ Mascot in Bottom-Right Corner */} +
+
diff --git a/apps/site/src/components/ChatModal.astro b/apps/site/src/components/ChatModal.astro index 96bfc5149..e93e2dbef 100644 --- a/apps/site/src/components/ChatModal.astro +++ b/apps/site/src/components/ChatModal.astro @@ -1,5 +1,6 @@ --- -// ChatModal.astro — Persistent Right-Side Resizable "Ask Agent" Drawer (Survives across page changes via soft-navigation + pre-paint storage sync) +// ChatModal.astro | Persistent Right-Side Resizable "Ask Agent" Drawer (Survives across page changes via soft-navigation + pre-paint storage sync) +import AjAnimatedMascot from "@/components/AjAnimatedMascot.astro"; ---