diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx
index f3b356c8..afab2e98 100644
--- a/app/[locale]/contact/page.tsx
+++ b/app/[locale]/contact/page.tsx
@@ -8,6 +8,7 @@ import { SITE_URL } from '@/lib/schema';
import { getOGImageMetadata } from '@/lib/metadata';
import { Suspense } from 'react';
import ContactMap from '@/components/ContactMap';
+import ObfuscatedEmail from '@/components/ObfuscatedEmail';
interface ContactPageProps {
params: Promise<{
@@ -204,12 +205,10 @@ export default async function ContactPage({ params }: ContactPageProps) {
{t('info.email')}
-
- info@klz-cables.com
-
+ />
diff --git a/components/ObfuscatedEmail.tsx b/components/ObfuscatedEmail.tsx
new file mode 100644
index 00000000..01a7ad04
--- /dev/null
+++ b/components/ObfuscatedEmail.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+
+interface ObfuscatedEmailProps {
+ email: string;
+ className?: string;
+ children?: React.ReactNode;
+}
+
+/**
+ * A component that helps protect email addresses from simple spambots.
+ * It uses client-side mounting to render the actual email address,
+ * making it harder for static crawlers to harvest.
+ */
+export default function ObfuscatedEmail({ email, className = '', children }: ObfuscatedEmailProps) {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ // Show a placeholder or obscured version during SSR
+ return (
+
+ {children || email.replace('@', ' [at] ').replace(/\./g, ' [dot] ')}
+
+ );
+ }
+
+ // Once mounted on the client, render the real mailto link
+ return (
+
+ {children || email}
+
+ );
+}
diff --git a/components/ObfuscatedPhone.tsx b/components/ObfuscatedPhone.tsx
new file mode 100644
index 00000000..0db10d57
--- /dev/null
+++ b/components/ObfuscatedPhone.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+
+interface ObfuscatedPhoneProps {
+ phone: string;
+ className?: string;
+ children?: React.ReactNode;
+}
+
+/**
+ * A component that helps protect phone numbers from simple spambots.
+ * It stays obscured during SSR and hydrates into a functional tel: link on the client.
+ */
+export default function ObfuscatedPhone({ phone, className = '', children }: ObfuscatedPhoneProps) {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ // Format phone number for tel: link (remove spaces, etc.)
+ const telLink = `tel:${phone.replace(/\s+/g, '')}`;
+
+ if (!mounted) {
+ // Show a placeholder or obscured version during SSR
+ // e.g. +49 881 925 [at] 37298
+ const obscured = phone.replace(/(\d{3})(\d{3})$/, ' $1...$2');
+ return (
+
+ {children || obscured}
+
+ );
+ }
+
+ return (
+
+ {children || phone}
+
+ );
+}
diff --git a/components/PayloadRichText.tsx b/components/PayloadRichText.tsx
index c050199f..0f55abb3 100644
--- a/components/PayloadRichText.tsx
+++ b/components/PayloadRichText.tsx
@@ -24,6 +24,8 @@ import Reveal from '@/components/Reveal';
import { Badge, Container, Heading, Section, Card } from '@/components/ui';
import TrackedLink from '@/components/analytics/TrackedLink';
import { useLocale } from 'next-intl';
+import ObfuscatedEmail from '@/components/ObfuscatedEmail';
+import ObfuscatedPhone from '@/components/ObfuscatedPhone';
import HomeHero from '@/components/home/Hero';
import ProductCategories from '@/components/home/ProductCategories';
@@ -56,13 +58,58 @@ const jsxConverters: JSXConverters = {
...defaultJSXConverters,
// Handle Lexical linebreak nodes (explicit shift+enter)
linebreak: () =>
,
- // Custom text converter: preserve \n inside text nodes as
+ // Custom text converter: preserve \n inside text nodes as
and obfuscate emails
text: ({ node }: any) => {
let content: React.ReactNode = node.text || '';
// Split newlines first
if (typeof content === 'string' && content.includes('\n')) {
content = textWithLineBreaks(content, `t-${(node.text || '').slice(0, 8)}`);
}
+
+ // Obfuscate emails in text content
+ if (typeof content === 'string' && content.includes('@')) {
+ const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
+ const parts = content.split(emailRegex);
+ content = parts.map((part, i) => {
+ if (part.match(emailRegex)) {
+ return ;
+ }
+ return part;
+ });
+ }
+
+ // Obfuscate phone numbers in text content (simple pattern for +XX XXX ...)
+ if (typeof content === 'string' && content.match(/\+\d+/)) {
+ const phoneRegex = /(\+\d{1,4}[\d\s-]{5,15})/g;
+ const parts = content.split(phoneRegex);
+ content = parts.map((part, i) => {
+ if (part.match(phoneRegex)) {
+ return ;
+ }
+ return part;
+ });
+ }
+
+ // Handle array content (from previous mappings)
+ if (Array.isArray(content)) {
+ content = content.map((item, idx) => {
+ if (typeof item === 'string') {
+ // Re-apply phone regex to strings in array
+ const phoneRegex = /(\+\d{1,4}[\d\s-]{5,15})/g;
+ if (item.match(phoneRegex)) {
+ const parts = item.split(phoneRegex);
+ return parts.map((part, i) => {
+ if (part.match(phoneRegex)) {
+ return ;
+ }
+ return part;
+ });
+ }
+ }
+ return item;
+ });
+ }
+
// Apply Lexical formatting flags
if (node.format) {
if (node.format & 1) content = {content};
@@ -209,6 +256,17 @@ const jsxConverters: JSXConverters = {
// Handling Payload CMS link nodes
const href = node?.fields?.url || node?.url || '#';
const newTab = node?.fields?.newTab || node?.newTab;
+
+ if (href.startsWith('mailto:')) {
+ const email = href.replace('mailto:', '');
+ return (
+
+ );
+ }
+
return (
),
+ 'block-email': ({ node }: any) => {
+ const { email, label } = node.fields;
+ return {label || email};
+ },
+ 'block-phone': ({ node }: any) => {
+ const { phone, label } = node.fields;
+ return {label || phone};
+ },
},
// Custom converter for the Payload "upload" Lexical node (Media collection)
// This natively reconstructs Next.js tags pointing to the focal-point cropped sizes
diff --git a/payload.config.ts b/payload.config.ts
index 289ba29c..eac89177 100644
--- a/payload.config.ts
+++ b/payload.config.ts
@@ -21,6 +21,8 @@ import { Posts } from './src/payload/collections/Posts';
import { FormSubmissions } from './src/payload/collections/FormSubmissions';
import { Products } from './src/payload/collections/Products';
import { Pages } from './src/payload/collections/Pages';
+import { Email } from './src/payload/blocks/Email';
+import { Phone } from './src/payload/blocks/Phone';
import { seedDatabase } from './src/payload/seed';
const filename = fileURLToPath(import.meta.url);
@@ -62,6 +64,7 @@ export default buildConfig({
...defaultFeatures,
BlocksFeature({
blocks: payloadBlocks,
+ inlineBlocks: [Email, Phone],
}),
],
}),
diff --git a/src/payload/blocks/Email.ts b/src/payload/blocks/Email.ts
new file mode 100644
index 00000000..b2460939
--- /dev/null
+++ b/src/payload/blocks/Email.ts
@@ -0,0 +1,25 @@
+import { Block } from 'payload';
+
+export const Email: Block = {
+ slug: 'email',
+ interfaceName: 'EmailBlock',
+ labels: {
+ singular: 'Email (Inline)',
+ plural: 'Emails (Inline)',
+ },
+ fields: [
+ {
+ name: 'email',
+ type: 'text',
+ required: true,
+ },
+ {
+ name: 'label',
+ type: 'text',
+ required: false,
+ admin: {
+ placeholder: 'Optional: Custom link text',
+ },
+ },
+ ],
+};
diff --git a/src/payload/blocks/Phone.ts b/src/payload/blocks/Phone.ts
new file mode 100644
index 00000000..8370860a
--- /dev/null
+++ b/src/payload/blocks/Phone.ts
@@ -0,0 +1,28 @@
+import { Block } from 'payload';
+
+export const Phone: Block = {
+ slug: 'phone',
+ interfaceName: 'PhoneBlock',
+ labels: {
+ singular: 'Phone (Inline)',
+ plural: 'Phones (Inline)',
+ },
+ fields: [
+ {
+ name: 'phone',
+ type: 'text',
+ required: true,
+ admin: {
+ placeholder: '+49 123 456 789',
+ },
+ },
+ {
+ name: 'label',
+ type: 'text',
+ required: false,
+ admin: {
+ placeholder: 'Optional: Custom link text',
+ },
+ },
+ ],
+};
diff --git a/src/payload/blocks/allBlocks.ts b/src/payload/blocks/allBlocks.ts
index d5d63066..16d718b6 100644
--- a/src/payload/blocks/allBlocks.ts
+++ b/src/payload/blocks/allBlocks.ts
@@ -1,4 +1,6 @@
import { AnimatedImage } from './AnimatedImage';
+import { Email } from './Email';
+import { Phone } from './Phone';
import { Callout } from './Callout';
import { CategoryGrid } from './CategoryGrid';
import { ChatBubble } from './ChatBubble';
@@ -21,6 +23,8 @@ import { homeBlocksArray } from './HomeBlocks';
export const payloadBlocks = [
...homeBlocksArray,
AnimatedImage,
+ Email,
+ Phone,
Callout,
CategoryGrid,
ChatBubble,