Compare commits

...

9 Commits

Author SHA1 Message Date
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
1a68af0eec fix(pdf): align AGB page PDF layout with datasheet design tokens
Some checks failed
Nightly QA / 🔗 Links & Deps (push) Successful in 2m21s
Nightly QA / 🎭 Lighthouse (push) Successful in 4m15s
Nightly QA / 🔍 Static Analysis (push) Failing after 4m45s
Nightly QA / ♿ Accessibility (push) Successful in 5m16s
Nightly QA / 🔔 Notify (push) Successful in 3s
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 57s
Build & Deploy / 🏗️ Build (push) Failing after 18s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-13 22:24:33 +01:00
275784745d feat(db): add migration for pages redirect fields
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 54s
Build & Deploy / 🏗️ Build (push) Failing after 15s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-13 22:19:29 +01:00
4aef49cf2c fix: remove agbs rewrite rules that conflict with slug-mapping (redirect loop)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 56s
Build & Deploy / 🏗️ Build (push) Failing after 16s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-13 15:35:01 +01:00
8ad3abb6f3 fix(docker): restore valid v1.8.20 base image tag
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 1m0s
Build & Deploy / 🏗️ Build (push) Failing after 15s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / 🔗 Links & Deps (push) Successful in 2m32s
Nightly QA / 🎭 Lighthouse (push) Successful in 3m3s
Nightly QA / 🔍 Static Analysis (push) Failing after 4m43s
Nightly QA / ♿ Accessibility (push) Successful in 5m20s
Nightly QA / 🔔 Notify (push) Successful in 14s
2026-03-12 19:12:56 +01:00
1d75b60236 fix(docker): use correctly versioned mintel v2 base images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 53s
Build & Deploy / 🏗️ Build (push) Failing after 15s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-03-12 14:39:05 +01:00
7 changed files with 97 additions and 94 deletions

View File

@@ -1,5 +1,9 @@
# Stage 1: Builder # Stage 1: Builder
FROM git.infra.mintel.me/mmintel/nextjs:latest 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 WORKDIR /app
# Arguments for build-time configuration # Arguments for build-time configuration
@@ -52,12 +56,17 @@ ENV UV_THREADPOOL_SIZE=3
RUN pnpm build RUN pnpm build
# Stage 2: Runner # Stage 2: Runner
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner FROM node:20-alpine AS runner
WORKDIR /app 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) # Create nextjs user and group (standardized in runtime image but ensuring local ownership)
USER root RUN addgroup --system --gid 1001 nodejs && \
RUN chown -R nextjs:nodejs /app adduser --system --uid 1001 nextjs && \
chown -R nextjs:nodejs /app
USER nextjs USER nextjs
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"

View File

@@ -132,11 +132,7 @@ export default async function Layout(props: {
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true'; const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
return ( return (
<html <html lang={safeLocale} className={`overflow-x-hidden ${inter.variable}`}>
lang={safeLocale}
className={`scroll-smooth overflow-x-hidden ${inter.variable}`}
data-scroll-behavior="smooth"
>
<head> <head>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />

View File

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

View File

@@ -1,21 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer'; import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
// Register fonts (using system fonts for now, can be customized) // Standard fonts like Helvetica are built-in to PDF and don't require registration
Font.register({ // unless we want to use specific TTF files. Using built-in Helvetica for maximum stability.
family: 'Helvetica',
fonts: [
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
],
});
// ─── Brand Tokens (matching datasheet) ────────────────────────────────── // ─── Brand Tokens (matching datasheet) ──────────────────────────────────
const C = { const C = {
navy: '#001a4d', navy: '#001a4d',
navyDeep: '#000d26', navyDeep: '#000d26',
green: '#4da612', accent: '#82ed20',
greenLight: '#e8f5d8',
white: '#FFFFFF', white: '#FFFFFF',
offWhite: '#f8f9fa', offWhite: '#f8f9fa',
gray100: '#f3f4f6', gray100: '#f3f4f6',
@@ -26,7 +19,7 @@ const C = {
gray900: '#111827', gray900: '#111827',
}; };
const MARGIN = 56; const MARGIN = 72;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
page: { page: {
@@ -34,7 +27,7 @@ const styles = StyleSheet.create({
lineHeight: 1.5, lineHeight: 1.5,
backgroundColor: C.white, backgroundColor: C.white,
paddingTop: 0, paddingTop: 0,
paddingBottom: 80, paddingBottom: 100,
fontFamily: 'Helvetica', fontFamily: 'Helvetica',
}, },
@@ -42,11 +35,12 @@ const styles = StyleSheet.create({
hero: { hero: {
backgroundColor: C.white, backgroundColor: C.white,
paddingTop: 24, paddingTop: 24,
paddingBottom: 0, paddingBottom: 20,
paddingHorizontal: MARGIN, paddingHorizontal: MARGIN,
marginBottom: 20, marginBottom: 20,
position: 'relative', position: 'relative',
borderBottomWidth: 0, borderBottomWidth: 1,
borderBottomColor: C.gray200,
}, },
header: { header: {
@@ -57,42 +51,45 @@ const styles = StyleSheet.create({
}, },
logoText: { logoText: {
fontSize: 22, fontSize: 24,
fontWeight: 700, fontWeight: 700,
color: C.navyDeep, color: C.navyDeep,
letterSpacing: 2, letterSpacing: 1,
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
docTitle: { docTitle: {
fontSize: 8, fontSize: 10,
fontWeight: 700, fontWeight: 700,
color: C.green, color: C.navy,
letterSpacing: 2, letterSpacing: 2,
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
// Content Area productHero: {
content: { marginTop: 0,
paddingHorizontal: MARGIN,
}, },
pageTitle: { pageTitle: {
fontSize: 24, fontSize: 24,
fontWeight: 700, fontWeight: 700,
color: C.navyDeep, color: C.navyDeep,
marginBottom: 8, marginBottom: 0,
marginTop: 10,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: -0.5, letterSpacing: -0.5,
}, },
accentBar: { accentBar: {
width: 30, width: 30,
height: 2, height: 3,
backgroundColor: C.green, backgroundColor: C.accent,
marginBottom: 20, marginTop: 8,
borderRadius: 1, borderRadius: 1.5,
},
// Content Area
content: {
paddingHorizontal: MARGIN,
}, },
// Lexical Elements // Lexical Elements
@@ -136,7 +133,7 @@ const styles = StyleSheet.create({
listItemBullet: { listItemBullet: {
width: 12, width: 12,
fontSize: 10, fontSize: 10,
color: C.green, color: C.accent,
fontWeight: 700, fontWeight: 700,
}, },
listItemContent: { listItemContent: {
@@ -146,7 +143,7 @@ const styles = StyleSheet.create({
lineHeight: 1.7, lineHeight: 1.7,
}, },
link: { link: {
color: C.green, color: C.accent,
textDecoration: 'none', textDecoration: 'none',
}, },
textBold: { textBold: {
@@ -161,31 +158,31 @@ const styles = StyleSheet.create({
// Footer — matches brochure style // Footer — matches brochure style
footer: { footer: {
position: 'absolute', position: 'absolute',
bottom: 28, bottom: 40,
left: MARGIN, left: MARGIN,
right: MARGIN, right: MARGIN,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
paddingTop: 12, paddingTop: 24,
borderTopWidth: 2, borderTopWidth: 1,
borderTopColor: C.green, borderTopColor: C.gray200,
}, },
footerText: { footerText: {
fontSize: 7, fontSize: 8,
color: C.gray400, color: C.gray400,
fontWeight: 400, fontWeight: 500,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.8, letterSpacing: 1,
}, },
footerBrand: { footerBrand: {
fontSize: 9, fontSize: 10,
fontWeight: 700, fontWeight: 700,
color: C.navyDeep, color: C.navyDeep,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 1.5, letterSpacing: 1,
}, },
}); });
@@ -305,12 +302,14 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
</View> </View>
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text> <Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
</View> </View>
<View style={styles.productHero}>
<Text style={styles.pageTitle}>{page.title}</Text>
<View style={styles.accentBar} />
</View>
</View> </View>
<View style={styles.content}> <View style={styles.content}>
<Text style={styles.pageTitle}>{page.title}</Text>
<View style={styles.accentBar} />
<View> <View>
{page.content?.root?.children?.map((node: any, i: number) => {page.content?.root?.children?.map((node: any, i: number) =>
renderLexicalNode(node, i), renderLexicalNode(node, i),

View File

@@ -443,10 +443,6 @@ const nextConfig = {
source: '/de/kontakt', source: '/de/kontakt',
destination: '/de/contact', destination: '/de/contact',
}, },
{
source: '/de/agbs',
destination: '/de/terms',
},
// Safety rewrites for English locale using German slugs (legacy or content errors) // Safety rewrites for English locale using German slugs (legacy or content errors)
{ {
source: '/en/produkte', source: '/en/produkte',
@@ -468,10 +464,6 @@ const nextConfig = {
source: '/en/datenschutz', source: '/en/datenschutz',
destination: '/en/privacy-policy', destination: '/en/privacy-policy',
}, },
{
source: '/en/agbs',
destination: '/en/terms',
},
], ],
afterFiles: [], afterFiles: [],
fallback: [], fallback: [],

View File

@@ -0,0 +1,22 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
export async function up({ db }: MigrateUpArgs): Promise<void> {
// redirect_permanent is a non-localized checkbox → stored on the main pages table
await db.execute(sql`
ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "redirect_permanent" boolean DEFAULT true;
`);
// redirect_url is a localized text field → stored on the pages_locales table
await db.execute(sql`
ALTER TABLE "pages_locales" ADD COLUMN IF NOT EXISTS "redirect_url" varchar;
`);
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "pages" DROP COLUMN IF EXISTS "redirect_permanent";
`);
await db.execute(sql`
ALTER TABLE "pages_locales" DROP COLUMN IF EXISTS "redirect_url";
`);
}

View File

@@ -3,6 +3,7 @@ import * as migration_20260223_195151_remove_sku_unique from './20260223_195151_
import * as migration_20260225_003500_add_pages_collection from './20260225_003500_add_pages_collection'; import * as migration_20260225_003500_add_pages_collection from './20260225_003500_add_pages_collection';
import * as migration_20260225_175000_native_localization from './20260225_175000_native_localization'; import * as migration_20260225_175000_native_localization from './20260225_175000_native_localization';
import * as migration_20260305_215000_products_featured_image from './20260305_215000_products_featured_image'; import * as migration_20260305_215000_products_featured_image from './20260305_215000_products_featured_image';
import * as migration_20260312_120000_pages_redirect_fields from './20260312_120000_pages_redirect_fields';
export const migrations = [ export const migrations = [
{ {
@@ -30,4 +31,9 @@ export const migrations = [
down: migration_20260305_215000_products_featured_image.down, down: migration_20260305_215000_products_featured_image.down,
name: '20260305_215000_products_featured_image', name: '20260305_215000_products_featured_image',
}, },
{
up: migration_20260312_120000_pages_redirect_fields.up,
down: migration_20260312_120000_pages_redirect_fields.down,
name: '20260312_120000_pages_redirect_fields',
},
]; ];