Skip to content

Commit 2c75761

Browse files
committed
feat(site): 2026 Duolingo Chalkboard & Excalidraw redesign + partner link tracker
1 parent 1b14647 commit 2c75761

36 files changed

Lines changed: 2486 additions & 264 deletions

‎DESIGN.md‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
name: CodingCat.dev 2026 — Duolingo Chalkboard & Excalidraw
3+
colors:
4+
background: '#131F24'
5+
surface: '#131F24'
6+
surface-card: '#202F36'
7+
surface-elevated: '#283B44'
8+
surface-border: '#37464F'
9+
chalk-white: '#FFFFFF'
10+
chalk-muted: '#DCE6EC'
11+
chalk-subtle: '#8FA6B2'
12+
primary: '#58CC02'
13+
primary-lip: '#46A302'
14+
primary-hover: '#89E219'
15+
secondary: '#1CB0F6'
16+
secondary-lip: '#1899D6'
17+
tertiary: '#FF9600'
18+
tertiary-lip: '#E58700'
19+
highlight-yellow: '#FFC800'
20+
highlight-yellow-lip: '#E5B400'
21+
danger-coral: '#FF4B4B'
22+
danger-coral-lip: '#EA2B2B'
23+
accent-purple: '#CE82FF'
24+
accent-purple-lip: '#A560E8'
25+
canvas-diagram: '#0D1519'
26+
typography:
27+
display-xl:
28+
fontFamily: Nunito
29+
fontSize: 56px
30+
fontWeight: '900'
31+
lineHeight: 62px
32+
letterSpacing: '-0.02em'
33+
display-lg:
34+
fontFamily: Nunito
35+
fontSize: 44px
36+
fontWeight: '900'
37+
lineHeight: 50px
38+
letterSpacing: '-0.02em'
39+
headline-lg:
40+
fontFamily: Nunito
41+
fontSize: 32px
42+
fontWeight: '800'
43+
lineHeight: 38px
44+
headline-md:
45+
fontFamily: Nunito
46+
fontSize: 24px
47+
fontWeight: '800'
48+
lineHeight: 32px
49+
sketch-label:
50+
fontFamily: Caveat
51+
fontSize: 24px
52+
fontWeight: '700'
53+
lineHeight: 28px
54+
body-lg:
55+
fontFamily: Plus Jakarta Sans
56+
fontSize: 18px
57+
fontWeight: '500'
58+
lineHeight: 28px
59+
body-md:
60+
fontFamily: Plus Jakarta Sans
61+
fontSize: 16px
62+
fontWeight: '500'
63+
lineHeight: 24px
64+
code-inline:
65+
fontFamily: Space Mono
66+
fontSize: 14px
67+
fontWeight: '400'
68+
lineHeight: 20px
69+
rounded:
70+
sm: 0.5rem
71+
DEFAULT: 0.75rem
72+
md: 1rem
73+
lg: 1.25rem
74+
xl: 1.5rem
75+
full: 9999px
76+
---
77+
78+
# CodingCat.dev 2026 — Duolingo Chalkboard & Hand-Drawn Excalidraw Design System
79+
80+
## 1. Core Brand Philosophy ("free. fun. purrfect.")
81+
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.
82+
83+
- **No Generic Corporate Slop:** Never use muted slate-purple SaaS dashboards, tiny low-contrast text, or cramped 4-column bento grids.
84+
- **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.
85+
86+
## 2. Color Palette & Tactile 3D Tokens
87+
88+
| Token Role | Name | Main Hex | 3D Lip / Shadow Hex | Usage |
89+
| :--- | :--- | :--- | :--- | :--- |
90+
| **Chalkboard Canvas** | Deep Teal-Charcoal | `#131F24` | `#0D1519` (Diagram Well) | Primary background across all pages |
91+
| **Elevated Card Surface** | Chalk Slate | `#202F36` | `#37464F` (`2px–3px` border + `4px` bottom lip) | Topic cards, podcast cards, interactive modules |
92+
| **Primary Brand & Action** | Feather Green | `#58CC02` | `#46A302` (`4px` bottom lip) | Display headlines (`free. fun. purrfect.`), primary CTAs, AJ's eyes, `✓` pass states |
93+
| **Secondary / Builder** | Macaw Sky Blue | `#1CB0F6` | `#1899D6` (`4px` bottom lip) | Inline highlighted copy, AJ's headphones, Excalidraw diagram boxes, Builder track |
94+
| **Yarn & Energy Accent** | Fox Flame Orange | `#FF9600` | `#E58700` (`4px` bottom lip) | AJ's glowing yarn ball & scroll-unwinding string, cross-hatched Excalidraw nodes |
95+
| **Spark & Tag Accent** | Bee Sunny Yellow | `#FFC800` | `#E5B400` (`4px` bottom lip) | AJ's headphone ear-cups & collar tag, star doodles, annotation callouts |
96+
| **Play / Anti-Pattern** | Cardinal Coral Red | `#FF4B4B` | `#EA2B2B` (`4px` bottom lip) | YouTube/Podcast play buttons & Excalidraw `✗` anti-pattern strokes |
97+
| **Pro / Agent Accent** | Beetle Candy Purple | `#CE82FF` | `#A560E8` (`4px` bottom lip) | Advanced AI Agent nodes & pro badges |
98+
| **Chalk Ink** | Crisp White Chalk | `#FFFFFF` | `#DCE6EC` (Body) | Hand-drawn Excalidraw box strokes, sketchy arrows, and primary copy |
99+
100+
## 3. AJ the Mascot (2026 Feather Green & Macaw Blue Edition)
101+
- **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.
102+
- **Eyes:** Huge, expressive **Feather Green (`#58CC02`)** eyes with white catchlights.
103+
- **Headphones & Collar:** Chunky **Macaw Sky Blue (`#1CB0F6`)** headband and outer cups accented with **Sunny Yellow (`#FFC800`)** ear-cup cushions and collar + golden `"AJ"` tag.
104+
- **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.
105+
106+
## 4. Hand-Drawn Excalidraw Diagram & Card Conventions
107+
- **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).
108+
- **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.
109+
- **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.

‎apps/sanity/schemas/documents/sponsor.ts‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,71 @@ export default defineType({
1111
fields: [
1212
...baseType.fields,
1313
defineField({
14-
title: "Link",
14+
title: "Partner Landing / Tracking Link",
1515
name: "url",
1616
type: "url",
17-
validation: (Rule) => Rule.required(),
17+
description:
18+
"Canonical sponsor URL including contracted coupon or UTM parameters (e.g. https://www.sanity.io/manage?coupon=CodingCatDev)",
19+
validation: (Rule) =>
20+
Rule.required().custom((val) => {
21+
if (!val) return true;
22+
try {
23+
const parsed = new URL(val);
24+
if (
25+
parsed.hostname.endsWith("sanity.io") &&
26+
!parsed.searchParams.has("coupon")
27+
) {
28+
return "Sanity partner links must include ?coupon=CodingCatDev";
29+
}
30+
return true;
31+
} catch {
32+
return "Must be a valid URL";
33+
}
34+
}),
35+
}),
36+
defineField({
37+
title: "Match Domains",
38+
name: "matchDomains",
39+
type: "array",
40+
of: [{ type: "string" }],
41+
description:
42+
"Bare domains (e.g. sanity.io, algolia.com) that automatically rewrite to use this partner's coupon or UTM parameters across the site.",
43+
}),
44+
defineField({
45+
title: "Coupon Code",
46+
name: "couponCode",
47+
type: "string",
48+
description: "Partner coupon code (e.g. CodingCatDev)",
49+
}),
50+
defineField({
51+
title: "Rewrite Mode",
52+
name: "rewriteMode",
53+
type: "string",
54+
initialValue: "root_or_append",
55+
options: {
56+
list: [
57+
{
58+
title: "Rewrite root /manage to Landing URL & append params to deep links",
59+
value: "root_or_append",
60+
},
61+
{
62+
title: "Always preserve path & append coupon/UTM query params",
63+
value: "append_params",
64+
},
65+
{
66+
title: "Replace bare root domain only (for shortlinks like cld.media / mule.to)",
67+
value: "replace_root_only",
68+
},
69+
],
70+
},
71+
}),
72+
defineField({
73+
title: "Active Partner",
74+
name: "active",
75+
type: "boolean",
76+
initialValue: true,
1877
}),
1978
],
2079
});
2180

81+

‎apps/sanity/schemas/singletons/sponsorConfig.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,43 @@ export default defineType({
105105
description: "Maximum sponsor emails to send per cron run. Keeps volume manageable and avoids spam flags",
106106
initialValue: 10,
107107
}),
108+
defineField({
109+
name: "partnerTrackers",
110+
title: "Partner Link & UTM/Coupon Trackers",
111+
type: "array",
112+
description:
113+
"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).",
114+
of: [
115+
{
116+
type: "object",
117+
fields: [
118+
defineField({ name: "name", title: "Partner Name", type: "string" }),
119+
defineField({
120+
name: "matchDomains",
121+
title: "Match Domains",
122+
type: "array",
123+
of: [{ type: "string" }],
124+
}),
125+
defineField({
126+
name: "landingUrl",
127+
title: "Canonical Partner URL",
128+
type: "url",
129+
}),
130+
defineField({
131+
name: "couponCode",
132+
title: "Coupon Code",
133+
type: "string",
134+
}),
135+
defineField({
136+
name: "utmSource",
137+
title: "UTM Source",
138+
type: "string",
139+
initialValue: "codingcatdev",
140+
}),
141+
],
142+
},
143+
],
144+
}),
108145
],
109146
preview: {
110147
prepare() {

‎apps/site/astro.config.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ export default defineConfig({
6969
subsets: ["latin"],
7070
fallbacks: ["system-ui", "sans-serif"],
7171
},
72+
{
73+
provider: fontProviders.google(),
74+
name: "Caveat",
75+
cssVariable: "--font-caveat",
76+
weights: ["400 700"],
77+
styles: ["normal"],
78+
subsets: ["latin"],
79+
fallbacks: ["cursive", "sans-serif"],
80+
},
7281
],
7382
// Legacy RSS feed redirects. Listing stubs (/blog/page, /podcasts/page,
7483
// /authors, /guests, /sponsors) are implemented as dedicated SSR redirect
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
/**
3+
* AJ the CodingCat.dev Mascot — 2026 Feather Green & Macaw Blue Edition (Column 1).
4+
*
5+
* Features AJ with a bold white die-cut sticker rim-light, Feather Green (#58CC02)
6+
* eyes, Macaw Sky Blue (#1CB0F6) & Sunny Yellow (#FFC800) headphones, and a
7+
* glowing Flame Orange (#FF9600) yarn ball + hand-drawn Excalidraw speech/action badge.
8+
*/
9+
import AjHeadphones from "@/components/icons/AjHeadphones.astro";
10+
import AjPrimaryAlt from "@/components/icons/AjPrimaryAlt.astro";
11+
12+
interface Props {
13+
pose?: "yarn-chase" | "code-peek" | "lever-pull" | "headphones";
14+
caption?: string;
15+
cls?: string;
16+
showYarnBall?: boolean;
17+
}
18+
19+
const {
20+
pose = "yarn-chase",
21+
caption,
22+
cls = "w-44 h-44",
23+
showYarnBall = true,
24+
} = Astro.props;
25+
---
26+
27+
<div class="relative inline-flex flex-col items-center select-none group">
28+
{
29+
caption && (
30+
<div class="mb-2 rounded-2xl border-2 border-[#FFFFFF] bg-[#202F36] px-3.5 py-1.5 font-hand text-xl font-bold text-[#FFC800] shadow-[0_3px_0_#37464F] -rotate-2 transition-transform group-hover:rotate-1">
31+
{caption}
32+
</div>
33+
)
34+
}
35+
<div class="relative flex items-center justify-center">
36+
{
37+
pose === "code-peek" || pose === "lever-pull" ? (
38+
<AjPrimaryAlt cls={`${cls} drop-shadow-[0_8px_20px_rgba(88,204,2,0.18)] transition-transform duration-300 group-hover:scale-105`} />
39+
) : (
40+
<AjHeadphones cls={`${cls} drop-shadow-[0_8px_20px_rgba(28,176,246,0.22)] transition-transform duration-300 group-hover:scale-105`} />
41+
)
42+
}
43+
44+
{
45+
showYarnBall && (
46+
<svg
47+
class="yarn-glow absolute -bottom-3 -right-4 h-16 w-16 transition-transform duration-300 group-hover:rotate-12 group-hover:scale-110"
48+
viewBox="0 0 80 80"
49+
fill="none"
50+
xmlns="http://www.w3.org/2000/svg"
51+
aria-hidden="true"
52+
>
53+
{/* Glowing Flame Orange Yarn Ball with sketchy Excalidraw cross-strands */}
54+
<circle
55+
cx="40"
56+
cy="40"
57+
r="24"
58+
fill="#FF9600"
59+
stroke="#FFFFFF"
60+
stroke-width="3"
61+
/>
62+
<path
63+
d="M22 32C30 24 48 24 58 34M20 42C32 34 48 36 60 46M26 52C36 44 48 46 54 54"
64+
stroke="#131F24"
65+
stroke-width="2.5"
66+
stroke-linecap="round"
67+
/>
68+
<path
69+
d="M32 18C26 30 28 48 38 60M44 18C38 32 40 48 50 60"
70+
stroke="#FFC800"
71+
stroke-width="2.2"
72+
stroke-linecap="round"
73+
/>
74+
{/* Trailing loose yarn tail */}
75+
<path
76+
d="M58 52C66 58 72 64 76 74"
77+
stroke="#FF9600"
78+
stroke-width="4"
79+
stroke-linecap="round"
80+
/>
81+
</svg>
82+
)
83+
}
84+
</div>
85+
</div>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
/**
3+
* Scroll-driven Flame Orange (#FF9600) unwinding yarn string that connects
4+
* sections down the page, inspired by Duolingo's playful path progression.
5+
*/
6+
interface Props {
7+
variant?: "left-to-right" | "right-to-left" | "center-wave";
8+
}
9+
10+
const { variant = "left-to-right" } = Astro.props;
11+
12+
const pathD =
13+
variant === "left-to-right"
14+
? "M 180 0 C 180 70, 820 50, 820 120"
15+
: variant === "right-to-left"
16+
? "M 820 0 C 820 70, 180 50, 180 120"
17+
: "M 500 0 C 360 40, 640 80, 500 120";
18+
---
19+
20+
<div class="relative mx-auto my-2 hidden w-full max-w-5xl select-none md:block" aria-hidden="true">
21+
<svg
22+
viewBox="0 0 1000 120"
23+
fill="none"
24+
class="h-24 w-full overflow-visible"
25+
preserveAspectRatio="none"
26+
>
27+
{/* Sketchy white chalk guide shadow */}
28+
<path
29+
d={pathD}
30+
stroke="#37464F"
31+
stroke-width="6"
32+
stroke-dasharray="10 10"
33+
stroke-linecap="round"
34+
/>
35+
{/* Glowing Flame Orange Yarn String */}
36+
<path
37+
d={pathD}
38+
stroke="#FF9600"
39+
stroke-width="5"
40+
stroke-linecap="round"
41+
class="yarn-glow"
42+
/>
43+
{/* Playful yarn knot midpoint */}
44+
<circle
45+
cx="500"
46+
cy="60"
47+
r="9"
48+
fill="#FF9600"
49+
stroke="#FFFFFF"
50+
stroke-width="2.5"
51+
/>
52+
</svg>
53+
</div>

0 commit comments

Comments
 (0)