diff --git a/public/team-hannah.jpeg b/public/team-hannah.jpeg new file mode 100644 index 0000000..3be0522 Binary files /dev/null and b/public/team-hannah.jpeg differ diff --git a/public/team-vetri.png b/public/team-vetri.png new file mode 100644 index 0000000..f98c54d Binary files /dev/null and b/public/team-vetri.png differ diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index cf8d323..f16cd86 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -17,7 +17,7 @@ export const metadata: Metadata = { title: "About Us", description: "A student-run nonprofit making tech education free and accessible for everyone, everywhere.", - alternates: { canonical: "/about" }, + alternates: { canonical: "/about/" }, }; const founders: TeamMember[] = [ @@ -139,6 +139,21 @@ const teamMembers: TeamMember[] = [ photo: images.team.bhoumik, photoClass: "object-[50%_0%] scale-[2.5] translate-y-[45%]", }, + { + name: "Hannah Mauro", + role: "Social Media", + photo: images.team.hannah, + photoClass: "object-[50%_8%]", + instagram: "https://www.instagram.com/hannah_gm.9045/", + bio: "Hi! I assist with media at Code With Purpose. I'm a student researcher interested in neuroscience and abnormal/forensic psychiatry, and have worked with professors from BU and Harvard on research into Parkinson's Disease as well as behavioral economics and neuroscience more broadly. In the future I hope to pursue a career in forensic psychiatry. In my free time I enjoy hanging out with my friends, family, and dogs, playing the violin and guitar, playing volleyball, and dancing.", + }, + { + name: "Vetrivel Jagath", + role: "Instructor", + photo: images.team.vetri, + photoClass: "object-[48%_30%] scale-[1.75] -translate-y-[5%]", + bio: "Hi, I'm Vetri, a rising freshman with a strong interest in math and helping others learn.", + }, ]; export default function AboutPage() { diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 548b1a4..5ac313c 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -23,7 +23,7 @@ export async function generateMetadata({ return { title: post.title, description: post.excerpt, - alternates: { canonical: `/blog/${slug}` }, + alternates: { canonical: `/blog/${slug}/` }, openGraph: { title: post.title, description: post.excerpt, diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index b353497..15abb3c 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -8,7 +8,7 @@ import { CONTACT_EMAIL, CONTACT_EMAIL_HREF } from "@/lib/links"; export const metadata: Metadata = { title: "Contact", description: "Get in touch with the CodeWithPurpose team.", - alternates: { canonical: "/contact" }, + alternates: { canonical: "/contact/" }, }; export default function ContactPage() { diff --git a/src/app/courses/page.tsx b/src/app/courses/page.tsx index e520125..30a2f41 100644 --- a/src/app/courses/page.tsx +++ b/src/app/courses/page.tsx @@ -26,7 +26,7 @@ export const metadata: Metadata = { title: "Free Coding Courses", description: "Free courses and interactive lessons for students worldwide. Python, Vibecoding, Machine Learning, Financial Literacy, and Health in Tech.", - alternates: { canonical: "/courses" }, + alternates: { canonical: "/courses/" }, }; interface Course { diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 0d08e40..f012d95 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -10,7 +10,8 @@ export const metadata: Metadata = { title: "My Progress", description: "Track your progress across every CodeWithPurpose course, earn XP and badges, unlock Koda avatars, and keep a whiteboard — all saved right on your device, no account needed.", - alternates: { canonical: "/dashboard" }, + alternates: { canonical: "/dashboard/" }, + robots: { index: false, follow: true }, }; export default function DashboardPage() { diff --git a/src/app/donate/page.tsx b/src/app/donate/page.tsx index 8b53c32..b558c46 100644 --- a/src/app/donate/page.tsx +++ b/src/app/donate/page.tsx @@ -10,7 +10,7 @@ export const metadata: Metadata = { title: "Donate", description: "Support CodeWithPurpose. Every dollar helps a student start their coding journey today.", - alternates: { canonical: "/donate" }, + alternates: { canonical: "/donate/" }, }; const impactAreas = [ diff --git a/src/app/impact/page.tsx b/src/app/impact/page.tsx index 4bc225b..6d27703 100644 --- a/src/app/impact/page.tsx +++ b/src/app/impact/page.tsx @@ -21,7 +21,7 @@ export const metadata: Metadata = { title: "Impact & Stories", description: "4,000+ students across 130+ countries, and the stories behind the numbers.", - alternates: { canonical: "/impact" }, + alternates: { canonical: "/impact/" }, }; const stats = [ diff --git a/src/app/join/page.tsx b/src/app/join/page.tsx index 9ad3e1b..d3507ab 100644 --- a/src/app/join/page.tsx +++ b/src/app/join/page.tsx @@ -15,7 +15,7 @@ export const metadata: Metadata = { title: "Join Us", description: "Volunteer with CodeWithPurpose. Teach, mentor, and help bring free coding education to students worldwide.", - alternates: { canonical: "/join" }, + alternates: { canonical: "/join/" }, }; const roles = [ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 86c679c..66a60da 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -81,7 +81,11 @@ export const metadata: Metadata = { images: ["/seo/og-image.png"], }, icons: { - icon: [{ url: "/icon.svg", type: "image/svg+xml" }], + icon: [ + { url: "/icon.svg", type: "image/svg+xml" }, + { url: "/seo/favicon-32.png", sizes: "32x32", type: "image/png" }, + ], + apple: [{ url: "/seo/apple-touch-icon.png", sizes: "180x180", type: "image/png" }], }, }; diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx index 8d0d36d..4fd2f26 100644 --- a/src/app/leaderboard/page.tsx +++ b/src/app/leaderboard/page.tsx @@ -10,7 +10,8 @@ export const metadata: Metadata = { title: "Leaderboard", description: "See how CodeWithPurpose students stack up. Earn XP by completing lessons and climb the ranks.", - alternates: { canonical: "/leaderboard" }, + alternates: { canonical: "/leaderboard/" }, + robots: { index: false, follow: true }, }; export default function LeaderboardPage() { diff --git a/src/app/learn/financial-literacy/(chapters)/[slug]/page.tsx b/src/app/learn/financial-literacy/(chapters)/[slug]/page.tsx index b7ba929..343bb18 100644 --- a/src/app/learn/financial-literacy/(chapters)/[slug]/page.tsx +++ b/src/app/learn/financial-literacy/(chapters)/[slug]/page.tsx @@ -82,7 +82,7 @@ export async function generateMetadata({ return { title: chapter.title, description: chapter.description, - alternates: { canonical: `/learn/financial-literacy/${slug}` }, + alternates: { canonical: `/learn/financial-literacy/${slug}/` }, }; } diff --git a/src/app/learn/financial-literacy/page.tsx b/src/app/learn/financial-literacy/page.tsx index ea748da..13fc115 100644 --- a/src/app/learn/financial-literacy/page.tsx +++ b/src/app/learn/financial-literacy/page.tsx @@ -1,4 +1,6 @@ import type { Metadata } from "next"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { courseJsonLd, breadcrumbJsonLd } from "@/lib/seo"; import Link from "next/link"; import { PageHero, PageSection } from "@/components/PageHero"; import { PageShell } from "@/components/PageShell"; @@ -15,12 +17,15 @@ export const metadata: Metadata = { title: "Financial Literacy", description: "Free interactive financial literacy lessons from CodeWithPurpose, running from your first budget through investing, taxes, and retirement accounts.", - alternates: { canonical: "/learn/financial-literacy" }, + alternates: { canonical: "/learn/financial-literacy/" }, }; export default function LearnFinancialLiteracyPage() { return ( - + <> + + + - + + ); } diff --git a/src/app/learn/health-in-tech/(chapters)/[slug]/page.tsx b/src/app/learn/health-in-tech/(chapters)/[slug]/page.tsx index def2e5f..4feedce 100644 --- a/src/app/learn/health-in-tech/(chapters)/[slug]/page.tsx +++ b/src/app/learn/health-in-tech/(chapters)/[slug]/page.tsx @@ -82,7 +82,7 @@ export async function generateMetadata({ return { title: chapter.title, description: chapter.description, - alternates: { canonical: `/learn/health-in-tech/${slug}` }, + alternates: { canonical: `/learn/health-in-tech/${slug}/` }, }; } diff --git a/src/app/learn/health-in-tech/page.tsx b/src/app/learn/health-in-tech/page.tsx index c0a5c75..672b851 100644 --- a/src/app/learn/health-in-tech/page.tsx +++ b/src/app/learn/health-in-tech/page.tsx @@ -1,4 +1,6 @@ import type { Metadata } from "next"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { courseJsonLd, breadcrumbJsonLd } from "@/lib/seo"; import Link from "next/link"; import { PageHero, PageSection } from "@/components/PageHero"; import { PageShell } from "@/components/PageShell"; @@ -15,12 +17,15 @@ export const metadata: Metadata = { title: "Health in Tech", description: "Free interactive Health in Tech lessons from CodeWithPurpose, running from what health tech actually is through AI diagnosis bias, cybersecurity, and where it's headed.", - alternates: { canonical: "/learn/health-in-tech" }, + alternates: { canonical: "/learn/health-in-tech/" }, }; export default function LearnHealthInTechPage() { return ( - + <> + + + - + + ); } diff --git a/src/app/learn/ml/(chapters)/[slug]/page.tsx b/src/app/learn/ml/(chapters)/[slug]/page.tsx index 2489bbd..d0fed55 100644 --- a/src/app/learn/ml/(chapters)/[slug]/page.tsx +++ b/src/app/learn/ml/(chapters)/[slug]/page.tsx @@ -78,7 +78,7 @@ export async function generateMetadata({ return { title: chapter.title, description: chapter.description, - alternates: { canonical: `/learn/ml/${slug}` }, + alternates: { canonical: `/learn/ml/${slug}/` }, }; } diff --git a/src/app/learn/ml/page.tsx b/src/app/learn/ml/page.tsx index 45cc183..8534390 100644 --- a/src/app/learn/ml/page.tsx +++ b/src/app/learn/ml/page.tsx @@ -1,4 +1,6 @@ import type { Metadata } from "next"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { courseJsonLd, breadcrumbJsonLd } from "@/lib/seo"; import Link from "next/link"; import { PageHero, PageSection } from "@/components/PageHero"; import { ContributeBand } from "@/components/learn/cards/ContributeBand"; @@ -19,7 +21,7 @@ export const metadata: Metadata = { title: "Machine Learning", description: "Free interactive machine learning lessons from CodeWithPurpose. Start from nothing: what ML is, what data has to look like, how a model learns, and how to tell whether it worked.", - alternates: { canonical: "/learn/ml" }, + alternates: { canonical: "/learn/ml/" }, }; export default function LearnMlPage() { @@ -30,7 +32,10 @@ export default function LearnMlPage() { const firstLesson = lessons[0]; return ( - + <> + + + - + + ); } diff --git a/src/app/learn/python/(chapters)/[slug]/page.tsx b/src/app/learn/python/(chapters)/[slug]/page.tsx index b1b6419..de7a9a8 100644 --- a/src/app/learn/python/(chapters)/[slug]/page.tsx +++ b/src/app/learn/python/(chapters)/[slug]/page.tsx @@ -82,7 +82,7 @@ export async function generateMetadata({ return { title: chapter.title, description: chapter.description, - alternates: { canonical: `/learn/python/${slug}` }, + alternates: { canonical: `/learn/python/${slug}/` }, }; } diff --git a/src/app/learn/python/page.tsx b/src/app/learn/python/page.tsx index 1a52a00..20aee1b 100644 --- a/src/app/learn/python/page.tsx +++ b/src/app/learn/python/page.tsx @@ -1,4 +1,6 @@ import type { Metadata } from "next"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { courseJsonLd, breadcrumbJsonLd } from "@/lib/seo"; import Link from "next/link"; import { PageHero, PageSection } from "@/components/PageHero"; import { PageShell } from "@/components/PageShell"; @@ -15,12 +17,15 @@ export const metadata: Metadata = { title: "Python", description: "Free interactive Python lessons from CodeWithPurpose, running from your first program through decorators, generators, and shipping tested code.", - alternates: { canonical: "/learn/python" }, + alternates: { canonical: "/learn/python/" }, }; export default function LearnPythonPage() { return ( - + <> + + + - + + ); } diff --git a/src/app/learn/vibecoding/(chapters)/[slug]/page.tsx b/src/app/learn/vibecoding/(chapters)/[slug]/page.tsx index 79e3d0b..d64bbda 100644 --- a/src/app/learn/vibecoding/(chapters)/[slug]/page.tsx +++ b/src/app/learn/vibecoding/(chapters)/[slug]/page.tsx @@ -92,7 +92,7 @@ export async function generateMetadata({ return { title: chapter.title, description: chapter.description, - alternates: { canonical: `/learn/vibecoding/${slug}` }, + alternates: { canonical: `/learn/vibecoding/${slug}/` }, }; } diff --git a/src/app/learn/vibecoding/page.tsx b/src/app/learn/vibecoding/page.tsx index 86db30c..01c2662 100644 --- a/src/app/learn/vibecoding/page.tsx +++ b/src/app/learn/vibecoding/page.tsx @@ -1,4 +1,6 @@ import type { Metadata } from "next"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { courseJsonLd, breadcrumbJsonLd } from "@/lib/seo"; import Link from "next/link"; import { PageHero, PageSection } from "@/components/PageHero"; import { PageShell } from "@/components/PageShell"; @@ -19,7 +21,7 @@ export const metadata: Metadata = { title: "Vibe Coding", description: "Free interactive vibe coding lessons from CodeWithPurpose. Learn to prompt, pair-program, debug, and ship real apps with AI.", - alternates: { canonical: "/learn/vibecoding" }, + alternates: { canonical: "/learn/vibecoding/" }, }; export default function LearnVibecodingPage() { @@ -29,7 +31,10 @@ export default function LearnVibecodingPage() { const firstLesson = getChapters("vibecoding")[0]; return ( - + <> + + + - + + ); } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 3fa4641..5476128 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -7,7 +7,8 @@ export const metadata: Metadata = { title: "Log in", description: "Sign in to CodeWithPurpose to sync your course progress across devices and compete on the student leaderboard.", - alternates: { canonical: "/login" }, + alternates: { canonical: "/login/" }, + robots: { index: false, follow: true }, }; export default function LoginPage() { diff --git a/src/app/sign-up/page.tsx b/src/app/sign-up/page.tsx index 17073ae..f737d6b 100644 --- a/src/app/sign-up/page.tsx +++ b/src/app/sign-up/page.tsx @@ -7,7 +7,8 @@ export const metadata: Metadata = { title: "Sign up", description: "Create a free CodeWithPurpose account to sync your progress across devices and join the student leaderboard.", - alternates: { canonical: "/sign-up" }, + alternates: { canonical: "/sign-up/" }, + robots: { index: false, follow: true }, }; export default function SignUpPage() { diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 208661e..ffb5683 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,9 +1,17 @@ import type { MetadataRoute } from "next"; import { SITE_URL } from "@/lib/links"; import { posts } from "@/lib/posts"; -import { getChapters } from "@/lib/learn-nav"; + +/** Absolute URL with a trailing slash, matching how the site serves pages + * (`trailingSlash: true`) so canonicals and the sitemap agree. */ +function abs(path: string): string { + return `${SITE_URL}${path === "" ? "/" : `${path}/`}`; +} export default function sitemap(): MetadataRoute.Sitemap { + // Individual lesson pages (/learn//) require a login, so they are + // deliberately left out — we don't advertise pages Google can't crawl. The + // track index pages stay in; they're public course landing pages. const staticRoutes = [ "", "/about", @@ -20,29 +28,18 @@ export default function sitemap(): MetadataRoute.Sitemap { "/learn/financial-literacy", "/learn/health-in-tech", ].map((path) => ({ - url: `${SITE_URL}${path}`, + url: abs(path), lastModified: new Date(), changeFrequency: "weekly" as const, priority: path === "" ? 1 : 0.8, })); const postRoutes = posts.map((post) => ({ - url: `${SITE_URL}/blog/${post.slug}`, + url: abs(`/blog/${post.slug}`), lastModified: new Date(), changeFrequency: "monthly" as const, priority: 0.6, })); - const lessonRoutes = ( - ["ml", "vibecoding", "python", "financial-literacy", "health-in-tech"] as const - ).flatMap((track) => - getChapters(track).map((chapter) => ({ - url: `${SITE_URL}/learn/${track}/${chapter.slug}`, - lastModified: new Date(), - changeFrequency: "monthly" as const, - priority: 0.7, - })), - ); - - return [...staticRoutes, ...postRoutes, ...lessonRoutes]; + return [...staticRoutes, ...postRoutes]; } diff --git a/src/app/toolkit/page.tsx b/src/app/toolkit/page.tsx index a2114d3..0144ff0 100644 --- a/src/app/toolkit/page.tsx +++ b/src/app/toolkit/page.tsx @@ -10,7 +10,8 @@ export const metadata: Metadata = { title: "Learning Toolkit", description: "Free note templates and guides to help students take better notes and apply what they learn — a debugging log, concept breakdown, daily reflection, and project planner. Fill them in and save as PDF.", - alternates: { canonical: "/toolkit" }, + alternates: { canonical: "/toolkit/" }, + robots: { index: false, follow: true }, }; export default function ToolkitPage() { diff --git a/src/components/FaqSection.tsx b/src/components/FaqSection.tsx index a1a6aa4..4d0e433 100644 --- a/src/components/FaqSection.tsx +++ b/src/components/FaqSection.tsx @@ -2,6 +2,8 @@ import { useId, useState } from "react"; import { CONTACT_HREF } from "@/lib/links"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { faqJsonLd } from "@/lib/seo"; interface Faq { question: string; @@ -97,6 +99,7 @@ function FaqItem({ faq }: { faq: Faq }) { export function FaqSection() { return (
+

diff --git a/src/components/seo/JsonLd.tsx b/src/components/seo/JsonLd.tsx new file mode 100644 index 0000000..2f3b167 --- /dev/null +++ b/src/components/seo/JsonLd.tsx @@ -0,0 +1,12 @@ +/** + * Renders a JSON-LD structured-data block. Server-safe; Google reads it from the + * rendered HTML. Keep the data plain and serialisable. + */ +export function JsonLd({ data }: { data: Record }) { + return ( +