From 72f7833c6cea8c6b3a77cfd6fb9e5ce5a74c56f0 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Wed, 6 Aug 2025 22:38:22 +0100 Subject: [PATCH] seo: sitemap, robots, og tags --- frontend/src/app/layout.tsx | 34 ++++++++++- frontend/src/app/patterns/[slug]/page.tsx | 34 +++++++++-- frontend/src/app/questions/[slug]/page.tsx | 35 ++++++++++-- frontend/src/app/robots.ts | 14 +++++ frontend/src/app/sitemap.ts | 65 ++++++++++++++++++++++ 5 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/robots.ts create mode 100644 frontend/src/app/sitemap.ts diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 606db4d..5f832b3 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -7,10 +7,40 @@ import "@fontsource/inter/700.css"; import { Providers } from "./providers"; import "./globals.css"; +const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://codetutor.example.com"; + export const metadata: Metadata = { - title: "CodeTutor - Coding Interview Preparation", + title: { + default: "CodeTutor - Coding Interview Preparation", + template: "%s | CodeTutor", + }, description: - "Master coding interviews with curated questions, detailed explanations, and optimal solutions.", + "Master coding interviews with curated questions, detailed explanations, and optimal solutions. Practice 400+ problems with interactive code editor.", + keywords: [ + "coding interview", + "leetcode", + "algorithm", + "data structures", + "programming practice", + "software engineering", + ], + authors: [{ name: "Kai Chappell" }], + openGraph: { + type: "website", + locale: "en_US", + url: siteUrl, + siteName: "CodeTutor", + title: "CodeTutor - Coding Interview Preparation", + description: + "Master coding interviews with curated questions, detailed explanations, and optimal solutions.", + }, + twitter: { + card: "summary_large_image", + title: "CodeTutor - Coding Interview Preparation", + description: + "Master coding interviews with curated questions, detailed explanations, and optimal solutions.", + }, + metadataBase: new URL(siteUrl), }; export default function RootLayout({ diff --git a/frontend/src/app/patterns/[slug]/page.tsx b/frontend/src/app/patterns/[slug]/page.tsx index 8f51f15..799816c 100644 --- a/frontend/src/app/patterns/[slug]/page.tsx +++ b/frontend/src/app/patterns/[slug]/page.tsx @@ -1,13 +1,39 @@ +import type { Metadata } from "next"; import { getPattern, getQuestions } from "@/lib/api"; import { QuestionCard } from "@/components/questions/question-card"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { notFound } from "next/navigation"; -export default async function PatternDetailPage({ - params, -}: { +interface PageProps { params: Promise<{ slug: string }>; -}) { +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + + try { + const pattern = await getPattern(slug); + const description = + pattern.description || + `Learn the ${pattern.name} pattern with ${pattern.question_count} practice problems.`; + + return { + title: `${pattern.name} Pattern`, + description, + openGraph: { + title: `${pattern.name} Pattern | CodeTutor`, + description, + type: "article", + }, + }; + } catch { + return { + title: "Pattern Not Found", + }; + } +} + +export default async function PatternDetailPage({ params }: PageProps) { const { slug } = await params; let pattern; diff --git a/frontend/src/app/questions/[slug]/page.tsx b/frontend/src/app/questions/[slug]/page.tsx index bb2a0a5..574c509 100644 --- a/frontend/src/app/questions/[slug]/page.tsx +++ b/frontend/src/app/questions/[slug]/page.tsx @@ -1,13 +1,40 @@ +import type { Metadata } from "next"; import { getQuestion } from "@/lib/api"; import { QuestionDetail } from "@/components/questions/question-detail"; import { ProblemWorkspace } from "@/components/editor"; import { notFound } from "next/navigation"; -export default async function QuestionDetailPage({ - params, -}: { +interface PageProps { params: Promise<{ slug: string }>; -}) { +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + + try { + const question = await getQuestion(slug); + const description = question.description + .replace(/[#*`]/g, "") + .substring(0, 160) + .trim(); + + return { + title: question.title, + description, + openGraph: { + title: `${question.title} | CodeTutor`, + description, + type: "article", + }, + }; + } catch { + return { + title: "Question Not Found", + }; + } +} + +export default async function QuestionDetailPage({ params }: PageProps) { const { slug } = await params; let question; diff --git a/frontend/src/app/robots.ts b/frontend/src/app/robots.ts new file mode 100644 index 0000000..e4c853b --- /dev/null +++ b/frontend/src/app/robots.ts @@ -0,0 +1,14 @@ +import { MetadataRoute } from "next"; + +const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://codetutor.example.com"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + disallow: ["/api/"], + }, + sitemap: `${BASE_URL}/sitemap.xml`, + }; +} diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts new file mode 100644 index 0000000..1ca6262 --- /dev/null +++ b/frontend/src/app/sitemap.ts @@ -0,0 +1,65 @@ +import { MetadataRoute } from "next"; +import { getQuestions, getPatterns } from "@/lib/api"; + +const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://codetutor.example.com"; + +export default async function sitemap(): Promise { + const entries: MetadataRoute.Sitemap = [ + { + url: BASE_URL, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 1, + }, + { + url: `${BASE_URL}/questions`, + lastModified: new Date(), + changeFrequency: "daily", + priority: 0.9, + }, + { + url: `${BASE_URL}/categories`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: `${BASE_URL}/patterns`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + ]; + + try { + // Fetch all questions for individual pages + const questionsResponse = await getQuestions({ limit: 1000 }); + for (const question of questionsResponse.items) { + entries.push({ + url: `${BASE_URL}/questions/${question.slug}`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.7, + }); + } + } catch { + // Continue without question pages if API fails + } + + try { + // Fetch all patterns for individual pages + const patternsResponse = await getPatterns(); + for (const pattern of patternsResponse.items) { + entries.push({ + url: `${BASE_URL}/patterns/${pattern.slug}`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.6, + }); + } + } catch { + // Continue without pattern pages if API fails + } + + return entries; +}