@@ -171,11 +174,14 @@ export default async function BlogPost({ params }: BlogPostProps) {
{getReadingTime(rawTextContent)} min read
diff --git a/app/[locale]/blog/page.tsx b/app/[locale]/blog/page.tsx
index 2144da2c..c12e171e 100644
--- a/app/[locale]/blog/page.tsx
+++ b/app/[locale]/blog/page.tsx
@@ -198,11 +198,14 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
diff --git a/app/stats/api/send/route.ts b/app/stats/api/send/route.ts
index 3acd30f4..3f94b7f8 100644
--- a/app/stats/api/send/route.ts
+++ b/app/stats/api/send/route.ts
@@ -24,14 +24,34 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ status: 'ignored_in_dev' }, { status: 200 });
}
- const body = await request.json();
+ let body;
+ try {
+ body = await request.json();
+ } catch (parseError) {
+ logger.warn('Received malformed or empty JSON in analytics proxy');
+ return NextResponse.json({ status: 'ignored_bad_payload' }, { status: 200 });
+ }
+
+ if (!body || typeof body !== 'object') {
+ logger.warn('Received invalid body type in analytics proxy', { type: typeof body });
+ return NextResponse.json({ status: 'ignored_invalid_body' }, { status: 200 });
+ }
+
const { type, payload } = body;
+ if (!type || !payload) {
+ logger.warn('Received incomplete analytics payload', {
+ hasType: !!type,
+ hasPayload: !!payload
+ });
+ return NextResponse.json({ status: 'ignored_incomplete_payload' }, { status: 200 });
+ }
+
// Inject the secret websiteId from server config
const websiteId = config.analytics.umami.websiteId;
if (!websiteId) {
logger.warn('Umami tracking received but no Website ID configured on server');
- return NextResponse.json({ status: 'ignored' }, { status: 200 });
+ return NextResponse.json({ status: 'ignored_no_config' }, { status: 200 });
}
// Prepare the enhanced payload with the secret ID
diff --git a/components/JsonLd.tsx b/components/JsonLd.tsx
index 042f4093..150ebd08 100644
--- a/components/JsonLd.tsx
+++ b/components/JsonLd.tsx
@@ -7,16 +7,14 @@ interface JsonLdProps {
export default function JsonLd({ id, data }: JsonLdProps) {
// If data is provided, use it. Otherwise, use the default Organization + WebSite schema.
- const schemaData = data || [
+ const rawData = data || [
{
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'KLZ Cables',
url: 'https://klz-cables.com',
logo: 'https://klz-cables.com/logo-blue.svg',
- sameAs: [
- 'https://www.linkedin.com/company/klz-cables',
- ],
+ sameAs: ['https://www.linkedin.com/company/klz-cables'],
description: 'Premium Cable Solutions for Renewable Energy and Infrastructure.',
address: {
'@type': 'PostalAddress',
@@ -36,15 +34,32 @@ export default function JsonLd({ id, data }: JsonLdProps) {
},
'query-input': 'required name=search_term_string',
},
- }
+ },
];
+ // Harden schema items: Ensure each item is an object and has @context
+ const schemaData = (Array.isArray(rawData) ? rawData : [rawData])
+ .filter((item): item is WithContext
=> {
+ // Basic sanity check: must be an object and not null
+ return typeof item === 'object' && item !== null;
+ })
+ .map((item) => {
+ // Ensure @context is present and correctly typed for consumer libraries
+ if (!item['@context']) {
+ return {
+ ...item,
+ '@context': 'https://schema.org',
+ };
+ }
+ return item;
+ });
+
return (
);
diff --git a/components/home/RecentPosts.tsx b/components/home/RecentPosts.tsx
index 02111a09..0d44fe09 100644
--- a/components/home/RecentPosts.tsx
+++ b/components/home/RecentPosts.tsx
@@ -74,11 +74,14 @@ export default async function RecentPosts({ locale, data }: RecentPostsProps) {
suppressHydrationWarning
className="px-3 py-1 text-white/80 text-[10px] md:text-xs font-bold uppercase tracking-widest border border-white/20 rounded-full bg-white/10 backdrop-blur-md"
>
- {new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- })}
+ {new Date(post.frontmatter.date).toLocaleDateString(
+ locale === 'en' ? 'en-US' : 'de-DE',
+ {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ },
+ )}
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
diff --git a/lib/config.ts b/lib/config.ts
index 150b913a..3cbac033 100644
--- a/lib/config.ts
+++ b/lib/config.ts
@@ -66,8 +66,8 @@ function createConfig() {
port: env.MAIL_PORT,
user: env.MAIL_USERNAME,
pass: env.MAIL_PASSWORD,
- from: env.MAIL_FROM,
- recipients: env.MAIL_RECIPIENTS,
+ from: env.MAIL_FROM || 'KLZ Cables ',
+ recipients: env.MAIL_RECIPIENTS || 'info@klz-cables.com',
},
infraCMS: {
url: env.INFRA_DIRECTUS_URL,
diff --git a/middleware.ts b/middleware.ts
index 62cd5429..e2c8ebe1 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -14,8 +14,12 @@ export default async function middleware(request: NextRequest) {
const { method, url, headers } = request;
const { pathname } = request.nextUrl;
- // Explicit bypass for infrastructure and Payload CMS routes to avoid locale redirects/interception
+ // Explicit bypass for infrastructure, Payload CMS, and Server Actions
+ // Next-Action header is present for all Next.js Server Actions
+ const isServerAction = headers.has('next-action');
+
if (
+ isServerAction ||
pathname.startsWith('/admin') ||
pathname.startsWith('/api') ||
pathname.startsWith('/stats') ||
@@ -55,10 +59,12 @@ export default async function middleware(request: NextRequest) {
// Only create a new request for GET/HEAD to avoid consuming the body stream on POST/PUT
// This resolves the "failed to pipe response" error (160k occurrences in GlitchTip)
if (['GET', 'HEAD'].includes(request.method)) {
+ // Clone headers to ensure all Next.js internal headers (like router state) are preserved
+ const clonedHeaders = new Headers(request.headers);
+
effectiveRequest = new NextRequest(urlObj, {
- headers: request.headers,
+ headers: clonedHeaders,
method: request.method,
- // No body for GET/HEAD
});
if (process.env.NODE_ENV !== 'production' || !process.env.CI) {
diff --git a/scripts/check-forms.ts b/scripts/check-forms.ts
index bd1ef579..83237ba3 100644
--- a/scripts/check-forms.ts
+++ b/scripts/check-forms.ts
@@ -103,8 +103,10 @@ async function main() {
console.log(`\n๐งช Testing Contact Form on: ${contactUrl}`);
await page.goto(contactUrl, { waitUntil: 'networkidle0', timeout: 30000 });
- // Ensure React has hydrated completely
- await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 }).catch(() => {});
+ // Ensure React has hydrated completely - CI runners can be slow
+ console.log(` โณ Waiting for hydration (5s)...`);
+ await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 5000)));
+ await page.waitForNetworkIdle({ idleTime: 1000, timeout: 20000 }).catch(() => {});
// Ensure form is visible and interactive
try {
@@ -129,18 +131,32 @@ async function main() {
// Give state a moment to settle
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500)));
- console.log(` Submitting Contact Form...`);
-
- // Explicitly click submit and wait for navigation/state-change
- await Promise.all([
- page.waitForSelector('[role="alert"]', { timeout: 15000 }),
- page.click('button[type="submit"]'),
- ]);
+ console.log(` โณ Waiting for alert selector (30s)...`);
+ try {
+ // Explicitly click submit and wait for navigation/state-change
+ await Promise.all([
+ page.waitForSelector('[role="alert"]', { timeout: 30000 }),
+ page.click('button[type="submit"]'),
+ ]);
+ } catch (e) {
+ console.error(`โ Timeout waiting for [role="alert"]. Dumping page content:`);
+ const bodyHTML = await page.evaluate(() => document.body.innerHTML.slice(0, 1000));
+ console.log(`๐ป PAGE BODY (partial): ${bodyHTML}`);
+ throw e;
+ }
const alertText = await page.$eval('[role="alert"]', (el) => el.textContent);
- console.log(` Alert text: ${alertText}`);
+ console.log(` ๐ Alert text: ${alertText}`);
- if (alertText?.includes('Failed') || alertText?.includes('went wrong')) {
+ // Detection robust for both English and German versions
+ const isError =
+ alertText?.toLowerCase().includes('failed') ||
+ alertText?.toLowerCase().includes('wrong') ||
+ alertText?.toLowerCase().includes('fehlgeschlagen') ||
+ alertText?.toLowerCase().includes('schief gelaufen') ||
+ alertText?.toLowerCase().includes('fehler');
+
+ if (isError) {
throw new Error(`Form submitted but showed error: ${alertText}`);
}
@@ -156,7 +172,9 @@ async function main() {
await page.goto(productUrl, { waitUntil: 'networkidle0', timeout: 30000 });
// Ensure React has hydrated completely
- await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 }).catch(() => {});
+ console.log(` โณ Waiting for hydration (5s)...`);
+ await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 5000)));
+ await page.waitForNetworkIdle({ idleTime: 1000, timeout: 20000 }).catch(() => {});
// The product form uses dynamic IDs, so we select by input type in the specific form context
try {
@@ -179,18 +197,31 @@ async function main() {
// Give state a moment to settle
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500)));
- console.log(` Submitting Product Quote Form...`);
-
- // Submit and wait for success state
- await Promise.all([
- page.waitForSelector('[role="alert"]', { timeout: 15000 }),
- page.click('form button[type="submit"]'),
- ]);
+ console.log(` โณ Waiting for alert selector (30s)...`);
+ try {
+ // Submit and wait for success state
+ await Promise.all([
+ page.waitForSelector('[role="alert"]', { timeout: 30000 }),
+ page.click('form button[type="submit"]'),
+ ]);
+ } catch (e) {
+ console.error(`โ Timeout waiting for [role="alert"] on Product page. Dumping content:`);
+ const bodyHTML = await page.evaluate(() => document.body.innerHTML.slice(0, 1000));
+ console.log(`๐ป PAGE BODY (partial): ${bodyHTML}`);
+ throw e;
+ }
const alertText = await page.$eval('[role="alert"]', (el) => el.textContent);
- console.log(` Alert text: ${alertText}`);
+ console.log(` ๐ Alert text: ${alertText}`);
- if (alertText?.includes('Failed') || alertText?.includes('went wrong')) {
+ const isError =
+ alertText?.toLowerCase().includes('failed') ||
+ alertText?.toLowerCase().includes('wrong') ||
+ alertText?.toLowerCase().includes('fehlgeschlagen') ||
+ alertText?.toLowerCase().includes('schief gelaufen') ||
+ alertText?.toLowerCase().includes('fehler');
+
+ if (isError) {
throw new Error(`Form submitted but showed error: ${alertText}`);
}
diff --git a/scripts/check-today-submissions.ts b/scripts/check-today-submissions.ts
new file mode 100644
index 00000000..f377e6f5
--- /dev/null
+++ b/scripts/check-today-submissions.ts
@@ -0,0 +1,29 @@
+import { Client } from 'pg';
+
+async function checkSubmissions() {
+ const client = new Client({
+ connectionString: "postgresql://payload:120in09oenaoinsd9iaidon@127.0.0.1:54322/payload"
+ });
+
+ try {
+ await client.connect();
+ console.log("Connected to database.");
+
+ const res = await client.query("SELECT * FROM form_submissions WHERE created_at >= '2026-04-12' ORDER BY created_at DESC;");
+
+ if (res.rows.length === 0) {
+ console.log("No submissions found for today.");
+ } else {
+ console.log(`Found ${res.rows.length} submissions for today:`);
+ res.rows.forEach(row => {
+ console.log(`- [${row.created_at}] ${row.name} (${row.email}): ${row.message.substring(0, 50)}...`);
+ });
+ }
+ } catch (err) {
+ console.error("Database query failed:", err.message);
+ } finally {
+ await client.end();
+ }
+}
+
+checkSubmissions();