TipTap (ProseMirror) was causing: - chunks 1007 (85 KiB) and 3207 (58 KiB) in the initial bundle - Array.prototype.at/flat/flatMap, Object.fromEntries/hasOwn polyfills (ProseMirror bundles core-js for these — the 12 KiB legacy JS flag) - 2+ seconds of main thread blocking on mobile Fix: move HTML conversion to the server (API route) and pass the resulting HTML string to the client, eliminating the need to import richTextToSafeHtml (and transitively TipTap) in any client component. Changes: - app/api/content/page/route.ts: call richTextToSafeHtml server-side, add html: string to response alongside existing content - app/components/RichTextClient.tsx: accept html string, remove all TipTap imports — TipTap/ProseMirror now has zero client bundle cost - app/components/About.tsx, Contact.tsx: use cmsHtml from API - app/legal-notice/page.tsx, privacy-policy/page.tsx: same - app/components/ClientWrappers.tsx: change static imports of About, Projects, Contact, Footer to next/dynamic so their JS is in separate lazy-loaded chunks, not in the initial bundle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getContentByKey } from "@/lib/content";
|
|
import { getContentPage } from "@/lib/directus";
|
|
import { richTextToSafeHtml } from "@/lib/richtext";
|
|
|
|
const CACHE_TTL = 300; // 5 minutes
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const { searchParams } = new URL(request.url);
|
|
const key = searchParams.get("key");
|
|
const locale = searchParams.get("locale") || "en";
|
|
|
|
if (!key) {
|
|
return NextResponse.json({ error: "key is required" }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
// 1) Try Directus first
|
|
const directusPage = await getContentPage(key, locale);
|
|
if (directusPage) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const html = directusPage.content ? richTextToSafeHtml(directusPage.content as any) : "";
|
|
return NextResponse.json(
|
|
{
|
|
content: {
|
|
title: directusPage.title,
|
|
slug: directusPage.slug,
|
|
locale: directusPage.locale || locale,
|
|
content: directusPage.content,
|
|
html,
|
|
},
|
|
source: "directus",
|
|
},
|
|
{ headers: { "Cache-Control": `public, s-maxage=${CACHE_TTL}, stale-while-revalidate=${CACHE_TTL * 2}` } }
|
|
);
|
|
}
|
|
|
|
// 2) Fallback: PostgreSQL
|
|
const translation = await getContentByKey({ key, locale });
|
|
if (!translation) {
|
|
return NextResponse.json(
|
|
{ content: null },
|
|
{ headers: { "Cache-Control": `public, s-maxage=${CACHE_TTL}, stale-while-revalidate=${CACHE_TTL * 2}` } }
|
|
);
|
|
}
|
|
return NextResponse.json(
|
|
{ content: translation, source: "postgresql" },
|
|
{ headers: { "Cache-Control": `public, s-maxage=${CACHE_TTL}, stale-while-revalidate=${CACHE_TTL * 2}` } }
|
|
);
|
|
} catch (error) {
|
|
// If DB isn't migrated/available, fail soft so the UI can fall back to next-intl strings.
|
|
if (process.env.NODE_ENV === "development") {
|
|
console.warn("Content API failed; returning null content:", error);
|
|
}
|
|
return NextResponse.json({ content: null });
|
|
}
|
|
}
|
|
|