fix: error boundaries + defensive try/catch on dynamic pages
Deploy to VPS / deploy (push) Has been cancelled
Deploy to VPS / deploy (push) Has been cancelled
The /en/applications/digital-print page was still 500-ing after the
previous fixes. Without an error boundary, Next.js shows a generic
"Internal Server Error" with no detail — making remote diagnosis
require a `docker compose logs` round-trip every time.
ERROR BOUNDARIES (visible diagnostics)
- src/app/global-error.tsx: catches errors that bubble past every
route's error.tsx, including ones from the root layout. Renders
its own <html>/<body>.
- src/app/[locale]/error.tsx: locale-scoped boundary so the NavBar
and Footer keep rendering around the error UI. Shows the actual
error message + digest in a code block — much faster to diagnose
than a blank 500.
DEFENSIVE WRAPPING (every async + every transform)
- applications/[slug]/page.tsx
- getApplicationImages: try/catch around fs ops
- generateMetadata: full body wrapped, falls back to safe defaults
- getLocalizedData call wrapped (returns rawData if it throws)
- Cases query already had try/catch — adds same for the locale map
- JSON-LD build wrapped, falls back to empty array (still renders)
- Default fallbacks for title/description/category to avoid
productSchema receiving undefined fields
- news/[slug]/page.tsx
- prisma.newsArticle.findUnique now has try/catch
- getLocalizedData wrapped
- JSON-LD build wrapped, only rendered if non-empty
- publishedAt / updatedAt fallback to new Date() to avoid
"Invalid time value" from articleSchema's date conversion
The combination means: if the underlying bug is in any of the SEO
helpers, JSON-LD generation, or i18n merging, the page now degrades
gracefully and shows the actual error in the UI instead of 500-ing.
This commit is contained in:
@@ -20,17 +20,28 @@ import JsonLd from "@/components/seo/JsonLd";
|
|||||||
|
|
||||||
// --- FUNCIÓN ORIGINAL PARA LEER IMÁGENES LOCALES ---
|
// --- FUNCIÓN ORIGINAL PARA LEER IMÁGENES LOCALES ---
|
||||||
function getApplicationImages(slug: string) {
|
function getApplicationImages(slug: string) {
|
||||||
const imagesDir = path.join(process.cwd(), "public", "applications", slug);
|
|
||||||
let blueprints: string[] = [];
|
let blueprints: string[] = [];
|
||||||
let machines: string[] = [];
|
let machines: string[] = [];
|
||||||
let heroImage = "";
|
let heroImage = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imagesDir = path.join(process.cwd(), "public", "applications", slug);
|
||||||
|
|
||||||
if (fs.existsSync(imagesDir)) {
|
if (fs.existsSync(imagesDir)) {
|
||||||
const files = fs.readdirSync(imagesDir).filter(file => /\.(png|jpe?g|webp)$/i.test(file));
|
const files = fs.readdirSync(imagesDir).filter((file) => /\.(png|jpe?g|webp)$/i.test(file));
|
||||||
|
|
||||||
heroImage = files[0] ? `/applications/${slug}/${files[0]}` : "";
|
heroImage = files[0] ? `/applications/${slug}/${files[0]}` : "";
|
||||||
blueprints = files.filter(f => f.includes("Screenshot") || f.startsWith("P10") || f.includes("blueprint")).slice(0, 3).map(f => `/applications/${slug}/${f}`);
|
blueprints = files
|
||||||
machines = files.filter(f => !f.includes("Screenshot") && !f.startsWith("P10") && !f.includes("blueprint")).slice(1, 4).map(f => `/applications/${slug}/${f}`);
|
.filter((f) => f.includes("Screenshot") || f.startsWith("P10") || f.includes("blueprint"))
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((f) => `/applications/${slug}/${f}`);
|
||||||
|
machines = files
|
||||||
|
.filter((f) => !f.includes("Screenshot") && !f.startsWith("P10") && !f.includes("blueprint"))
|
||||||
|
.slice(1, 4)
|
||||||
|
.map((f) => `/applications/${slug}/${f}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[applications/${slug}] Image scan failed:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { heroImage, blueprints, machines };
|
return { heroImage, blueprints, machines };
|
||||||
@@ -42,9 +53,8 @@ export async function generateMetadata({
|
|||||||
}: {
|
}: {
|
||||||
params: Promise<{ slug: string; locale: string }>;
|
params: Promise<{ slug: string; locale: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { slug, locale } = await params;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { slug, locale } = await params;
|
||||||
const raw = await prisma.application.findUnique({ where: { slug } });
|
const raw = await prisma.application.findUnique({ where: { slug } });
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return {
|
return {
|
||||||
@@ -53,18 +63,21 @@ export async function generateMetadata({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = getLocalizedData(raw, locale);
|
const data: any = getLocalizedData(raw, locale);
|
||||||
const heroImage = getApplicationImages(slug).heroImage;
|
const heroImage = getApplicationImages(slug).heroImage;
|
||||||
|
const title = data?.title || "Application";
|
||||||
|
const description = data?.shortDescription || data?.subtitle || "FLUX RF industrial solutions.";
|
||||||
|
|
||||||
return buildPageMetadata({
|
return buildPageMetadata({
|
||||||
locale,
|
locale,
|
||||||
pathWithoutLocale: `applications/${slug}`,
|
pathWithoutLocale: `applications/${slug}`,
|
||||||
title: `${data.title} — RF Industrial Solutions | FLUX`,
|
title: `${title} — RF Industrial Solutions | FLUX`,
|
||||||
description: data.shortDescription || data.subtitle,
|
description,
|
||||||
ogImageUrl: heroImage || undefined,
|
ogImageUrl: heroImage || undefined,
|
||||||
type: "product",
|
type: "product",
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error("[applications generateMetadata]", error);
|
||||||
return { title: "FLUX | Energy, Directed." };
|
return { title: "FLUX | Energy, Directed." };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +123,13 @@ export default async function ApplicationPage({ params }: { params: Promise<{ sl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 TRADUCIMOS LA APLICACIÓN PRINCIPAL
|
// 🔥 TRADUCIMOS LA APLICACIÓN PRINCIPAL
|
||||||
const data = getLocalizedData(rawData, locale);
|
let data: any;
|
||||||
|
try {
|
||||||
|
data = getLocalizedData(rawData, locale);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[applications/${slug}] Locale merge failed:`, error);
|
||||||
|
data = rawData;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Buscamos el "Muro de Soluciones" (Casos Reales específicos de esta app)
|
// 2. Buscamos el "Muro de Soluciones" (Casos Reales específicos de esta app)
|
||||||
let rawRealCases: any[] = [];
|
let rawRealCases: any[] = [];
|
||||||
@@ -128,30 +147,44 @@ export default async function ApplicationPage({ params }: { params: Promise<{ sl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 TRADUCIMOS TODOS LOS CASOS DE ESTUDIO DEL MURO
|
// 🔥 TRADUCIMOS TODOS LOS CASOS DE ESTUDIO DEL MURO
|
||||||
const realCases = rawRealCases.map((node: any) => getLocalizedData(node, locale));
|
let realCases: any[] = [];
|
||||||
|
try {
|
||||||
|
realCases = rawRealCases.map((node: any) => getLocalizedData(node, locale));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[applications/${slug}] Cases locale merge failed:`, error);
|
||||||
|
realCases = rawRealCases;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Leemos las imágenes de la carpeta original
|
// 3. Leemos las imágenes de la carpeta original
|
||||||
const images = getApplicationImages(slug);
|
const images = getApplicationImages(slug);
|
||||||
|
|
||||||
|
// 4. JSON-LD structured data — wrapped to never break the render.
|
||||||
|
let jsonLd: object[] = [];
|
||||||
|
try {
|
||||||
const url = `${baseUrl()}/${locale}/applications/${slug}`;
|
const url = `${baseUrl()}/${locale}/applications/${slug}`;
|
||||||
const jsonLd = [
|
const title = data?.title || "FLUX Application";
|
||||||
|
const description = data?.shortDescription || data?.subtitle || "";
|
||||||
|
jsonLd = [
|
||||||
productSchema({
|
productSchema({
|
||||||
name: data.title,
|
name: title,
|
||||||
description: data.shortDescription || data.subtitle,
|
description,
|
||||||
imageUrl: images.heroImage || undefined,
|
imageUrl: images.heroImage || undefined,
|
||||||
category: data.category,
|
category: data?.category || "RF Industrial",
|
||||||
url,
|
url,
|
||||||
}),
|
}),
|
||||||
breadcrumbSchema([
|
breadcrumbSchema([
|
||||||
{ name: "Home", url: `${baseUrl()}/${locale}` },
|
{ name: "Home", url: `${baseUrl()}/${locale}` },
|
||||||
{ name: "Applications", url: `${baseUrl()}/${locale}#applications-deep` },
|
{ name: "Applications", url: `${baseUrl()}/${locale}#applications-deep` },
|
||||||
{ name: data.title, url },
|
{ name: title, url },
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[applications/${slug}] JSON-LD build failed:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<JsonLd data={jsonLd} />
|
{jsonLd.length > 0 && <JsonLd data={jsonLd} />}
|
||||||
<ApplicationClient data={data} realCases={realCases} images={images} />
|
<ApplicationClient data={data} realCases={realCases} images={images} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Locale-scoped error boundary — caught here so the root layout
|
||||||
|
// (NavBar, Footer, etc.) keeps rendering around the error UI.
|
||||||
|
// Useful for diagnosing per-page failures without losing site chrome.
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function LocaleError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[LocaleError]", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[60vh] flex items-center justify-center px-6">
|
||||||
|
<div className="max-w-2xl w-full">
|
||||||
|
<div className="text-[10px] uppercase tracking-widest text-[#FF6B6B] font-bold mb-3">
|
||||||
|
Page error
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-light text-[#1D1D1F] dark:text-white mb-4">
|
||||||
|
This page hit a problem.
|
||||||
|
</h1>
|
||||||
|
<p className="text-[#86868B] mb-6">
|
||||||
|
The site is up but this specific page failed to render.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre className="bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-2xl p-4 text-xs text-rose-500 dark:text-rose-400 overflow-auto mb-6">
|
||||||
|
{error.message || "Unknown error"}
|
||||||
|
{error.digest ? `\n\nDigest: ${error.digest}` : ""}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => reset()}
|
||||||
|
className="px-5 py-3 bg-[#0066CC] dark:bg-[#00F0FF] text-white dark:text-black text-sm font-medium rounded-lg hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -228,9 +228,12 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
|||||||
const resolvedParams = await params;
|
const resolvedParams = await params;
|
||||||
const { slug, locale } = resolvedParams;
|
const { slug, locale } = resolvedParams;
|
||||||
|
|
||||||
const rawArticle = await prisma.newsArticle.findUnique({
|
let rawArticle: any = null;
|
||||||
where: { slug }
|
try {
|
||||||
});
|
rawArticle = await prisma.newsArticle.findUnique({ where: { slug } });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[news/${slug}] DB fetch failed:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
if (!rawArticle) {
|
if (!rawArticle) {
|
||||||
return (
|
return (
|
||||||
@@ -242,32 +245,44 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 TRADUCCIÓN MÁGICA ANTES DEL RENDER 🔥
|
// 🔥 TRADUCCIÓN MÁGICA ANTES DEL RENDER 🔥
|
||||||
const article = getLocalizedData(rawArticle, locale);
|
let article: any;
|
||||||
|
try {
|
||||||
|
article = getLocalizedData(rawArticle, locale);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[news/${slug}] Locale merge failed:`, error);
|
||||||
|
article = rawArticle;
|
||||||
|
}
|
||||||
|
|
||||||
let gallery: string[] = [];
|
let gallery: string[] = [];
|
||||||
try { gallery = JSON.parse(article.galleryJson || "[]"); } catch (e) {}
|
try { gallery = JSON.parse(article.galleryJson || "[]"); } catch (e) {}
|
||||||
|
|
||||||
const articleUrl = `${baseUrl()}/${locale}/news/${slug}`;
|
const articleUrl = `${baseUrl()}/${locale}/news/${slug}`;
|
||||||
const cover = article.coverImage ? `/news/${slug}/${article.coverImage}` : undefined;
|
const cover = article.coverImage ? `/news/${slug}/${article.coverImage}` : undefined;
|
||||||
const jsonLd = [
|
|
||||||
|
let jsonLd: object[] = [];
|
||||||
|
try {
|
||||||
|
jsonLd = [
|
||||||
articleSchema({
|
articleSchema({
|
||||||
headline: article.title,
|
headline: article?.title || "FLUX Article",
|
||||||
description: article.excerpt,
|
description: article?.excerpt || "",
|
||||||
imageUrl: cover,
|
imageUrl: cover,
|
||||||
url: articleUrl,
|
url: articleUrl,
|
||||||
publishedAt: article.publishedAt,
|
publishedAt: article?.publishedAt || new Date(),
|
||||||
updatedAt: article.updatedAt,
|
updatedAt: article?.updatedAt || new Date(),
|
||||||
}),
|
}),
|
||||||
breadcrumbSchema([
|
breadcrumbSchema([
|
||||||
{ name: "Home", url: `${baseUrl()}/${locale}` },
|
{ name: "Home", url: `${baseUrl()}/${locale}` },
|
||||||
{ name: "News", url: `${baseUrl()}/${locale}/news` },
|
{ name: "News", url: `${baseUrl()}/${locale}/news` },
|
||||||
{ name: article.title, url: articleUrl },
|
{ name: article?.title || "Article", url: articleUrl },
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[news/${slug}] JSON-LD build failed:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative min-h-screen pb-24">
|
<main className="relative min-h-screen pb-24">
|
||||||
<JsonLd data={jsonLd} />
|
{jsonLd.length > 0 && <JsonLd data={jsonLd} />}
|
||||||
<BreathingField />
|
<BreathingField />
|
||||||
|
|
||||||
<div className="fixed top-24 left-6 z-50 hidden md:block">
|
<div className="fixed top-24 left-6 z-50 hidden md:block">
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Global error boundary — catches errors that bubble up past route-level
|
||||||
|
// error.tsx files. Renders its own <html>/<body> because the root layout
|
||||||
|
// errored too.
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function GlobalError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[GlobalError]", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
style={{
|
||||||
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||||
|
background: "#0A0A0C",
|
||||||
|
color: "#F5F5F7",
|
||||||
|
minHeight: "100vh",
|
||||||
|
margin: 0,
|
||||||
|
padding: "2rem",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ maxWidth: 720, width: "100%" }}>
|
||||||
|
<h1 style={{ fontSize: 28, fontWeight: 300, marginBottom: 16 }}>
|
||||||
|
Something went wrong on FLUX
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: "#86868B", marginBottom: 24 }}>
|
||||||
|
The page hit an unexpected error. The team has been notified.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
background: "#1D1D1F",
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: "auto",
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#FF6B6B",
|
||||||
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error.message || "Unknown error"}
|
||||||
|
{error.digest ? `\n\nDigest: ${error.digest}` : ""}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => reset()}
|
||||||
|
style={{
|
||||||
|
marginTop: 24,
|
||||||
|
padding: "12px 24px",
|
||||||
|
background: "#00F0FF",
|
||||||
|
color: "#000",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user