diff --git a/apps/web/app/about/page.tsx b/apps/web/app/about/page.tsx index 5d411a4..281f923 100644 --- a/apps/web/app/about/page.tsx +++ b/apps/web/app/about/page.tsx @@ -1,23 +1,28 @@ -import * as React from 'react'; -import Image from 'next/image'; -import { PageHeader } from '../../src/components/PageHeader'; -import { Section } from '../../src/components/Section'; -import { Reveal } from '../../src/components/Reveal'; -import { - ExperienceIllustration, - ResponsibilityIllustration, +import Image from "next/image"; +import { PageHeader } from "../../src/components/PageHeader"; +import { Section } from "../../src/components/Section"; +import { Reveal } from "../../src/components/Reveal"; +import { + ExperienceIllustration, + ResponsibilityIllustration, ResultIllustration, ConceptSystem, - ConceptTarget, ContactIllustration, HeroLines, ParticleNetwork, - GridLines -} from '../../src/components/Landing'; -import { Check } from 'lucide-react'; -import { H3, H4, LeadText, BodyText, Label, MonoLabel } from '../../src/components/Typography'; -import { BackgroundGrid, Card, Container } from '../../src/components/Layout'; -import { Button } from '../../src/components/Button'; + GridLines, +} from "../../src/components/Landing"; +import { Check } from "lucide-react"; +import { + H3, + H4, + LeadText, + BodyText, + Label, + MonoLabel, +} from "../../src/components/Typography"; +import { BackgroundGrid, Card, Container } from "../../src/components/Layout"; +import { Button } from "../../src/components/Button"; export default function AboutPage() { return ( @@ -29,12 +34,12 @@ export default function AboutPage() { {/* Hero Section */}
- +
- +
- +
@@ -42,12 +47,14 @@ export default function AboutPage() { {/* Structural rings around avatar */}
- +
- Marc Mintel
@@ -58,13 +65,19 @@ export default function AboutPage() {
-
- Digital Architect -
+
+ + Digital Architect + +
- Über mich.} + + Über mich. + + } description="Warum ich tue, was ich tue – und wie Sie davon profitieren." className="pt-0 md:pt-0" /> @@ -87,7 +100,9 @@ export default function AboutPage() {

15 Jahre Web-Entwicklung.
- Vom Designer zum Architekten. + + Vom Designer zum Architekten. +

@@ -95,13 +110,17 @@ export default function AboutPage() {
- Ich habe Agenturen, Konzerne und Startups von innen gesehen. Dabei habe ich gelernt, what really counts: Ergebnisse, nicht Prozesse. + Ich habe Agenturen, Konzerne und Startups von innen gesehen. + Dabei habe ich gelernt, what really counts:{" "} + + Ergebnisse, nicht Prozesse. +
    {[ - 'Komplexe Systeme vereinfacht', - 'Performance-Probleme gelöst', - 'Nachhaltige Software-Architekturen gebaut' + "Komplexe Systeme vereinfacht", + "Performance-Probleme gelöst", + "Nachhaltige Software-Architekturen gebaut", ].map((item, i) => (
  • @@ -112,11 +131,22 @@ export default function AboutPage() {
    - -

    Mein Fokus heute: Direkte Zusammenarbeit ohne Reibungsverluste.

    + +

    + Mein Fokus heute: Direkte Zusammenarbeit ohne + Reibungsverluste. +

    - {['Effizient', 'Pragmatisch', 'Verlässlich'].map((tag, i) => ( - + {["Effizient", "Pragmatisch", "Verlässlich"].map((tag, i) => ( + ))} @@ -147,14 +177,24 @@ export default function AboutPage() {
    - In der klassischen Agenturwelt verschwindet Verantwortung oft hinter Hierarchien. Bei mir gibt es nur einen Ansprechpartner: Mich. + In der klassischen Agenturwelt verschwindet Verantwortung oft + hinter Hierarchien. Bei mir gibt es nur{" "} + einen Ansprechpartner:{" "} + Mich. - -
    !
    + +
    + ! +
    - Ich übernehme die volle Verantwortung für die technische Umsetzung und Qualität Ihres Projekts. Ohne Ausreden. + Ich übernehme die volle Verantwortung für die technische + Umsetzung und Qualität Ihres Projekts. Ohne Ausreden.
    @@ -182,11 +222,20 @@ export default function AboutPage() {
    - Ich baue keine Wegwerf-Produkte. Meine Systeme sind so konzipiert, dass sie mit Ihrem Unternehmen wachsen können. + Ich baue keine Wegwerf-Produkte. Meine Systeme sind so + konzipiert, dass sie mit Ihrem Unternehmen{" "} + wachsen können.
    - {['Skalierbar', 'Wartbar', 'Performant', 'Sicher', 'Unabhängig', 'Zukunftssicher'].map((item, i) => ( + {[ + "Skalierbar", + "Wartbar", + "Performant", + "Sicher", + "Unabhängig", + "Zukunftssicher", + ].map((item, i) => (
    @@ -199,11 +248,18 @@ export default function AboutPage() {
    - +
    -

    Kein Vendor Lock-in.

    +

    + Kein Vendor Lock-in. +

    - Sie behalten die volle Kontrolle über Ihren Code und Ihre Daten. Keine Abhängigkeit von proprietären Systemen. + Sie behalten die volle Kontrolle über Ihren Code und Ihre + Daten. Keine Abhängigkeit von proprietären Systemen. @@ -231,9 +287,19 @@ export default function AboutPage() {
    - {['Agentur-Zirkus', 'Meeting-Marathon', 'Ticket-Wahnsinn', 'CMS-Frust'].map((item, i) => ( - - {item} + {[ + "Agentur-Zirkus", + "Meeting-Marathon", + "Ticket-Wahnsinn", + "CMS-Frust", + ].map((item, i) => ( + + + {item} + ))}
    @@ -244,9 +310,18 @@ export default function AboutPage() {
    {[ - { label: 'Direkte Kommunikation', desc: 'Kurze Wege, schnelle Entscheidungen.' }, - { label: 'Echte Expertise', desc: 'Fundiertes Wissen aus 15 Jahren Praxis.' }, - { label: 'Messbare Qualität', desc: 'Code, der hält, was er verspricht.' } + { + label: "Direkte Kommunikation", + desc: "Kurze Wege, schnelle Entscheidungen.", + }, + { + label: "Echte Expertise", + desc: "Fundiertes Wissen aus 15 Jahren Praxis.", + }, + { + label: "Messbare Qualität", + desc: "Code, der hält, was er verspricht.", + }, ].map((item, i) => (
    @@ -255,7 +330,9 @@ export default function AboutPage() {

    {item.label}

    - {item.desc} + + {item.desc} +
    @@ -283,18 +360,22 @@ export default function AboutPage() { - +
    - +
    - Lassen Sie uns gemeinsam etwas bauen, das wirklich funktioniert. + Lassen Sie uns gemeinsam etwas bauen, das{" "} + wirklich funktioniert. - +
    - +
    diff --git a/apps/web/app/blog/[slug]/page.tsx b/apps/web/app/blog/[slug]/page.tsx index 8b78170..05c093c 100644 --- a/apps/web/app/blog/[slug]/page.tsx +++ b/apps/web/app/blog/[slug]/page.tsx @@ -1,17 +1,19 @@ -import * as React from 'react'; -import { notFound } from 'next/navigation'; -import { blogPosts } from '../../../src/data/blogPosts'; -import { Tag } from '../../../src/components/Tag'; -import { CodeBlock } from '../../../src/components/ArticleBlockquote'; -import { H2 } from '../../../src/components/ArticleHeading'; -import { Paragraph, LeadParagraph } from '../../../src/components/ArticleParagraph'; -import { UL, LI } from '../../../src/components/ArticleList'; -import { FileExamplesList } from '../../../src/components/FileExamplesList'; -import { FileExampleManager } from '../../../src/data/fileExamples'; -import { BlogPostClient } from '../../../src/components/BlogPostClient'; -import { PageHeader } from '../../../src/components/PageHeader'; -import { Section } from '../../../src/components/Section'; -import { Reveal } from '../../../src/components/Reveal'; +import * as React from "react"; +import { notFound } from "next/navigation"; +import { blogPosts } from "../../../src/data/blogPosts"; +import { Tag } from "../../../src/components/Tag"; +import { CodeBlock } from "../../../src/components/ArticleBlockquote"; +import { H2 } from "../../../src/components/ArticleHeading"; +import { + Paragraph, + LeadParagraph, +} from "../../../src/components/ArticleParagraph"; +import { UL, LI } from "../../../src/components/ArticleList"; +import { FileExamplesList } from "../../../src/components/FileExamplesList"; +import { FileExampleManager } from "../../../src/data/fileExamples"; +import { BlogPostClient } from "../../../src/components/BlogPostClient"; +import { PageHeader } from "../../../src/components/PageHeader"; +import { Section } from "../../../src/components/Section"; export async function generateStaticParams() { return blogPosts.map((post) => ({ @@ -19,7 +21,11 @@ export async function generateStaticParams() { })); } -export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { +export default async function BlogPostPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { const { slug } = await params; const post = blogPosts.find((p) => p.slug === slug); @@ -27,17 +33,23 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: notFound(); } - const formattedDate = new Date(post.date).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric' + const formattedDate = new Date(post.date).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", }); const wordCount = post.description.split(/\s+/).length + 100; const readingTime = Math.max(1, Math.ceil(wordCount / 200)); - const showFileExamples = post.tags?.some(tag => - ['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag) + const showFileExamples = post.tags?.some((tag) => + [ + "architecture", + "design-patterns", + "system-design", + "docker", + "deployment", + ].includes(tag), ); // Load file examples for the post @@ -59,10 +71,10 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
    - @@ -83,14 +95,16 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
    )} - {slug === 'first-note' && ( + {slug === "first-note" && ( <> - This blog is a public notebook. It's where I document things I learn, problems I solve, and tools I test. + This blog is a public notebook. It's where I document things I + learn, problems I solve, and tools I test.

    Why write in public?

    - I forget things. Writing them down helps. Making them public helps me think more clearly and might help someone else. + I forget things. Writing them down helps. Making them public + helps me think more clearly and might help someone else.

    What to expect

      @@ -101,91 +115,114 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
    )} - - {slug === 'debugging-tips' && ( + + {slug === "debugging-tips" && ( <> - Sometimes the simplest debugging tool is the best one. Print statements get a bad reputation, but they're often exactly what you need. + Sometimes the simplest debugging tool is the best one. Print + statements get a bad reputation, but they're often exactly + what you need.

    Why print statements work

    - Debuggers are powerful, but they change how your code runs. Print statements don't. + Debuggers are powerful, but they change how your code runs. + Print statements don't. -{`def process_data(data): + {`def process_data(data): print(f"Processing {len(data)} items") result = expensive_operation(data) print(f"Operation result: {result}") return result`} - +

    Complete examples

    - Here are some practical file examples you can copy and download. These include proper error handling and logging. + Here are some practical file examples you can copy and + download. These include proper error handling and logging. - +
    )} - {slug === 'architecture-patterns' && ( + {slug === "architecture-patterns" && ( <> - Good software architecture is about making the right decisions early. Here are some patterns I've found useful in production systems. + Good software architecture is about making the right decisions + early. Here are some patterns I've found useful in production + systems.

    Repository Pattern

    - The repository pattern provides a clean separation between your business logic and data access layer. It makes your code more testable and maintainable. + The repository pattern provides a clean separation between + your business logic and data access layer. It makes your code + more testable and maintainable. - +

    Service Layer

    - Services orchestrate business logic and coordinate between repositories and domain events. They keep your controllers thin and your business rules organized. + Services orchestrate business logic and coordinate between + repositories and domain events. They keep your controllers + thin and your business rules organized. - +

    Domain Events

    - Domain events help you decouple components and react to changes in your system. They're essential for building scalable, event-driven architectures. + Domain events help you decouple components and react to + changes in your system. They're essential for building + scalable, event-driven architectures. - +

    Complete examples

    - These TypeScript examples demonstrate modern architecture patterns for scalable applications. You can copy them directly into your project. + These TypeScript examples demonstrate modern architecture + patterns for scalable applications. You can copy them directly + into your project. - +
    )} - {slug === 'docker-deployment' && ( + {slug === "docker-deployment" && ( <> - Docker has become the standard for containerizing applications. Here's how to set up production-ready deployments that are secure, efficient, and maintainable. + Docker has become the standard for containerizing + applications. Here's how to set up production-ready + deployments that are secure, efficient, and maintainable.

    Multi-stage builds

    - Multi-stage builds keep your production images small and secure by separating build and runtime environments. This reduces attack surface and speeds up deployments. + Multi-stage builds keep your production images small and + secure by separating build and runtime environments. This + reduces attack surface and speeds up deployments. - +

    Health checks and monitoring

    - Proper health checks ensure your containers are running correctly. Combined with restart policies, this gives you resilient, self-healing deployments. + Proper health checks ensure your containers are running + correctly. Combined with restart policies, this gives you + resilient, self-healing deployments. - +

    Orchestration with Docker Compose

    - Docker Compose makes it easy to manage multi-service applications in development and production. Define services, networks, and volumes in a single file. + Docker Compose makes it easy to manage multi-service + applications in development and production. Define services, + networks, and volumes in a single file. - +

    Complete examples

    - These Docker configurations are production-ready. Use them as a starting point for your own deployments. + These Docker configurations are production-ready. Use them as + a starting point for your own deployments. - +
    diff --git a/apps/web/app/case-studies/klz-cables/page.tsx b/apps/web/app/case-studies/klz-cables/page.tsx index d5d3391..2ce937d 100644 --- a/apps/web/app/case-studies/klz-cables/page.tsx +++ b/apps/web/app/case-studies/klz-cables/page.tsx @@ -1,29 +1,29 @@ -'use client'; +"use client"; -import React from 'react'; -import { motion, useScroll, useTransform } from 'framer-motion'; -import { Section } from '../../../src/components/Section'; -import { Reveal } from '../../../src/components/Reveal'; -import { H1, H2, H3, LeadText, Label, MonoLabel, BodyText } from '../../../src/components/Typography'; -import { BackgroundGrid, Container } from '../../../src/components/Layout'; -import { MotionButton } from '../../../src/components/Button'; -import { IframeSection } from '../../../src/components/IframeSection'; +import React from "react"; +import { motion, useScroll, useTransform } from "framer-motion"; +import { Section } from "../../../src/components/Section"; +import { Reveal } from "../../../src/components/Reveal"; import { - Activity, - Database, - Layout, - Users, - ArrowRight, - Zap, - ShieldCheck, - Globe2, - Settings, - Search, - Monitor, - Cpu, - Server, - Layers -} from 'lucide-react'; + H1, + H2, + H3, + LeadText, + Label, + MonoLabel, + BodyText, +} from "../../../src/components/Typography"; +import { BackgroundGrid, Container } from "../../../src/components/Layout"; +import { MotionButton } from "../../../src/components/Button"; +import { IframeSection } from "../../../src/components/IframeSection"; +import { + Activity, + ArrowRight, + ShieldCheck, + Cpu, + Server, + Layers, +} from "lucide-react"; /** * TECHNICAL MARKER COMPONENT @@ -31,461 +31,554 @@ import { * Updated: Only yellow marker as requested. */ const Marker: React.FC<{ children: React.ReactNode; delay?: number }> = ({ - children, - delay = 0 + children, + delay = 0, }) => { - return ( - - - ); + return ( + + + ); }; export default function KLZCablesCaseStudy() { - const { scrollYProgress } = useScroll(); - const heroY = useTransform(scrollYProgress, [0, 0.2], [0, -20]); - const heroOpacity = useTransform(scrollYProgress, [0, 0.15], [1, 0]); - const gridRotate = useTransform(scrollYProgress, [0, 1], [0, 2]); + const { scrollYProgress } = useScroll(); + const heroY = useTransform(scrollYProgress, [0, 0.2], [0, -20]); + const heroOpacity = useTransform(scrollYProgress, [0, 0.15], [1, 0]); + const gridRotate = useTransform(scrollYProgress, [0, 1], [0, 2]); - return ( -
    - - - + return ( +
    + + + - {/* --- HERO: INDUSTRIAL INFRASTRUCTURE --- */} -
    + {/* --- HERO: INDUSTRIAL INFRASTRUCTURE --- */} +
    + + + +
    + +
    +
    + + SYSTEM-ARCHITEKTUR // 2025 + + +
    +
    +
    - -
    - -
    - -
    - SYSTEM-ARCHITEKTUR // 2025 - -
    -
    -
    +
    + +

    + KLZ Cables +
    + Case Study. +

    +
    -
    - -

    - KLZ Cables
    - Case Study. -

    -
    + +
    + + Engineering eines
    + B2B Commerce Systems. +
    + + Vom statischen Altsystem zum industriellen Standard. Ich + habe das KLZ-System auf das Wesentliche reduziert: Hardened + Infrastructure, parametrische Datenpflege und zero + maintenance. + +
    +
    +
    - -
    - - Engineering eines
    - B2B Commerce Systems. -
    - - Vom statischen Altsystem zum industriellen Standard. Ich habe das KLZ-System auf das Wesentliche reduziert: Hardened Infrastructure, parametrische Datenpflege und zero maintenance. - -
    -
    + +
    +
    + +
    +
    + + Relational Data + +
    +
    +
    + +
    + + WP + Varnish +
    +
    +
    + +
    + +
    + + {/* --- SECTION 01: ARCHITECTURE --- */} +
    +
    +
    + +

    + Architektur-
    + Refactor. +

    +
    +
    +
    + +
    + + Vom statischen HTML zur zentralen Daten-Instanz. + + + Ich habe die KLZ-Architektur radikal auf einen entkoppelten + High-Performance-Stack umgestellt. WordPress fungiert hier + nicht als CMS-Baukasten, sondern als{" "} + Headless JSON-Provider. Durch die + Implementierung nativer PHP-Microservices und den Verzicht auf + volatile Drittanbieter-Plugins wurde ein System geschaffen, + das keine technologischen Überraschungen zulässt.{" "} + Stability by Design. + +
    +
    +
    +
    + + +
    + +
    + {[ + { + label: "Edge Caching", + desc: "Varnish + W3TC Object Cache", + icon: , + }, + { + label: "Analytics", + desc: "Independent (Global Data Compliance)", + icon: , + }, + { + label: "Custom Core", + desc: "REST via Native Services", + icon: , + }, + ].map((item, i) => ( + +
    {item.icon}
    +
    + + {item.label} + + + {item.desc} +
    - - -
    -
    - -
    -
    - Relational Data -
    -
    -
    - -
    - - WP + Varnish -
    -
    -
    - -
    - -
    - - {/* --- SECTION 01: ARCHITECTURE --- */} -
    -
    -
    - -

    - Architektur-
    Refactor. -

    -
    -
    -
    - -
    - - Vom statischen HTML zur zentralen Daten-Instanz. - - - Ich habe die KLZ-Architektur radikal auf einen entkoppelten High-Performance-Stack umgestellt. WordPress fungiert hier nicht als CMS-Baukasten, sondern als Headless JSON-Provider. Durch die Implementierung nativer PHP-Microservices und den Verzicht auf volatile Drittanbieter-Plugins wurde ein System geschaffen, das keine technologischen Überraschungen zulässt. Stability by Design. - -
    -
    -
    -
    - - -
    - -
    - {[ - { label: 'Edge Caching', desc: 'Varnish + W3TC Object Cache', icon: }, - { label: 'Analytics', desc: 'Independent (Global Data Compliance)', icon: }, - { label: 'Custom Core', desc: 'REST via Native Services', icon: } - ].map((item, i) => ( - -
    {item.icon}
    -
    - {item.label} - {item.desc} -
    -
    - ))} -
    -
    -
    -
    -
    + + ))} +
    -
- - {/* --- SHOWCASE: LANDING --- */} -
-
- -
-
- -

Global Hub.

-
-
-
- - - - - -
-
- - {/* --- SECTION 02: TECHNICAL DETAIL --- */} -
-
-
- -
- -

Automated Documentation.

-
-
- - - Für Hochspannungs-N2XS(F)2Y Kabel ist Datentreue eine Sicherheitsanforderung. Ich habe eine automatisierte Asset-Pipeline entwickelt, die technische Datenblätter serverseitig generiert und validiert. - - -
- - - -
- -
-
-
-
-
- - {/* --- SECTION 03: COMMERCE --- */} -
-
-
- -

- Fokus auf
Spezifikationen. -

-
-
- -
- - - - - -
- -
- -
- - - Der Produktbereich wurde konsequent auf die Bedürfnisse technischer Planer optimiert. Klare Hierarchien und der Verzicht auf E-Commerce-Rauschen ermöglichen einen direkten Zugriff auf Kabel-Parameter und Datenblätter. - - - - Strukturierte Aufbereitung technischer Produktdaten. - -
-
-
-
-
- - {/* --- SECTION 04: CONTENT ENGINE --- */} -
-
-
- -
- -

Insights & News.

- - Die News-Engine dient als technischer Hub für Industrie-Standards. Durch die Implementierung eines performanten Blog-Systems wird Fachwissen direkt an die Zielgruppe kommuniziert. - -
-
-
-
- - - - - -
-
-
- - {/* --- SECTION 05: TEAM & TRUST --- */} -
-
- -

System-Lifecycle.

- - Die Migration von einer statischen Datei-Struktur zu einer zentralisierten Daten-Instanz eliminiert technische Schulden und manuelle Fehlerquellen. Das Ergebnis ist eine wartungsfreie Architektur, die technische Datentreue über den gesamten Produkt-Lifecycle sicherstellt. - -
- - -
-
- -
-
-
-
-
- - {/* --- SECTION 06: CONVERSION --- */} -
-
-
- - - - - -
-
- -
- -

Direkter Draht.

- - Das Kontakt-System wurde auf maximale Reduktion getrimmt. Keine unnötigen Hürden, sondern ein direkter Kommunikations-Kanal zwischen technischem Bedarf und individueller Beratung. - -
-
-
-
-
- - {/* --- FINAL CTA: ARCHITECTURE & VALUE --- */} -
- - -
-
- -
- CONSULTING // ENGINEERING -

- Architektur
- ohne Altlasten. -

-
-
- - - Vom Prototyp zum industriellen Standard. Ich entwickle digitale Infrastrukturen, die technische Freiheit und operative Stabilität garantieren – wartungsfrei und skalierbar. - - -
- -
- -
-
- Operational Excellence -
-
- {[ - { title: "Hardened Infrastructure", desc: "Zentralisierte Datenpflege und entkoppelte WordPress-Instanzen." }, - { title: "Automated Data Pipelines", desc: "Validierung technischer Spezifikationen ohne manuelle Eingriffe." }, - { title: "Maintenance-Free Core", desc: "Plugin-freie Logik für deterministische System-Sicherheit." } - ].map((item, i) => ( -
- {item.title} - {item.desc} -
- ))} -
- - - - System-Analyse anfragen - - - -
-
- -
+ + + - ); -} + + {/* --- SHOWCASE: LANDING --- */} +
+
+ +
+
+ +

+ Global Hub. +

+
+
+
+ + + + + +
+
+ + {/* --- SECTION 02: TECHNICAL DETAIL --- */} +
+
+
+ +
+ +

+ Automated Documentation. +

+
+
+ + + Für Hochspannungs-N2XS(F)2Y Kabel ist Datentreue eine + Sicherheitsanforderung. Ich habe eine automatisierte + Asset-Pipeline entwickelt, die technische Datenblätter + serverseitig generiert und validiert. + + +
+ + + +
+ +
+
+
+
+
+ + {/* --- SECTION 03: COMMERCE --- */} +
+
+
+ +

+ Fokus auf
+ Spezifikationen. +

+
+
+ +
+ + + + + +
+ +
+ +
+ + + Der Produktbereich wurde konsequent auf die Bedürfnisse + technischer Planer optimiert. Klare Hierarchien und der + Verzicht auf E-Commerce-Rauschen ermöglichen einen direkten + Zugriff auf Kabel-Parameter und Datenblätter. + + + + + Strukturierte Aufbereitung technischer Produktdaten. + + +
+
+
+
+
+ + {/* --- SECTION 04: CONTENT ENGINE --- */} +
+
+
+ +
+ +

+ Insights & News. +

+ + Die News-Engine dient als technischer Hub für + Industrie-Standards. Durch die Implementierung eines + performanten Blog-Systems wird Fachwissen direkt an die + Zielgruppe kommuniziert. + +
+
+
+
+ + + + + +
+
+
+ + {/* --- SECTION 05: TEAM & TRUST --- */} +
+
+ +

+ System-Lifecycle. +

+ + Die Migration von einer statischen Datei-Struktur zu einer + zentralisierten Daten-Instanz eliminiert technische Schulden und + manuelle Fehlerquellen. Das Ergebnis ist eine wartungsfreie + Architektur, die technische Datentreue über den gesamten + Produkt-Lifecycle sicherstellt. + +
+ + +
+
+ +
+
+
+
+
+ + {/* --- SECTION 06: CONVERSION --- */} +
+
+
+ + + + + +
+
+ +
+ +

+ Direkter Draht. +

+ + Das Kontakt-System wurde auf maximale Reduktion getrimmt. + Keine unnötigen Hürden, sondern ein direkter + Kommunikations-Kanal zwischen technischem Bedarf und + individueller Beratung. + +
+
+
+
+
+ + {/* --- FINAL CTA: ARCHITECTURE & VALUE --- */} +
+ + +
+
+ +
+ + CONSULTING // ENGINEERING + +

+ Architektur
+ ohne Altlasten. +

+
+
+ + + Vom Prototyp zum industriellen Standard. Ich entwickle + digitale Infrastrukturen, die technische Freiheit und + operative Stabilität garantieren – wartungsfrei und + skalierbar. + + +
+ +
+ +
+
+ Operational Excellence +
+
+ {[ + { + title: "Hardened Infrastructure", + desc: "Zentralisierte Datenpflege und entkoppelte WordPress-Instanzen.", + }, + { + title: "Automated Data Pipelines", + desc: "Validierung technischer Spezifikationen ohne manuelle Eingriffe.", + }, + { + title: "Maintenance-Free Core", + desc: "Plugin-freie Logik für deterministische System-Sicherheit.", + }, + ].map((item, i) => ( +
+ + {item.title} + + + {item.desc} + +
+ ))} +
+ + + + System-Analyse anfragen + + + +
+
+ +
+ + ); +} diff --git a/apps/web/app/case-studies/page.tsx b/apps/web/app/case-studies/page.tsx index 6ab393c..acd131e 100644 --- a/apps/web/app/case-studies/page.tsx +++ b/apps/web/app/case-studies/page.tsx @@ -1,88 +1,105 @@ -'use client'; +"use client"; -import React from 'react'; -import { PageHeader } from '../../src/components/PageHeader'; -import { Section } from '../../src/components/Section'; -import { Reveal } from '../../src/components/Reveal'; -import { H3, LeadText, Label } from '../../src/components/Typography'; -import { BackgroundGrid, Card, Container } from '../../src/components/Layout'; -import { MotionButton } from '../../src/components/Button'; -import Image from 'next/image'; +import React from "react"; +import { PageHeader } from "../../src/components/PageHeader"; +import { Section } from "../../src/components/Section"; +import { Reveal } from "../../src/components/Reveal"; +import { H3, LeadText, Label } from "../../src/components/Typography"; +import { BackgroundGrid, Card } from "../../src/components/Layout"; +import { MotionButton } from "../../src/components/Button"; +import Image from "next/image"; export default function CaseStudiesPage() { - return ( -
- + return ( +
+ - Case Studies:
Qualität in jedem Detail.} - description="Ein Blick hinter die Kulissen ausgewählter Projekte. Von der ersten Idee bis zum fertigen Hochleistungssystem." - backLink={{ href: '/', label: 'Zurück' }} - backgroundSymbol="C" - /> + + Case Studies:
+ Qualität in jedem Detail. + + } + description="Ein Blick hinter die Kulissen ausgewählter Projekte. Von der ersten Idee bis zum fertigen Hochleistungssystem." + backLink={{ href: "/", label: "Zurück" }} + backgroundSymbol="C" + /> -
-
- - -
- {/* We'll use a placeholder or a screenshot if available. +
+
+ + +
+ {/* We'll use a placeholder or a screenshot if available. Since we have the cloned site, we could technically iframe a preview here too, but a static image or a styled div is more standard for a card. */} -
- KLZ Cables Logo -
-
- -
- -

KLZ Cables – Digitaler Netzbau

- - Wie wir eine komplexe WordPress-Struktur in ein performantes, sauberes und langlebiges Web-System verwandelt haben. Fokus auf Performance, SEO und Benutzerführung. - - -
- - Case Study lesen - -
-
-
-
- - -
- -

Weitere Projekte sind in Arbeit.

- - Ich dokumentiere gerade weitere spannende Projekte aus den Bereichen SaaS, E-Commerce und Systemarchitektur. - -
-
+
+ KLZ Cables Logo
-
+
-
-
- -

- Warum ich Case Studies zeige?
- Weil Code mehr als Text ist. -

-
- - - In diesen Case Studies geht es nicht nur um bunte Bilder. Es geht um die technischen Entscheidungen, die ein Projekt erfolgreich machen. Schnelle Ladezeiten, SEO-Exzellenz und wartbarer Code sind keine Zufälle, sondern das Ergebnis von präziser Planung. - - +
+ +

+ KLZ Cables – Digitaler Netzbau +

+ + Wie wir eine komplexe WordPress-Struktur in ein performantes, + sauberes und langlebiges Web-System verwandelt haben. Fokus + auf Performance, SEO und Benutzerführung. + + +
+ + Case Study lesen +
-
+
+ + + + +
+ +

+ Weitere Projekte sind in Arbeit. +

+ + Ich dokumentiere gerade weitere spannende Projekte aus den + Bereichen SaaS, E-Commerce und Systemarchitektur. + +
+
- ); + + +
+
+ +

+ Warum ich Case Studies zeige?
+ + Weil Code mehr als Text ist. + +

+
+ + + In diesen Case Studies geht es nicht nur um bunte Bilder. Es geht + um die technischen Entscheidungen, die ein Projekt erfolgreich + machen. Schnelle Ladezeiten, SEO-Exzellenz und wartbarer Code sind + keine Zufälle, sondern das Ergebnis von präziser Planung. + + +
+
+
+ ); } diff --git a/apps/web/scripts/ai-estimate.ts b/apps/web/scripts/ai-estimate.ts index 3ef0452..b7b9a46 100644 --- a/apps/web/scripts/ai-estimate.ts +++ b/apps/web/scripts/ai-estimate.ts @@ -1,4 +1,4 @@ -import { CheerioCrawler, RequestQueue } from "crawlee"; +import { CheerioCrawler } from "crawlee"; import * as path from "node:path"; import * as fs from "node:fs/promises"; import { existsSync } from "node:fs"; @@ -1055,7 +1055,7 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} finalState.sitemap = finalState.sitemap.sitemap; else { const entries = Object.entries(finalState.sitemap); - if (entries.every(([_, v]) => Array.isArray(v))) { + if (entries.every(([__, v]) => Array.isArray(v))) { finalState.sitemap = entries.map(([category, pages]) => ({ category, pages, diff --git a/apps/web/scripts/clone-page.ts b/apps/web/scripts/clone-page.ts index baa6d53..745fb6b 100644 --- a/apps/web/scripts/clone-page.ts +++ b/apps/web/scripts/clone-page.ts @@ -1,322 +1,398 @@ -import { chromium, type Page } from 'playwright'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import fs from 'node:fs'; -import axios from 'axios'; +import { chromium } from "playwright"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import fs from "node:fs"; +import axios from "axios"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36'; +const USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"; function sanitizePath(rawPath: string) { - return rawPath.split('/').map(p => p.replace(/[^a-z0-9._-]/gi, '_')).join('/'); + return rawPath + .split("/") + .map((p) => p.replace(/[^a-z0-9._-]/gi, "_")) + .join("/"); } async function downloadFile(url: string, assetsDir: string) { - if (url.startsWith('//')) url = `https:${url}`; - if (!url.startsWith('http')) return null; + if (url.startsWith("//")) url = `https:${url}`; + if (!url.startsWith("http")) return null; - try { - const u = new URL(url); - // Create a collision-resistant local path - const relPath = sanitizePath(u.hostname + u.pathname); - const dest = path.join(assetsDir, relPath); + try { + const u = new URL(url); + // Create a collision-resistant local path + const relPath = sanitizePath(u.hostname + u.pathname); + const dest = path.join(assetsDir, relPath); - if (fs.existsSync(dest)) return `./assets/${relPath}`; + if (fs.existsSync(dest)) return `./assets/${relPath}`; - const res = await axios.get(url, { - responseType: 'arraybuffer', - headers: { 'User-Agent': USER_AGENT }, - timeout: 15000, - validateStatus: () => true - }); + const res = await axios.get(url, { + responseType: "arraybuffer", + headers: { "User-Agent": USER_AGENT }, + timeout: 15000, + validateStatus: () => true, + }); - if (res.status !== 200) return null; + if (res.status !== 200) return null; - if (!fs.existsSync(path.dirname(dest))) fs.mkdirSync(path.dirname(dest), { recursive: true }); - fs.writeFileSync(dest, Buffer.from(res.data)); - return `./assets/${relPath}`; - } catch { - return null; // Fail silently, proceed with original URL - } + if (!fs.existsSync(path.dirname(dest))) + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, Buffer.from(res.data)); + return `./assets/${relPath}`; + } catch { + return null; // Fail silently, proceed with original URL + } } -async function processCssRecursively(cssContent: string, cssUrl: string, assetsDir: string, urlMap: Record, depth = 0) { - if (depth > 5) return cssContent; +async function processCssRecursively( + cssContent: string, + cssUrl: string, + assetsDir: string, + urlMap: Record, + depth = 0, +) { + if (depth > 5) return cssContent; - // Capture both standard url(...) and @import url(...) - const urlRegex = /(?:url\(["']?|@import\s+["'])([^"'\)]+)["']?\)?/gi; - let match; - let newContent = cssContent; + // Capture both standard url(...) and @import url(...) + const urlRegex = /(?:url\(["']?|@import\s+["'])([^"'\)]+)["']?\)?/gi; + let match; + let newContent = cssContent; - while ((match = urlRegex.exec(cssContent)) !== null) { - const originalUrl = match[1]; - if (originalUrl.startsWith('data:') || originalUrl.startsWith('blob:')) continue; + while ((match = urlRegex.exec(cssContent)) !== null) { + const originalUrl = match[1]; + if (originalUrl.startsWith("data:") || originalUrl.startsWith("blob:")) + continue; - try { - const absUrl = new URL(originalUrl, cssUrl).href; - const local = await downloadFile(absUrl, assetsDir); + try { + const absUrl = new URL(originalUrl, cssUrl).href; + const local = await downloadFile(absUrl, assetsDir); - if (local) { - // Calculate relative path from CSS file to Asset - const u = new URL(cssUrl); - const cssPath = u.hostname + u.pathname; - const assetPath = new URL(absUrl).hostname + new URL(absUrl).pathname; + if (local) { + // Calculate relative path from CSS file to Asset + const u = new URL(cssUrl); + const cssPath = u.hostname + u.pathname; + const assetPath = new URL(absUrl).hostname + new URL(absUrl).pathname; - // We need to route from the folder containing the CSS to the asset - const rel = path.relative(path.dirname(sanitizePath(cssPath)), sanitizePath(assetPath)); + // We need to route from the folder containing the CSS to the asset + const rel = path.relative( + path.dirname(sanitizePath(cssPath)), + sanitizePath(assetPath), + ); - // Replace strictly the URL part - newContent = newContent.split(originalUrl).join(rel); - urlMap[absUrl] = local; - } - } catch { } + // Replace strictly the URL part + newContent = newContent.split(originalUrl).join(rel); + urlMap[absUrl] = local; + } + } catch { + // Ignore URL resolution errors } - return newContent; + } + return newContent; } async function run() { - const rawUrl = process.argv[2]; - if (!rawUrl) { - console.error('Usage: npm run clone-page '); - process.exit(1); + const rawUrl = process.argv[2]; + if (!rawUrl) { + console.error("Usage: npm run clone-page "); + process.exit(1); + } + const targetUrl = rawUrl.trim(); + const urlObj = new URL(targetUrl); + + // Setup Output Directories + const domainSlug = urlObj.hostname.replace("www.", ""); + const domainDir = path.resolve(__dirname, `../public/showcase/${domainSlug}`); + const assetsDir = path.join(domainDir, "assets"); + if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir, { recursive: true }); + + let pageSlug = urlObj.pathname.split("/").filter(Boolean).join("-"); + if (!pageSlug) pageSlug = "index"; + const htmlFilename = `${pageSlug}.html`; + + console.log(`🚀 INDUSTRIAL CLONE: ${targetUrl}`); + + const browser = await chromium.launch({ headless: true }); + // Start with a standard viewport, we will resize widely later + const context = await browser.newContext({ + userAgent: USER_AGENT, + viewport: { width: 1920, height: 1080 }, + }); + const page = await context.newPage(); + + const urlMap: Record = {}; + const foundAssets = new Set(); + + // 1. Live Network Interception + page.on("response", (response) => { + const url = response.url(); + if (response.status() === 200) { + // Capture anything that looks like a static asset + if ( + url.match( + /\.(css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)/i, + ) + ) { + foundAssets.add(url); + } } - const targetUrl = rawUrl.trim(); - const urlObj = new URL(targetUrl); + }); - // Setup Output Directories - const domainSlug = urlObj.hostname.replace('www.', ''); - const domainDir = path.resolve(__dirname, `../public/showcase/${domainSlug}`); - const assetsDir = path.join(domainDir, 'assets'); - if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir, { recursive: true }); + try { + console.log("🌐 Loading page (Waiting for Network Idle)..."); + await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 90000 }); - let pageSlug = urlObj.pathname.split('/').filter(Boolean).join('-'); - if (!pageSlug) pageSlug = 'index'; - const htmlFilename = `${pageSlug}.html`; + console.log( + '🌊 Executing "Scroll Wave" to trigger all lazy loaders naturally...', + ); + await page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 400; + const timer = setInterval(() => { + const scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; - console.log(`🚀 INDUSTRIAL CLONE: ${targetUrl}`); - - const browser = await chromium.launch({ headless: true }); - // Start with a standard viewport, we will resize widely later - const context = await browser.newContext({ userAgent: USER_AGENT, viewport: { width: 1920, height: 1080 } }); - const page = await context.newPage(); - - const urlMap: Record = {}; - const foundAssets = new Set(); - - // 1. Live Network Interception - page.on('response', response => { - const url = response.url(); - if (response.status() === 200) { - // Capture anything that looks like a static asset - if (url.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)/i)) { - foundAssets.add(url); - } - } + if (totalHeight >= scrollHeight) { + clearInterval(timer); + window.scrollTo(0, 0); // Reset to top + resolve(true); + } + }, 100); + }); }); - try { - console.log('🌐 Loading page (Waiting for Network Idle)...'); - await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 90000 }); + console.log( + '📐 Expanding Viewport to "Giant Mode" for final asset capture...', + ); + const fullHeight = await page.evaluate(() => document.body.scrollHeight); + await page.setViewportSize({ width: 1920, height: fullHeight + 1000 }); - console.log('🌊 Executing "Scroll Wave" to trigger all lazy loaders naturally...'); - await page.evaluate(async () => { - await new Promise((resolve) => { - let totalHeight = 0; - const distance = 400; - const timer = setInterval(() => { - const scrollHeight = document.body.scrollHeight; - window.scrollBy(0, distance); - totalHeight += distance; + // Final settlement wait + await page.waitForTimeout(3000); - if (totalHeight >= scrollHeight) { - clearInterval(timer); - window.scrollTo(0, 0); // Reset to top - resolve(true); - } - }, 100); + console.log("💧 Final DOM Hydration & Sanitization..."); + await page.evaluate(() => { + // A. Deterministic Attribute Hydration (Generic) + // Scours every element for attributes that look like asset URLs and promotes them + const assetPattern = + /\.(jpg|jpeg|png|gif|svg|webp|mp4|webm|woff2?|ttf|otf)/i; + + document.querySelectorAll("*").forEach((el) => { + // 0. Skip Meta/Head/Script/Style/SVG tags for attribute promotion + if ( + ["META", "LINK", "HEAD", "SCRIPT", "STYLE", "SVG", "PATH"].includes( + el.tagName, + ) + ) + return; + + // 1. Force Visibility (Anti-Flicker) + const htmlEl = el as HTMLElement; + const style = window.getComputedStyle(htmlEl); + if (style.opacity === "0" || style.visibility === "hidden") { + htmlEl.style.setProperty("opacity", "1", "important"); + htmlEl.style.setProperty("visibility", "visible", "important"); + } + + // 2. Promote Data Attributes + for (const attr of Array.from(el.attributes)) { + const name = attr.name.toLowerCase(); + const val = attr.value; + + if ( + assetPattern.test(val) || + name.includes("src") || + name.includes("image") + ) { + // Standard Image/Video/Source promotion + if (el.tagName === "IMG") { + const img = el as HTMLImageElement; + if (name.includes("srcset")) img.srcset = val; + else if (!img.src || img.src.includes("data:")) img.src = val; + } + if (el.tagName === "SOURCE") { + const source = el as HTMLSourceElement; + if (name.includes("srcset")) source.srcset = val; + } + if (el.tagName === "VIDEO" || el.tagName === "AUDIO") { + const media = el as HTMLMediaElement; + if (!media.src) media.src = val; + } + + // Background Image Promotion + if (val.match(/^(https?:\/\/|\/\/|\/)/) && !name.includes("href")) { + const bg = htmlEl.style.backgroundImage; + if (!bg || bg === "none") { + htmlEl.style.backgroundImage = `url('${val}')`; + } + } + } + } + }); + + // B. Ensure basic structural elements are visible post-scroll + const body = document.body; + if (body) { + body.style.setProperty("opacity", "1", "important"); + body.style.setProperty("visibility", "visible", "important"); + } + }); + + console.log("⏳ Waiting for network idle..."); + await page.waitForLoadState("networkidle"); + + // 1.5 FINAL SETTLEMENT: Let any scroll-triggered JS finish + await page.waitForTimeout(1000); + + // 2. Static Snapshot + let content = await page.content(); + + // 3. Post-Snapshot Asset Discovery (Regex) + // Catches assets that never triggered a network request but exist in the markup + const regexPatterns = [ + /(?:src|href|url|data-[a-z-]+|srcset)=["']([^"'<>\s]+?\.(?:css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)(?:\?[^"']*)?)["']/gi, + // Capture CSS url() inside style blocks + /url\(["']?([^"'\)]+)["']?\)/gi, + ]; + + for (const pattern of regexPatterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + try { + foundAssets.add(new URL(match[1], targetUrl).href); + } catch { + // Ignore invalid URLs in content + } + } + } + + // Specific srcset parsing + const srcsetRegex = /[a-z0-9-]+srcset=["']([^"']+)["']/gi; + let match; + while ((match = srcsetRegex.exec(content)) !== null) { + match[1].split(",").forEach((rule) => { + const parts = rule.trim().split(/\s+/); + if (parts[0] && !parts[0].startsWith("data:")) { + try { + foundAssets.add(new URL(parts[0], targetUrl).href); + } catch { + // Ignore invalid srcset URLs + } + } + }); + } + + console.log(`🔍 Processing ${foundAssets.size} discovered assets...`); + + // 4. Download & Map + for (const url of foundAssets) { + const local = await downloadFile(url, assetsDir); + if (local) { + urlMap[url] = local; + const clean = url.split("?")[0]; + urlMap[clean] = local; + + // Handle CSS recursively + if (clean.endsWith(".css")) { + try { + const { data } = await axios.get(url, { + headers: { "User-Agent": USER_AGENT }, }); - }); - - console.log('📐 Expanding Viewport to "Giant Mode" for final asset capture...'); - const fullHeight = await page.evaluate(() => document.body.scrollHeight); - await page.setViewportSize({ width: 1920, height: fullHeight + 1000 }); - - // Final settlement wait - await page.waitForTimeout(3000); - - console.log('💧 Final DOM Hydration & Sanitization...'); - await page.evaluate(() => { - // A. Deterministic Attribute Hydration (Generic) - // Scours every element for attributes that look like asset URLs and promotes them - const assetPattern = /\.(jpg|jpeg|png|gif|svg|webp|mp4|webm|woff2?|ttf|otf)/i; - - document.querySelectorAll('*').forEach(el => { - // 0. Skip Meta/Head/Script/Style/SVG tags for attribute promotion - if (['META', 'LINK', 'HEAD', 'SCRIPT', 'STYLE', 'SVG', 'PATH'].includes(el.tagName)) return; - - // 1. Force Visibility (Anti-Flicker) - const htmlEl = el as HTMLElement; - const style = window.getComputedStyle(htmlEl); - if (style.opacity === '0' || style.visibility === 'hidden') { - htmlEl.style.setProperty('opacity', '1', 'important'); - htmlEl.style.setProperty('visibility', 'visible', 'important'); - } - - // 2. Promote Data Attributes - for (const attr of Array.from(el.attributes)) { - const name = attr.name.toLowerCase(); - const val = attr.value; - - if (assetPattern.test(val) || name.includes('src') || name.includes('image')) { - // Standard Image/Video/Source promotion - if (el.tagName === 'IMG') { - const img = el as HTMLImageElement; - if (name.includes('srcset')) img.srcset = val; - else if (!img.src || img.src.includes('data:')) img.src = val; - } - if (el.tagName === 'SOURCE') { - const source = el as HTMLSourceElement; - if (name.includes('srcset')) source.srcset = val; - } - if (el.tagName === 'VIDEO' || el.tagName === 'AUDIO') { - const media = el as HTMLMediaElement; - if (!media.src) media.src = val; - } - - // Background Image Promotion - if (val.match(/^(https?:\/\/|\/\/|\/)/) && !name.includes('href')) { - const bg = htmlEl.style.backgroundImage; - if (!bg || bg === 'none') { - htmlEl.style.backgroundImage = `url('${val}')`; - } - } - } - } - }); - - // B. Ensure basic structural elements are visible post-scroll - const body = document.body; - if (body) { - body.style.setProperty('opacity', '1', 'important'); - body.style.setProperty('visibility', 'visible', 'important'); - } - }); - - console.log('⏳ Waiting for network idle...'); - await page.waitForLoadState('networkidle'); - - // 1.5 FINAL SETTLEMENT: Let any scroll-triggered JS finish - await page.waitForTimeout(1000); - - // 2. Static Snapshot - let content = await page.content(); - - // 3. Post-Snapshot Asset Discovery (Regex) - // Catches assets that never triggered a network request but exist in the markup - const regexPatterns = [ - /(?:src|href|url|data-[a-z-]+|srcset)=["']([^"'<>\s]+?\.(?:css|js|png|jpg|jpeg|gif|svg|woff2?|ttf|otf|mp4|webm|webp|ico)(?:\?[^"']*)?)["']/gi, - // Capture CSS url() inside style blocks - /url\(["']?([^"'\)]+)["']?\)/gi - ]; - - for (const pattern of regexPatterns) { - let match; - while ((match = pattern.exec(content)) !== null) { - try { foundAssets.add(new URL(match[1], targetUrl).href); } catch { } - } + // Process CSS and save it + const processedCss = await processCssRecursively( + data, + url, + assetsDir, + urlMap, + ); + const relPath = sanitizePath( + new URL(url).hostname + new URL(url).pathname, + ); + fs.writeFileSync(path.join(assetsDir, relPath), processedCss); + } catch { + // Ignore CSS fetch/process errors + } } + } + } - // Specific srcset parsing - const srcsetRegex = /[a-z0-9-]+srcset=["']([^"']+)["']/gi; - let match; - while ((match = srcsetRegex.exec(content)) !== null) { - match[1].split(',').forEach(rule => { - const parts = rule.trim().split(/\s+/); - if (parts[0] && !parts[0].startsWith('data:')) { - try { foundAssets.add(new URL(parts[0], targetUrl).href); } catch { } - } - }); + console.log("🛠️ Finalizing Static Mirror..."); + let finalContent = content; + + // A. Apply URL Map Replacements + // Longer paths first to prevent partial replacement errors + const sortedUrls = Object.keys(urlMap).sort((a, b) => b.length - a.length); + if (sortedUrls.length > 0) { + const escaped = sortedUrls.map((u) => + u.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), + ); + // Create a massive regex for single-pass replacement + const masterRegex = new RegExp(`(${escaped.join("|")})`, "g"); + finalContent = finalContent.replace( + masterRegex, + (match) => urlMap[match] || match, + ); + } + + // B. Global Root-Relative Path Cleanup + // Catches things like /wp-content/ that weren't distinct assets or were missed + const commonDirs = [ + "/wp-content/", + "/wp-includes/", + "/assets/", + "/static/", + "/images/", + ]; + for (const dir of commonDirs) { + const localDir = `./assets/${urlObj.hostname}${dir}`; + finalContent = finalContent.split(`"${dir}`).join(`"${localDir}`); + finalContent = finalContent.split(`'${dir}`).join(`'${localDir}`); + finalContent = finalContent.split(`(${dir}`).join(`(${localDir}`); + } + + // C. Domain Nuke + // Replace absolute links to the original domain with relative or # + const domainPattern = new RegExp( + `https?://(www\\.)?${urlObj.hostname.replace(/\./g, "\\.")}[^"']*`, + "gi", + ); + // We carefully only replace if it looks like a resource link, or neutralize if it's a navigation link + // For simplicity and "solidness", we'll rely on the specific replacements above first. + // This catch-all nuke ensures we don't leak requests. + // Convert remaining absolute domain links to relative . + finalContent = finalContent.replace(domainPattern, (match) => { + // If we have a map for it, it should have been replaced. + // If not, it's likely a navigation link or an uncaptured asset. + // Safe fallback: + return "./"; + }); + + // D. Static Stability & Cleanup + // Remove tracking/analytics/lazy-load scripts that ruins stability + finalContent = finalContent.replace( + /]*>([\s\S]*?)<\/script>/gi, + (match, content) => { + const lower = content.toLowerCase(); + if ( + lower.includes("google-analytics") || + lower.includes("gtag") || + lower.includes("fbq") || + lower.includes("lazy") || + lower.includes("tracker") + ) { + return ""; } + return match; + }, + ); - console.log(`🔍 Processing ${foundAssets.size} discovered assets...`); - - // 4. Download & Map - for (const url of foundAssets) { - const local = await downloadFile(url, assetsDir); - if (local) { - urlMap[url] = local; - const clean = url.split('?')[0]; - urlMap[clean] = local; - - // Handle CSS recursively - if (clean.endsWith('.css')) { - try { - const { data } = await axios.get(url, { headers: { 'User-Agent': USER_AGENT } }); - // Process CSS and save it - const processedCss = await processCssRecursively(data, url, assetsDir, urlMap); - const relPath = sanitizePath(new URL(url).hostname + new URL(url).pathname); - fs.writeFileSync(path.join(assetsDir, relPath), processedCss); - } catch { } - } - } - } - - console.log('🛠️ Finalizing Static Mirror...'); - let finalContent = content; - - // A. Apply URL Map Replacements - // Longer paths first to prevent partial replacement errors - const sortedUrls = Object.keys(urlMap).sort((a, b) => b.length - a.length); - if (sortedUrls.length > 0) { - const escaped = sortedUrls.map(u => u.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); - // Create a massive regex for single-pass replacement - const masterRegex = new RegExp(`(${escaped.join('|')})`, 'g'); - finalContent = finalContent.replace(masterRegex, (match) => urlMap[match] || match); - } - - // B. Global Root-Relative Path Cleanup - // Catches things like /wp-content/ that weren't distinct assets or were missed - const commonDirs = ['/wp-content/', '/wp-includes/', '/assets/', '/static/', '/images/']; - for (const dir of commonDirs) { - const localDir = `./assets/${urlObj.hostname}${dir}`; - finalContent = finalContent.split(`"${dir}`).join(`"${localDir}`); - finalContent = finalContent.split(`'${dir}`).join(`'${localDir}`); - finalContent = finalContent.split(`(${dir}`).join(`(${localDir}`); - } - - // C. Domain Nuke - // Replace absolute links to the original domain with relative or # - const domainPattern = new RegExp(`https?://(www\\.)?${urlObj.hostname.replace(/\./g, '\\.')}[^"']*`, 'gi'); - // We carefully only replace if it looks like a resource link, or neutralize if it's a navigation link - // For simplicity and "solidness", we'll rely on the specific replacements above first. - // This catch-all nuke ensures we don't leak requests. - // Convert remaining absolute domain links to relative . - finalContent = finalContent.replace(domainPattern, (match) => { - // If we have a map for it, it should have been replaced. - // If not, it's likely a navigation link or an uncaptured asset. - // Safe fallback: - return './'; - }); - - // D. Static Stability & Cleanup - // Remove tracking/analytics/lazy-load scripts that ruins stability - finalContent = finalContent.replace(/]*>([\s\S]*?)<\/script>/gi, (match, content) => { - const lower = content.toLowerCase(); - if (lower.includes('google-analytics') || - lower.includes('gtag') || - lower.includes('fbq') || - lower.includes('lazy') || - lower.includes('tracker')) { - return ''; - } - return match; - }); - - // E. CSS Injections for Stability - const headEnd = finalContent.indexOf(''); - if (headEnd > -1) { - const stabilityCss = ` + // E. CSS Injections for Stability + const headEnd = finalContent.indexOf(""); + if (headEnd > -1) { + const stabilityCss = ` `; - finalContent = finalContent.slice(0, headEnd) + stabilityCss + finalContent.slice(headEnd); - } - - // Save - const finalPath = path.join(domainDir, htmlFilename); - fs.writeFileSync(finalPath, finalContent); - console.log(`✅ SUCCESS: Cloned to ${finalPath}`); - - } catch (err) { - console.error('❌ FATAL ERROR:', err); - } finally { - await browser.close(); + finalContent = + finalContent.slice(0, headEnd) + + stabilityCss + + finalContent.slice(headEnd); } + + // Save + const finalPath = path.join(domainDir, htmlFilename); + fs.writeFileSync(finalPath, finalContent); + console.log(`✅ SUCCESS: Cloned to ${finalPath}`); + } catch (err) { + console.error("❌ FATAL ERROR:", err); + } finally { + await browser.close(); + } } run(); diff --git a/apps/web/scripts/clone-recursive.ts b/apps/web/scripts/clone-recursive.ts index 998b003..0409ceb 100644 --- a/apps/web/scripts/clone-recursive.ts +++ b/apps/web/scripts/clone-recursive.ts @@ -1,228 +1,223 @@ // @ts-ignore -import scrape from 'website-scraper'; +import scrape from "website-scraper"; // @ts-ignore -import PuppeteerPlugin from 'website-scraper-puppeteer'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import fs from 'node:fs'; +import PuppeteerPlugin from "website-scraper-puppeteer"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import fs from "node:fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -class CustomFilenameGeneratorPlugin { - apply(registerAction: any) { - registerAction('generateFilename', ({ resource }: any) => { - const url = new URL(resource.url); - const ext = path.extname(url.pathname); +async function run() { + const targetUrl = process.argv[2]; + if (!targetUrl) { + console.error("Usage: npm run clone-website [output-dir]"); + process.exit(1); + } - // Clean the path - let safePath = url.pathname; - if (safePath.endsWith('/')) { - safePath += 'index.html'; - } else if (!ext && !resource.isHtml()) { - // If no extension and not HTML, guess based on content type? - // But usually safe to leave as is or add extension if known. - } else if (!ext && resource.isHtml()) { - safePath += '.html'; + const urlObj = new URL(targetUrl); + const domain = urlObj.hostname; + const safeDomain = domain.replace(/[^a-z0-9-]/gi, "_"); + const outputDir = process.argv[3] + ? path.resolve(process.cwd(), process.argv[3]) + : path.resolve(__dirname, "../cloned-websites", safeDomain); + + if (fs.existsSync(outputDir)) { + console.log(`Cleaning existing directory: ${outputDir}`); + fs.rmSync(outputDir, { recursive: true, force: true }); + } + + console.log(`🚀 Starting recursive clone of ${targetUrl}`); + console.log(`📂 Output: ${outputDir}`); + + const options = { + urls: [targetUrl], + directory: outputDir, + recursive: true, + maxDepth: 5, + // Custom filename generation to avoid "https:/" folders + plugins: [ + new PuppeteerPlugin({ + launchOptions: { + headless: true, + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + ], + }, + scrollToBottom: { timeout: 10000, viewportN: 10 }, + blockNavigation: false, + }), + new (class LoggerPlugin { + apply(registerAction: any) { + registerAction("onResourceSaved", ({ resource }: any) => { + console.log(` 💾 Saved: ${resource.url} -> ${resource.filename}`); + }); + registerAction("onResourceError", ({ resource, error }: any) => { + console.error(` ❌ Error: ${resource.url} - ${error.message}`); + }); + } + })(), + new (class FilenamePlugin { + apply(registerAction: any) { + registerAction("generateFilename", ({ resource }: any) => { + const u = new URL(resource.url); + let filename = u.pathname; + + // normalize + if (filename.endsWith("/")) filename += "index.html"; + else if (!path.extname(filename) && resource.url.includes(domain)) + filename += "/index.html"; // Assume folder if internal link without ext + + // If it's an external asset, put it in a separate folder + if (u.hostname !== domain) { + filename = `_external/${u.hostname}${filename}`; } - // Handle query strings if needed (simplifying by ignoring them for static local files usually better, - // unless they determine content. For a clean clone, we usually ignore unique query params) - // But if the site relies on routing via query params (e.g. ?page=2), we might want to encode them. - // For now, let's keep it simple and clean. + // Sanitize filename + filename = filename + .split("/") + .map((part) => part.replace(/[^a-z0-9._-]/gi, "_")) + .join("/"); // Remove leading slash - if (safePath.startsWith('/')) safePath = safePath.substring(1); + if (filename.startsWith("/")) filename = filename.substring(1); - // Sanitization - safePath = safePath.replace(/[:*?"<>|]/g, '_'); + // Handle "Unnamed page" by checking if empty + if (!filename || filename === "index.html") + return { filename: "index.html" }; - // External assets go to a separate folder to avoid collision - // We can detect external by checking if the resource parent is different? - // Actually, simply using the hostname mapping is safer. - - // However, the USER wants "local cloned pages". - // If we just use the path, we merge everything into one root. - // If there are collision (e.g. same path on different domains), this is bad. - // But typically we clone ONE site. - - return { filename: safePath }; - }); - } -} - -async function run() { - const targetUrl = process.argv[2]; - if (!targetUrl) { - console.error('Usage: npm run clone-website [output-dir]'); - process.exit(1); - } - - const urlObj = new URL(targetUrl); - const domain = urlObj.hostname; - const safeDomain = domain.replace(/[^a-z0-9-]/gi, '_'); - const outputDir = process.argv[3] - ? path.resolve(process.cwd(), process.argv[3]) - : path.resolve(__dirname, '../cloned-websites', safeDomain); - - if (fs.existsSync(outputDir)) { - console.log(`Cleaning existing directory: ${outputDir}`); - fs.rmSync(outputDir, { recursive: true, force: true }); - } - - console.log(`🚀 Starting recursive clone of ${targetUrl}`); - console.log(`📂 Output: ${outputDir}`); - - const options = { - urls: [targetUrl], - directory: outputDir, - recursive: true, - maxDepth: 5, - // Custom filename generation to avoid "https:/" folders - plugins: [ - new PuppeteerPlugin({ - launchOptions: { - headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] - }, - scrollToBottom: { timeout: 10000, viewportN: 10 }, - blockNavigation: false - }), - new class LoggerPlugin { - apply(registerAction: any) { - registerAction('onResourceSaved', ({ resource }: any) => { - console.log(` 💾 Saved: ${resource.url} -> ${resource.filename}`); - }); - registerAction('onResourceError', ({ resource, error }: any) => { - console.error(` ❌ Error: ${resource.url} - ${error.message}`); - }); - } - }, - new class FilenamePlugin { - apply(registerAction: any) { - registerAction('generateFilename', ({ resource }: any) => { - const u = new URL(resource.url); - let filename = u.pathname; - - // normalize - if (filename.endsWith('/')) filename += 'index.html'; - else if (!path.extname(filename) && resource.url.includes(domain)) filename += '/index.html'; // Assume folder if internal link without ext - - // If it's an external asset, put it in a separate folder - if (u.hostname !== domain) { - filename = `_external/${u.hostname}${filename}`; - } - - // Sanitize filename - filename = filename.split('/').map(part => part.replace(/[^a-z0-9._-]/gi, '_')).join('/'); - - // Remove leading slash - if (filename.startsWith('/')) filename = filename.substring(1); - - // Handle "Unnamed page" by checking if empty - if (!filename || filename === 'index.html') return { filename: 'index.html' }; - - return { filename }; - }); - } - } - ], - - urlFilter: (url: string) => { - const u = new URL(url); - const isTargetDomain = u.hostname === domain; - const isGoogleFonts = u.hostname.includes('fonts.googleapis.com') || u.hostname.includes('fonts.gstatic.com'); - // Allow assets from anywhere - const isAsset = /\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|mp4|webm|ico|json|webp)$/i.test(u.pathname); - // Allow fonts/css from common CDNs if standard extension check fails - const isCommonAsset = u.pathname.includes('/css/') || u.pathname.includes('/js/') || u.pathname.includes('/static/') || u.pathname.includes('/assets/') || u.pathname.includes('/uploads/'); - - return isTargetDomain || isAsset || isCommonAsset || isGoogleFonts; - }, - - - sources: [ - { selector: 'img', attr: 'src' }, - { selector: 'img', attr: 'srcset' }, - { selector: 'source', attr: 'src' }, - { selector: 'source', attr: 'srcset' }, - { selector: 'link[rel="stylesheet"]', attr: 'href' }, - { selector: 'link[rel="preload"]', attr: 'href' }, - { selector: 'link[rel="prefetch"]', attr: 'href' }, - { selector: 'script', attr: 'src' }, - { selector: 'video', attr: 'src' }, - { selector: 'video', attr: 'poster' }, - { selector: 'iframe', attr: 'src' }, - { selector: 'link[rel*="icon"]', attr: 'href' }, - { selector: 'link[rel="manifest"]', attr: 'href' }, - { selector: 'meta[property="og:image"]', attr: 'content' } - ], - - request: { - headers: { - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' - } + return { filename }; + }); } - }; + })(), + ], - try { - // @ts-ignore - const result = await scrape(options); - console.log(`\n✅ Successfully cloned ${result.length} resources to ${outputDir}`); + urlFilter: (url: string) => { + const u = new URL(url); + const isTargetDomain = u.hostname === domain; + const isGoogleFonts = + u.hostname.includes("fonts.googleapis.com") || + u.hostname.includes("fonts.gstatic.com"); + // Allow assets from anywhere + const isAsset = + /\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|mp4|webm|ico|json|webp)$/i.test( + u.pathname, + ); + // Allow fonts/css from common CDNs if standard extension check fails + const isCommonAsset = + u.pathname.includes("/css/") || + u.pathname.includes("/js/") || + u.pathname.includes("/static/") || + u.pathname.includes("/assets/") || + u.pathname.includes("/uploads/"); - // Post-processing: Sanitize HTML to remove Next.js hydration scripts - // This prevents the static site from trying to "hydrate" and breaking images/links - console.log('🧹 Sanitizing HTML files...'); - sanitizeHtmlFiles(outputDir); + return isTargetDomain || isAsset || isCommonAsset || isGoogleFonts; + }, - console.log(`open "${path.join(outputDir, 'index.html')}"`); - } catch (error) { - console.error('❌ Error cloning website:', error); - process.exit(1); - } + sources: [ + { selector: "img", attr: "src" }, + { selector: "img", attr: "srcset" }, + { selector: "source", attr: "src" }, + { selector: "source", attr: "srcset" }, + { selector: 'link[rel="stylesheet"]', attr: "href" }, + { selector: 'link[rel="preload"]', attr: "href" }, + { selector: 'link[rel="prefetch"]', attr: "href" }, + { selector: "script", attr: "src" }, + { selector: "video", attr: "src" }, + { selector: "video", attr: "poster" }, + { selector: "iframe", attr: "src" }, + { selector: 'link[rel*="icon"]', attr: "href" }, + { selector: 'link[rel="manifest"]', attr: "href" }, + { selector: 'meta[property="og:image"]', attr: "content" }, + ], + + request: { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + }, + }, + }; + + try { + // @ts-ignore + const result = await scrape(options); + console.log( + `\n✅ Successfully cloned ${result.length} resources to ${outputDir}`, + ); + + // Post-processing: Sanitize HTML to remove Next.js hydration scripts + // This prevents the static site from trying to "hydrate" and breaking images/links + console.log("🧹 Sanitizing HTML files..."); + sanitizeHtmlFiles(outputDir); + + console.log(`open "${path.join(outputDir, "index.html")}"`); + } catch (error) { + console.error("❌ Error cloning website:", error); + process.exit(1); + } } function sanitizeHtmlFiles(dir: string) { - const files = fs.readdirSync(dir); - for (const file of files) { - const fullPath = path.join(dir, file); - if (fs.statSync(fullPath).isDirectory()) { - sanitizeHtmlFiles(fullPath); - } else if (file.endsWith('.html')) { - let content = fs.readFileSync(fullPath, 'utf8'); + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + sanitizeHtmlFiles(fullPath); + } else if (file.endsWith(".html")) { + let content = fs.readFileSync(fullPath, "utf8"); - // Remove Next.js data script - content = content.replace(/`; - }); + // Convert Breeze dynamic script/styles into actual tags if possible + // match
URL
+ content = content.replace( + /]+class="breeze-scripts-load"[^>]*>([^<]+)<\/div>/gi, + (match, url) => { + if (url.endsWith(".css")) + return ``; + return ``; + }, + ); - // Inject Fonts (Fix for missing dynamic fonts) - // We inject Inter and Montserrat as safe defaults for industrial/modern sites - // Check specifically for a stylesheet link to google fonts - const hasGoogleFontStylesheet = /]+rel="stylesheet"[^>]+href="[^"]*fonts\.googleapis\.com/i.test(content); - if (!hasGoogleFontStylesheet) { - const fontLink = ``; - const styleBlock = ``; - content = content.replace('', `${fontLink}${styleBlock}`); - } + content = content.replace("", `${fontLink}${styleBlock}`); + } - // Force column layout on product pages - if (content.includes('class="products')) { - const layoutScript = ` + // Force column layout on product pages + if (content.includes('class="products')) { + const layoutScript = ` `; - content = content.replace('', `${layoutScript}`); - } + content = content.replace("", `${layoutScript}`); + } - fs.writeFileSync(fullPath, content); - } + fs.writeFileSync(fullPath, content); } + } } run(); diff --git a/apps/web/scripts/clone-website.ts b/apps/web/scripts/clone-website.ts index 5c15c27..21028a0 100644 --- a/apps/web/scripts/clone-website.ts +++ b/apps/web/scripts/clone-website.ts @@ -1,8 +1,8 @@ -import scrape from 'website-scraper'; -import PuppeteerPlugin from 'website-scraper-puppeteer'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import fs from 'fs'; +import scrape from "website-scraper"; +import PuppeteerPlugin from "website-scraper-puppeteer"; +import path from "path"; +import { fileURLToPath } from "url"; +import fs from "fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -11,45 +11,55 @@ const __dirname = path.dirname(__filename); class PortfolioPlugin { apply(registerAction: any) { // 1. Add more sources before starting - registerAction('beforeStart', ({ options }: any) => { + registerAction("beforeStart", ({ options }: any) => { if (!options.sources) options.sources = []; - options.sources.push({ selector: 'img', attr: 'data-nimg' }); - options.sources.push({ selector: 'img', attr: 'data-src' }); - options.sources.push({ selector: 'img', attr: 'data-srcset' }); - options.sources.push({ selector: 'video', attr: 'poster' }); - options.sources.push({ selector: 'source', attr: 'data-srcset' }); - options.sources.push({ selector: '[style*="background-image"]', attr: 'style' }); - options.sources.push({ selector: 'link[as="font"]', attr: 'href' }); - options.sources.push({ selector: 'link[as="image"]', attr: 'href' }); - options.sources.push({ selector: 'link[as="style"]', attr: 'href' }); - options.sources.push({ selector: 'link[as="script"]', attr: 'href' }); + options.sources.push({ selector: "img", attr: "data-nimg" }); + options.sources.push({ selector: "img", attr: "data-src" }); + options.sources.push({ selector: "img", attr: "data-srcset" }); + options.sources.push({ selector: "video", attr: "poster" }); + options.sources.push({ selector: "source", attr: "data-srcset" }); + options.sources.push({ + selector: '[style*="background-image"]', + attr: "style", + }); + options.sources.push({ selector: 'link[as="font"]', attr: "href" }); + options.sources.push({ selector: 'link[as="image"]', attr: "href" }); + options.sources.push({ selector: 'link[as="style"]', attr: "href" }); + options.sources.push({ selector: 'link[as="script"]', attr: "href" }); }); // 2. Sanitize filenames and handle Next.js optimized images - registerAction('generateFilename', ({ resource, filename }: any) => { + registerAction("generateFilename", ({ resource, filename }: any) => { const url = resource.getUrl(); let result = filename; // Handle Next.js optimized images: /_next/image?url=...&w=... - if (url.includes('/_next/image')) { + if (url.includes("/_next/image")) { try { const urlParams = new URL(url).searchParams; - const originalUrl = urlParams.get('url'); + const originalUrl = urlParams.get("url"); if (originalUrl) { - const cleanPath = originalUrl.split('?')[0]; - const ext = path.extname(cleanPath) || '.webp'; + const cleanPath = originalUrl.split("?")[0]; + const ext = path.extname(cleanPath) || ".webp"; const name = path.basename(cleanPath, ext); - const width = urlParams.get('w') || 'auto'; + const width = urlParams.get("w") || "auto"; result = `_next/optimized/${name}-${width}${ext}`; } - } catch (e) {} + } catch (e) { + // Ignore invalid optimized image URLs + } } // CRITICAL MAC FIX: Replace .app with -app in all paths to prevent hidden Application Bundles // We split by / to ensure we only replace .app at the end of a directory name or filename - result = result.split('/').map((segment: string) => - segment.endsWith('.app') ? segment.replace(/\.app$/, '-app') : segment - ).join('/'); + result = result + .split("/") + .map((segment: string) => + segment.endsWith(".app") + ? segment.replace(/\.app$/, "-app") + : segment, + ) + .join("/"); return { filename: result }; }); @@ -59,19 +69,23 @@ class PortfolioPlugin { async function cloneWebsite() { const url = process.argv[2]; if (!url) { - console.error('Please provide a URL as an argument.'); + console.error("Please provide a URL as an argument."); process.exit(1); } const domain = new URL(url).hostname; - let outputDirName = process.argv[3] || domain.replace(/\./g, '-'); - + let outputDirName = process.argv[3] || domain.replace(/\./g, "-"); + // Sanitize top-level folder name for Mac - if (outputDirName.endsWith('.app')) { - outputDirName = outputDirName.replace(/\.app$/, '-app'); + if (outputDirName.endsWith(".app")) { + outputDirName = outputDirName.replace(/\.app$/, "-app"); } - - const outputDir = path.resolve(__dirname, '../cloned-websites', outputDirName); + + const outputDir = path.resolve( + __dirname, + "../cloned-websites", + outputDirName, + ); if (fs.existsSync(outputDir)) { fs.rmSync(outputDir, { recursive: true, force: true }); @@ -88,61 +102,84 @@ async function cloneWebsite() { requestConcurrency: 10, plugins: [ new PuppeteerPlugin({ - launchOptions: { headless: true, args: ['--no-sandbox'] }, - gotoOptions: { waitUntil: 'networkidle0', timeout: 60000 }, - scrollToBottom: { timeout: 20000, viewportN: 20 }, + launchOptions: { headless: true, args: ["--no-sandbox"] }, + gotoOptions: { waitUntil: "networkidle0", timeout: 60000 }, + scrollToBottom: { timeout: 20000, viewportN: 20 }, }), - new PortfolioPlugin() + new PortfolioPlugin(), ], sources: [ - { selector: 'img', attr: 'src' }, - { selector: 'img', attr: 'srcset' }, - { selector: 'img', attr: 'data-src' }, - { selector: 'img', attr: 'data-srcset' }, - { selector: 'link[rel="stylesheet"]', attr: 'href' }, - { selector: 'link[rel*="icon"]', attr: 'href' }, - { selector: 'script', attr: 'src' }, - { selector: 'link[rel="preload"]', attr: 'href' }, - { selector: 'link[rel="prefetch"]', attr: 'href' }, - { selector: 'link[rel="modulepreload"]', attr: 'href' }, - { selector: 'link[rel="apple-touch-icon"]', attr: 'href' }, - { selector: 'link[rel="mask-icon"]', attr: 'href' }, - { selector: 'source', attr: 'src' }, - { selector: 'source', attr: 'srcset' }, - { selector: 'video', attr: 'src' }, - { selector: 'video', attr: 'poster' }, - { selector: 'audio', attr: 'src' }, - { selector: 'iframe', attr: 'src' }, - { selector: 'meta[property="og:image"]', attr: 'content' }, - { selector: 'meta[name="twitter:image"]', attr: 'content' }, - { selector: '[style]', attr: 'style' }, + { selector: "img", attr: "src" }, + { selector: "img", attr: "srcset" }, + { selector: "img", attr: "data-src" }, + { selector: "img", attr: "data-srcset" }, + { selector: 'link[rel="stylesheet"]', attr: "href" }, + { selector: 'link[rel*="icon"]', attr: "href" }, + { selector: "script", attr: "src" }, + { selector: 'link[rel="preload"]', attr: "href" }, + { selector: 'link[rel="prefetch"]', attr: "href" }, + { selector: 'link[rel="modulepreload"]', attr: "href" }, + { selector: 'link[rel="apple-touch-icon"]', attr: "href" }, + { selector: 'link[rel="mask-icon"]', attr: "href" }, + { selector: "source", attr: "src" }, + { selector: "source", attr: "srcset" }, + { selector: "video", attr: "src" }, + { selector: "video", attr: "poster" }, + { selector: "audio", attr: "src" }, + { selector: "iframe", attr: "src" }, + { selector: 'meta[property="og:image"]', attr: "content" }, + { selector: 'meta[name="twitter:image"]', attr: "content" }, + { selector: "[style]", attr: "style" }, ], urlFilter: (link: string) => { - const isAsset = /\.(js|css|jpg|jpeg|png|gif|svg|webp|woff|woff2|ttf|eot|otf|mp4|webm|mov|ogg|pdf|ico)(\?.*)?$/i.test(link); - const isNextAsset = link.includes('/_next/'); - const isSameDomain = link.startsWith(url) || link.startsWith('/') || !link.includes('://') || link.includes(domain); - const isGoogleTagManager = link.includes('googletagmanager.com'); - const isAnalytics = link.includes('analytics.mintel.me'); - const isVercelApp = link.includes('vercel.app'); - const isDataUrl = link.startsWith('data:'); - const isMailto = link.startsWith('mailto:'); - const isTel = link.startsWith('tel:'); - return (isAsset || isNextAsset || isSameDomain || isGoogleTagManager || isAnalytics || isVercelApp) && !isDataUrl && !isMailto && !isTel; + const isAsset = + /\.(js|css|jpg|jpeg|png|gif|svg|webp|woff|woff2|ttf|eot|otf|mp4|webm|mov|ogg|pdf|ico)(\?.*)?$/i.test( + link, + ); + const isNextAsset = link.includes("/_next/"); + const isSameDomain = + link.startsWith(url) || + link.startsWith("/") || + !link.includes("://") || + link.includes(domain); + const isGoogleTagManager = link.includes("googletagmanager.com"); + const isAnalytics = link.includes("analytics.mintel.me"); + const isVercelApp = link.includes("vercel.app"); + const isDataUrl = link.startsWith("data:"); + const isMailto = link.startsWith("mailto:"); + const isTel = link.startsWith("tel:"); + return ( + (isAsset || + isNextAsset || + isSameDomain || + isGoogleTagManager || + isAnalytics || + isVercelApp) && + !isDataUrl && + !isMailto && + !isTel + ); }, - filenameGenerator: 'bySiteStructure', + filenameGenerator: "bySiteStructure", subdirectories: [ - { directory: 'img', extensions: ['.jpg', '.png', '.svg', '.webp', '.gif', '.ico'] }, - { directory: 'js', extensions: ['.js'] }, - { directory: 'css', extensions: ['.css'] }, - { directory: 'fonts', extensions: ['.woff', '.woff2', '.ttf', '.eot', '.otf'] }, - { directory: 'videos', extensions: ['.mp4', '.webm', '.mov', '.ogg'] }, + { + directory: "img", + extensions: [".jpg", ".png", ".svg", ".webp", ".gif", ".ico"], + }, + { directory: "js", extensions: [".js"] }, + { directory: "css", extensions: [".css"] }, + { + directory: "fonts", + extensions: [".woff", ".woff2", ".ttf", ".eot", ".otf"], + }, + { directory: "videos", extensions: [".mp4", ".webm", ".mov", ".ogg"] }, ], }); - console.log('✅ Website cloned successfully!'); + console.log("✅ Website cloned successfully!"); console.log(`Location: ${outputDir}`); } catch (error) { - console.error('❌ Error cloning website:', error); + console.error("❌ Error cloning website:", error); process.exit(1); } } diff --git a/apps/web/scripts/generate-estimate.ts b/apps/web/scripts/generate-estimate.ts index 7255bce..65dabb2 100644 --- a/apps/web/scripts/generate-estimate.ts +++ b/apps/web/scripts/generate-estimate.ts @@ -4,10 +4,7 @@ import * as readline from "node:readline/promises"; import { fileURLToPath } from "node:url"; import { createElement } from "react"; import { renderToFile } from "@react-pdf/renderer"; -import { - calculatePositions, - calculateTotals, -} from "../src/logic/pricing/calculator.js"; +import { calculateTotals } from "../src/logic/pricing/calculator.js"; import { CombinedQuotePDF } from "../src/components/CombinedQuotePDF.js"; import { initialState, PRICING } from "../src/logic/pricing/constants.js"; import { @@ -18,7 +15,6 @@ import { } from "../src/logic/content-provider.js"; const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); async function main() { const args = process.argv.slice(2); diff --git a/apps/web/scripts/pagespeed-sitemap.ts b/apps/web/scripts/pagespeed-sitemap.ts index a89c61e..feccbc5 100644 --- a/apps/web/scripts/pagespeed-sitemap.ts +++ b/apps/web/scripts/pagespeed-sitemap.ts @@ -1,8 +1,8 @@ -import axios from 'axios'; -import * as cheerio from 'cheerio'; -import { execSync } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; +import axios from "axios"; +import * as cheerio from "cheerio"; +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; /** * PageSpeed Test Script @@ -13,10 +13,15 @@ import * as path from 'path'; */ const targetUrl = - process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'https://testing.mintel.me'; -const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; -const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mintel'; -const gatekeeperCookie = process.env.AUTH_COOKIE_NAME || 'mintel_gatekeeper_session'; + process.argv[2] || + process.env.NEXT_PUBLIC_BASE_URL || + "https://testing.mintel.me"; +const limit = process.env.PAGESPEED_LIMIT + ? parseInt(process.env.PAGESPEED_LIMIT) + : 20; +const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mintel"; +const gatekeeperCookie = + process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session"; async function main() { console.log(`\n🚀 Starting PageSpeed test for: ${targetUrl}`); @@ -24,7 +29,7 @@ async function main() { try { // 1. Fetch Sitemap - const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`; + const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`; console.log(`📥 Fetching sitemap from ${sitemapUrl}...`); // We might need to bypass gatekeeper for the sitemap fetch too @@ -36,21 +41,21 @@ async function main() { }); const $ = cheerio.load(response.data, { xmlMode: true }); - let urls = $('url loc') - .map((i, el) => $(el).text()) + let urls = $("url loc") + .map((_i, el) => $(el).text()) .get(); // Cleanup, filter and normalize domains to targetUrl const urlPattern = /https?:\/\/[^\/]+/; urls = [...new Set(urls)] - .filter((u) => u.startsWith('http')) - .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ''))) + .filter((u) => u.startsWith("http")) + .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ""))) .sort(); console.log(`✅ Found ${urls.length} URLs in sitemap.`); if (urls.length === 0) { - console.error('❌ No URLs found in sitemap. Is the site up?'); + console.error("❌ No URLs found in sitemap. Is the site up?"); process.exit(1); } @@ -59,7 +64,9 @@ async function main() { `⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`, ); // Try to pick a variety: home, some products, some blog posts - const home = urls.filter((u) => u.endsWith('/de') || u.endsWith('/en') || u === targetUrl); + const home = urls.filter( + (u) => u.endsWith("/de") || u.endsWith("/en") || u === targetUrl, + ); const others = urls.filter((u) => !home.includes(u)); urls = [...home, ...others.slice(0, limit - home.length)]; } @@ -69,7 +76,7 @@ async function main() { // 2. Prepare LHCI command // We use --collect.url multiple times - const urlArgs = urls.map((u) => `--collect.url="${u}"`).join(' '); + const urlArgs = urls.map((u) => `--collect.url="${u}"`).join(" "); // Handle authentication for staging/testing // Lighthouse can set cookies via --collect.settings.extraHeaders @@ -77,12 +84,15 @@ async function main() { Cookie: `${gatekeeperCookie}=${gatekeeperPassword}`, }); - const chromePath = process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH; - const chromePathArg = chromePath ? `--collect.chromePath="${chromePath}"` : ''; + const chromePath = + process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH; + const chromePathArg = chromePath + ? `--collect.chromePath="${chromePath}"` + : ""; // Clean up old reports - if (fs.existsSync('.lighthouseci')) { - fs.rmSync('.lighthouseci', { recursive: true, force: true }); + if (fs.existsSync(".lighthouseci")) { + fs.rmSync(".lighthouseci", { recursive: true, force: true }); } // Using a more robust way to execute and capture output @@ -93,27 +103,31 @@ async function main() { try { execSync(lhciCommand, { - encoding: 'utf8', - stdio: 'inherit', + encoding: "utf8", + stdio: "inherit", }); } catch (err: any) { - console.warn('⚠️ LHCI assertion finished with warnings or errors.'); + console.warn("⚠️ LHCI assertion finished with warnings or errors."); // We continue to show the table even if assertions failed } // 3. Summarize Results (Local & Independent) - const manifestPath = path.join(process.cwd(), '.lighthouseci', 'manifest.json'); + const manifestPath = path.join( + process.cwd(), + ".lighthouseci", + "manifest.json", + ); if (fs.existsSync(manifestPath)) { - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); console.log(`\n📊 PageSpeed Summary (FOSS - Local Report):\n`); const summaryTable = manifest.map((entry: any) => { const s = entry.summary; return { - URL: entry.url.replace(targetUrl, ''), + URL: entry.url.replace(targetUrl, ""), Perf: Math.round(s.performance * 100), Acc: Math.round(s.accessibility * 100), - BP: Math.round(s['best-practices'] * 100), + BP: Math.round(s["best-practices"] * 100), SEO: Math.round(s.seo * 100), }; }); @@ -123,24 +137,30 @@ async function main() { // Calculate Average const avg = { Perf: Math.round( - summaryTable.reduce((acc: any, curr: any) => acc + curr.Perf, 0) / summaryTable.length, + summaryTable.reduce((acc: any, curr: any) => acc + curr.Perf, 0) / + summaryTable.length, ), Acc: Math.round( - summaryTable.reduce((acc: any, curr: any) => acc + curr.Acc, 0) / summaryTable.length, + summaryTable.reduce((acc: any, curr: any) => acc + curr.Acc, 0) / + summaryTable.length, ), BP: Math.round( - summaryTable.reduce((acc: any, curr: any) => acc + curr.BP, 0) / summaryTable.length, + summaryTable.reduce((acc: any, curr: any) => acc + curr.BP, 0) / + summaryTable.length, ), SEO: Math.round( - summaryTable.reduce((acc: any, curr: any) => acc + curr.SEO, 0) / summaryTable.length, + summaryTable.reduce((acc: any, curr: any) => acc + curr.SEO, 0) / + summaryTable.length, ), }; console.log(`\n📈 Average Scores:`); - console.log(` Performance: ${avg.Perf > 90 ? '✅' : '⚠️'} ${avg.Perf}`); - console.log(` Accessibility: ${avg.Acc > 90 ? '✅' : '⚠️'} ${avg.Acc}`); - console.log(` Best Practices: ${avg.BP > 90 ? '✅' : '⚠️'} ${avg.BP}`); - console.log(` SEO: ${avg.SEO > 90 ? '✅' : '⚠️'} ${avg.SEO}`); + console.log( + ` Performance: ${avg.Perf > 90 ? "✅" : "⚠️"} ${avg.Perf}`, + ); + console.log(` Accessibility: ${avg.Acc > 90 ? "✅" : "⚠️"} ${avg.Acc}`); + console.log(` Best Practices: ${avg.BP > 90 ? "✅" : "⚠️"} ${avg.BP}`); + console.log(` SEO: ${avg.SEO > 90 ? "✅" : "⚠️"} ${avg.SEO}`); } console.log(`\n✨ PageSpeed tests completed successfully!`); diff --git a/apps/web/scripts/verify-components.ts b/apps/web/scripts/verify-components.ts index dfccbba..26a9174 100644 --- a/apps/web/scripts/verify-components.ts +++ b/apps/web/scripts/verify-components.ts @@ -3,83 +3,90 @@ * Verify components can be imported and used */ -import { join } from 'path'; +import { join } from "path"; -console.log('🔍 Verifying Embed Components...\n'); +console.log("🔍 Verifying Embed Components...\n"); // Test 1: Check if components can be imported try { - const YouTubePath = join(process.cwd(), 'src', 'components', 'YouTubeEmbed.astro'); - const TwitterPath = join(process.cwd(), 'src', 'components', 'TwitterEmbed.astro'); - const GenericPath = join(process.cwd(), 'src', 'components', 'GenericEmbed.astro'); - - console.log('✅ YouTubeEmbed.astro exists'); - console.log('✅ TwitterEmbed.astro exists'); - console.log('✅ GenericEmbed.astro exists'); - + console.log("✅ YouTubeEmbed.astro exists"); + console.log("✅ TwitterEmbed.astro exists"); + console.log("✅ GenericEmbed.astro exists"); } catch (error) { - console.log('❌ Component import error:', error); + console.log("❌ Component import error:", error); } // Test 2: Check demo post accessibility try { - const demoPath = join(process.cwd(), 'src', 'pages', 'blog', 'embed-demo.astro'); - const { readFileSync } = require('fs'); - - if (require('fs').existsSync(demoPath)) { - const content = readFileSync(demoPath, 'utf-8'); - + const demoPath = join( + process.cwd(), + "src", + "pages", + "blog", + "embed-demo.astro", + ); + const { readFileSync } = require("fs"); + + if (require("fs").existsSync(demoPath)) { + const content = readFileSync(demoPath, "utf-8"); + // Check if demo has proper structure - const hasImports = content.includes('import YouTubeEmbed') && - content.includes('import TwitterEmbed') && - content.includes('import GenericEmbed'); - - const hasUsage = content.includes(''); - + const hasImports = + content.includes("import YouTubeEmbed") && + content.includes("import TwitterEmbed") && + content.includes("import GenericEmbed"); + + const hasUsage = + content.includes(""); + if (hasImports && hasUsage) { - console.log('✅ Demo post has correct imports and usage'); + console.log("✅ Demo post has correct imports and usage"); } else { - console.log('❌ Demo post missing imports or usage'); + console.log("❌ Demo post missing imports or usage"); } - + // Check if it has BaseLayout - if (content.includes('BaseLayout')) { - console.log('✅ Demo post uses BaseLayout'); + if (content.includes("BaseLayout")) { + console.log("✅ Demo post uses BaseLayout"); } else { - console.log('❌ Demo post missing BaseLayout'); + console.log("❌ Demo post missing BaseLayout"); } } } catch (error) { - console.log('❌ Demo post check error:', error); + console.log("❌ Demo post check error:", error); } // Test 3: Check blogPosts array try { - const blogPostsPath = join(process.cwd(), 'src', 'data', 'blogPosts.ts'); - const { readFileSync } = require('fs'); - - const content = readFileSync(blogPostsPath, 'utf-8'); - + const blogPostsPath = join(process.cwd(), "src", "data", "blogPosts.ts"); + const { readFileSync } = require("fs"); + + const content = readFileSync(blogPostsPath, "utf-8"); + // Check if embed-demo needs to be added - if (!content.includes('embed-demo')) { - console.log('⚠️ embed-demo not in blogPosts array - this is why it won\'t show in blog list'); - console.log(' But it should still be accessible at /blog/embed-demo directly'); + if (!content.includes("embed-demo")) { + console.log( + "⚠️ embed-demo not in blogPosts array - this is why it won't show in blog list", + ); + console.log( + " But it should still be accessible at /blog/embed-demo directly", + ); } else { - console.log('✅ embed-demo found in blogPosts array'); + console.log("✅ embed-demo found in blogPosts array"); } } catch (error) { - console.log('❌ blogPosts check error:', error); + console.log("❌ blogPosts check error:", error); } -console.log('\n' + '='.repeat(60)); -console.log('📋 SUMMARY:'); -console.log('• Components are created and structured correctly'); -console.log('• Demo post exists at src/pages/blog/embed-demo.astro'); -console.log('• Demo post has all required imports and usage'); -console.log('\n🔧 TO FIX BLOG LISTING:'); -console.log('Add embed-demo to src/data/blogPosts.ts array'); -console.log('\n🚀 TO TEST COMPONENTS:'); -console.log('Visit: http://localhost:4321/blog/embed-demo'); -console.log('If that 404s, the demo post needs to be added to blogPosts.ts'); \ No newline at end of file +console.log("\n" + "=".repeat(60)); +console.log("📋 SUMMARY:"); +console.log("• Components are created and structured correctly"); +console.log("• Demo post exists at src/pages/blog/embed-demo.astro"); +console.log("• Demo post has all required imports and usage"); +console.log("\n🔧 TO FIX BLOG LISTING:"); +console.log("Add embed-demo to src/data/blogPosts.ts array"); +console.log("\n🚀 TO TEST COMPONENTS:"); +console.log("Visit: http://localhost:4321/blog/embed-demo"); +console.log("If that 404s, the demo post needs to be added to blogPosts.ts"); diff --git a/apps/web/src/components/AgbsPDF.tsx b/apps/web/src/components/AgbsPDF.tsx index 75211dc..66a1561 100644 --- a/apps/web/src/components/AgbsPDF.tsx +++ b/apps/web/src/components/AgbsPDF.tsx @@ -1,154 +1,241 @@ -'use client'; +"use client"; -import * as React from 'react'; +import * as React from "react"; import { - Page as PDFPage, - Text as PDFText, - View as PDFView, - StyleSheet as PDFStyleSheet, -} from '@react-pdf/renderer'; -import { pdfStyles, Header, Footer, FoldingMarks, DocumentTitle } from './pdf/SharedUI'; -import { SimpleLayout } from './pdf/SimpleLayout'; + Page as PDFPage, + Text as PDFText, + View as PDFView, + StyleSheet as PDFStyleSheet, +} from "@react-pdf/renderer"; +import { + pdfStyles, + Header, + Footer, + FoldingMarks, + DocumentTitle, +} from "./pdf/SharedUI"; +import { SimpleLayout } from "./pdf/SimpleLayout"; const localStyles = PDFStyleSheet.create({ - sectionContainer: { - marginTop: 0, - }, - agbSection: { - marginBottom: 20, - }, - labelRow: { - flexDirection: 'row', - alignItems: 'baseline', - marginBottom: 6, - }, - monoNumber: { - fontSize: 7, - fontWeight: 'bold', - color: '#94a3b8', - letterSpacing: 2, - width: 25, - }, - sectionTitle: { - fontSize: 9, - fontWeight: 'bold', - color: '#000000', - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - officialText: { - fontSize: 8, - lineHeight: 1.5, - color: '#334155', - textAlign: 'justify', - paddingLeft: 25, - } + sectionContainer: { + marginTop: 0, + }, + agbSection: { + marginBottom: 20, + }, + labelRow: { + flexDirection: "row", + alignItems: "baseline", + marginBottom: 6, + }, + monoNumber: { + fontSize: 7, + fontWeight: "bold", + color: "#94a3b8", + letterSpacing: 2, + width: 25, + }, + sectionTitle: { + fontSize: 9, + fontWeight: "bold", + color: "#000000", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + officialText: { + fontSize: 8, + lineHeight: 1.5, + color: "#334155", + textAlign: "justify", + paddingLeft: 25, + }, }); -const AGBSection = ({ index, title, children }: { index: string; title: string; children: React.ReactNode }) => ( - {index}{title}{children} +const AGBSection = ({ + index, + title, + children, +}: { + index: string; + title: string; + children: React.ReactNode; +}) => ( + + + {index} + {title} + + {children} + ); interface AgbsPDFProps { - state: any; - headerIcon?: string; - footerLogo?: string; - mode?: 'estimation' | 'full'; + headerIcon?: string; + footerLogo?: string; + mode?: "estimation" | "full"; } -export const AgbsPDF = ({ state, headerIcon, footerLogo, mode = 'full' }: AgbsPDFProps) => { - const date = new Date().toLocaleDateString('de-DE', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); +export const AgbsPDF = ({ + headerIcon, + footerLogo, + mode = "full", +}: AgbsPDFProps) => { + const date = new Date().toLocaleDateString("de-DE", { + year: "numeric", + month: "long", + day: "numeric", + }); - const companyData = { - name: "Marc Mintel", - address1: "Georg-Meistermann-Straße 7", - address2: "54586 Schüller", - ustId: "DE367588065" - }; + const companyData = { + name: "Marc Mintel", + address1: "Georg-Meistermann-Straße 7", + address2: "54586 Schüller", + ustId: "DE367588065", + }; - const bankData = { - name: "N26", - bic: "NTSBDEB1XXX", - iban: "DE50 1001 1001 2620 4328 65" - }; + const bankData = { + name: "N26", + bic: "NTSBDEB1XXX", + iban: "DE50 1001 1001 2620 4328 65", + }; - const content = ( - <> - - - - Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend „Auftragnehmer“) und dem jeweiligen Kunden (nachfolgend „Auftraggeber“). Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch wenn ihrer Geltung nicht ausdrücklich widersprochen wird. - + const content = ( + <> + + + + Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge + zwischen Marc Mintel (nachfolgend „Auftragnehmer“) und dem jeweiligen + Kunden (nachfolgend „Auftraggeber“). Abweichende oder ergänzende + Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch + wenn ihrer Geltung nicht ausdrücklich widersprochen wird. + - - Der Auftragnehmer erbringt Dienstleistungen im Bereich: Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen, Schnittstellen und Automatisierungen sowie Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbard. Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch einen wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten, Suchmaschinen-Rankings oder rechtliche Ergebnisse. - + + Der Auftragnehmer erbringt Dienstleistungen im Bereich: + Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen, + Schnittstellen und Automatisierungen sowie Hosting, Betrieb und + Wartung, sofern ausdrücklich vereinbard. Der Auftragnehmer schuldet + ausschließlich die vereinbarte technische Leistung, nicht jedoch einen + wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten, + Suchmaschinen-Rankings oder rechtliche Ergebnisse. + - - Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback, Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum, DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen aller Termine ohne Schadensersatzanspruch. - + + Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung + erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen + rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen + insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback, + Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum, + DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen + aller Termine ohne Schadensersatzanspruch. + - - Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als verbindlich vereinbart wurden. - + + Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine + garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie + ausdrücklich schriftlich als verbindlich vereinbart wurden. + - - Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar. - + + Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv + nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine + wesentlichen Mängel angezeigt werden. Optische Abweichungen, + Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel + dar. + - - Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für entgangenen Gewinn, Umsatzausfälle, Datenverlust, Betriebsunterbrechungen, mittelbare oder Folgeschäden ist ausgeschlossen, soweit gesetzlich zulässig. - + + Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder + grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für + entgangenen Gewinn, Umsatzausfälle, Datenverlust, + Betriebsunterbrechungen, mittelbare oder Folgeschäden ist + ausgeschlossen, soweit gesetzlich zulässig. + - - Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine permanente Verfügbarkeit. Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen Einschränkungen führen und begründen keine Haftungsansprüche. - + + Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine + permanente Verfügbarkeit. Wartungsarbeiten, Updates, + Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen + Einschränkungen führen und begründen keine Haftungsansprüche. + - - Die Betriebs- und Pflegeleistung umfasst ausschließlich die Sicherstellung des technischen Betriebs, Wartung, Updates, Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender Datensätze ohne Strukturänderung. Nicht Bestandteil sind die Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle Tätigkeiten, strategische Planung oder der Aufbau neuer Features/Datenmodelle. Leistungen darüber hinaus gelten als Neuentwicklung. - + + Die Betriebs- und Pflegeleistung umfasst ausschließlich die + Sicherstellung des technischen Betriebs, Wartung, Updates, + Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender + Datensätze ohne Strukturänderung. Nicht Bestandteil sind die + Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle + Tätigkeiten, strategische Planung oder der Aufbau neuer + Features/Datenmodelle. Leistungen darüber hinaus gelten als + Neuentwicklung. + - - Der Auftragnehmer übernimmt keine Verantwortung für Leistungen, Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden. - + + Der Auftragnehmer übernimmt keine Verantwortung für Leistungen, + Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder + Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der + jeweils aktuellen externen Schnittstellen gewährleistet werden. + - - Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten. Der Auftragnehmer übernimmt keine rechtliche Prüfung. - + + Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche + Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten. + Der Auftragnehmer übernimmt keine rechtliche Prüfung. + - - Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt, Leistungen auszusetzen, Systeme offline zu nehmen oder laufende Arbeiten zu stoppen. - + + Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen + fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt, + Leistungen auszusetzen, Systeme offline zu nehmen oder laufende + Arbeiten zu stoppen. + - - Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes vereinbart ist. - + + Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist + von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes + vereinbart ist. + - - Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt. - - - - ); - - if (mode === 'full') { - return ( - - {content} - - ); - } + + Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist + der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein, + bleibt die Wirksamkeit der übrigen Regelungen unberührt. + + + + ); + if (mode === "full") { return ( - - -
- {content} -