chore: stabilize pipeline and harden infrastructure
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Failing after 1m43s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s

- Harden E2E smoke tests with increased timeouts and German keyword detection
- Implement defensive body parsing in analytics relay to prevent crashes
- Refine middleware to bypass i18n logic for Server Actions
- Fix date formatting consistency and JsonLd validation
This commit is contained in:
2026-04-12 22:38:59 +02:00
parent 0091b56dd1
commit 64ec24f8b2
9 changed files with 167 additions and 54 deletions

View File

@@ -134,11 +134,14 @@ export default async function BlogPost({ params }: BlogPostProps) {
</Heading> </Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium"> <div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium">
<time dateTime={post.frontmatter.date} suppressHydrationWarning> <time dateTime={post.frontmatter.date} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', { {new Date(post.frontmatter.date).toLocaleDateString(
year: 'numeric', locale === 'en' ? 'en-US' : 'de-DE',
month: 'long', {
day: 'numeric', year: 'numeric',
})} month: 'long',
day: 'numeric',
},
)}
</time> </time>
<span className="w-1 h-1 bg-white/30 rounded-full" /> <span className="w-1 h-1 bg-white/30 rounded-full" />
<span>{getReadingTime(rawTextContent)} min read</span> <span>{getReadingTime(rawTextContent)} min read</span>
@@ -171,11 +174,14 @@ export default async function BlogPost({ params }: BlogPostProps) {
</Heading> </Heading>
<div className="flex items-center gap-6 text-text-primary/80 font-medium"> <div className="flex items-center gap-6 text-text-primary/80 font-medium">
<time dateTime={post.frontmatter.date} suppressHydrationWarning> <time dateTime={post.frontmatter.date} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', { {new Date(post.frontmatter.date).toLocaleDateString(
year: 'numeric', locale === 'en' ? 'en-US' : 'de-DE',
month: 'long', {
day: 'numeric', year: 'numeric',
})} month: 'long',
day: 'numeric',
},
)}
</time> </time>
<span className="w-1 h-1 bg-neutral-400 rounded-full" /> <span className="w-1 h-1 bg-neutral-400 rounded-full" />
<span>{getReadingTime(rawTextContent)} min read</span> <span>{getReadingTime(rawTextContent)} min read</span>

View File

@@ -198,11 +198,14 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
<div className="flex items-center gap-3 text-xs md:text-sm font-bold text-white/80 mb-3 tracking-widest uppercase"> <div className="flex items-center gap-3 text-xs md:text-sm font-bold text-white/80 mb-3 tracking-widest uppercase">
<time dateTime={post.frontmatter.date} suppressHydrationWarning> <time dateTime={post.frontmatter.date} suppressHydrationWarning>
{new Date(post.frontmatter.date).toLocaleDateString(locale || 'de', { {new Date(post.frontmatter.date).toLocaleDateString(
year: 'numeric', locale === 'en' ? 'en-US' : 'de-DE',
month: 'long', {
day: 'numeric', year: 'numeric',
})} month: 'long',
day: 'numeric',
},
)}
</time> </time>
</div> </div>

View File

@@ -24,14 +24,34 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ status: 'ignored_in_dev' }, { status: 200 }); 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; 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 // Inject the secret websiteId from server config
const websiteId = config.analytics.umami.websiteId; const websiteId = config.analytics.umami.websiteId;
if (!websiteId) { if (!websiteId) {
logger.warn('Umami tracking received but no Website ID configured on server'); 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 // Prepare the enhanced payload with the secret ID

View File

@@ -7,16 +7,14 @@ interface JsonLdProps {
export default function JsonLd({ id, data }: JsonLdProps) { export default function JsonLd({ id, data }: JsonLdProps) {
// If data is provided, use it. Otherwise, use the default Organization + WebSite schema. // If data is provided, use it. Otherwise, use the default Organization + WebSite schema.
const schemaData = data || [ const rawData = data || [
{ {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'Organization', '@type': 'Organization',
name: 'KLZ Cables', name: 'KLZ Cables',
url: 'https://klz-cables.com', url: 'https://klz-cables.com',
logo: 'https://klz-cables.com/logo-blue.svg', logo: 'https://klz-cables.com/logo-blue.svg',
sameAs: [ sameAs: ['https://www.linkedin.com/company/klz-cables'],
'https://www.linkedin.com/company/klz-cables',
],
description: 'Premium Cable Solutions for Renewable Energy and Infrastructure.', description: 'Premium Cable Solutions for Renewable Energy and Infrastructure.',
address: { address: {
'@type': 'PostalAddress', '@type': 'PostalAddress',
@@ -36,15 +34,32 @@ export default function JsonLd({ id, data }: JsonLdProps) {
}, },
'query-input': 'required name=search_term_string', '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<Thing> => {
// 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 ( return (
<script <script
id={id} id={id}
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: JSON.stringify(schemaData), __html: JSON.stringify(schemaData.length === 1 ? schemaData[0] : schemaData),
}} }}
/> />
); );

View File

@@ -74,11 +74,14 @@ export default async function RecentPosts({ locale, data }: RecentPostsProps) {
suppressHydrationWarning 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" 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', { {new Date(post.frontmatter.date).toLocaleDateString(
year: 'numeric', locale === 'en' ? 'en-US' : 'de-DE',
month: 'short', {
day: 'numeric', year: 'numeric',
})} month: 'short',
day: 'numeric',
},
)}
</time> </time>
{(new Date(post.frontmatter.date) > new Date() || {(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && ( post.frontmatter.public === false) && (

View File

@@ -66,8 +66,8 @@ function createConfig() {
port: env.MAIL_PORT, port: env.MAIL_PORT,
user: env.MAIL_USERNAME, user: env.MAIL_USERNAME,
pass: env.MAIL_PASSWORD, pass: env.MAIL_PASSWORD,
from: env.MAIL_FROM, from: env.MAIL_FROM || 'KLZ Cables <postmaster@mg.mintel.me>',
recipients: env.MAIL_RECIPIENTS, recipients: env.MAIL_RECIPIENTS || 'info@klz-cables.com',
}, },
infraCMS: { infraCMS: {
url: env.INFRA_DIRECTUS_URL, url: env.INFRA_DIRECTUS_URL,

View File

@@ -14,8 +14,12 @@ export default async function middleware(request: NextRequest) {
const { method, url, headers } = request; const { method, url, headers } = request;
const { pathname } = request.nextUrl; 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 ( if (
isServerAction ||
pathname.startsWith('/admin') || pathname.startsWith('/admin') ||
pathname.startsWith('/api') || pathname.startsWith('/api') ||
pathname.startsWith('/stats') || 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 // 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) // This resolves the "failed to pipe response" error (160k occurrences in GlitchTip)
if (['GET', 'HEAD'].includes(request.method)) { 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, { effectiveRequest = new NextRequest(urlObj, {
headers: request.headers, headers: clonedHeaders,
method: request.method, method: request.method,
// No body for GET/HEAD
}); });
if (process.env.NODE_ENV !== 'production' || !process.env.CI) { if (process.env.NODE_ENV !== 'production' || !process.env.CI) {

View File

@@ -103,8 +103,10 @@ async function main() {
console.log(`\n🧪 Testing Contact Form on: ${contactUrl}`); console.log(`\n🧪 Testing Contact Form on: ${contactUrl}`);
await page.goto(contactUrl, { waitUntil: 'networkidle0', timeout: 30000 }); await page.goto(contactUrl, { waitUntil: 'networkidle0', timeout: 30000 });
// Ensure React has hydrated completely // Ensure React has hydrated completely - CI runners can be slow
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(() => {});
// Ensure form is visible and interactive // Ensure form is visible and interactive
try { try {
@@ -129,18 +131,32 @@ async function main() {
// Give state a moment to settle // Give state a moment to settle
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500))); await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500)));
console.log(` Submitting Contact Form...`); console.log(` ⏳ Waiting for alert selector (30s)...`);
try {
// Explicitly click submit and wait for navigation/state-change // Explicitly click submit and wait for navigation/state-change
await Promise.all([ await Promise.all([
page.waitForSelector('[role="alert"]', { timeout: 15000 }), page.waitForSelector('[role="alert"]', { timeout: 30000 }),
page.click('button[type="submit"]'), 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); 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}`); throw new Error(`Form submitted but showed error: ${alertText}`);
} }
@@ -156,7 +172,9 @@ async function main() {
await page.goto(productUrl, { waitUntil: 'networkidle0', timeout: 30000 }); await page.goto(productUrl, { waitUntil: 'networkidle0', timeout: 30000 });
// Ensure React has hydrated completely // 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 // The product form uses dynamic IDs, so we select by input type in the specific form context
try { try {
@@ -179,18 +197,31 @@ async function main() {
// Give state a moment to settle // Give state a moment to settle
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500))); await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500)));
console.log(` Submitting Product Quote Form...`); console.log(` ⏳ Waiting for alert selector (30s)...`);
try {
// Submit and wait for success state // Submit and wait for success state
await Promise.all([ await Promise.all([
page.waitForSelector('[role="alert"]', { timeout: 15000 }), page.waitForSelector('[role="alert"]', { timeout: 30000 }),
page.click('form button[type="submit"]'), 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); 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}`); throw new Error(`Form submitted but showed error: ${alertText}`);
} }

View File

@@ -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();