Files
Leon SerfatyandClaude Opus 5 3e9ba07175 feat: Cloudflare Turnstile on auth, CSP fixes, admin/SEO/analytics additions
Turnstile bot protection (sign-in, sign-up, password-reset):
- Register Better Auth's captcha plugin with the cloudflare-turnstile
  provider; endpoints listed explicitly rather than relying on defaults.
  /reset-password is intentionally excluded — it is reached only via a
  single-use emailed token.
- Add an explicit-render Turnstile widget component. Tokens are single-use,
  so each form resets the challenge after a failed submit; submit stays
  disabled until a token is held.
- Read the site key server-side and pass it down as a prop, so rotating it
  does not require a rebuild.
- Fail fast in production when TURNSTILE_SECRET_KEY is missing, and when a
  secret is set without a site key (that combination would demand a token
  no form can produce, locking every user out).
- Pass a throwaway secret during `next build` in the Dockerfile, mirroring
  the existing BETTER_AUTH_SECRET treatment, so image builds don't need it.

CSP fixes in middleware (these blocked Turnstile entirely):
- Add frame-src for challenges.cloudflare.com. Without it the widget's
  iframe fell back to default-src 'self' and was blocked outright.
- Allow 'unsafe-eval' and websockets in DEVELOPMENT only. `next dev`
  compiles with eval(), so the strict policy threw EvalError and killed
  hydration — no client JS ran at all, which also meant form submit
  handlers never fired. Production policy is unchanged and still strict.

Also included (concurrent work in the tree):
- Admin organizations pages and lib/admin/orgs.
- Episode moderation migration, SEO metadata (sitemap, robots, JSON-LD,
  OG/Twitter images, manifest), Umami analytics, not-found page.

Local dev database: docker-compose.dev.yml provisions Postgres 18 on port
5443 (5432-5442 are in use by other local projects).

Note: `npx tsc --noEmit` currently fails in app/(app)/team/page.tsx — an
`invitations` prop the component does not accept. This predates the commit
and will fail `next build` until fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 11:10:55 -04:00

218 lines
9.1 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import {
Sparkles,
Wand2,
ShieldCheck,
Globe,
Zap,
HeartHandshake,
ArrowRight,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { JsonLd } from "@/components/seo/json-ld";
import { breadcrumbSchema, graph, webPageSchema } from "@/lib/schema";
import { pageMetadata } from "@/lib/seo";
/** Shared by the page metadata and this page's structured data. */
const DESCRIPTION =
"Podcast Distribution AI is an AI studio that turns a single idea into a finished, publishable podcast — script, voices, and cover art — in minutes. Learn why we built it and what we believe.";
export const metadata: Metadata = pageMetadata({
title: "About",
description: DESCRIPTION,
path: "/about",
});
const STATS = [
{ value: "3", label: "AI models in one workflow" },
{ value: "14+", label: "Realistic narrator voices" },
{ value: "13+", label: "Languages supported" },
{ value: "Minutes", label: "From idea to finished episode" },
];
const VALUES = [
{
icon: Sparkles,
title: "Creativity for everyone",
body: "Powerful production shouldn't require gear, a budget, or technical skill. If you can describe an idea, you can turn it into a finished episode.",
},
{
icon: Wand2,
title: "Quality you'd actually publish",
body: "AI drafts are a starting point, not the finish line. Every script and voice is editable, so the final cut always sounds like you.",
},
{
icon: ShieldCheck,
title: "Built responsibly",
body: "We screen content against a clear policy, never use your private work to train our models, and make sure you own everything you create.",
},
{
icon: Globe,
title: "Global from day one",
body: "Stories aren't only told in English. With 13+ languages, you can produce for an audience anywhere — no re-recording required.",
},
{
icon: Zap,
title: "Fast, but never careless",
body: "Minutes from idea to episode, with the controls to get the details right before you hit publish. Speed and craft aren't a trade-off.",
},
{
icon: HeartHandshake,
title: "Honest and transparent",
body: "Simple plans, clear limits, cancel anytime. No dark patterns and no surprises — just a tool that respects your time and your work.",
},
];
export default function AboutPage() {
return (
<>
<JsonLd
data={graph(
{
...webPageSchema({
path: "/about",
name: "About",
description: DESCRIPTION,
}),
"@type": "AboutPage",
},
breadcrumbSchema([{ name: "About", path: "/about" }])
)}
/>
{/* Hero */}
<section className="bg-hero-wash">
<div className="container max-w-4xl py-24 text-center md:py-32">
<p className="text-[13px] font-semibold uppercase tracking-[0.04em] text-brand">About us</p>
<h1 className="mt-4 text-balance font-display text-5xl font-extrabold leading-[1.05] tracking-tight md:text-6xl">
Making great podcasts possible for everyone
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground sm:text-xl">
Podcast Distribution AI is an AI studio that turns a single idea into a finished, publishable episode
script, voices, and cover art in minutes. No microphone, no editing suite, no production
budget.
</p>
</div>
</section>
{/* Stats */}
<section className="border-y border-border bg-card">
<div className="container grid grid-cols-2 gap-8 py-14 lg:grid-cols-4">
{STATS.map((s) => (
<div key={s.label} className="text-center">
<p className="font-display text-4xl font-extrabold tracking-tight text-brand md:text-5xl">
{s.value}
</p>
<p className="mt-2 text-sm text-muted-foreground">{s.label}</p>
</div>
))}
</div>
</section>
{/* Mission */}
<section className="py-24 md:py-28">
<div className="container max-w-3xl text-center">
<p className="text-[13px] font-semibold uppercase tracking-[0.04em] text-brand">Our mission</p>
<p className="mt-5 text-balance font-display text-3xl font-bold leading-tight tracking-tight md:text-4xl">
To remove every barrier between a great idea and a published podcast so anyone with
something to say can be{" "}
<span className="text-brand">heard</span>.
</p>
</div>
</section>
{/* Story */}
<section className="border-t border-border bg-secondary py-24 md:py-28">
<div className="container max-w-3xl">
<p className="text-[13px] font-semibold uppercase tracking-[0.04em] text-brand">Our story</p>
<h2 className="mt-3 font-display text-3xl font-extrabold tracking-tight md:text-4xl">
Why we built Podcast Distribution AI
</h2>
<div className="mt-8 space-y-5 text-lg leading-relaxed text-muted-foreground">
<p>
Podcasting is one of the most personal ways to reach an audience and one of the
hardest to start. Scripting, recording, editing, designing artwork, and publishing can
take days and a stack of tools and skills most people simply don&apos;t have. Brilliant
ideas die in that gap between &ldquo;I should make a podcast&rdquo; and actually
shipping one.
</p>
<p>
We thought the modern AI stack could collapse all of that into a single workflow. GPT-4
can write a tight, on-brand script. ElevenLabs can voice it with realistic, multi-speaker
narration. DALL·E can design cover art that matches the topic. Stitched together, they
turn &ldquo;I have a topic&rdquo; into &ldquo;I have an episode.&rdquo;
</p>
<p>
So we built Podcast Distribution AI: a studio that lives in your browser. Describe an idea, fine-tune
the result in a real editor, download the MP3 then repurpose it into a blog post, a
social thread, or a newsletter. We&apos;re a small team of engineers, audio nerds, and
storytellers obsessed with making production effortless without making it feel generic.
</p>
</div>
</div>
</section>
{/* Values */}
<section className="py-24 md:py-28">
<div className="container">
<div className="mx-auto max-w-2xl text-center">
<p className="text-[13px] font-semibold uppercase tracking-[0.04em] text-brand">
What we believe
</p>
<h2 className="mt-3 font-display text-3xl font-extrabold tracking-tight md:text-4xl">
The principles behind the product
</h2>
</div>
<div className="mt-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{VALUES.map((v) => (
<div
key={v.title}
className="rounded-2xl border border-border bg-card p-7 transition-all hover:-translate-y-0.5 hover:shadow-md"
>
<span className="flex h-12 w-12 items-center justify-center rounded-2xl bg-brand/10 text-brand">
<v.icon className="h-6 w-6" />
</span>
<h3 className="mt-5 font-display text-lg font-bold tracking-tight">{v.title}</h3>
<p className="mt-2 leading-relaxed text-muted-foreground">{v.body}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="pb-24 md:pb-28">
<div className="container">
<div className="relative overflow-hidden rounded-3xl bg-primary px-8 py-20 text-center text-primary-foreground">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(60%_80%_at_50%_0%,hsl(var(--brand)/0.35),transparent_70%)]" />
<div className="relative">
<h2 className="font-display text-4xl font-extrabold tracking-tight sm:text-5xl">
Come make something worth listening to
</h2>
<p className="mx-auto mt-4 max-w-xl text-lg text-primary-foreground/75">
Spin up a fully produced episode on the free plan in a couple of minutes then decide.
</p>
<div className="mt-9 flex flex-col justify-center gap-3 sm:flex-row">
<Button asChild size="lg" variant="brand">
<Link href="/sign-up">
Start free <ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="border-white/25 bg-transparent text-primary-foreground hover:bg-white/10 hover:text-primary-foreground"
>
<Link href="/pricing">See pricing</Link>
</Button>
</div>
</div>
</div>
</div>
</section>
</>
);
}