import type { Metadata } from "next"; import Image from "next/image"; import Script from "next/script"; import { notFound } from "next/navigation"; import { headers } from "next/headers"; import Link from "next/link"; import { Clock, Users, ChefHat, Globe } from "lucide-react"; import { auth } from "@/lib/auth/server"; import { db, recipes, eq } from "@epicure/db"; import { Badge } from "@/components/ui/badge"; import { buttonVariants } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { ServingScaler } from "@/components/recipe/serving-scaler"; import { getPublicUrl } from "@/lib/storage"; import { hasQuantity } from "@/lib/fractions"; import { cn } from "@/lib/utils"; import { getMessages, formatMessage } from "@/lib/i18n/server"; type Params = { params: Promise<{ id: string }> }; export async function generateMetadata({ params }: Params): Promise { const { id } = await params; const recipe = await db.query.recipes.findFirst({ where: (r, { and, eq }) => and(eq(r.id, id), eq(r.visibility, "public")), }); if (!recipe) return {}; return { title: recipe.title, description: recipe.description ?? undefined, openGraph: { title: recipe.title, description: recipe.description ?? undefined, type: "article", }, }; } export default async function PublicRecipePage({ params }: Params) { const { id } = await params; const session = await auth.api.getSession({ headers: await headers() }).catch(() => null); const m = getMessages((session?.user as { locale?: string } | undefined)?.locale); const unitPref = (session?.user as { unitPref?: string } | undefined)?.unitPref === "imperial" ? "imperial" : "metric"; const recipe = await db.query.recipes.findFirst({ where: (r, { and, eq, ne }) => and(eq(r.id, id), ne(r.visibility, "private")), with: { author: true, ingredients: { orderBy: (t, { asc }) => asc(t.order) }, steps: { orderBy: (t, { asc }) => asc(t.order) }, photos: { orderBy: (t, { asc }) => asc(t.order) }, }, }); if (!recipe) notFound(); const cover = recipe.photos.find((p) => p.isCover) ?? recipe.photos[0]; const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0); const activeTags = Object.entries(recipe.dietaryTags ?? {}) .filter(([, v]) => v) .map(([k]) => m.recipe.dietary[k as keyof typeof m.recipe.dietary]) .filter(Boolean); const jsonLd = { "@context": "https://schema.org", "@type": "Recipe", name: recipe.title, description: recipe.description, author: { "@type": "Person", name: recipe.author.name }, recipeYield: String(recipe.baseServings), prepTime: recipe.prepMins ? `PT${recipe.prepMins}M` : undefined, cookTime: recipe.cookMins ? `PT${recipe.cookMins}M` : undefined, recipeIngredient: recipe.ingredients.map((i) => [hasQuantity(i.quantity) ? i.quantity : null, i.unit, i.rawName].filter(Boolean).join(" ") ), recipeInstructions: recipe.steps.map((s) => ({ "@type": "HowToStep", text: s.instruction, })), image: cover ? [getPublicUrl(cover.storageKey)] : undefined, }; return ( <>