Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a68af0eec | |||
| 275784745d | |||
| 4aef49cf2c | |||
| 8ad3abb6f3 | |||
| 1d75b60236 | |||
| 3dff19eca2 | |||
| 07b01c622a | |||
| 50de18c09c | |||
| dbee0cd8bc | |||
| f30f8ddd8d | |||
| bb9fd65dbb | |||
| 036fba8b53 | |||
| 3e8d5ad8b6 | |||
| 70ad2e3041 | |||
| 5376b939d5 |
@@ -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 { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
@@ -62,6 +62,15 @@ export default async function StandardPage({ params }: PageProps) {
|
||||
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
|
||||
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
|
||||
const correctSlug = await mapFileSlugToTranslated(fileSlug, locale);
|
||||
|
||||
@@ -11,10 +11,21 @@ export async function getOgFonts() {
|
||||
|
||||
try {
|
||||
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
||||
const boldFont = readFileSync(boldFontPath);
|
||||
const regularFont = readFileSync(regularFontPath);
|
||||
const boldFontBuffer = readFileSync(boldFontPath);
|
||||
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(
|
||||
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`,
|
||||
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface PageFrontmatter {
|
||||
|
||||
export interface PageData {
|
||||
slug: string;
|
||||
redirectUrl?: string;
|
||||
redirectPermanent?: boolean;
|
||||
frontmatter: PageFrontmatter;
|
||||
content: any; // Lexical AST Document
|
||||
}
|
||||
@@ -96,6 +98,8 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageD
|
||||
|
||||
return {
|
||||
slug: doc.slug,
|
||||
redirectUrl: doc.redirectUrl,
|
||||
redirectPermanent: doc.redirectPermanent ?? true,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
|
||||
@@ -14,8 +14,7 @@ Font.register({
|
||||
const C = {
|
||||
navy: '#001a4d',
|
||||
navyDeep: '#000d26',
|
||||
green: '#4da612',
|
||||
greenLight: '#e8f5d8',
|
||||
accent: '#82ed20',
|
||||
white: '#FFFFFF',
|
||||
offWhite: '#f8f9fa',
|
||||
gray100: '#f3f4f6',
|
||||
@@ -26,7 +25,7 @@ const C = {
|
||||
gray900: '#111827',
|
||||
};
|
||||
|
||||
const MARGIN = 56;
|
||||
const MARGIN = 72;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
@@ -34,7 +33,7 @@ const styles = StyleSheet.create({
|
||||
lineHeight: 1.5,
|
||||
backgroundColor: C.white,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 80,
|
||||
paddingBottom: 100,
|
||||
fontFamily: 'Helvetica',
|
||||
},
|
||||
|
||||
@@ -57,17 +56,17 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
logoText: {
|
||||
fontSize: 22,
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
letterSpacing: 2,
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
||||
docTitle: {
|
||||
fontSize: 8,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
color: C.green,
|
||||
color: C.navy,
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
@@ -89,10 +88,10 @@ const styles = StyleSheet.create({
|
||||
|
||||
accentBar: {
|
||||
width: 30,
|
||||
height: 2,
|
||||
backgroundColor: C.green,
|
||||
height: 3,
|
||||
backgroundColor: C.accent,
|
||||
marginBottom: 20,
|
||||
borderRadius: 1,
|
||||
borderRadius: 1.5,
|
||||
},
|
||||
|
||||
// Lexical Elements
|
||||
@@ -136,7 +135,7 @@ const styles = StyleSheet.create({
|
||||
listItemBullet: {
|
||||
width: 12,
|
||||
fontSize: 10,
|
||||
color: C.green,
|
||||
color: C.accent,
|
||||
fontWeight: 700,
|
||||
},
|
||||
listItemContent: {
|
||||
@@ -146,7 +145,7 @@ const styles = StyleSheet.create({
|
||||
lineHeight: 1.7,
|
||||
},
|
||||
link: {
|
||||
color: C.green,
|
||||
color: C.accent,
|
||||
textDecoration: 'none',
|
||||
},
|
||||
textBold: {
|
||||
@@ -161,31 +160,31 @@ const styles = StyleSheet.create({
|
||||
// Footer — matches brochure style
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 28,
|
||||
bottom: 40,
|
||||
left: MARGIN,
|
||||
right: MARGIN,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 2,
|
||||
borderTopColor: C.green,
|
||||
paddingTop: 24,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: C.gray200,
|
||||
},
|
||||
|
||||
footerText: {
|
||||
fontSize: 7,
|
||||
fontSize: 8,
|
||||
color: C.gray400,
|
||||
fontWeight: 400,
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
|
||||
footerBrand: {
|
||||
fontSize: 9,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
color: C.navyDeep,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1.5,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"pages": {
|
||||
"impressum": "impressum",
|
||||
"datenschutz": "datenschutz",
|
||||
"agbs": "agbs",
|
||||
"agbs": "terms",
|
||||
"kontakt": "contact",
|
||||
"team": "team",
|
||||
"blog": "blog",
|
||||
@@ -74,7 +74,7 @@
|
||||
"privacyPolicy": "Datenschutz",
|
||||
"privacyPolicySlug": "datenschutz",
|
||||
"terms": "AGB",
|
||||
"termsSlug": "agbs",
|
||||
"termsSlug": "terms",
|
||||
"products": "Produkte",
|
||||
"lowVoltage": "Niederspannungskabel",
|
||||
"mediumVoltage": "Mittelspannungskabel",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"pages": {
|
||||
"legal-notice": "impressum",
|
||||
"privacy-policy": "datenschutz",
|
||||
"terms": "agbs",
|
||||
"terms": "terms",
|
||||
"contact": "contact",
|
||||
"team": "team",
|
||||
"blog": "blog",
|
||||
@@ -396,4 +396,4 @@
|
||||
"cta": "Back to Safety"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,18 @@ const nextConfig = {
|
||||
maxInactiveAge: 60 * 1000,
|
||||
},
|
||||
experimental: {
|
||||
staleTimes: {
|
||||
dynamic: 0,
|
||||
static: 30,
|
||||
},
|
||||
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
||||
cpus: 3,
|
||||
workerThreads: false,
|
||||
serverActions: {
|
||||
allowedOrigins: ["*.klz-cables.com", "*.branch.klz-cables.com", "localhost:3000", "klz.localhost"],
|
||||
},
|
||||
},
|
||||
reactStrictMode: false,
|
||||
swcMinify: true,
|
||||
productionBrowserSourceMaps: false,
|
||||
logging: {
|
||||
fetches: {
|
||||
@@ -458,10 +464,6 @@ const nextConfig = {
|
||||
source: '/en/datenschutz',
|
||||
destination: '/en/privacy-policy',
|
||||
},
|
||||
{
|
||||
source: '/en/agbs',
|
||||
destination: '/en/terms',
|
||||
},
|
||||
],
|
||||
afterFiles: [],
|
||||
fallback: [],
|
||||
|
||||
@@ -87,7 +87,9 @@ export interface Config {
|
||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<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-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
@@ -98,6 +100,9 @@ export interface Config {
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: 'de' | 'en';
|
||||
widgets: {
|
||||
collections: CollectionsWidget;
|
||||
};
|
||||
user: User;
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
@@ -328,6 +333,14 @@ export interface Page {
|
||||
layout?: ('default' | 'fullBleed') | null;
|
||||
excerpt?: string | null;
|
||||
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: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -574,6 +587,8 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
layout?: T;
|
||||
excerpt?: T;
|
||||
featuredImage?: T;
|
||||
redirectUrl?: T;
|
||||
redirectPermanent?: T;
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
@@ -619,6 +634,16 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
updatedAt?: 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
|
||||
* via the `definition` "StatsBlock".
|
||||
@@ -957,7 +982,6 @@ export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
}
|
||||
|
||||
3541
pnpm-lock.yaml
generated
3541
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,6 @@ fi
|
||||
|
||||
DB_NAME="${PAYLOAD_DB_NAME:-payload}"
|
||||
DB_USER="${PAYLOAD_DB_USER:-payload}"
|
||||
DB_CONTAINER="klz-2026-klz-db-1"
|
||||
BACKUP_DIR="./backups"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz"
|
||||
@@ -21,20 +20,21 @@ BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz"
|
||||
# Ensure backup directory exists
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Check if container is running
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "$DB_CONTAINER"; then
|
||||
echo "❌ Database container '$DB_CONTAINER' is not running."
|
||||
echo " Start it with: docker compose up -d klz-db"
|
||||
exit 1
|
||||
# Check if database container is running
|
||||
if ! docker compose ps --services --filter "status=running" | grep -qx "klz-db"; then
|
||||
echo "⚠️ Database container 'klz-db' is not running. Starting it..."
|
||||
docker compose up -d klz-db
|
||||
echo "⏳ Waiting for database to be ready..."
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
echo "📦 Backing up Payload database..."
|
||||
echo " Container: $DB_CONTAINER"
|
||||
echo " Service: klz-db"
|
||||
echo " Database: $DB_NAME"
|
||||
echo " Output: $BACKUP_FILE"
|
||||
|
||||
# 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
|
||||
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
|
||||
22
src/migrations/20260312_120000_pages_redirect_fields.ts
Normal file
22
src/migrations/20260312_120000_pages_redirect_fields.ts
Normal 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";
|
||||
`);
|
||||
}
|
||||
@@ -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_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_20260312_120000_pages_redirect_fields from './20260312_120000_pages_redirect_fields';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -30,4 +31,9 @@ export const migrations = [
|
||||
down: migration_20260305_215000_products_featured_image.down,
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -72,6 +72,33 @@ export const Pages: CollectionConfig = {
|
||||
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',
|
||||
type: 'richText',
|
||||
|
||||
Reference in New Issue
Block a user