Compare commits

...

6 Commits

Author SHA1 Message Date
6207e04bf5 2.2.13
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 4m35s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 3m35s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / 🎭 Lighthouse (push) Successful in 2m57s
Nightly QA / 🔗 Links & Deps (push) Successful in 3m15s
Nightly QA / ♿ Accessibility (push) Successful in 4m57s
Nightly QA / 🔍 Static Analysis (push) Successful in 7m0s
Nightly QA / 🔔 Notify (push) Successful in 2s
2026-03-16 23:15:10 +01:00
8ffb5967d3 fix(seo): correct canonical tags and localized blog post hreflang 2026-03-16 23:15:04 +01:00
8ba1c7ea38 style(pdf): align AGB layout with technical datasheet hero and spacing
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 53s
Build & Deploy / 🏗️ Build (push) Successful in 2m13s
Build & Deploy / 🚀 Deploy (push) Successful in 15s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 4m16s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-16 07:53:29 +01:00
a546ffe69c fix(pdf): remove broken helvetica font registration causing 500 error
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Successful in 2m29s
Build & Deploy / 🚀 Deploy (push) Failing after 15s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-16 07:47:47 +01:00
15740db51e chore(ci): re-trigger pipeline after testing db schema hotfix
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 51s
Build & Deploy / 🏗️ Build (push) Successful in 2m20s
Build & Deploy / 🚀 Deploy (push) Successful in 15s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 4m7s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-16 07:38:54 +01:00
13ab755857 fix(docker): bypass internal registry for base images to prevent 404s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 51s
Build & Deploy / 🏗️ Build (push) Successful in 2m23s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m33s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / 🔗 Links & Deps (push) Successful in 2m57s
Nightly QA / 🎭 Lighthouse (push) Successful in 3m32s
Nightly QA / 🔍 Static Analysis (push) Failing after 4m49s
Nightly QA / ♿ Accessibility (push) Successful in 5m12s
Nightly QA / 🔔 Notify (push) Successful in 19s
2026-03-15 23:39:22 +01:00
7 changed files with 127 additions and 86 deletions

View File

@@ -1,5 +1,9 @@
# Stage 1: Builder
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.20 AS base
FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat curl
# Enable pnpm
RUN corepack enable && corepack prepare pnpm@10.3.0 --activate
WORKDIR /app
# Arguments for build-time configuration
@@ -52,12 +56,17 @@ ENV UV_THREADPOOL_SIZE=3
RUN pnpm build
# Stage 2: Runner
FROM registry.infra.mintel.me/mintel/runtime:v1.8.20 AS runner
FROM node:20-alpine AS runner
WORKDIR /app
# Install curl for health checks
RUN apk add --no-cache curl
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
USER root
RUN chown -R nextjs:nodejs /app
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs && \
chown -R nextjs:nodejs /app
USER nextjs
ENV HOSTNAME="0.0.0.0"

View File

@@ -6,6 +6,7 @@ import {
getAdjacentPosts,
getReadingTime,
extractLexicalHeadings,
getPostSlugs,
} from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
@@ -33,12 +34,21 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
if (!post) return {};
const slugs = await getPostSlugs(slug, locale);
const deSlug = slugs?.de || post.slug;
const enSlug = slugs?.en || post.slug;
const description = post.frontmatter.excerpt || '';
return {
title: post.frontmatter.title,
description: description,
alternates: {
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
languages: {
de: `${SITE_URL}/de/blog/${deSlug}`,
en: `${SITE_URL}/en/blog/${enSlug}`,
'x-default': `${SITE_URL}/en/blog/${enSlug}`,
},
},
openGraph: {
title: `${post.frontmatter.title} | KLZ Cables`,
@@ -134,13 +144,13 @@ export default async function BlogPost({ params }: BlogPostProps) {
<span>{getReadingTime(rawTextContent)} min read</span>
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
<>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
Draft Preview
</span>
</>
)}
<>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
Draft Preview
</span>
</>
)}
</div>
</div>
</div>
@@ -171,13 +181,13 @@ export default async function BlogPost({ params }: BlogPostProps) {
<span>{getReadingTime(rawTextContent)} min read</span>
{(new Date(post.frontmatter.date) > new Date() ||
post.frontmatter.public === false) && (
<>
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
<span className="px-2 py-0.5 border border-orange-500/50 text-orange-600 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
Draft Preview
</span>
</>
)}
<>
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
<span className="px-2 py-0.5 border border-orange-500/50 text-orange-600 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
Draft Preview
</span>
</>
)}
</div>
</div>
</header>

View File

@@ -35,13 +35,6 @@ export async function generateMetadata(props: {
},
metadataBase: new URL(baseUrl),
manifest: '/manifest.webmanifest',
alternates: {
canonical: `${baseUrl}/${locale}`,
languages: {
de: `${baseUrl}/de`,
en: `${baseUrl}/en`,
},
},
icons: {
icon: [
{ url: '/favicon.ico', sizes: 'any' },
@@ -132,11 +125,7 @@ export default async function Layout(props: {
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
return (
<html
lang={safeLocale}
className={`scroll-smooth overflow-x-hidden ${inter.variable}`}
data-scroll-behavior="smooth"
>
<html lang={safeLocale} className={`overflow-x-hidden ${inter.variable}`}>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />

View File

@@ -136,6 +136,60 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
}
}
export async function getPostSlugs(slug: string, locale: string): Promise<Record<string, string>> {
try {
const payload = await getPayload({ config: configPromise });
// First, find the post in the current locale to get its ID
let { docs } = await payload.find({
collection: 'posts',
where: {
slug: { equals: slug },
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
},
locale: locale as any,
draft: config.showDrafts,
limit: 1,
});
if (!docs || docs.length === 0) {
// Fallback: search across all locales
const { docs: crossLocaleDocs } = await payload.find({
collection: 'posts',
where: {
slug: { equals: slug },
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
},
locale: 'all',
draft: config.showDrafts,
limit: 1,
});
docs = crossLocaleDocs;
}
if (!docs || docs.length === 0) return {};
const postId = docs[0].id;
// Fetch the post with locale 'all' to get all localized fields
const { docs: allLocalesDocs } = await payload.find({
collection: 'posts',
where: {
id: { equals: postId },
},
locale: 'all',
draft: config.showDrafts,
limit: 1,
});
if (!allLocalesDocs || allLocalesDocs.length === 0) return {};
return (allLocalesDocs[0].slug as unknown as Record<string, string>) || {};
} catch (error) {
console.error(`[Payload] getPostSlugs failed for ${slug}:`, error);
return {};
}
}
export async function getAllPosts(locale: string): Promise<PostData[]> {
try {
const payload = await getPayload({ config: configPromise });

View File

@@ -1,22 +1,8 @@
import * as React from 'react';
import {
Document,
Page,
View,
Text,
Image,
StyleSheet,
Font,
} from '@react-pdf/renderer';
import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer';
// Register fonts (using system fonts for now, can be customized)
Font.register({
family: 'Helvetica',
fonts: [
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
],
});
// Standard fonts like Helvetica are built-in to PDF and don't require registration
// unless we want to use specific TTF files. Using built-in Helvetica for maximum stability.
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
const styles = StyleSheet.create({
@@ -302,10 +288,7 @@ const getLabels = (locale: 'en' | 'de') => {
return labels[locale];
};
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
product,
locale,
}) => {
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) => {
const labels = getLabels(locale);
return (
@@ -317,9 +300,7 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
<View>
<Text style={styles.logoText}>KLZ</Text>
</View>
<Text style={styles.docTitle}>
{labels.productDatasheet}
</Text>
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
</View>
<View style={styles.productRow}>
@@ -328,7 +309,8 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<Text key={index} style={styles.productMeta}>
{cat.name}{index < product.categories.length - 1 ? ' • ' : ''}
{cat.name}
{index < product.categories.length - 1 ? ' • ' : ''}
</Text>
))}
</View>
@@ -337,12 +319,8 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
</View>
<View style={styles.productImageCol}>
{product.featuredImage ? (
<Image
src={product.featuredImage}
style={styles.heroImage}
/>
<Image src={product.featuredImage} style={styles.heroImage} />
) : (
<Text style={styles.noImage}>{labels.noImage}</Text>
)}
</View>
@@ -356,7 +334,11 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
<Text style={styles.sectionTitle}>{labels.description}</Text>
<View style={styles.sectionAccent} />
<Text style={styles.description}>
{stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml)}
{stripHtml(
product.applicationHtml ||
product.shortDescriptionHtml ||
product.descriptionHtml,
)}
</Text>
</View>
)}
@@ -372,17 +354,14 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
key={index}
style={[
styles.specsTableRow,
index === product.attributes.length - 1 &&
styles.specsTableRowLast,
index === product.attributes.length - 1 && styles.specsTableRowLast,
]}
>
<View style={styles.specsTableLabelCell}>
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
</View>
<View style={styles.specsTableValueCell}>
<Text style={styles.specsTableValueText}>
{attr.options.join(', ')}
</Text>
<Text style={styles.specsTableValueText}>{attr.options.join(', ')}</Text>
</View>
</View>
))}

View File

@@ -1,14 +1,8 @@
import * as React from 'react';
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
// Register fonts (using system fonts for now, can be customized)
Font.register({
family: 'Helvetica',
fonts: [
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
],
});
// Standard fonts like Helvetica are built-in to PDF and don't require registration
// unless we want to use specific TTF files. Using built-in Helvetica for maximum stability.
// ─── Brand Tokens (matching datasheet) ──────────────────────────────────
const C = {
@@ -41,11 +35,12 @@ const styles = StyleSheet.create({
hero: {
backgroundColor: C.white,
paddingTop: 24,
paddingBottom: 0,
paddingBottom: 20,
paddingHorizontal: MARGIN,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
borderBottomWidth: 1,
borderBottomColor: C.gray200,
},
header: {
@@ -71,17 +66,15 @@ const styles = StyleSheet.create({
textTransform: 'uppercase',
},
// Content Area
content: {
paddingHorizontal: MARGIN,
productHero: {
marginTop: 0,
},
pageTitle: {
fontSize: 24,
fontWeight: 700,
color: C.navyDeep,
marginBottom: 8,
marginTop: 10,
marginBottom: 0,
textTransform: 'uppercase',
letterSpacing: -0.5,
},
@@ -90,10 +83,15 @@ const styles = StyleSheet.create({
width: 30,
height: 3,
backgroundColor: C.accent,
marginBottom: 20,
marginTop: 8,
borderRadius: 1.5,
},
// Content Area
content: {
paddingHorizontal: MARGIN,
},
// Lexical Elements
paragraph: {
fontSize: 10,
@@ -304,12 +302,14 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
</View>
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
</View>
<View style={styles.productHero}>
<Text style={styles.pageTitle}>{page.title}</Text>
<View style={styles.accentBar} />
</View>
</View>
<View style={styles.content}>
<Text style={styles.pageTitle}>{page.title}</Text>
<View style={styles.accentBar} />
<View>
{page.content?.root?.children?.map((node: any, i: number) =>
renderLexicalNode(node, i),

View File

@@ -139,7 +139,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.2.12",
"version": "2.2.13",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",