-
Notifications
You must be signed in to change notification settings - Fork 1
feat: /compare page — Recoup vs agencies, teams, and generic AI #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| import type { Metadata } from "next"; | ||
| import Link from "next/link"; | ||
| import { Check, X, Minus, ArrowRight } from "lucide-react"; | ||
| import { compareCopy, type SupportLevel } from "@/lib/copy/compare"; | ||
| import { buildPageMetadata } from "@/lib/seo"; | ||
| import { siteConfig } from "@/lib/config"; | ||
|
|
||
| export const metadata: Metadata = buildPageMetadata({ | ||
| title: `Why Recoup? — Compare AI Music Agents vs Agencies & Generic AI | ${siteConfig.name}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Title resolves to ~73 characters, exceeding the recommended 50-60 char range from the SEO guidelines. Truncated titles get cut off in SERP snippets. Shorten the title to stay within 60 chars — e.g. drop "& Agencies" or shorten "Generic AI" since the description already names each alternative. Prompt for AI agents |
||
| description: | ||
| "See how Recoup's purpose-built music agents compare to hiring staff, creative agencies, and generic AI like ChatGPT. Real cost savings from real labels.", | ||
| path: "/compare", | ||
| }); | ||
|
|
||
| /* ── support-level icon ── */ | ||
| function SupportIcon({ level }: { level: SupportLevel }) { | ||
| if (level === "full") | ||
| return ( | ||
| <span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/15 text-emerald-400"> | ||
| <Check className="w-3.5 h-3.5" /> | ||
| </span> | ||
| ); | ||
| if (level === "partial") | ||
| return ( | ||
| <span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-amber-500/15 text-amber-400"> | ||
| <Minus className="w-3.5 h-3.5" /> | ||
| </span> | ||
| ); | ||
| return ( | ||
| <span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-red-500/10 text-red-400/70"> | ||
| <X className="w-3.5 h-3.5" /> | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| export default function ComparePage() { | ||
| const c = compareCopy; | ||
|
|
||
| return ( | ||
| <div className="max-w-5xl mx-auto px-4 py-16 sm:py-24"> | ||
| {/* ── Hero ── */} | ||
| <header className="text-center mb-20"> | ||
| <h1 className="text-4xl sm:text-5xl font-bold tracking-tight text-[var(--foreground)] mb-6"> | ||
| {c.title} | ||
| </h1> | ||
| <p className="text-lg sm:text-xl text-[var(--muted-foreground)] max-w-2xl mx-auto leading-relaxed"> | ||
| {c.description} | ||
| </p> | ||
| </header> | ||
|
|
||
| {/* ── Comparison Grid ── */} | ||
| <section className="mb-24"> | ||
| {/* column headers — desktop */} | ||
| <div className="hidden md:grid grid-cols-5 gap-3 mb-4 text-xs font-medium uppercase tracking-wider text-neutral-500 px-4"> | ||
| <div>Capability</div> | ||
| {c.columns.map((col) => ( | ||
| <div key={col} className="text-center"> | ||
| {col} | ||
| </div> | ||
| ))} | ||
| </div> | ||
|
|
||
| <div className="space-y-3"> | ||
| {c.rows.map((row) => ( | ||
| <div | ||
| key={row.capability} | ||
| className="rounded-xl border border-white/[0.06] bg-white/[0.02] hover:border-white/[0.12] transition-colors" | ||
| > | ||
| {/* Desktop row */} | ||
| <div className="hidden md:grid grid-cols-5 gap-3 items-center px-6 py-5"> | ||
| <div className="font-medium text-[var(--foreground)]"> | ||
| {row.capability} | ||
| </div> | ||
| {(["recoup", "humans", "agency", "genericAI"] as const).map( | ||
| (key) => ( | ||
| <div key={key} className="flex flex-col items-center gap-1.5 text-center"> | ||
| <SupportIcon level={row[key].level} /> | ||
| <span className="text-xs text-neutral-400 leading-tight max-w-[180px]"> | ||
| {row[key].note} | ||
| </span> | ||
| </div> | ||
| ) | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Mobile card */} | ||
| <div className="md:hidden p-5"> | ||
| <h3 className="font-medium text-[var(--foreground)] mb-4"> | ||
| {row.capability} | ||
| </h3> | ||
| <div className="grid grid-cols-2 gap-3"> | ||
| {(["recoup", "humans", "agency", "genericAI"] as const).map( | ||
| (key, i) => ( | ||
| <div key={key} className="flex items-start gap-2"> | ||
| <SupportIcon level={row[key].level} /> | ||
| <div> | ||
| <div className="text-[10px] uppercase tracking-wider text-neutral-500 mb-0.5"> | ||
| {c.columns[i]} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Column labels in the mobile view are resolved by positional index ( Prompt for AI agents |
||
| </div> | ||
| <div className="text-xs text-neutral-400 leading-tight"> | ||
| {row[key].note} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </section> | ||
|
|
||
| {/* ── Cost Comparison ── */} | ||
| <section className="mb-24"> | ||
| <h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-[var(--foreground)] mb-3 text-center"> | ||
| {c.costTitle} | ||
| </h2> | ||
| <p className="text-[var(--muted-foreground)] text-center mb-12 max-w-xl mx-auto"> | ||
| {c.costDescription} | ||
| </p> | ||
|
|
||
| <div className="grid sm:grid-cols-3 gap-4"> | ||
| {c.costs.map((cost) => ( | ||
| <div | ||
| key={cost.label} | ||
| className="rounded-2xl border border-white/[0.06] bg-white/[0.02] p-8 hover:border-white/[0.12] transition-colors" | ||
| > | ||
| <span className="text-xs font-medium uppercase tracking-wider text-neutral-500"> | ||
| {cost.source} | ||
| </span> | ||
| <h3 className="text-xl font-semibold text-[var(--foreground)] mt-3 mb-2"> | ||
| {cost.label} | ||
| </h3> | ||
| <div className="text-3xl font-bold text-red-400/80 line-through mb-1"> | ||
| {cost.traditional} | ||
| </div> | ||
| <div className="text-emerald-400 font-medium mb-4"> | ||
| {cost.recoup} | ||
| </div> | ||
| <p className="text-sm text-neutral-400">{cost.savings}</p> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </section> | ||
|
|
||
| {/* ── ChatGPT Objection ── */} | ||
| <section className="mb-24"> | ||
| <div className="rounded-2xl border border-white/[0.06] bg-white/[0.02] p-8 sm:p-12"> | ||
| <h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-[var(--foreground)] mb-4"> | ||
| {c.chatgptTitle} | ||
| </h2> | ||
| <p className="text-[var(--muted-foreground)] leading-relaxed mb-8 max-w-2xl"> | ||
| {c.chatgptDescription} | ||
| </p> | ||
|
|
||
| <div className="grid sm:grid-cols-2 gap-x-8 gap-y-3 mb-8"> | ||
| {c.chatgptPoints.map((point) => ( | ||
| <div key={point} className="flex items-start gap-3"> | ||
| <span className="mt-1 inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-500/10 text-red-400/70 flex-shrink-0"> | ||
| <X className="w-3 h-3" /> | ||
| </span> | ||
| <span className="text-sm text-neutral-300">{point}</span> | ||
| </div> | ||
| ))} | ||
| </div> | ||
|
|
||
| <p className="text-[var(--foreground)] font-medium text-lg"> | ||
| {c.chatgptPunchline} | ||
| </p> | ||
| </div> | ||
| </section> | ||
|
|
||
| {/* ── CTA ── */} | ||
| <section className="text-center"> | ||
| <div className="flex flex-col sm:flex-row items-center justify-center gap-4"> | ||
| <Link | ||
| href={c.ctaPrimary.href} | ||
| className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full bg-white text-black font-medium text-sm hover:bg-neutral-200 transition-colors" | ||
| > | ||
| {c.ctaPrimary.label} | ||
| <ArrowRight className="w-4 h-4" /> | ||
| </Link> | ||
| <Link | ||
| href={c.ctaSecondary.href} | ||
| className="inline-flex items-center gap-2 px-8 py-3.5 rounded-full border border-white/[0.12] text-[var(--foreground)] font-medium text-sm hover:border-white/[0.25] transition-colors" | ||
| > | ||
| {c.ctaSecondary.label} | ||
| </Link> | ||
| </div> | ||
| </section> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /** | ||
| * Compare page copy — single source of truth. | ||
| * Competitive comparison: Recoup vs alternatives. | ||
| */ | ||
|
|
||
| export type SupportLevel = "full" | "partial" | "none"; | ||
|
|
||
| export interface ComparisonRow { | ||
| capability: string; | ||
| recoup: { level: SupportLevel; note: string }; | ||
| humans: { level: SupportLevel; note: string }; | ||
| agency: { level: SupportLevel; note: string }; | ||
| genericAI: { level: SupportLevel; note: string }; | ||
| } | ||
|
|
||
| export interface CostRow { | ||
| label: string; | ||
| traditional: string; | ||
| recoup: string; | ||
| savings: string; | ||
| source: string; | ||
| } | ||
|
|
||
| export const compareCopy = { | ||
| title: "Why Recoup?", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The brand name "Recoup" is hardcoded throughout this copy object (title, description, chatgptDescription, chatgptPunchline, costDescription) rather than being derived from Prompt for AI agents |
||
| description: | ||
| "Generic AI doesn't understand music. Agencies are slow and expensive. Recoup is purpose-built music intelligence — agents that know your artists, your data, and your industry.", | ||
|
|
||
| columns: ["Recoup", "In-House Team", "Creative Agency", "ChatGPT / Claude"] as const, | ||
|
Comment on lines
+24
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Avoid hardcoding brand values and ensure SEO title limits. As per coding guidelines, brand values should never be hardcoded; they must be imported from
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| rows: [ | ||
| { | ||
| capability: "Content Creation", | ||
| recoup: { level: "full", note: "30 days of branded content in one session" }, | ||
| humans: { level: "full", note: "High quality, slow turnaround" }, | ||
| agency: { level: "full", note: "$5K+/mo, days per batch" }, | ||
| genericAI: { level: "partial", note: "Can write, but no artist context" }, | ||
| }, | ||
| { | ||
| capability: "Artist Research", | ||
| recoup: { level: "full", note: "Streaming, social, audience, playlists — live data" }, | ||
| humans: { level: "partial", note: "Manual data pulls, hours per report" }, | ||
| agency: { level: "none", note: "Not typically offered" }, | ||
| genericAI: { level: "partial", note: "No access to streaming APIs" }, | ||
| }, | ||
| { | ||
| capability: "Release Strategy", | ||
| recoup: { level: "full", note: "Data-informed, auto-generated per release" }, | ||
| humans: { level: "full", note: "Experienced, but expensive" }, | ||
| agency: { level: "partial", note: "Template-based, not data-driven" }, | ||
| genericAI: { level: "partial", note: "Generic advice, no real data" }, | ||
| }, | ||
| { | ||
| capability: "Social Analytics", | ||
| recoup: { level: "full", note: "Cross-platform scraping + insights" }, | ||
| humans: { level: "partial", note: "Manual exports from each platform" }, | ||
| agency: { level: "partial", note: "Monthly reports, often delayed" }, | ||
| genericAI: { level: "none", note: "No platform access" }, | ||
| }, | ||
| { | ||
| capability: "Catalog Valuation", | ||
| recoup: { level: "full", note: "Real-time from streaming data" }, | ||
| humans: { level: "partial", note: "Requires specialist + weeks" }, | ||
| agency: { level: "none", note: "Not offered" }, | ||
| genericAI: { level: "none", note: "No valuation models" }, | ||
| }, | ||
| { | ||
| capability: "Automated Reporting", | ||
| recoup: { level: "full", note: "Weekly reports, zero human effort" }, | ||
| humans: { level: "partial", note: "40+ hours/month for a team of 5" }, | ||
| agency: { level: "partial", note: "Monthly cadence, manual process" }, | ||
| genericAI: { level: "none", note: "No scheduling or automation" }, | ||
| }, | ||
| { | ||
| capability: "Agent Plugin Ecosystem", | ||
| recoup: { level: "full", note: "Install plugins in Claude, Cursor, Codex" }, | ||
| humans: { level: "none", note: "N/A" }, | ||
| agency: { level: "none", note: "N/A" }, | ||
| genericAI: { level: "partial", note: "General tools, no music plugins" }, | ||
| }, | ||
| ] satisfies ComparisonRow[], | ||
|
|
||
| costTitle: "The real cost of alternatives", | ||
| costDescription: | ||
| "These aren't hypothetical. They're from real labels who switched to Recoup.", | ||
|
|
||
| costs: [ | ||
| { | ||
| label: "Creative Director", | ||
| traditional: "$10,000/mo", | ||
| recoup: "72-hour campaign delivery", | ||
| savings: "$10K saved on a single campaign", | ||
| source: "Fat Beats", | ||
| }, | ||
| { | ||
| label: "Content Agency", | ||
| traditional: "$5,000/mo", | ||
| recoup: "Same price, 10× the output", | ||
| savings: "Agency eliminated + new AI artist co-created", | ||
| source: "Rostrum Records", | ||
| }, | ||
| { | ||
| label: "Manual Reporting", | ||
| traditional: "40+ staff hours/mo", | ||
| recoup: "Automated, $0 marginal cost", | ||
| savings: "$3K–$5K/mo in staff time", | ||
| source: "Parlophone (Warner)", | ||
| }, | ||
| ] satisfies CostRow[], | ||
|
|
||
| chatgptTitle: "But can't I just use ChatGPT?", | ||
| chatgptDescription: | ||
| "ChatGPT can write a marketing plan. Recoup writes one informed by your artist's actual streaming data, audience demographics, playlist history, and competitive landscape.", | ||
|
|
||
| chatgptPoints: [ | ||
| "No access to streaming analytics (Spotify, Apple Music, Chartmetric)", | ||
| "No playlist intelligence or placement tracking", | ||
| "No artist context — brand voice, catalog history, audience segments", | ||
| "No automated workflows — can't schedule reports or content pipelines", | ||
| "No music industry data models — catalog valuation, royalty estimation", | ||
| "No plugin ecosystem — can't extend with music-specific tools", | ||
| ], | ||
|
|
||
| chatgptPunchline: | ||
| "Generic AI is a blank canvas. Recoup is a music business that already knows your artists.", | ||
|
|
||
| ctaPrimary: { label: "See it in action", href: "/valuation" }, | ||
| ctaSecondary: { label: "View pricing", href: "/pricing" }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The metadata title will render with "Recoup" appearing twice in the browser tab. The string already includes
| Recoupat the end, and the root layout'stitle.template: "%s | Recoup"applies another suffix on top. The fix: remove| \${siteConfig.name}from the title string and let the root layout template handle the suffix, or switch totitle: { absolute: "..." }if you want full control. Since every page in the codebase follows this same pattern, you may want to fix all at once to keep titles consistent.Prompt for AI agents