Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"code-review@claude-plugins-official": true
}
}
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@ EMAIL_PRIVACY=privacy@... # Privacy contact email shown in legal page

**Audio path**: `quality=audio` streams directly (ffmpeg → PassThrough → Response). Thumbnail fetched to a temp file for album art embedding, deleted after ffmpeg closes.

## i18n — Adding a new locale or language variant

Active locales (BCP 47): `en`, `fr-FR`, `es-419`, `pt-BR`.

**To add a new locale** (e.g. `fr-CA`, `pt-PT`, `es-MX`):

1. Add the locale code to `locales` in `i18n/routing.ts`
2. Create `messages/<locale>.json` — copy the closest existing locale as a base
3. Add a display label in `components/custom/LocaleSwitcher.tsx` (`LOCALE_LABELS`)
4. Translate `messages/<locale>.json`

Everything else (sitemap, hreflang alternates, static params) auto-updates via `buildAlternates()` in `i18n/metadata.ts` and `routing.locales`.

**Locale code conventions used here:**

- Generic English: `en` (covers all regions — do NOT use `en-US`)
- French France: `fr-FR`
- Latin American Spanish: `es-419` (UN M.49 region code, Google-supported)
- Brazilian Portuguese: `pt-BR`

## Prisma 7 Notes

**Singleton required**: Always use `import { prisma } from "@/lib/prisma"` — never `new PrismaClient()`. Prisma 7 requires a `PrismaLibSql` driver adapter; bare `new PrismaClient()` throws at runtime.
Expand Down
29 changes: 22 additions & 7 deletions app/fetch/page.tsx → app/[locale]/fetch/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { Suspense } from "react";
import { GetterInput } from "@/components/custom/GetterInput";
import { SkeletonInput } from "@/components/custom/SkeletonInput";
import { VideoLoading } from "@/components/custom/VideoLoading";
import { VideoSelect } from "@/components/custom/VideoSelect";

export const metadata = {
title: "Download video",
robots: { index: false },
};
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" });
return {
title: t("fetchTitle"),
robots: { index: false },
};
}

export default async function QualityVideoSelection({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);

export default function QualityVideoSelection() {
return (
<section className="bg-stroy-500 px-4 py-8 md:px-14">
{/* URL recap / edit bar */}
<div className="mx-auto mb-10 max-w-5xl">
<Suspense fallback={<SkeletonInput />}>
<GetterInput />
</Suspense>
</div>

{/* Result card + format picker */}
<Suspense fallback={<VideoLoading />}>
<VideoSelect />
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import { AlertTriangle, ArrowRight } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { setRequestLocale } from "next-intl/server";
import { JsonLd } from "@/components/custom/JsonLd";
import { buildAlternates } from "@/i18n/metadata";
import { siteConfig } from "@/lib/site-config";

export const metadata: Metadata = {
title: "How to download a YouTube video as MP4 or MP3 in 2026",
description:
"Free, no-install walkthrough using StroyGetter — the open-source YouTube downloader. Download MP4 video or MP3 audio on Chrome, Firefox, Safari and mobile.",
keywords: [
"how to download youtube video",
"youtube to mp4",
"youtube to mp3",
"download youtube video free",
"youtube downloader no install",
],
alternates: { canonical: "/how-to-download-youtube-videos" },
openGraph: {
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const path = "/how-to-download-youtube-videos";
return {
title: "How to download a YouTube video as MP4 or MP3 in 2026",
description:
"Free, no-install walkthrough using StroyGetter. Download MP4 video or MP3 audio directly from your browser.",
url: `${siteConfig.url}/how-to-download-youtube-videos`,
},
};
"Free, no-install walkthrough using StroyGetter — the open-source YouTube downloader. Download MP4 video or MP3 audio on Chrome, Firefox, Safari and mobile.",
keywords: [
"how to download youtube video",
"youtube to mp4",
"youtube to mp3",
"download youtube video free",
"youtube downloader no install",
],
alternates: buildAlternates(locale, path),
openGraph: {
title: "How to download a YouTube video as MP4 or MP3 in 2026",
description:
"Free, no-install walkthrough using StroyGetter. Download MP4 video or MP3 audio directly from your browser.",
url: `${siteConfig.url}${path}`,
},
};
}

const TOC = [
"What you need before starting",
Expand Down Expand Up @@ -54,7 +64,13 @@ const ARTICLE_FAQS = [
},
];

export default function HowToDownloadYouTube() {
export default async function HowToDownloadYouTube({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
return (
<>
<JsonLd
Expand Down
Loading