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
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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) && (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
scripts/check-today-submissions.ts
Normal file
29
scripts/check-today-submissions.ts
Normal 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();
|
||||||
Reference in New Issue
Block a user