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