fix(templates): wire template detail page to real content service

/templates/[id] only searched the hardcoded demo catalog, so real published
containers (e.g. insta-promo) 404'd even though the browser listed and linked them.
Now resolveTemplate() fetches the container by slug via fetchProject(), falling back
to the demo catalog, else notFound(). Page + generateMetadata made async (await params).

Also fix TemplateDetailBreadcrumb: it called server-only getTranslations while
rendered inside the client TemplateDetailContent tree (500 at request time) — switched
to the useTranslations hook. Was latent because demo pages were static-prerendered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-05 06:45:02 +03:30
parent d4fee8d1d7
commit baf6e40dde
2 changed files with 30 additions and 9 deletions
+25 -6
View File
@@ -2,24 +2,43 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { TemplateDetailContent } from "@/components/templates/TemplateDetailContent";
import { VIDEO_TEMPLATES_CATALOG } from "@/lib/video-templates-catalog";
import { fetchProject } from "@/lib/admin-api";
import {
adminProjectToCatalogTemplate,
VIDEO_TEMPLATES_CATALOG,
type VideoCatalogTemplate,
} from "@/lib/video-templates-catalog";
interface TemplateDetailPageProps {
params: { id: string };
params: Promise<{ id: string }>;
}
/**
* Resolve a template by its `[id]` route param — which is the container SLUG used
* in catalog links. Real (admin-managed) templates come from the content service
* via fetchProject(slug); the hardcoded demo catalog is the offline fallback.
*/
async function resolveTemplate(id: string): Promise<VideoCatalogTemplate | null> {
const admin = await fetchProject(id);
if (admin) return adminProjectToCatalogTemplate(admin);
return VIDEO_TEMPLATES_CATALOG.find((item) => item.id === id) ?? null;
}
// Pre-render the demo catalog; real container slugs render on demand (ISR).
export function generateStaticParams() {
return VIDEO_TEMPLATES_CATALOG.map((template) => ({ id: template.id }));
}
export function generateMetadata({ params }: TemplateDetailPageProps): Metadata {
const template = VIDEO_TEMPLATES_CATALOG.find((item) => item.id === params.id);
export async function generateMetadata({ params }: TemplateDetailPageProps): Promise<Metadata> {
const { id } = await params;
const template = await resolveTemplate(id);
if (!template) return {};
return { title: `${template.name} — FlatRender` };
}
export default function TemplateDetailPage({ params }: TemplateDetailPageProps) {
const template = VIDEO_TEMPLATES_CATALOG.find((item) => item.id === params.id);
export default async function TemplateDetailPage({ params }: TemplateDetailPageProps) {
const { id } = await params;
const template = await resolveTemplate(id);
if (!template) notFound();
return (
<main className="min-h-screen bg-white">
@@ -1,15 +1,17 @@
"use client";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { useTranslations } from "next-intl";
interface TemplateDetailBreadcrumbProps {
templateName: string;
}
export async function TemplateDetailBreadcrumb({
export function TemplateDetailBreadcrumb({
templateName,
}: TemplateDetailBreadcrumbProps) {
const t = await getTranslations("auto.componentsTemplatesTemplateDetailBreadcrumb");
const t = useTranslations("auto.componentsTemplatesTemplateDetailBreadcrumb");
return (
<nav aria-label={t("breadcrumbAriaLabel")} className="text-sm text-gray-500">