diff --git a/apps/web/app/blog/[slug]/opengraph-image.tsx b/apps/web/app/blog/[slug]/opengraph-image.tsx index ddbaf54..5024191 100644 --- a/apps/web/app/blog/[slug]/opengraph-image.tsx +++ b/apps/web/app/blog/[slug]/opengraph-image.tsx @@ -3,6 +3,8 @@ import { allPosts } from "contentlayer/generated"; import { blogThumbnails } from "../../../src/components/blog/blogThumbnails"; import { OGImageTemplate } from "../../../src/components/OGImageTemplate"; import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; export const size = OG_IMAGE_SIZE; export const contentType = "image/png"; @@ -15,6 +17,24 @@ export default async function Image({ }) { const { slug } = await params; const post = allPosts.find((p) => p.slug === slug); + + // If we have a custom generated thumbnail, serve it directly as the OG image + if (post?.thumbnail) { + try { + const filePath = path.join(process.cwd(), "public", post.thumbnail); + const fileBuffer = await fs.readFile(filePath); + return new Response(fileBuffer, { + headers: { + "Content-Type": "image/png", + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); + } catch (err) { + console.warn(`[OG Image Generator] Could not read thumbnail file for ${slug}:`, err); + // Fall through to dynamic generation + } + } + const thumbnail = blogThumbnails[slug]; const title = post?.title || "Marc Mintel"; diff --git a/apps/web/content/blog/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.mdx b/apps/web/content/blog/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.mdx index 0328a4b..9596741 100644 --- a/apps/web/content/blog/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.mdx +++ b/apps/web/content/blog/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.mdx @@ -7,27 +7,29 @@ tags: ["privacy", "analytics"] --- - "Ich brauche Google Analytics, um zu wissen, was meine Nutzer tun." – Diese weit verbreitete Fehlannahme kostet B2B-Unternehmen täglich Vertrauen und Datenqualität. + "Ich brauche Google Analytics, um zu wissen, was meine Nutzer tun." – Diese weit verbreitete Fehlannahme kostet B2B-Unternehmen täglich Vertrauen und massive Datenqualität. + In meiner täglichen Arbeit als Digital Architect beweise ich das Gegenteil: Maximale geschäftliche Erkenntnis erfordert keine maximale private Überwachung. + - Ich zeige Ihnen, wie wir Erfolg präzise messen, die DSGVO-Konformität systemisch verankern und dabei die Privatsphäre Ihrer Kunden schützen. + Ich zeige Ihnen, wie wir Erfolg präzise messen, die DSGVO-Konformität systemisch verankern und dabei die Privatsphäre Ihrer Kunden schützen, ohne auf valide Daten zu verzichten. -**TL;DR:** Klassisches Tracking via Google Analytics verliert durch Consent-Ablehnung bis zu 60% der Daten. Privacy-First Analytics arbeitet serverseitig, kommt ohne Banner aus und liefert 100% genauere Trends bei maximalem Datenschutz und besserer PageSpeed. +**TL;DR:** Klassisches Tracking via Google Analytics verliert durch Consent-Ablehnung bis zu 70% der Daten. Privacy-First Analytics arbeitet serverseitig, kommt ohne Banner aus und liefert 100% genauere Trends bei maximalem Datenschutz und besserem PageSpeed.

Analytics ohne den Beigeschmack der Überwachung

- Klassische Analytics-Tools funktionieren oft wie ein Trojaner. Sie sammeln riesige Mengen an persönlichen Daten, um daraus Profile zu bilden, die weit über Ihre Website hinausgehen. Dies zwingt Sie rechtlich in die Knie: Sie benötigen komplexe Consent-Tools und riskieren dennoch Abmahnungen. Laut einer Untersuchung des HTTP Archive macht die mediane Webseite zahlreiche Anfragen an Drittanbieter-Tracking-Domains, was die Ladezeit massiv belastet, ohne immer direkten Business-Value zu generieren. + Klassische Analytics-Tools funktionieren oft wie ein Trojaner. Sie sammeln riesige Mengen an persönlichen Daten, um daraus Profile zu bilden, die weit über Ihre Website hinausgehen. Dies zwingt Sie rechtlich in die Knie: Sie benötigen komplexe Consent-Management-Plattformen (CMPs) und riskieren dennoch Abmahnungen. Laut einer Untersuchung des HTTP Archive machen Third-Party-Requests einen massiven Teil des Datenaufkommens aus, was die Ladezeit unnötig belastet. - Das Paradoxon: Die meisten dieser Daten benötigen Sie für Ihre operativen Entscheidungen gar nicht. Sie wollen wissen, welche Inhalte funktionieren, welche Kampagnen konvertieren und wo Nutzer abspringen – nicht, welche anderen Websites Ihre Besucher in ihrer Freizeit besuchen. Viele Organisationen analysieren laut Deloitte ohnehin weniger als 5% der gesammelten Daten. + Das Paradoxon: Die meisten dieser Daten benötigen Sie für Ihre operativen Entscheidungen gar nicht. Sie wollen wissen, welche Inhalte funktionieren, welche Kampagnen konvertieren und wo Nutzer abspringen – nicht, welche anderen Websites Ihre Besucher in ihrer Freizeit besuchen. Studien von Google Developers zeigen zudem, dass Third-Party-JavaScript oft zum Performance-Flaschenhals wird.
@@ -43,38 +45,33 @@ graph LR
-

Qualität der Daten vor Quantität der Profile

+

Das Problem der Datenlücke durch Consent-Zwang

- Mein Ansatz basiert auf aggregierten Trends statt auf individueller Verfolgung. Wir messen Seitenaufrufe, Verweildauer und Conversions anonym und oft direkt am [Edge-Server](/blog/professional-hosting-operations). Eine Studie von McKinsey zeigt, dass Unternehmen, die Daten konsequent ins Zentrum ihrer Entscheidungen stellen, ihren Marketing-ROI um 15-20% steigern können. Dies gelingt jedoch nur mit validen Daten – nicht mit lückenhaften Statistiken aus abgelehnten Cookie-Bannern. + Die Realität in Europa ist ernüchternd für Marketer, die auf klassische Tools setzen. Eine Studie von Deloitte zeigt bei europäischen Websites Datenverluste zwischen 30% und über 70% allein durch DSGVO-Consent-Anforderungen. Wenn der Nutzer "Ablehnen" klickt, bleibt das Unternehmen blind. - + - Wahre Souveränität bedeutet, Insights zu generieren, ohne sich von der Komplexität eines GA4 abhängig zu machen. Wer [Websites ohne Cookie-Banner](/blog/website-without-cookie-banners) betreibt, signalisiert Professionalität und Respekt gegenüber dem Nutzer. + Wahre Souveränität bedeutet, Insights zu generieren, ohne sich von der Komplexität eines GA4-Setups abhängig zu machen. Wer [Websites ohne Cookie-Banner](/blog/website-without-cookie-banners) betreibt, signalisiert Professionalität und Respekt gegenüber dem Nutzer. Zudem heben Studien der Nielsen Norman Group hervor, dass langsame Ladezeiten – oft verursacht durch drittanbieter-lastige Tracking-Skripte – direkt zu Frustration und Abbruch führen.

Die Architektur für ethische Insights

- Ich integriere Analytics direkt in Ihre Plattform-Architektur. Das bedeutet: Keine externen Scripte, was die Performance massiv verbessert. Google Developers betont regelmäßig, dass schlecht konfigurierte Tracking-Skripte die Ladezeiten signifikant bremsen. + Ich integriere Analytics direkt in Ihre Plattform-Architektur. Das bedeutet: Keine externen Scripte von US-Servern, was die Performance massiv verbessert. Ein sauberer [systemischer Architektur-Ansatz für die DSGVO](/blog/gdpr-conformity-system-approach) ist hier der Schlüssel. - Cookieless Tracking: Wir erkennen wiederkehrende Nutzer über kurzlebige, anonyme Hashes. Keine Speicherung am Endgerät (LocalStorage/Cookies) notwendig. + Cookieless Tracking: Wir erkennen wiederkehrende Nutzer über kurzlebige, anonyme Hashes innerhalb von 24h. Keine Speicherung am Endgerät notwendig. - First-Party Data: Alle Daten bleiben in Ihrem Hoheitsbereich. Kein Abfluss an Werbenetzwerke zur Profilbildung. + First-Party Data: Alle Daten bleiben in Ihrem Hoheitsbereich (z.B. auf einem [professionellen Hosting](/blog/professional-hosting-operations)). Kein Datenabfluss an Werbenetzwerke. - Lightweight Implementation: Statt 100KB Analytics-Ballast nutzen wir Lösungen unter 1KB. Geschwindigkeit trifft auf Erkenntnis. + Lightweight Implementation: Statt 100KB Tracking-Ballast nutzen wir Lösungen unter 1KB. Geschwindigkeit trifft auf Erkenntnis. @@ -82,57 +79,68 @@ graph LR + + Laut Nielsen Norman Group führen intrusive oder unklare Cookie-Banner zu deutlich niedrigeren Akzeptanzraten. Ein Verzicht auf diese Barrieren verbessert also nicht nur die UX, sondern ironischerweise auch die Datenbasis für Ihre Business-Entscheidungen. + +

Der strategische Haken: Transparenz schafft Vertrauen

- Als Digital Architect ist es meine Pflicht, auch die Grenzen moderner Cookieless-Systeme aufzuzeigen. Während wir die Privatsphäre schützen, müssen wir technische Kompromisse bei der Langzeit-Erkennung eingehen. + Als Digital Architect ist es meine Pflicht, auch die Grenzen moderner Cookieless-Systeme aufzuzeigen. Während wir die Privatsphäre schützen, müssen wir bei der Langzeit-Kohorten-Analyse Kompromisse eingehen. - Studien von Nielsen Norman Group zeigen, dass Usability-Probleme oft durch mangelnden Fokus auf Nutzerverhalten entstehen. Privacy-Analytics löst dies, indem es den Fokus auf *was* getan wird lenkt, statt auf das *wer* es tut. Dennoch: IP-basiertes Tracking ohne Cookies erreicht laut Google nur eine Genauigkeit von ca. 60-70%, da dynamische IPs und VPNs die Zuordnung erschweren. + Google versucht mit dem "Consent Mode" zwar Datenlücken durch Modellierung zu schließen (Google Developers), doch eine vollständige Wiederherstellung der Daten ist technisch nicht möglich und bleibt rechtlich oft eine Grauzone. Mein Ansatz verfolgt das Prinzip der [Digital Longevity](/blog/digital-longevity-architecture): Ein System, das stabil und ohne rechtliche Zitterpartie funktioniert. -

Der unternehmerische Hebel: Banner-freie Leads

+

Der unternehmerische Hebel: Performance ist Umsatz

- Wenn Sie auf Ihrer Website kein Banner benötigen, messen Sie 100% Ihres Traffics. Bei klassischen Lösungen verlieren Sie oft 40-60% der Daten, weil Nutzer den Consent ablehnen. Mein System liefert Ihnen die echten Zahlen, da die technische Hürde der Zustimmung entfällt. + Daten von Deloitte belegen, dass die Verbesserung der Ladezeit die Conversion-Rates signifikant steigert. Jedes gesparte Tracking-Skript zahlt direkt auf diesen ROI ein. Ein stabiles System ist kein Kostenfaktor, sondern ein [ROI-Beschleuniger](/blog/clean-code-for-business-value). + +
- Ehrlichkeit zahlt sich hier direkt in der Genauigkeit Ihrer Marketing-Planung aus. Ein stabiles System ist kein Kostenfaktor, sondern ein [ROI-Beschleuniger](/blog/clean-code-for-business-value). + Wenn Sie auf Ihrer Website kein Banner benötigen, messen Sie 100% Ihres Traffics aggregiert. Es ist die ehrlichere Metrik für Ihre Marketing-Planung. Erfahren Sie hier mehr über die [versteckten Kosten von zu vielen Plugins](/blog/hidden-costs-of-wordpress-plugins), die oft auch Tracking-Balast mitbringen. - +

Fazit: Wissen ist Macht, Respekt ist Zukunft

- Messen Sie, was zählt – und schützen Sie, wer zählt. Ein moderner [systemischer Architektur-Ansatz für die DSGVO](/blog/gdpr-conformity-system-approach) macht Datenschutz zu Ihrem Wettbewerbsvorteil. Ich begleite Sie bei der Installation einer Lösung, die Professionalität und Ethik brillant vereint. Insights ohne Reue – das ist modernes digitales Management. + Messen Sie, was zählt – und schützen Sie, wer zählt. Ein moderner Ansatz macht Datenschutz zu Ihrem Wettbewerbsvorteil statt zum Compliance-Albtraum. Die Kombination aus maximaler Performance, Banner-Freiheit und präzisen Business-Insights ist heute kein "Entweder-oder" mehr. Wer seine Architektur heute auf Privacy-First umstellt, sichert sich die Datenhoheit für das nächste Jahrzehnt. -

Wofür brauche ich Analytics ohne Cookies?

- Es ermöglicht Ihnen, 100% Ihrer Besucherströme DSGVO-konform zu erfassen, ohne Nutzer mit nervigen Cookie-Bannern abzuschrecken. Gleichzeitig verbessern Sie Ihre Ladezeiten massiv, da schwerfällige externe Skripte entfallen. +

Verliere ich durch Cookieless Analytics wichtige Funktionen?

+ Sie verlieren die Möglichkeit, Einzelnutzer über Monate hinweg zu verfolgen. Für die meisten B2B-Unternehmen ist jedoch der Gewinn von 100% sauberen Trend-Daten und einer bannerfreien User Experience weitaus wertvoller. -

Ist Cookieless Tracking wirklich genau genug?

- Für Business-Insights und Trend-Analysen ist es exzellent geeignet. Obwohl die Wiedererkennung über sehr lange Zeiträume (Wochen/Monate) schwieriger ist als mit Cookies, überwiegt der Vorteil der lückenlosen Datenerfassung ohne Consent-Verluste. +

Ist Analytics ohne Banner wirklich 100% rechtssicher?

+ Ja, sofern keine personenbezogenen Daten (PII) gespeichert werden und die Anonymisierung bereits auf dem Server (First-Party) stattfindet. Dies eliminiert die Notwendigkeit für eine Einwilligung nach TTDSG/DSGVO. -

Erhöht der Verzicht auf Tracker die SEO-Performance?

- Ja, indirekt sehr stark. Durch den Verzicht auf schwere Tracking-Bibliotheken sinkt die Zeit bis zur Interaktion (INP) und der Lade-Speed (LCP) verbessert sich, was positive Signale an Google sendet. +

Wie wirkt sich der Verzicht auf Tracker auf SEO aus?

+ Extrem positiv. Durch verbesserte Core Web Vitals (da weniger JavaScript die Main-Thread-Arbeit blockiert) wertet Google die Seite höher ein, was zu besseren Rankings führen kann.
\ No newline at end of file diff --git a/apps/web/content/blog/build-first-digital-architecture.mdx b/apps/web/content/blog/build-first-digital-architecture.mdx index 0606a16..072e3ad 100644 --- a/apps/web/content/blog/build-first-digital-architecture.mdx +++ b/apps/web/content/blog/build-first-digital-architecture.mdx @@ -43,24 +43,7 @@ tags: ["architecture", "business"] durch maßgeschneiderte Systeme. -
- -graph TD - Need["Individuelles Business-Bedürfnis"] --> Path["Strategische Entscheidung"] - Path --> Buy["Software-Abo (SaaS)"] - Path --> Build["Bespoke Architecture (Mintel)"] - Buy --> Compromise["Kompromisse & Monatliche Fixkosten"] - Build --> Competitive["Wettbewerbsvorteil & Unendliche Freiheit"] - Compromise --> Stagnation["Digitaler Stillstand"] - Competitive --> Growth["Skalierung ohne Grenzen"] - style Build fill:#4ade80,stroke:#333 - style Growth fill:#4ade80,stroke:#333 - -
- Build vs. Buy: Investieren Sie in Ihr eigenes geistiges Eigentum statt - in die monatliche Miete von Fremdprodukten. -
-
+

Bauen bedeutet heute: Strategisches Kombinieren

diff --git a/apps/web/content/blog/digital-longevity-architecture.mdx b/apps/web/content/blog/digital-longevity-architecture.mdx index 1683b6b..af9adcb 100644 --- a/apps/web/content/blog/digital-longevity-architecture.mdx +++ b/apps/web/content/blog/digital-longevity-architecture.mdx @@ -44,23 +44,7 @@ tags: ["architecture", "longevity"] Abrissbirne. -
- -graph TD - Logic["Zukunftsfähige Kern-Logik"] --> Standards["Offene Web-Standards"] - Logic --> Modular["Modulare Komponenten"] - Standards --> Decade["Lebensdauer > 10 Jahre"] - Modular --> Update["Einfache Teil-Modernisierung"] - Decade --> ROI["Maximaler Return on Investment"] - Update --> ROI - style ROI fill:#4ade80,stroke:#333 - style Decade fill:#4ade80,stroke:#333 - -
- Architektur der Langlebigkeit: Durch die Trennung von Logik und Trends - sichern wir den Wert Ihrer digitalen Investition über Generationen. -
-
+

Die Ästhetik der Zeitlosigkeit

Langlebigkeit hat auch eine visuelle Komponente. diff --git a/apps/web/content/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist.mdx b/apps/web/content/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist.mdx index d7bec97..fb7a1f0 100644 --- a/apps/web/content/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist.mdx +++ b/apps/web/content/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist.mdx @@ -30,7 +30,6 @@ tags: ["performance", "seo", "conversion-optimization"] isCompany={true} source="Amazon CDN Study" sourceUrl="https://vwo.com/blog/100ms-latency-cost-amazon-1-percent-sales/" - translated={true} /> @@ -181,6 +180,8 @@ graph TD + + Mein System fungiert als ROI-Beschleuniger für Ihren gesamten digitalen Auftritt. Warum viele Agenturen bei diesem Thema scheitern, erkläre ich im Detail in meinem Artikel [Warum Ihre Agentur für kleine Änderungen Wochen braucht](/blog/why-agencies-are-slow). diff --git a/apps/web/content/blog/slow-loading-costs-customers.mdx b/apps/web/content/blog/slow-loading-costs-customers.mdx index d8d34f5..a9e21b6 100644 --- a/apps/web/content/blog/slow-loading-costs-customers.mdx +++ b/apps/web/content/blog/slow-loading-costs-customers.mdx @@ -122,6 +122,8 @@ graph LR
+ + { diff --git a/apps/web/scripts/verify-embeds.ts b/apps/web/scripts/verify-embeds.ts index 2ddd69e..8429b66 100644 --- a/apps/web/scripts/verify-embeds.ts +++ b/apps/web/scripts/verify-embeds.ts @@ -4,7 +4,7 @@ import puppeteer from 'puppeteer'; try { console.log("Starting Chrome..."); const browser = await puppeteer.launch({ - headless: 'new', + headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); diff --git a/apps/web/src/components/BoldNumber.tsx b/apps/web/src/components/BoldNumber.tsx index 122bcae..776f5db 100644 --- a/apps/web/src/components/BoldNumber.tsx +++ b/apps/web/src/components/BoldNumber.tsx @@ -1,6 +1,7 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useId } from 'react'; +import { ComponentShareButton } from './ComponentShareButton'; interface BoldNumberProps { /** The number to display, e.g. "53%" or "2.5M€" or "-20%" */ @@ -28,6 +29,7 @@ export const BoldNumber: React.FC = ({ const ref = useRef(null); const [isVisible, setIsVisible] = useState(false); const [displayValue, setDisplayValue] = useState(''); + const shareId = `boldnum-${useId().replace(/:/g, '')}`; // Extract numeric part for animation const numericMatch = value.match(/^([+-]?)(\d+(?:[.,]\d+)?)(.*)/); @@ -84,22 +86,16 @@ export const BoldNumber: React.FC = ({ return () => clearInterval(timer); }, [isVisible, value, prefix, suffix, targetNum, hasDecimals, numStr]); - const handleShare = async () => { - const shareText = `${value} — ${label}${source ? ` (${source})` : ''}`; - try { - if (navigator.share) { - await navigator.share({ text: shareText }); - } else { - await navigator.clipboard.writeText(shareText); - } - } catch { /* user cancelled */ } - }; - return (
+
+ +
+
{displayValue || value} @@ -119,19 +115,6 @@ export const BoldNumber: React.FC = ({ )}
- - {/* Share button - subtle now */} -
); }; diff --git a/apps/web/src/components/Button.tsx b/apps/web/src/components/Button.tsx index de3ca92..0c7224f 100644 --- a/apps/web/src/components/Button.tsx +++ b/apps/web/src/components/Button.tsx @@ -98,10 +98,10 @@ export const Button: React.FC = ({ // Binary stream overlay colors by variant const binaryColor = variant === "primary" - ? "rgba(255,255,255,0.06)" + ? "rgba(255,255,255,0.12)" : variant === "outline" - ? "rgba(59,130,246,0.08)" - : "rgba(148,163,184,0.06)"; + ? "rgba(59,130,246,0.15)" + : "rgba(148,163,184,0.15)"; const inner = ( = ({ style={{ opacity: hovered ? 1 : 0, transition: "opacity 0.3s ease" }} > = ({ targetId, - title = "Component", + title = "Mintel Architektur Insights", + shareText = "Schauen Sie sich diese spannende interaktive Simulation an:", className = "" }) => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -27,31 +30,131 @@ export const ComponentShareButton: React.FC = ({ ? `${window.location.origin}${window.location.pathname}#${targetId}` : ""; + const applyWatermark = async (base64Img: string, qrCodeSrc: string): Promise => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + // html-to-image uses pixelRatio: 2, so img.width/img.height are 2x + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + if (!ctx) return resolve(base64Img); + + // Draw main image + ctx.drawImage(img, 0, 0); + + // Watermark styling for 2x resolution + const padding = 32; + const wHeight = 110; + const wWidth = 360; + const x = canvas.width - wWidth - padding; + const y = canvas.height - wHeight - padding; + + // Draw pill shape background + ctx.fillStyle = "rgba(255, 255, 255, 0.95)"; + ctx.shadowColor = "rgba(0, 0, 0, 0.05)"; + ctx.shadowBlur = 10; + ctx.shadowOffsetY = 4; + + if (ctx.roundRect) { + ctx.beginPath(); + ctx.roundRect(x, y, wWidth, wHeight, 24); + ctx.fill(); + } else { + ctx.fillRect(x, y, wWidth, wHeight); + } + + // Draw text + ctx.shadowColor = "transparent"; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + + // "mintel." + ctx.font = "900 36px sans-serif"; + const textX = x + wWidth - wHeight - 12; // Right of text, left of QR code + const mintelText = "mintel"; + const dotText = "."; + const dotWidth = ctx.measureText(dotText).width; + const mintelWidth = ctx.measureText(mintelText).width; + + ctx.fillStyle = "#0ea5e9"; + ctx.fillText(dotText, textX, y + 40); + ctx.fillStyle = "#0f172a"; + ctx.fillText(mintelText, textX - dotWidth, y + 40); + + // "Artikel lesen" + ctx.font = "600 20px sans-serif"; + ctx.fillStyle = "#64748b"; + ctx.fillText("Zum Artikel", textX, y + 76); + + // Draw solid line separator + ctx.beginPath(); + ctx.moveTo(textX + 6, y + 20); + ctx.lineTo(textX + 6, y + wHeight - 20); + ctx.strokeStyle = "#e2e8f0"; + ctx.lineWidth = 2; + ctx.stroke(); + + // Draw QR Code + const qrImg = new Image(); + qrImg.onload = () => { + const qrSize = wHeight - 24; // padding 12*2 inside pill + const qrX = x + wWidth - qrSize - 12; + const qrY = y + 12; + ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize); + resolve(canvas.toDataURL("image/png")); + }; + qrImg.onerror = () => resolve(base64Img); // Error fallback + qrImg.src = qrCodeSrc; + }; + img.onerror = () => resolve(base64Img); // Error fallback + img.src = base64Img; + }); + }; + const handleOpenModal = async () => { setIsCapturing(true); try { const element = document.getElementById(targetId); if (element) { - const dataUrl = await htmlToImage.toPng(element, { + // Force explicit dimensions to prevent html-to-image from clipping + const w = element.offsetWidth; + const h = element.offsetHeight; + + const rawDataUrl = await htmlToImage.toPng(element, { quality: 1, type: 'image/png', pixelRatio: 2, - backgroundColor: 'transparent', + backgroundColor: 'white', skipFonts: true, - // Filter out any buttons that are part of the UI but shouldn't be in the image + width: w, + height: h, + style: { + transform: 'none', + margin: '0', + width: `${w}px`, + height: `${h}px` + }, filter: (node) => { - if (node.tagName === 'BUTTON' || (node as HTMLElement).dataset?.shareButton === 'true') { + const el = node as HTMLElement; + if (el.tagName === 'BUTTON' || el.dataset?.shareButton === 'true' || el.dataset?.shareWrapper === 'true') { return false; } return true; - }, - style: { - // Ensure we don't accidentally cut off by resetting transform/margins inside the clone - transform: 'none', } }); - setGeneratedImage(dataUrl); + // Generate QR Code + const qrDataUrl = await QRCode.toDataURL(currentUrl, { + width: 120, // slightly larger for sharper scaling + margin: 0, + color: { dark: '#0f172a', light: '#ffffff' } + }); + + // Add Watermark + const watermarkedImage = await applyWatermark(rawDataUrl, qrDataUrl); + setGeneratedImage(watermarkedImage); } } catch (err) { console.error("Failed to capture component:", err); @@ -60,40 +163,41 @@ export const ComponentShareButton: React.FC = ({ setIsModalOpen(true); trackEvent("component_share_opened", { component_id: targetId, - component_title: title, + component_title: title }); } }; return ( - <> +
- {/* ShareModal expects a direct image string in 'qrCodeData' or 'diagramImage' (except diagramImage specifically assumes SVGs). - Because ShareModal has logic that redraws diagramImage on a canvas assuming it's an SVG string, - we must bypass the SVG renderer. However, if we look at ShareModal, we need a way to pass a raw PNG. - Passing it as qrCodeData is a hack, or we can just send it via diagramImage and hope the canvas ignores it if it's already a Data URL. - Wait: ShareModal expects `diagramImage` (svg string) AND re-renders it. - Let's just pass our Data URL into a NEW prop or hijack the qrCodeData if necessary, but actually ShareModal only allows `diagramImage` as SVG logic right now. - Let's see if ShareModal needs an update to accept pure images, we'll check it. for now, let's pass it via diagramImage and see if we can adapt ShareModal. */} - - {/* We will adapt ShareModal to handle both SVG strings & base64 PNG inputs via `diagramImage` */} setIsModalOpen(false)} url={currentUrl} title={title} + description={shareText} diagramImage={generatedImage} + qrCodeData={generatedImage} // Let's try sending it to both to be safe depending on ShareModal's internals /> - +
); }; diff --git a/apps/web/src/components/MemeCard.tsx b/apps/web/src/components/MemeCard.tsx index dddd431..68109c3 100644 --- a/apps/web/src/components/MemeCard.tsx +++ b/apps/web/src/components/MemeCard.tsx @@ -25,11 +25,11 @@ export const MemeCard: React.FC = ({ template, captions, image, c if (image) { return ( -
+
-
-
+
+
{/* eslint-disable-next-line @next/next/no-img-element */} @@ -37,7 +37,9 @@ export const MemeCard: React.FC = ({ template, captions, image, c src={image} alt={`Meme: ${template} - ${captionList.join(' ')}`} className="w-full h-auto object-cover block" - loading="lazy" + loading="eager" + decoding="sync" + crossOrigin="anonymous" />
@@ -47,11 +49,11 @@ export const MemeCard: React.FC = ({ template, captions, image, c return ( -
+
-
-
+
+
@@ -62,9 +64,7 @@ export const MemeCard: React.FC = ({ template, captions, image, c {template === 'clown' && } {template === 'expanding' && } {template === 'distracted' && } - {!['drake', 'ds', 'gru', 'fine', 'clown', 'expanding', 'distracted'].includes(template) && ( - - )} +
diff --git a/apps/web/src/components/ShareModal.tsx b/apps/web/src/components/ShareModal.tsx index d93883e..66745f4 100644 --- a/apps/web/src/components/ShareModal.tsx +++ b/apps/web/src/components/ShareModal.tsx @@ -12,6 +12,7 @@ interface ShareModalProps { url: string; qrCodeData?: string; title?: string; + description?: string; diagramImage?: string; } @@ -21,6 +22,7 @@ export function ShareModal({ url, qrCodeData, title, + description, diagramImage, }: ShareModalProps) { const [copied, setCopied] = useState(false); @@ -47,6 +49,9 @@ export function ShareModal({ if (ogImage || ogTitle || ogDesc) { setOgData({ image: ogImage, title: ogTitle, description: ogDesc }); } + } else { + // Force clear OG data if we HAVE a diagramImage, to prevent fallback flashes + setOgData(null); } }, [isOpen, diagramImage, qrCodeData, title]); @@ -63,13 +68,6 @@ export function ShareModal({ const svgUrl = URL.createObjectURL(svgBlob); setImagePreview(svgUrl); } - - // Optional: If we want to strictly apply the watermark via Canvas, we would do it here. - // But for the sake of getting the preview to work reliably first, just setting the imagePreview - // directly to the data URL or SVG blob URL is the safest approach. The watermark logic was - // likely failing because `IconBlack` wasn't resolving correctly or `img.onload` wasn't firing - // properly in all environments. - } }, [diagramImage, isOpen]); @@ -83,7 +81,8 @@ export function ShareModal({ if (navigator.share) { try { const shareData: ShareData = { - title: title || "Mintel Diagramm", + title: title || "Mintel Architektur Insights", + text: description || "Schauen Sie sich diesen Beitrag an.", url: url, }; @@ -125,12 +124,13 @@ export function ShareModal({ }; const handleShareX = () => { - const text = encodeURIComponent(title || "Mintel Diagramm"); + const text = encodeURIComponent(`${description ? description + " " : ""}${title || "Mintel Architektur Insights"}`); const shareUrl = `https://twitter.com/intent/tweet?text=${text}&url=${encodeURIComponent(url)}`; window.open(shareUrl, "_blank", "width=550,height=420"); }; const handleShareLinkedIn = () => { + // LinkedIn share doesn't accept text directly via URL for personal profiles easily, but we pass url const shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`; window.open(shareUrl, "_blank", "width=550,height=420"); }; @@ -181,7 +181,7 @@ export function ShareModal({ {title
@@ -288,11 +288,11 @@ export function ShareModal({
-
+
)}
@@ -332,10 +332,10 @@ export function ShareModal({ {imagePreview && ( )}
diff --git a/apps/web/src/components/YouTubeEmbed.tsx b/apps/web/src/components/YouTubeEmbed.tsx index a9c6e62..5339238 100644 --- a/apps/web/src/components/YouTubeEmbed.tsx +++ b/apps/web/src/components/YouTubeEmbed.tsx @@ -16,7 +16,6 @@ export function YouTubeEmbed({ videoId, title, className = "" }: YouTubeEmbedPro className="absolute inset-0 w-full h-full" src={`https://www.youtube-nocookie.com/embed/${videoId}?rel=0&modestbranding=1`} title={title || "YouTube video player"} - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen loading="lazy" /> diff --git a/apps/web/src/components/simulations/ArchitectureBuilder.tsx b/apps/web/src/components/simulations/ArchitectureBuilder.tsx new file mode 100644 index 0000000..21ce62c --- /dev/null +++ b/apps/web/src/components/simulations/ArchitectureBuilder.tsx @@ -0,0 +1,136 @@ +"use client"; + +import React, { useState } from 'react'; +import { ComponentShareButton } from '../ComponentShareButton'; +import { Reveal } from '../Reveal'; +import { Check, X, Shield, Zap, Expand, Database } from 'lucide-react'; + +export function ArchitectureBuilder({ className = '' }: { className?: string }) { + const [stack, setStack] = useState<'saas' | 'custom'>('saas'); + + return ( + +
+
+ +
+
+ +
+ +
+
+ 🏗️ +

Architektur-Vergleich: Mieten vs. Bauen

+
+

+ Entdecken Sie die strukturellen Unterschiede zwischen einem "Out-of-the-Box" Baukasten und einer maßgeschneiderten Architektur für Ihr Unternehmen. +

+
+ +
+ {/* Control Panel */} +
+ + + + + +
+ + {/* Visualization Panel */} +
+ + {stack === 'saas' ? ( +
+
+
+
📦
+
Standard-Features
+
Sie zahlen auch für 80%, die Sie nicht nutzen.
+
+
+
+
+
+
🔌
+
Plugin-Chaos
+
Jedes Update ist ein Sicherheitsrisiko.
+
+
+ +
+
+
+ Abhängigkeit von Preiserhöhungen +
+
+
+ Kein Aufbau von eigenem Firmenwert (IP) +
+
+
+ Träge Ladezeiten durch geteilte Server +
+
+
+ ) : ( +
+
+
+
+
Edge Content
+
Instante globale Ladezeiten.
+
+
+
+
Headless API
+
Daten systemunabhängig.
+
+
+
+
Custom UI
+
100% Passform für Sie.
+
+
+ +
+
+
+ 0€ monatliche Fixkosten für Lizenzen +
+
+
+ Sie besitzen den Code = Echter Firmenwertaufbau +
+
+
+ Maximale Security (keine Standard-Plugins als Einfallstor) +
+
+
+ )} + +
+
+
+
+ + ); +} diff --git a/apps/web/src/components/simulations/DigitalAssetVisualizer.tsx b/apps/web/src/components/simulations/DigitalAssetVisualizer.tsx new file mode 100644 index 0000000..55c204c --- /dev/null +++ b/apps/web/src/components/simulations/DigitalAssetVisualizer.tsx @@ -0,0 +1,156 @@ +"use client"; + +import React, { useState } from 'react'; +import { ComponentShareButton } from '../ComponentShareButton'; +import { Reveal } from '../Reveal'; +import { TrendingUp, Coins, PiggyBank, Briefcase } from 'lucide-react'; + +export function DigitalAssetVisualizer({ className = '' }: { className?: string }) { + const [view, setView] = useState<'liability' | 'asset'>('asset'); + + return ( + +
+
+ +
+
+ +
+ +
+
+ 💎 +

Software: Ausgabe oder Investment?

+
+

+ Visualisieren Sie den Unterschied zwischen Software, die monatlich Geld verbrennt (SaaS-Miete), und Software, die als Firmenwert bilanziert werden kann (Eigenbau). +

+
+ +
+ {/* Control Panel */} +
+ + + + + +
+ + {/* Visualization Panel */} +
+ + {view === 'liability' ? ( +
+
+ + {/* Money Burn Animation */} +
+ {/* Fire background blob */} +
+ + {/* Money flying away */} +
-€
+
-€
+
-€
+ +
+ 💸 +
+
+ +
+
Der ewige Geldabfluss
+

+ Jeden Monat fließt Kapital an externe Software-Anbieter ab. Kündigen Sie, ist Ihr System sofort offline. +

+
+
+ +
+
+
Unternehmenswert
+
0,00 €
+
Software gehört Anbieter
+
+
+
Skalierungs-Kosten
+
Linear
+
Mehr User = Mehr Kosten
+
+
+
+ ) : ( +
+
+ + {/* Asset Building Animation */} +
+ {/* Glow background blob */} +
+ + {/* Value accumulating */} +
+
+ +
+ +
+
+ +
+
Das aktivierbare Asset
+

+ Maßgeschneiderte Software gehört zu 100% Ihnen. Das Intellectual Property (IP) mehrt den Wert Ihrer GmbH. +

+
+
+ +
+
+
Unternehmenswert
+
Steigt aktiv
+
Kann bilanziert werden
+
+
+
Skalierungs-Kosten
+
Flach (0€ Lizenzen)
+
Nur reine Serverkosten
+
+
+
+ )} + +
+
+
+
+ + ); +} diff --git a/apps/web/src/components/simulations/LoadTimeSimulator.tsx b/apps/web/src/components/simulations/LoadTimeSimulator.tsx new file mode 100644 index 0000000..5f0a478 --- /dev/null +++ b/apps/web/src/components/simulations/LoadTimeSimulator.tsx @@ -0,0 +1,201 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { ComponentShareButton } from '../ComponentShareButton'; +import { Reveal } from '../Reveal'; +import { Play, RotateCcw } from 'lucide-react'; + +export function LoadTimeSimulator({ className = '' }: { className?: string }) { + const [isRunning, setIsRunning] = useState(false); + const [timeElapsed, setTimeElapsed] = useState(0); + const [legacyState, setLegacyState] = useState(0); + const [mintelState, setMintelState] = useState(0); + + useEffect(() => { + let interval: NodeJS.Timeout; + if (isRunning && timeElapsed < 7500) { + interval = setInterval(() => { + setTimeElapsed(prev => { + const next = prev + 50; + + // Legacy Loading Logic (stuttering, slow, layout shifts) + if (next >= 1200 && next < 3000) setLegacyState(1); // Header only + else if (next >= 3000 && next < 5500) setLegacyState(2); // Text appears + else if (next >= 5500 && next < 7000) setLegacyState(3); // Image loads (CLS) + else if (next >= 7000) setLegacyState(4); // Finally interactive + + // Mintel Loading Logic (Instant Edge Delivery) + if (next >= 350) setMintelState(1); // Fully loaded instantly + + return next; + }); + }, 50); + } else if (timeElapsed >= 7500) { + setIsRunning(false); + } + return () => clearInterval(interval); + }, [isRunning, timeElapsed]); + + const startRace = () => { + setTimeElapsed(0); + setLegacyState(0); + setMintelState(0); + setIsRunning(true); + }; + + return ( + +
+
+ +
+
+ +
+ +
+
+
+ 🏎️ +

Architektur-Rennen: Legacy CMS vs. Mintel Stack

+
+

+ Simulieren Sie den Unterschied zwischen dynamischem Server-Rendering (PHP/MySQL) und statischer Edge-Auslieferung (TTV < 500ms). +

+
+ +
+ +
+ + {/* LEGACY LANE */} +
+
+ Legacy Monolith + + {legacyState === 4 ? "7.00s" : (timeElapsed / 1000).toFixed(2) + "s"} + +
+ + {/* Browser Mockup */} +
+
+
+
+
+
+
+ {/* Spinner */} + {legacyState === 0 && ( +
+
+ TTFB Waiting... +
+ )} + + {/* Content that shuffles around to simulate CLS */} +
= 1 ? 'opacity-100' : 'opacity-0'}`}> +
{/* Header */} +
+ + {/* + Simulate Layout Shift: + Text loads first (state 2), then at state 3 an image drops in above it, pushing the text down. + */} +
+ {legacyState >= 3 && ( +
+
+ 🖼️ +
+
+ )} +
= 2 ? 'opacity-100' : 'opacity-0'} ${legacyState === 3 ? 'translate-y-2' : ''}`}> +
+
+
+
+
+ + {legacyState === 4 && ( +
+
+ Interaktiv (Hydrated) +
+
+ )} +
+
+
+ {legacyState === 0 && timeElapsed > 0 && Waiting for Server...} + {legacyState === 1 && Parsing HTML...} + {legacyState === 2 && Downloading Assets...} + {legacyState === 3 && Layout Shift Detected!} + {legacyState === 4 && Finished in 7.0s} +
+
+ + {/* MINTEL LANE */} +
+
+ Mintel Stack (Edge) + + {mintelState === 1 ? "0.35s" : (timeElapsed / 1000).toFixed(2) + "s"} + +
+ + {/* Browser Mockup */} +
+
+
+
+
+
+
+ + {mintelState === 0 && ( +
+ CDN Hit... +
+ )} + + {/* Instant Load: Everything drops in perfectly immediately */} +
+
+ +
+ 🚀 +
+ +
+
+
+
+
+ +
+
+ Instant Interactive +
+
+
+
+
+
+ {mintelState === 0 && timeElapsed > 0 && Routing to Edge...} + {mintelState === 1 && Loaded & Ready in 350ms} +
+
+ +
+
+
+ + ); +} diff --git a/apps/web/src/components/simulations/PerformanceROICalculator.tsx b/apps/web/src/components/simulations/PerformanceROICalculator.tsx new file mode 100644 index 0000000..6ca15ec --- /dev/null +++ b/apps/web/src/components/simulations/PerformanceROICalculator.tsx @@ -0,0 +1,124 @@ +"use client"; + +import React, { useState } from 'react'; +import { ComponentShareButton } from '../ComponentShareButton'; +import { Reveal } from '../Reveal'; + +export function PerformanceROICalculator({ className = '' }: { className?: string }) { + const [traffic, setTraffic] = useState(100); // 1000 visitors/month (approx 30/day) + const [aov, setAov] = useState(100); // e.g. average value of a craftsman quote or small B2B service + const [loadTime, setLoadTime] = useState(2.0); + const [baseConv, setBaseConv] = useState(2.0); + + // B2B Deloitte metric: 0.1s improvement = 8.4% conversion relative lift + // We cap the improvement to realistic bounds (e.g. optimizing to 1.5s) + const targetLoadTime = 1.5; + const secondsSaved = Math.max(0, loadTime - targetLoadTime); + + // Relative lift calculation + const relativeLiftPercentage = secondsSaved * 10 * 8.4; + const newConv = baseConv * (1 + (relativeLiftPercentage / 100)); + + const currentRevenue = traffic * (baseConv / 100) * aov; + const newRevenue = traffic * (newConv / 100) * aov; + const additionalAnnualRevenue = (newRevenue - currentRevenue) * 12; + + const formatCurrency = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(val); + + return ( + +
+
+ +
+
+ +
+ +
+
+ +

Performance ROI für den Mittelstand

+
+

+ Simulieren Sie, wie sich die Ladezeit auf die Jahresumsätze Ihres Betriebs (Handwerk, Kanzlei, B2B-Service) auswirkt. Basierend auf der Branchen-Erkenntnis: Schon 0.1s schnellere Ladezeit hebt die Kontakt-Wandlungsrate um bis zu 8,4%. +

+
+ +
+ {/* Inputs */} +
+
+
+ + {loadTime.toFixed(1)}s +
+ setLoadTime(Number(e.target.value))} + className="w-full accent-slate-900" + /> +
+ +
+
+ + {traffic.toLocaleString('de-DE')} +
+ setTraffic(Number(e.target.value))} + className="w-full accent-slate-900" + /> +
+ +
+
+ + {formatCurrency(aov)} +
+ setAov(Number(e.target.value))} + className="w-full accent-slate-900" + /> +
+
+ + {/* Outputs */} +
+
+
+
Status Quo Conv.
+
{baseConv.toFixed(2)}%
+
{formatCurrency(currentRevenue * 12)} / Jahr
+
+
+
Ziel Konversion (1.5s)
+
+ {newConv.toFixed(2)}% + {relativeLiftPercentage > 0 && +{relativeLiftPercentage.toFixed(1)}%} +
+
{formatCurrency(newRevenue * 12)} / Jahr
+
+
+ +
+
+
+
+
Potenzieller Mehrumsatz / Jahr
+
Durch {loadTime.toFixed(1)}s Ladezeit vs. Industrie-Standard (1.5s) entgangen.
+
+
+ {additionalAnnualRevenue > 0 ? '+' : ''}{formatCurrency(additionalAnnualRevenue)} +
+
+
+
+
+
+
+ + ); +} diff --git a/apps/web/src/content-engine/components.ts b/apps/web/src/content-engine/components.ts index 1a90da6..dafa8a0 100644 --- a/apps/web/src/content-engine/components.ts +++ b/apps/web/src/content-engine/components.ts @@ -37,6 +37,10 @@ import { TableOfContents } from '../components/TableOfContents'; import { RevenueLossCalculator } from "../components/RevenueLossCalculator"; import { PerformanceChart } from "../components/PerformanceChart"; +import { PerformanceROICalculator } from "../components/simulations/PerformanceROICalculator"; +import { LoadTimeSimulator } from "../components/simulations/LoadTimeSimulator"; +import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder"; +import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer"; import { TwitterEmbed } from '../components/TwitterEmbed'; import { YouTubeEmbed } from '../components/YouTubeEmbed'; @@ -79,6 +83,10 @@ export const mdxComponents = { TableOfContents, RevenueLossCalculator, PerformanceChart, + PerformanceROICalculator, + LoadTimeSimulator, + ArchitectureBuilder, + DigitalAssetVisualizer, TwitterEmbed, YouTubeEmbed, LinkedInEmbed, diff --git a/apps/web/src/content-engine/definitions.ts b/apps/web/src/content-engine/definitions.ts index 3505df7..e247db3 100644 --- a/apps/web/src/content-engine/definitions.ts +++ b/apps/web/src/content-engine/definitions.ts @@ -260,12 +260,32 @@ timeline }, { name: 'Button', - description: 'Premium pill-shaped button for high-impact CTAs. Variants: primary (solid dark), outline (bordered), ghost (text only). use size="large" for main sections.', + description: 'Premium pill-shaped button for high-impact CTAs. Variants: primary (solid dark), outline (bordered), ghost (text only). use size="large" for main sections. IMPORTANT: Write natural, punchy, conversational German CTAs (e.g. "Webprojekt anfragen", "Zur Beratung", "Mehr erfahren"). NEVER generate robotic or awkward phrases like "Ethisches Tracking anfragen" or "Ladezeit optimieren lassen". Keep it human and professional.', usageExample: '' }, + { + name: 'PerformanceROICalculator', + description: 'Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.', + usageExample: '' + }, + { + name: 'LoadTimeSimulator', + description: 'Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.', + usageExample: '' + }, { name: 'FAQSection', description: 'Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.', usageExample: '\n

Frage 1

\n Antwort 1\n
' + }, + { + name: 'ArchitectureBuilder', + description: 'Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.', + usageExample: '' + }, + { + name: 'DigitalAssetVisualizer', + description: 'Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.', + usageExample: '' } ];