Compare commits

...

13 Commits

Author SHA1 Message Date
3dff19eca2 chore: update auto-generated types
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 2m9s
Build & Deploy / 🏗️ Build (push) Successful in 5m38s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 5m12s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-03-12 13:41:23 +01:00
07b01c622a fix(deps): update pnpm-lock.yaml to fix CI registry checksums
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-03-12 13:34:32 +01:00
50de18c09c fix(docker): use latest tags for base images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 2m7s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-03-12 13:29:45 +01:00
dbee0cd8bc fix(docker): use correct mmintel namespace for base images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 2m12s
Build & Deploy / 🏗️ Build (push) Failing after 14s
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 13:24:33 +01:00
f30f8ddd8d fix(docker): migrate base image to git.infra.mintel.me registry
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m11s
Build & Deploy / 🏗️ Build (push) Failing after 14s
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 13:20:48 +01:00
bb9fd65dbb fix(og): convert font buffers to ArrayBuffer for Satori compatibility
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m8s
Build & Deploy / 🏗️ Build (push) Failing after 14s
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 13:17:18 +01:00
036fba8b53 feat(payload): add redirect settings to pages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 2m6s
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 13:12:13 +01:00
3e8d5ad8b6 chore: backup script
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m13s
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 13:05:19 +01:00
70ad2e3041 fix(build): remove swcMinify and fix staleTimes/serverActions config object to pass Next.js validation
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Successful in 2m14s
Build & Deploy / 🏗️ Build (push) Failing after 14s
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 12:52:51 +01:00
5376b939d5 fix(cache): disable client router cache and fix terms routing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m8s
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 1s
2026-03-12 12:44:42 +01:00
6f80e72c1d style: align PDF Page component with KLZ brand Design System
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 1m15s
Build & Deploy / 🏗️ Build (push) Has been skipped
Nightly QA / 🔗 Links & Deps (push) Successful in 3m32s
Nightly QA / 🎭 Lighthouse (push) Successful in 5m5s
Nightly QA / ♿ Accessibility (push) Successful in 5m28s
Nightly QA / 🔍 Static Analysis (push) Failing after 6m0s
Nightly QA / 🔔 Notify (push) Successful in 2s
2026-03-05 22:57:16 +01:00
d9334f558d fix(cms): add missing featured_image_id column to products via migration 2026-03-05 22:04:14 +01:00
cb436d31d0 fix(cms): disable migrationDir in production to prevent runtime TS import crashes 2026-03-05 21:51:55 +01:00
15 changed files with 1992 additions and 1918 deletions

View File

@@ -1,5 +1,5 @@
# Stage 1: Builder # Stage 1: Builder
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.20 AS base FROM git.infra.mintel.me/mmintel/nextjs:latest AS base
WORKDIR /app WORKDIR /app
# Arguments for build-time configuration # Arguments for build-time configuration
@@ -52,7 +52,7 @@ ENV UV_THREADPOOL_SIZE=3
RUN pnpm build RUN pnpm build
# Stage 2: Runner # Stage 2: Runner
FROM registry.infra.mintel.me/mintel/runtime:v1.8.20 AS runner FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
WORKDIR /app WORKDIR /app
# 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)

View File

@@ -1,4 +1,4 @@
import { notFound, redirect } from 'next/navigation'; import { notFound, redirect, permanentRedirect } from 'next/navigation';
import { Container, Badge, Heading } from '@/components/ui'; import { Container, Badge, Heading } from '@/components/ui';
import { getTranslations, setRequestLocale } from 'next-intl/server'; import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next'; import { Metadata } from 'next';
@@ -62,6 +62,15 @@ export default async function StandardPage({ params }: PageProps) {
notFound(); notFound();
} }
// Handle explicit CMS redirects (e.g. /en/terms -> /de/terms)
if (pageData.redirectUrl) {
if (pageData.redirectPermanent) {
permanentRedirect(pageData.redirectUrl);
} else {
redirect(pageData.redirectUrl);
}
}
// Redirect if accessed via a different locale's slug // Redirect if accessed via a different locale's slug
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale); const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
const correctSlug = await mapFileSlugToTranslated(fileSlug, locale); const correctSlug = await mapFileSlugToTranslated(fileSlug, locale);

View File

@@ -11,10 +11,21 @@ export async function getOgFonts() {
try { try {
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`); console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
const boldFont = readFileSync(boldFontPath); const boldFontBuffer = readFileSync(boldFontPath);
const regularFont = readFileSync(regularFontPath); const regularFontBuffer = readFileSync(regularFontPath);
// Satori (Vercel OG) strictly requires an ArrayBuffer, not a Node Buffer view.
const boldFont = boldFontBuffer.buffer.slice(
boldFontBuffer.byteOffset,
boldFontBuffer.byteOffset + boldFontBuffer.byteLength,
);
const regularFont = regularFontBuffer.buffer.slice(
regularFontBuffer.byteOffset,
regularFontBuffer.byteOffset + regularFontBuffer.byteLength,
);
console.log( console.log(
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`, `[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
); );
return [ return [

View File

@@ -15,6 +15,8 @@ export interface PageFrontmatter {
export interface PageData { export interface PageData {
slug: string; slug: string;
redirectUrl?: string;
redirectPermanent?: boolean;
frontmatter: PageFrontmatter; frontmatter: PageFrontmatter;
content: any; // Lexical AST Document content: any; // Lexical AST Document
} }
@@ -96,6 +98,8 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageD
return { return {
slug: doc.slug, slug: doc.slug,
redirectUrl: doc.redirectUrl,
redirectPermanent: doc.redirectPermanent ?? true,
frontmatter: { frontmatter: {
title: doc.title, title: doc.title,
excerpt: doc.excerpt || '', excerpt: doc.excerpt || '',

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Document, Page, View, Text, Image, 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) // Register fonts (using system fonts for now, can be customized)
Font.register({ Font.register({
@@ -10,120 +10,120 @@ Font.register({
], ],
}); });
// ─── Brand Tokens ──────────────────────────────────────── // ─── Brand Tokens (matching datasheet) ──────────────────────────────────
const C = { const C = {
navy: '#001a4d', navy: '#001a4d',
navyDeep: '#000d26', navyDeep: '#000d26',
green: '#4da612', green: '#4da612',
greenLight: '#e8f5d8',
white: '#FFFFFF', white: '#FFFFFF',
offWhite: '#f8f9fa', offWhite: '#f8f9fa',
gray100: '#f3f4f6',
gray200: '#e5e7eb', gray200: '#e5e7eb',
gray300: '#d1d5db',
gray400: '#9ca3af', gray400: '#9ca3af',
gray600: '#4b5563', gray600: '#4b5563',
gray900: '#111827', gray900: '#111827',
}; };
const MARGIN = 56; const MARGIN = 56;
const HEADER_H = 52;
const FOOTER_H = 48;
const BODY_TOP = HEADER_H + 20;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
page: { page: {
color: C.gray900, color: C.gray900,
lineHeight: 1.5, lineHeight: 1.5,
backgroundColor: C.white, backgroundColor: C.white,
paddingTop: BODY_TOP, paddingTop: 0,
paddingBottom: FOOTER_H + 40, paddingBottom: 80,
paddingHorizontal: MARGIN,
fontFamily: 'Helvetica', fontFamily: 'Helvetica',
}, },
header: {
position: 'absolute', // Hero-style header
top: 0, hero: {
left: 0, backgroundColor: C.white,
right: 0, paddingTop: 24,
height: HEADER_H, paddingBottom: 0,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
paddingHorizontal: MARGIN, paddingHorizontal: MARGIN,
paddingBottom: 12, marginBottom: 20,
position: 'relative',
borderBottomWidth: 0, borderBottomWidth: 0,
}, },
logoText: {
fontSize: 16, header: {
fontWeight: 700,
color: C.navy,
},
docTitleLabel: {
fontSize: 7,
fontWeight: 700,
color: C.gray400,
letterSpacing: 1.2,
textTransform: 'uppercase',
},
footer: {
position: 'absolute',
bottom: 20,
left: MARGIN,
right: MARGIN,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
borderTopWidth: 0.5, marginBottom: 16,
borderTopColor: C.gray200,
paddingTop: 8,
}, },
footerText: {
fontSize: 7, logoText: {
color: C.gray400, fontSize: 22,
letterSpacing: 0.8, fontWeight: 700,
color: C.navyDeep,
letterSpacing: 2,
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
docTitle: {
fontSize: 8,
fontWeight: 700,
color: C.green,
letterSpacing: 2,
textTransform: 'uppercase',
},
// Content Area
content: {
paddingHorizontal: MARGIN,
},
pageTitle: { pageTitle: {
fontSize: 24, fontSize: 24,
fontWeight: 700, fontWeight: 700,
color: C.navyDeep, color: C.navyDeep,
marginBottom: 16, marginBottom: 8,
marginTop: 10,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: -0.5, letterSpacing: -0.5,
}, },
accentBar: { accentBar: {
width: 40, width: 30,
height: 3, height: 2,
backgroundColor: C.green, backgroundColor: C.green,
marginBottom: 24, marginBottom: 20,
borderRadius: 1,
}, },
// Lexical Elements // Lexical Elements
paragraph: { paragraph: {
fontSize: 10, fontSize: 10,
color: C.gray600, color: C.gray600,
lineHeight: 1.6, lineHeight: 1.7,
marginBottom: 12, marginBottom: 12,
}, },
heading1: { heading1: {
fontSize: 18, fontSize: 16,
fontWeight: 700,
color: C.navyDeep,
marginTop: 20,
marginBottom: 10,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
heading2: {
fontSize: 12,
fontWeight: 700, fontWeight: 700,
color: C.navyDeep, color: C.navyDeep,
marginTop: 16, marginTop: 16,
marginBottom: 8, marginBottom: 8,
}, },
heading2: {
fontSize: 14,
fontWeight: 700,
color: C.navyDeep,
marginTop: 14,
marginBottom: 6,
},
heading3: { heading3: {
fontSize: 12, fontSize: 10,
fontWeight: 700, fontWeight: 700,
color: C.navyDeep, color: C.navyDeep,
marginTop: 12, marginTop: 12,
marginBottom: 4, marginBottom: 6,
}, },
list: { list: {
marginBottom: 12, marginBottom: 12,
@@ -137,12 +137,13 @@ const styles = StyleSheet.create({
width: 12, width: 12,
fontSize: 10, fontSize: 10,
color: C.green, color: C.green,
fontWeight: 700,
}, },
listItemContent: { listItemContent: {
flex: 1, flex: 1,
fontSize: 10, fontSize: 10,
color: C.gray600, color: C.gray600,
lineHeight: 1.6, lineHeight: 1.7,
}, },
link: { link: {
color: C.green, color: C.green,
@@ -154,7 +155,37 @@ const styles = StyleSheet.create({
color: C.navyDeep, color: C.navyDeep,
}, },
textItalic: { textItalic: {
fontStyle: 'italic', // Not actually working without proper font set, but we will fallback fontStyle: 'italic',
},
// Footer — matches brochure style
footer: {
position: 'absolute',
bottom: 28,
left: MARGIN,
right: MARGIN,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 12,
borderTopWidth: 2,
borderTopColor: C.green,
},
footerText: {
fontSize: 7,
color: C.gray400,
fontWeight: 400,
textTransform: 'uppercase',
letterSpacing: 0.8,
},
footerBrand: {
fontSize: 9,
fontWeight: 700,
color: C.navyDeep,
textTransform: 'uppercase',
letterSpacing: 1.5,
}, },
}); });
@@ -232,11 +263,10 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
} }
case 'linebreak': { case 'linebreak': {
// React-PDF doesn't handle `<br/>`, but a newline char usually works inside `<Text>`.
return <Text key={idx}>{'\n'}</Text>; return <Text key={idx}>{'\n'}</Text>;
} }
// Ignore payload blocks recursively to avoid crashing, as pages should mainly use rich text // Ignore payload blocks recursively to avoid crashing
case 'block': case 'block':
return null; return null;
@@ -267,20 +297,30 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
return ( return (
<Document> <Document>
<Page size="A4" style={styles.page}> <Page size="A4" style={styles.page}>
<View style={styles.header} fixed> {/* Hero Header */}
<Text style={styles.logoText}>KLZ</Text> <View style={styles.hero} fixed>
<Text style={styles.docTitleLabel}>{locale === 'en' ? 'Document' : 'Dokument'}</Text> <View style={styles.header}>
<View>
<Text style={styles.logoText}>KLZ</Text>
</View>
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
</View>
</View> </View>
<Text style={styles.pageTitle}>{page.title}</Text> <View style={styles.content}>
<View style={styles.accentBar} /> <Text style={styles.pageTitle}>{page.title}</Text>
<View style={styles.accentBar} />
<View> <View>
{page.content?.root?.children?.map((node: any, i: number) => renderLexicalNode(node, i))} {page.content?.root?.children?.map((node: any, i: number) =>
renderLexicalNode(node, i),
)}
</View>
</View> </View>
{/* Minimal footer */}
<View style={styles.footer} fixed> <View style={styles.footer} fixed>
<Text style={styles.footerText}>KLZ CABLES</Text> <Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text style={styles.footerText}>{dateStr}</Text> <Text style={styles.footerText}>{dateStr}</Text>
</View> </View>
</Page> </Page>

View File

@@ -3,7 +3,7 @@
"pages": { "pages": {
"impressum": "impressum", "impressum": "impressum",
"datenschutz": "datenschutz", "datenschutz": "datenschutz",
"agbs": "agbs", "agbs": "terms",
"kontakt": "contact", "kontakt": "contact",
"team": "team", "team": "team",
"blog": "blog", "blog": "blog",
@@ -74,7 +74,7 @@
"privacyPolicy": "Datenschutz", "privacyPolicy": "Datenschutz",
"privacyPolicySlug": "datenschutz", "privacyPolicySlug": "datenschutz",
"terms": "AGB", "terms": "AGB",
"termsSlug": "agbs", "termsSlug": "terms",
"products": "Produkte", "products": "Produkte",
"lowVoltage": "Niederspannungskabel", "lowVoltage": "Niederspannungskabel",
"mediumVoltage": "Mittelspannungskabel", "mediumVoltage": "Mittelspannungskabel",

View File

@@ -3,7 +3,7 @@
"pages": { "pages": {
"legal-notice": "impressum", "legal-notice": "impressum",
"privacy-policy": "datenschutz", "privacy-policy": "datenschutz",
"terms": "agbs", "terms": "terms",
"contact": "contact", "contact": "contact",
"team": "team", "team": "team",
"blog": "blog", "blog": "blog",
@@ -396,4 +396,4 @@
"cta": "Back to Safety" "cta": "Back to Safety"
} }
} }
} }

View File

@@ -12,12 +12,18 @@ const nextConfig = {
maxInactiveAge: 60 * 1000, maxInactiveAge: 60 * 1000,
}, },
experimental: { experimental: {
staleTimes: {
dynamic: 0,
static: 30,
},
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'], optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
cpus: 3, cpus: 3,
workerThreads: false, workerThreads: false,
serverActions: {
allowedOrigins: ["*.klz-cables.com", "*.branch.klz-cables.com", "localhost:3000", "klz.localhost"],
},
}, },
reactStrictMode: false, reactStrictMode: false,
swcMinify: true,
productionBrowserSourceMaps: false, productionBrowserSourceMaps: false,
logging: { logging: {
fetches: { fetches: {
@@ -437,6 +443,10 @@ 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',

View File

@@ -87,7 +87,9 @@ export interface Config {
products: ProductsSelect<false> | ProductsSelect<true>; products: ProductsSelect<false> | ProductsSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>; pages: PagesSelect<false> | PagesSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>; 'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents':
| PayloadLockedDocumentsSelect<false>
| PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>; 'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
}; };
@@ -98,6 +100,9 @@ export interface Config {
globals: {}; globals: {};
globalsSelect: {}; globalsSelect: {};
locale: 'de' | 'en'; locale: 'de' | 'en';
widgets: {
collections: CollectionsWidget;
};
user: User; user: User;
jobs: { jobs: {
tasks: unknown; tasks: unknown;
@@ -328,6 +333,14 @@ export interface Page {
layout?: ('default' | 'fullBleed') | null; layout?: ('default' | 'fullBleed') | null;
excerpt?: string | null; excerpt?: string | null;
featuredImage?: (number | null) | Media; featuredImage?: (number | null) | Media;
/**
* If set, visiting this page will immediately redirect the user to this URL (e.g. /de/terms).
*/
redirectUrl?: string | null;
/**
* Check for a permanent (301) redirect. Uncheck for a temporary (302) redirect.
*/
redirectPermanent?: boolean | null;
content: { content: {
root: { root: {
type: string; type: string;
@@ -574,6 +587,8 @@ export interface PagesSelect<T extends boolean = true> {
layout?: T; layout?: T;
excerpt?: T; excerpt?: T;
featuredImage?: T; featuredImage?: T;
redirectUrl?: T;
redirectPermanent?: T;
content?: T; content?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
@@ -619,6 +634,16 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collections_widget".
*/
export interface CollectionsWidget {
data?: {
[k: string]: unknown;
};
width: 'full';
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "StatsBlock". * via the `definition` "StatsBlock".
@@ -957,7 +982,6 @@ export interface Auth {
[k: string]: unknown; [k: string]: unknown;
} }
declare module 'payload' { declare module 'payload' {
export interface GeneratedTypes extends Config {} export interface GeneratedTypes extends Config {}
} }

View File

@@ -71,6 +71,8 @@ export default buildConfig({
}, },
db: postgresAdapter({ db: postgresAdapter({
prodMigrations: migrations, prodMigrations: migrations,
migrationDir:
process.env.NODE_ENV === 'production' ? undefined : path.resolve(dirname, 'src/migrations'),
pool: { pool: {
connectionString: connectionString:
process.env.DATABASE_URI || process.env.DATABASE_URI ||

3541
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,6 @@ fi
DB_NAME="${PAYLOAD_DB_NAME:-payload}" DB_NAME="${PAYLOAD_DB_NAME:-payload}"
DB_USER="${PAYLOAD_DB_USER:-payload}" DB_USER="${PAYLOAD_DB_USER:-payload}"
DB_CONTAINER="klz-2026-klz-db-1"
BACKUP_DIR="./backups" BACKUP_DIR="./backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S") TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz" BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz"
@@ -21,20 +20,21 @@ BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz"
# Ensure backup directory exists # Ensure backup directory exists
mkdir -p "$BACKUP_DIR" mkdir -p "$BACKUP_DIR"
# Check if container is running # Check if database container is running
if ! docker ps --format '{{.Names}}' | grep -q "$DB_CONTAINER"; then if ! docker compose ps --services --filter "status=running" | grep -qx "klz-db"; then
echo " Database container '$DB_CONTAINER' is not running." echo "⚠️ Database container 'klz-db' is not running. Starting it..."
echo " Start it with: docker compose up -d klz-db" docker compose up -d klz-db
exit 1 echo "⏳ Waiting for database to be ready..."
sleep 3
fi fi
echo "📦 Backing up Payload database..." echo "📦 Backing up Payload database..."
echo " Container: $DB_CONTAINER" echo " Service: klz-db"
echo " Database: $DB_NAME" echo " Database: $DB_NAME"
echo " Output: $BACKUP_FILE" echo " Output: $BACKUP_FILE"
# Run pg_dump inside the container and compress # Run pg_dump inside the container and compress
docker exec "$DB_CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists | gzip > "$BACKUP_FILE" docker compose exec -T klz-db pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists | gzip > "$BACKUP_FILE"
# Show result # Show result
SIZE=$(du -h "$BACKUP_FILE" | cut -f1) SIZE=$(du -h "$BACKUP_FILE" | cut -f1)

View File

@@ -0,0 +1,52 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
export async function up({ db }: MigrateUpArgs): Promise<void> {
// Add featured_image_id to products and _products_v
await db.execute(sql`
ALTER TABLE "products" ADD COLUMN IF NOT EXISTS "featured_image_id" integer;
`);
await db.execute(sql`
ALTER TABLE "_products_v" ADD COLUMN IF NOT EXISTS "version_featured_image_id" integer;
`);
// Add foreign key constraints
await db.execute(sql`
DO $$ BEGIN
ALTER TABLE "products" ADD CONSTRAINT "products_featured_image_id_media_id_fk" FOREIGN KEY ("featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION WHEN duplicate_object THEN null; END $$;
`);
await db.execute(sql`
DO $$ BEGIN
ALTER TABLE "_products_v" ADD CONSTRAINT "_products_v_version_featured_image_id_media_id_fk" FOREIGN KEY ("version_featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION WHEN duplicate_object THEN null; END $$;
`);
// Add indexes
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "products_featured_image_idx" ON "products" USING btree ("featured_image_id");
`);
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "_products_v_version_version_featured_image_idx" ON "_products_v" USING btree ("version_featured_image_id");
`);
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "products" DROP CONSTRAINT IF EXISTS "products_featured_image_id_media_id_fk";
`);
await db.execute(sql`
ALTER TABLE "_products_v" DROP CONSTRAINT IF EXISTS "_products_v_version_featured_image_id_media_id_fk";
`);
await db.execute(sql`
DROP INDEX IF EXISTS "products_featured_image_idx";
`);
await db.execute(sql`
DROP INDEX IF EXISTS "_products_v_version_version_featured_image_idx";
`);
await db.execute(sql`
ALTER TABLE "products" DROP COLUMN IF EXISTS "featured_image_id";
`);
await db.execute(sql`
ALTER TABLE "_products_v" DROP COLUMN IF EXISTS "version_featured_image_id";
`);
}

View File

@@ -2,6 +2,7 @@ import * as migration_20260223_195005_products_collection from './20260223_19500
import * as migration_20260223_195151_remove_sku_unique from './20260223_195151_remove_sku_unique'; import * as migration_20260223_195151_remove_sku_unique from './20260223_195151_remove_sku_unique';
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';
export const migrations = [ export const migrations = [
{ {
@@ -24,4 +25,9 @@ export const migrations = [
down: migration_20260225_175000_native_localization.down, down: migration_20260225_175000_native_localization.down,
name: '20260225_175000_native_localization', name: '20260225_175000_native_localization',
}, },
{
up: migration_20260305_215000_products_featured_image.up,
down: migration_20260305_215000_products_featured_image.down,
name: '20260305_215000_products_featured_image',
},
]; ];

View File

@@ -72,6 +72,33 @@ export const Pages: CollectionConfig = {
position: 'sidebar', position: 'sidebar',
}, },
}, },
{
type: 'collapsible',
label: 'Redirect Settings',
admin: {
position: 'sidebar',
},
fields: [
{
name: 'redirectUrl',
type: 'text',
localized: true,
admin: {
description:
'If set, visiting this page will immediately redirect the user to this URL (e.g. /de/terms).',
},
},
{
name: 'redirectPermanent',
type: 'checkbox',
defaultValue: true,
admin: {
description:
'Check for a permanent (301) redirect. Uncheck for a temporary (302) redirect.',
},
},
],
},
{ {
name: 'content', name: 'content',
type: 'richText', type: 'richText',