chore: merge branch fix/mobile-kontakt-button and fix og-image.test.ts

This commit is contained in:
2026-06-10 11:20:18 +02:00
12 changed files with 168 additions and 14 deletions

1
.data/leads-backup.jsonl Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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')}
</Button>

View File

@@ -82,8 +82,13 @@ export function Button({
);
if (href) {
const { type, disabled, ...linkProps } = props as any;
return (
<Link href={href} className={styles}>
<Link
href={disabled ? '#' : href}
className={cn(styles, disabled && 'pointer-events-none opacity-50')}
{...linkProps}
>
{content}
</Link>
);

View File

@@ -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<PostData | null> {
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<PostD
// Not JSON
}
const resolvedSlug = await mapFileSlugToTranslated(fileSlug, locale);
return {
slug,
slug: resolvedSlug,
frontmatter: {
title: data.title || '',
date: data.date || '',
@@ -98,8 +103,10 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
}
export async function getPostSlugs(slug: string, locale: string): Promise<Record<string, string>> {
// 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<PostData[]> {

View File

@@ -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<PageData | null> {
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, '&quot;') + '}"';
});
let parsedContent = fixedContent;
try {
if (content.trim().startsWith('{')) {
parsedContent = JSON.parse(content);

View File

@@ -39,7 +39,13 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
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, '&quot;') + '}"';
});
let parsedContent = fixedContent;
try {
if (content.trim().startsWith('{')) {
parsedContent = JSON.parse(content);

View File

@@ -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<Record<string, string>> {
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<string, string>;

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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`,

50
tests/pages.test.ts Normal file
View File

@@ -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');
});
});