diff --git a/.data/leads-backup.jsonl b/.data/leads-backup.jsonl
new file mode 100644
index 00000000..15997a5d
--- /dev/null
+++ b/.data/leads-backup.jsonl
@@ -0,0 +1 @@
+{"timestamp":"2026-06-09T17:18:52.824Z","name":"John Doe","email":"john@example.com","productName":null,"message":"Hello, this is a test message."}\n{"timestamp":"2026-06-09T17:18:52.855Z","name":"Tester","email":"testing@mintel.me","productName":null,"message":"Test"}\n{"timestamp":"2026-06-09T17:20:50.586Z","name":"John Doe","email":"john@example.com","productName":null,"message":"Hello, this is a test message."}\n{"timestamp":"2026-06-09T17:20:50.613Z","name":"Tester","email":"testing@mintel.me","productName":null,"message":"Test"}\n
\ No newline at end of file
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 99675ad1..0e740ceb 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -357,7 +357,7 @@ jobs:
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
-
+ ssh root@alpha.mintel.me "docker network prune -f"
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
- name: 🧹 Post-Deploy Cleanup (Runner)
diff --git a/components/Header.tsx b/components/Header.tsx
index b7213e4d..61d145d1 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -450,6 +450,13 @@ export default function Header() {
variant="accent"
size="lg"
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
+ onClick={() => {
+ setIsMobileMenuOpen(false);
+ trackEvent(AnalyticsEvents.BUTTON_CLICK, {
+ label: t('contact'),
+ location: 'mobile_menu_cta',
+ });
+ }}
>
{t('contact')}
diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx
index 6a853078..d95a573a 100644
--- a/components/ui/Button.tsx
+++ b/components/ui/Button.tsx
@@ -82,8 +82,13 @@ export function Button({
);
if (href) {
+ const { type, disabled, ...linkProps } = props as any;
return (
-
+
{content}
);
diff --git a/lib/blog.ts b/lib/blog.ts
index c0b3b245..a1e21fe8 100644
--- a/lib/blog.ts
+++ b/lib/blog.ts
@@ -56,9 +56,12 @@ export function isPostVisible(post: { frontmatter: { date: string; public?: bool
return !(postDate > now && config.isProduction);
}
+import { mapSlugToFileSlug, mapFileSlugToTranslated } from './slugs';
+
export async function getPostBySlug(slug: string, locale: string): Promise {
try {
- const filePath = path.join(process.cwd(), 'content', 'posts', `${slug}.mdx`);
+ const fileSlug = await mapSlugToFileSlug(slug, locale);
+ const filePath = path.join(process.cwd(), 'content', 'posts', `${fileSlug}.mdx`);
const fileContent = await fs.readFile(filePath, 'utf-8');
const { data, content } = matter(fileContent);
@@ -77,8 +80,10 @@ export async function getPostBySlug(slug: string, locale: string): Promise> {
- // Mock function, simply returns the slug for all locales
- return { de: slug, en: slug };
+ const fileSlug = await mapSlugToFileSlug(slug, locale);
+ const de = await mapFileSlugToTranslated(fileSlug, 'de');
+ const en = await mapFileSlugToTranslated(fileSlug, 'en');
+ return { de, en };
}
export async function getAllPosts(locale: string): Promise {
diff --git a/lib/pages.ts b/lib/pages.ts
index d417bc37..79464694 100644
--- a/lib/pages.ts
+++ b/lib/pages.ts
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import { config } from '@/lib/config';
+import { mapSlugToFileSlug } from './slugs';
export interface PageFrontmatter {
title: string;
@@ -47,11 +48,18 @@ function mapDoc(doc: any): PageData {
export async function getPageBySlug(slug: string, locale: string): Promise {
try {
- const filePath = path.join(process.cwd(), 'content', 'pages', `${slug}.mdx`);
+ const fileSlug = await mapSlugToFileSlug(slug, locale);
+ const filePath = path.join(process.cwd(), 'content', 'pages', `${fileSlug}.mdx`);
const fileContent = await fs.readFile(filePath, 'utf-8');
const { data, content } = matter(fileContent);
- let parsedContent = content;
+ // Fix MDX data props dropped by next-mdx-remote
+ // Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
+ const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
+ return 'data="{' + p1.replace(/"/g, '"') + '}"';
+ });
+
+ let parsedContent = fixedContent;
try {
if (content.trim().startsWith('{')) {
parsedContent = JSON.parse(content);
diff --git a/lib/products.ts b/lib/products.ts
index a170af90..7e901a3a 100644
--- a/lib/products.ts
+++ b/lib/products.ts
@@ -39,7 +39,13 @@ export async function getProductBySlug(slug: string, locale: string): Promise {
+ return 'data="{' + p1.replace(/"/g, '"') + '}"';
+ });
+
+ let parsedContent = fixedContent;
try {
if (content.trim().startsWith('{')) {
parsedContent = JSON.parse(content);
diff --git a/lib/slugs.ts b/lib/slugs.ts
index c4008674..157adb71 100644
--- a/lib/slugs.ts
+++ b/lib/slugs.ts
@@ -31,6 +31,11 @@ export async function mapSlugToFileSlug(translatedSlug: string, locale: string):
return slugs.categories[translatedSlug as keyof typeof slugs.categories];
}
+ // Check posts
+ if ((slugs as any).posts && translatedSlug in (slugs as any).posts) {
+ return (slugs as any).posts[translatedSlug];
+ }
+
// Return original slug if no mapping found
return translatedSlug;
}
@@ -45,7 +50,7 @@ export async function mapFileSlugToTranslated(fileSlug: string, locale: string):
const messages = getMessages(locale);
const slugs = messages.Slugs;
- const sections = [slugs.pages, slugs.products, slugs.categories];
+ const sections = [slugs.pages, slugs.products, slugs.categories, (slugs as any).posts];
for (const sectionData of sections) {
if (sectionData && typeof sectionData === 'object') {
@@ -63,16 +68,16 @@ export async function mapFileSlugToTranslated(fileSlug: string, locale: string):
/**
* Gets all translated slugs for a section
- * @param section - The section name (pages, products, categories)
+ * @param section - The section name (pages, products, categories, posts)
* @param locale - The current locale
* @returns Object mapping translated slugs to file slugs
*/
export async function getSlugMappings(
- section: 'pages' | 'products' | 'categories',
+ section: 'pages' | 'products' | 'categories' | 'posts',
locale: string,
): Promise> {
const messages = getMessages(locale);
- const sectionData = messages.Slugs[section];
+ const sectionData = (messages.Slugs as any)[section];
if (sectionData && typeof sectionData === 'object') {
return sectionData as Record;
diff --git a/messages/de.json b/messages/de.json
index 71c1e670..37d4e5ba 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -47,6 +47,39 @@
"mittelspannungskabel": "medium-voltage-cables",
"hochspannungskabel": "high-voltage-cables",
"solarkabel": "solar-cables"
+ },
+ "posts": {
+ "100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur": "100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur",
+ "das-muessen-sie-ueber-erneuerbare-energien-im-jahr-2025-wissen": "das-muessen-sie-ueber-erneuerbare-energien-im-jahr-2025-wissen",
+ "die-besten-erdkabel-fuer-windkraft-und-solar-jetzt-bei-uns-bestellen": "die-besten-erdkabel-fuer-windkraft-und-solar-jetzt-bei-uns-bestellen",
+ "die-kunst-der-kabellogistik-der-transport-des-fundamentes-moderner-energienetze": "die-kunst-der-kabellogistik-der-transport-des-fundamentes-moderner-energienetze",
+ "die-perfekte-kabelanfrage-so-sparen-sie-sich-unnoetige-rueckfragen": "die-perfekte-kabelanfrage-so-sparen-sie-sich-unnoetige-rueckfragen",
+ "erkenntnisse-ueber-die-gruene-energiewende-herausforderungen-und-chancen": "erkenntnisse-ueber-die-gruene-energiewende-herausforderungen-und-chancen",
+ "gruene-energie-beginnt-unter-der-erde-und-zwar-mit-plan": "gruene-energie-beginnt-unter-der-erde-und-zwar-mit-plan",
+ "johannes-gleich-startet-als-senior-key-account-manager-durch": "johannes-gleich-startet-als-senior-key-account-manager-durch",
+ "kabelabkuerzungen-entschluesselt-der-schluessel-zur-richtigen-kabelwahl": "kabelabkuerzungen-entschluesselt-der-schluessel-zur-richtigen-kabelwahl",
+ "kabeltrommelqualitaet-die-grundlage-der-kabelzuverlaessigkeit": "kabeltrommelqualitaet-die-grundlage-der-kabelzuverlaessigkeit",
+ "klimaneutral-bis-2050-was-wir-tun-muessen-um-das-ziel-zu-erreichen": "klimaneutral-bis-2050-was-wir-tun-muessen-um-das-ziel-zu-erreichen",
+ "klz-im-adressbuch-der-windenergie-2025": "klz-im-adressbuch-der-windenergie-2025",
+ "klz-waechst-weiter-neue-staerke-im-bereich-financial-sales": "klz-waechst-weiter-neue-staerke-im-bereich-financial-sales",
+ "kostenvergleich-kupfer-vs-aluminiumkabel-in-windparks-was-lohnt-sich-langfristig": "kostenvergleich-kupfer-vs-aluminiumkabel-in-windparks-was-lohnt-sich-langfristig",
+ "milliarden-paket-fuer-infrastruktur-der-kabel-boom-steht-bevor": "milliarden-paket-fuer-infrastruktur-der-kabel-boom-steht-bevor",
+ "n2xsf2y-mittelspannungskabel-energieprojekt": "n2xsf2y-mittelspannungskabel-energieprojekt",
+ "na2xsf2y-dreileiter-mittelspannungskabel-lieferbar": "na2xsf2y-dreileiter-mittelspannungskabel-lieferbar",
+ "netzausbau-2025-warum-jede-neue-leitung-ein-schritt-zur-energiewende-ist": "netzausbau-2025-warum-jede-neue-leitung-ein-schritt-zur-energiewende-ist",
+ "recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt": "recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt",
+ "reicht-windenergie-wirklich-nicht-ein-blick-hinter-die-schlagzeilen": "reicht-windenergie-wirklich-nicht-ein-blick-hinter-die-schlagzeilen",
+ "sicherheit-bei-kabeltrommeln-unfallfrei-und-effizient-arbeiten": "sicherheit-bei-kabeltrommeln-unfallfrei-und-effizient-arbeiten",
+ "so-waehlen-sie-das-richtige-kabel-fuer-ihr-naechstes-projekt-aus": "so-waehlen-sie-das-richtige-kabel-fuer-ihr-naechstes-projekt-aus",
+ "von-smart-bis-nachhaltig-so-sieht-die-energiewirtschaft-in-naher-zukunft-aus": "von-smart-bis-nachhaltig-so-sieht-die-energiewirtschaft-in-naher-zukunft-aus",
+ "warum-die-richtigen-kabel-der-geheime-held-der-gruenen-energie-sind": "warum-die-richtigen-kabel-der-geheime-held-der-gruenen-energie-sind",
+ "warum-windpark-netzanschlusskabel-extremen-belastungen-standhalten-muessen": "warum-windpark-netzanschlusskabel-extremen-belastungen-standhalten-muessen",
+ "was-macht-ein-erstklassiges-kabel-aus-finden-sie-es-hier-heraus": "was-macht-ein-erstklassiges-kabel-aus-finden-sie-es-hier-heraus",
+ "welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert": "welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert",
+ "wie-die-kabelbranche-nachhaltigkeit-und-erneuerbare-energien-vorantreibt": "wie-die-kabelbranche-nachhaltigkeit-und-erneuerbare-energien-vorantreibt",
+ "willkommen-in-der-zukunft-von-klz-unsere-neue-website-ist-online": "willkommen-in-der-zukunft-von-klz-unsere-neue-website-ist-online",
+ "windparkbau-im-fokus-drei-typische-kabelherausforderungen": "windparkbau-im-fokus-drei-typische-kabelherausforderungen",
+ "zukunft-sichern-mit-h1z2z2-k-unser-solarkabel-zur-intersolar-2025": "zukunft-sichern-mit-h1z2z2-k-unser-solarkabel-zur-intersolar-2025"
}
},
"Index": {
diff --git a/messages/en.json b/messages/en.json
index afa48d10..c92a9459 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -47,6 +47,37 @@
"medium-voltage-cables": "medium-voltage-cables",
"high-voltage-cables": "high-voltage-cables",
"solar-cables": "solar-cables"
+ },
+ "posts": {
+ "100-renewable-energy-only-with-the-right-cable-infrastructure": "100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur",
+ "billion-euro-package-for-infrastructure-the-cable-boom-is-coming": "milliarden-paket-fuer-infrastruktur-der-kabel-boom-steht-bevor",
+ "cable-abbreviations-decoded-the-key-to-choosing-the-right-cable": "kabelabkuerzungen-entschluesselt-der-schluessel-zur-richtigen-kabelwahl",
+ "cable-drum-quality-the-foundation-of-cable-reliability": "kabeltrommelqualitaet-die-grundlage-der-kabelzuverlaessigkeit",
+ "cable-drum-safety-ensuring-smooth-operations-and-accident-free-environments": "sicherheit-bei-kabeltrommeln-unfallfrei-und-effizient-arbeiten",
+ "climate-neutral-by-2050-what-we-need-to-do-to-achieve-this-goal": "klimaneutral-bis-2050-was-wir-tun-muessen-um-das-ziel-zu-erreichen",
+ "cost-comparison-copper-vs-aluminum-cables-in-wind-farms-which-is-worthwhile-in-the-long-term": "kostenvergleich-kupfer-vs-aluminiumkabel-in-windparks-was-lohnt-sich-langfristig",
+ "expanding-the-grid-by-2025-building-the-foundation-for-a-successful-energy-transition": "netzausbau-2025-warum-jede-neue-leitung-ein-schritt-zur-energiewende-ist",
+ "eye-opening-realities-of-green-energy-transformation": "erkenntnisse-ueber-die-gruene-energiewende-herausforderungen-und-chancen",
+ "focus-on-wind-farm-construction-three-typical-cable-challenges": "windparkbau-im-fokus-drei-typische-kabelherausforderungen",
+ "from-smart-to-sustainable-this-is-what-the-energy-industry-will-look-like-in-the-near-future": "von-smart-bis-nachhaltig-so-sieht-die-energiewirtschaft-in-naher-zukunft-aus",
+ "green-energy-starts-underground-and-with-a-plan": "gruene-energie-beginnt-unter-der-erde-und-zwar-mit-plan",
+ "how-the-cable-industry-is-driving-sustainability-and-renewable-energies-forward": "wie-die-kabelbranche-nachhaltigkeit-und-erneuerbare-energien-vorantreibt",
+ "how-the-right-cables-quietly-power-the-green-energy-revolution": "warum-die-richtigen-kabel-der-geheime-held-der-gruenen-energie-sind",
+ "how-to-choose-the-right-cable-for-your-next-project": "so-waehlen-sie-das-richtige-kabel-fuer-ihr-naechstes-projekt-aus",
+ "is-wind-energy-really-enough-a-deeper-dive-behind-the-headlines": "reicht-windenergie-wirklich-nicht-ein-blick-hinter-die-schlagzeilen",
+ "klz-in-the-directory-of-wind-energy-2025": "klz-im-adressbuch-der-windenergie-2025",
+ "na2xsf2y-three-conductor-medium-voltage-cable-available": "na2xsf2y-dreileiter-mittelspannungskabel-lieferbar",
+ "recycling-of-cable-drums-sustainability-in-wind-power-projects": "recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt",
+ "securing-the-future-with-h1z2z2-k-our-solar-cable-for-intersolar-2025": "zukunft-sichern-mit-h1z2z2-k-unser-solarkabel-zur-intersolar-2025",
+ "the-art-of-cable-logistics-moving-the-backbone-of-modern-energy-networks": "die-kunst-der-kabellogistik-der-transport-des-fundamentes-moderner-energienetze",
+ "the-best-underground-cables-for-wind-power-and-solar-order-from-us-now": "die-besten-erdkabel-fuer-windkraft-und-solar-jetzt-bei-uns-bestellen",
+ "the-perfect-cable-inquiry-how-to-save-yourself-unnecessary-queries": "die-perfekte-kabelanfrage-so-sparen-sie-sich-unnoetige-rueckfragen",
+ "this-what-you-need-to-know-about-renewable-energies-in-2025": "das-muessen-sie-ueber-erneuerbare-energien-im-jahr-2025-wissen",
+ "welcome-to-the-future-of-klz-our-new-website-is-live": "willkommen-in-der-zukunft-von-klz-unsere-neue-website-ist-online",
+ "what-makes-a-first-class-cable-find-out-here": "was-macht-ein-erstklassiges-kabel-aus-finden-sie-es-hier-heraus",
+ "which-cables-for-wind-power-differences-from-low-to-extra-high-voltage-explained-2": "welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert",
+ "why-the-n2xsf2y-is-the-ideal-cable-for-your-energy-project": "n2xsf2y-mittelspannungskabel-energieprojekt",
+ "why-wind-farm-grid-connection-cables-must-withstand-extreme-loads": "warum-windpark-netzanschlusskabel-extremen-belastungen-standhalten-muessen"
}
},
"Index": {
diff --git a/tests/og-image.test.ts b/tests/og-image.test.ts
index bce8febf..a1b286ed 100644
--- a/tests/og-image.test.ts
+++ b/tests/og-image.test.ts
@@ -93,11 +93,12 @@ describe('OG Image Generation', () => {
const url = `${BASE_URL}/de/blog/${slug}/opengraph-image`;
const response = await fetch(url);
+ const responseClone = response.clone();
await verifyImageResponse(response);
// Verify the image is substantially large (>50KB) to confirm it actually
// contains the featured photo and isn't just a tiny fallback/text-only image
- const buffer = await response.clone().arrayBuffer();
+ const buffer = await responseClone.arrayBuffer();
expect(
buffer.byteLength,
`OG image for "${slug}" is suspiciously small (${buffer.byteLength} bytes) — likely missing featured photo`,
diff --git a/tests/pages.test.ts b/tests/pages.test.ts
new file mode 100644
index 00000000..75cf9b77
--- /dev/null
+++ b/tests/pages.test.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest';
+import { getPageBySlug } from '../lib/pages';
+import { getPostBySlug, getPostSlugs } from '../lib/blog';
+
+describe('getPageBySlug translation routing', () => {
+ it('should resolve standard page in German', async () => {
+ const page = await getPageBySlug('impressum', 'de');
+ expect(page).not.toBeNull();
+ expect(page?.frontmatter.title).toBe('Impressum');
+ });
+
+ it('should resolve translated slug "legal-notice" for English', async () => {
+ const page = await getPageBySlug('legal-notice', 'en');
+ expect(page).not.toBeNull();
+ expect(page?.frontmatter.title).toBe('Impressum');
+ });
+
+ it('should resolve translated slug "privacy-policy" for English', async () => {
+ const page = await getPageBySlug('privacy-policy', 'en');
+ expect(page).not.toBeNull();
+ expect(page?.frontmatter.title).toBe('Datenschutzerklärung');
+ });
+
+ it('should return null for non-existent slugs', async () => {
+ const page = await getPageBySlug('does-not-exist', 'de');
+ expect(page).toBeNull();
+ });
+});
+
+describe('getPostBySlug and getPostSlugs translation routing', () => {
+ it('should resolve post by English slug', async () => {
+ const post = await getPostBySlug(
+ 'focus-on-wind-farm-construction-three-typical-cable-challenges',
+ 'en',
+ );
+ expect(post).not.toBeNull();
+ expect(post?.frontmatter.title).toBe(
+ 'Windparkbau im Fokus: drei typische Kabelherausforderungen',
+ );
+ });
+
+ it('should return correct localized post slugs', async () => {
+ const slugs = await getPostSlugs(
+ 'focus-on-wind-farm-construction-three-typical-cable-challenges',
+ 'en',
+ );
+ expect(slugs.de).toBe('windparkbau-im-fokus-drei-typische-kabelherausforderungen');
+ expect(slugs.en).toBe('focus-on-wind-farm-construction-three-typical-cable-challenges');
+ });
+});