feat: add per-platform SEO landing pages (/youtube-downloader, etc.)

Create a dedicated, dynamically-routed page for each of 15 curated
platforms (YouTube, TikTok, Instagram, Facebook, X, Reddit, Pinterest,
Vimeo, SoundCloud, Twitch, Dailymotion, LinkedIn, Tumblr, VK, Snapchat)
under /[locale]/[slug], with a tailored hero, an example URL matched
to the platform, a unique "about" paragraph, and a mix of
platform-specific and shared FAQ items, all translated across
en/fr/es/it. Unknown slugs 404.

Kept the scope to a curated list rather than all 1380 yt-dlp
extractors to avoid thin/duplicate "doorway page" content that search
engines penalize.

The homepage's platform badges and the "+1000 sites" links now point
to these pages, extracted the FAQ accordion markup into a shared
FaqAccordion component reused by both the homepage FAQ and the new
platform pages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 11:53:36 +02:00
co-authored by Claude Sonnet 5
parent bd863056a3
commit 948b77cef4
10 changed files with 672 additions and 57 deletions
+121
View File
@@ -0,0 +1,121 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getTranslations } from 'next-intl/server'
import { routing } from '@/i18n/routing'
import { Link } from '@/navigation'
import { getDownloaderPlatformBySlug } from '@/lib/downloader-platforms'
import { SubmitForm } from '@/components/SubmitForm'
import { ReassuranceSection } from '@/components/ReassuranceSection'
import { HowItWorksSection } from '@/components/HowItWorksSection'
import { FaqAccordion } from '@/components/FaqAccordion'
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}): Promise<Metadata> {
const { locale, slug } = await params
const platform = getDownloaderPlatformBySlug(slug)
if (!platform) return {}
const t = await getTranslations({ locale, namespace: 'downloaderPage' })
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? ''
return {
title: t('metaTitle', { platform: platform.name }),
description: t('metaDescription', { platform: platform.name }),
alternates: {
canonical: `${baseUrl}/${locale}/${slug}`,
languages: Object.fromEntries(
routing.locales.map((l) => [l, `/${l}/${slug}`])
),
},
}
}
export default async function DownloaderPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}) {
const { locale, slug } = await params
const platform = getDownloaderPlatformBySlug(slug)
if (!platform) notFound()
const t = await getTranslations({ locale, namespace: 'downloaderPage' })
const tPlatform = await getTranslations({ locale, namespace: `downloaderPages.${platform.id}` })
const tFaq = await getTranslations({ locale, namespace: 'faq' })
const faqItems = [
{ q: tPlatform('faqQ1'), a: tPlatform('faqA1') },
{ q: tPlatform('faqQ2'), a: tPlatform('faqA2') },
{ q: tFaq('q1'), a: tFaq('a1') },
{ q: tFaq('q2'), a: tFaq('a2') },
]
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqItems.map(({ q, a }) => ({
'@type': 'Question',
name: q,
acceptedAnswer: { '@type': 'Answer', text: a },
})),
}
return (
<main>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section
className="flex flex-col items-center justify-center px-6 pt-20 pb-16 text-center"
aria-labelledby="hero-heading"
>
<h1
id="hero-heading"
className="text-4xl md:text-5xl font-bold tracking-tight text-gray-900 dark:text-gray-50 mb-4 max-w-2xl"
>
{t('heading', { platform: platform.name })}
</h1>
<p className="text-lg text-gray-500 dark:text-gray-400 mb-10">
{t('subheading', { platform: platform.name })}
</p>
<SubmitForm examplePlaceholder={platform.exampleUrl} />
</section>
<ReassuranceSection locale={locale} />
<HowItWorksSection locale={locale} />
<section className="py-16 px-6" aria-labelledby="about-heading">
<div className="max-w-3xl mx-auto">
<h2 id="about-heading" className="text-2xl md:text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-50 text-center mb-6">
{t('aboutHeading', { platform: platform.name })}
</h2>
<p className="text-gray-600 dark:text-gray-400 text-center leading-relaxed">
{tPlatform('about')}
</p>
</div>
</section>
<section className="py-16 px-6 bg-gray-50 dark:bg-slate-900/50" aria-labelledby="faq-heading">
<div className="max-w-3xl mx-auto">
<h2 id="faq-heading" className="text-2xl md:text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-50 text-center mb-10">
{t('faqHeading', { platform: platform.name })}
</h2>
<FaqAccordion items={faqItems} />
</div>
</section>
<section className="pb-16 px-6 text-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('otherPlatforms')}{' '}
<Link href="/supported-sites" className="text-violet-600 dark:text-violet-400 hover:underline">
{t('browseAll')}
</Link>
</p>
</section>
</main>
)
}