<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>WebCraftDev | Professional Web Development & Mobile Apps | Europe</title>
  <meta name="description" content="Tired of websites that don't convert? WebCraftDev creates custom websites and mobile apps that turn visitors into paying customers. We've helped 50+ European businesses grow their online revenue by 300% on average. Professional development, 2-week delivery, ongoing support. Get your free consultation today." />
  <meta name="author" content="WebCraftDev" />
  <meta name="keywords" content="web developer France, mobile app developer Europe, French web development, European digital solutions, custom websites France, mobile apps Europe, freelance developer France, web development Paris, digital transformation Europe, responsive websites France" />
  <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
  <meta name="google-site-verification" content="INJ4GGgYIkp9XADsD_axOQyI6x28DlUuIUAVd9zk3Wo" />

  <!-- Security Headers -->
  <meta name="csp-nonce" content="webcraftdev-2025" />
  <meta http-equiv="X-Content-Type-Options" content="nosniff" />
  <!-- X-Frame-Options moved to Netlify headers for proper HTTP header implementation -->
  <meta http-equiv="X-XSS-Protection" content="1; mode=block" />
  <meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin" />

  <!-- Canonical URL is dynamically set by React components -->

  <!-- Open Graph / Facebook -->
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://webcraftdev.com/" />
  <meta property="og:title" content="WebCraftDev - Professional Web & Mobile Development" />
  <meta property="og:description" content="Professional web and mobile development services. Custom websites, mobile apps, and digital solutions that drive business growth." />
  <meta property="og:image" content="https://res.cloudinary.com/dq0miq9rj/image/upload/w_1200,h_630,c_fit,q_85,f_auto/v1752175976/Transform_2_yq77t1.png" />
  <meta property="og:image:secure_url" content="https://res.cloudinary.com/dq0miq9rj/image/upload/w_1200,h_630,c_fit,q_85,f_auto/v1752175976/Transform_2_yq77t1.png" />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
  <meta property="og:image:type" content="image/png" />
  <meta property="og:site_name" content="WebCraftDev" />
  <meta property="fb:app_id" content="1381450706244900" />





  <!-- Twitter Card meta tags -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="WebCraftDev - Professional Web & Mobile Development" />
  <meta name="twitter:description" content="Professional web and mobile development services. Custom websites, mobile apps, and digital solutions." />
  <meta name="twitter:image" content="https://res.cloudinary.com/dq0miq9rj/image/upload/w_1200,h_630,c_fit,q_85,f_auto/v1752175976/Transform_2_yq77t1.png" />
  <meta name="twitter:site" content="@webcraft_dev" />
  <meta name="twitter:creator" content="@webcraft_dev" />

  <link rel="icon" type="image/png" href="https://res.cloudinary.com/dq0miq9rj/image/upload/w_32,h_32,c_fit,q_85,f_auto/v1753684306/blue-website_xezanh.png" />
  <link rel="apple-touch-icon" href="https://res.cloudinary.com/dq0miq9rj/image/upload/w_180,h_180,c_fit,q_85,f_auto/v1753684306/blue-website_xezanh.png" />
  <link rel="shortcut icon" href="https://res.cloudinary.com/dq0miq9rj/image/upload/w_32,h_32,c_fit,q_85,f_auto/v1753684306/blue-website_xezanh.png" />

  <!-- Structured Data / JSON-LD -->
  <script type="application/ld+json" nonce="webcraftdev-2025">
  {
    "@context": "https://schema.org",
    "@type": ["Person", "ProfessionalService"],
    "name": "Mohamed Sahbi",
    "alternateName": "WebCraftDev",
    "jobTitle": "Full Stack Developer & Digital Architect",
    "description": "French-based web developer helping European businesses succeed online since 2015. Specializing in custom websites that convert and mobile apps that users actually enjoy.",
    "url": "https://webcraftdev.com",
    "image": "https://res.cloudinary.com/dq0miq9rj/image/upload/v1753645702/688328406f092abd9e938e47-HeadshotPro-ezgif.com-resize_lnu5oz.png",
    "email": "mohamed-sahbi@webcraftdev.com",
    "telephone": "+33784852149",
    "address": {
      "@type": "PostalAddress",
      "addressCountry": "FR",
      "addressRegion": "France"
    },
    "areaServed": [
      {
        "@type": "Country",
        "name": "France"
      },
      {
        "@type": "Continent",
        "name": "Europe"
      }
    ],
    "serviceArea": {
      "@type": "GeoCircle",
      "geoMidpoint": {
        "@type": "GeoCoordinates",
        "latitude": 46.603354,
        "longitude": 1.888334
      },
      "geoRadius": "2000000"
    },
    "sameAs": [
      "https://www.linkedin.com/in/mohamed-sahbi-530a60239/",
      "https://github.com/moeett"
    ],
    "knowsAbout": [
      "Web Development France",
      "Mobile App Development Europe",
      "Full Stack Development",
      "UX/UI Design",
      "SEO Optimization",
      "Performance Optimization",
      "React Development",
      "TypeScript",
      "Node.js",
      "European Market Digital Solutions"
    ],
    "hasOfferCatalog": {
      "@type": "OfferCatalog",
      "name": "Web Development Services",
      "itemListElement": [
        {
          "@type": "Offer",
          "itemOffered": {
            "@type": "Service",
            "name": "Custom Website Development",
            "description": "Professional websites that actually convert visitors into customers"
          }
        },
        {
          "@type": "Offer",
          "itemOffered": {
            "@type": "Service",
            "name": "Mobile App Development",
            "description": "Mobile applications that users love and businesses profit from"
          }
        },
        {
          "@type": "Offer",
          "itemOffered": {
            "@type": "Service",
            "name": "Digital Transformation",
            "description": "Complete digital solutions for European businesses"
          }
        }
      ]
    },
    "priceRange": "€450-€50000",
    "currenciesAccepted": "EUR",
    "paymentAccepted": "Bank Transfer, PayPal"
  }
  </script>

  <!-- Note: Crawler detection now handled server-side by blog-handler function -->

  <!-- Performance optimizations: DNS prefetch and preconnect -->
  <link rel="dns-prefetch" href="https://www.googletagmanager.com">
  <link rel="dns-prefetch" href="https://www.google-analytics.com">
  <link rel="dns-prefetch" href="https://connect.facebook.net">
  <link rel="dns-prefetch" href="https://res.cloudinary.com">
  <link rel="dns-prefetch" href="https://unpkg.com">
  <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
  <link rel="preconnect" href="https://connect.facebook.net" crossorigin>

  <!-- Self-hosted fonts for optimal performance with font-display swap -->
  <!-- Only preload fonts used in hero section to avoid unused preload warnings -->
  <link rel="preload" href="/fonts/inter-600.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="stylesheet" href="/fonts/inter.css">

  <!-- Prefetch other font weights for later use -->
  <link rel="prefetch" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="prefetch" href="/fonts/inter-500.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="prefetch" href="/fonts/inter-700.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="prefetch" href="/fonts/inter-800.woff2" as="font" type="font/woff2" crossorigin>

  <!-- Critical CSS inlined to eliminate render-blocking -->
  <style>
    /* Critical above-the-fold styles - Updated to remove conflicting .hero-title definitions */
    *,*::before,*::after{box-sizing:border-box}html{font-size:16px;line-height:1.5;-webkit-text-size-adjust:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;padding:0;min-height:100vh;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background-color:#0f172a;color:#f8fafc}#root{min-height:100vh;isolation:isolate}.loading-screen{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:9999}.loading-content{text-align:center;max-width:600px;padding:2rem}.code-container{font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:1.5rem;margin-bottom:3rem;position:relative}.code-bracket-left,.code-bracket-right{color:#ef4444;font-weight:bold;opacity:0}.code-line{color:#64748b;margin:0.5rem 0;opacity:0}.typing-cursor{color:#ef4444;animation:blink 1s infinite;opacity:0}@keyframes blink{0%,50%{opacity:1}51%,100%{opacity:0}}.navigation{position:fixed;top:0;left:0;right:0;z-index:1000;background:rgba(15,23,42,0.8);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-bottom:1px solid rgba(148,163,184,0.1)}.hero-section{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);position:relative;overflow:hidden}.hero-content{text-align:center;max-width:1200px;padding:2rem;z-index:2}.hero-subtitle{font-size:clamp(1.1rem,3vw,1.5rem);color:#94a3b8;margin-bottom:2rem;max-width:600px;margin-left:auto;margin-right:auto}.btn-primary{display:inline-flex;align-items:center;justify-content:center;padding:0.75rem 2rem;font-size:1rem;font-weight:600;text-decoration:none;border-radius:0.5rem;background:linear-gradient(135deg,#ef4444 0%,#dc2626 100%);color:white;border:none;cursor:pointer;transition:all 0.2s ease;transform:translateZ(0)}.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 25px rgba(239,68,68,0.3)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.container{width:100%;max-width:1200px;margin:0 auto;padding:0 1rem}@media (max-width:768px){.hero-subtitle{font-size:1.1rem}.container{padding:0 1rem}}.performance-critical{contain:layout style paint;will-change:transform}.gpu-accelerated{transform:translate3d(0,0,0);backface-visibility:hidden}:root{--background:220 15% 6%;--foreground:0 0% 98%;--primary:0 84% 60%;--primary-foreground:0 0% 98%;--secondary:220 15% 6%;--secondary-foreground:0 0% 98%;--muted:220 15% 15%;--muted-foreground:220 15% 65%;--accent:220 15% 15%;--accent-foreground:0 0% 98%;--destructive:0 84% 60%;--destructive-foreground:0 0% 98%;--border:220 15% 15%;--input:220 15% 15%;--ring:0 84% 60%;--radius:0.5rem}.non-critical{opacity:0;transition:opacity 0.3s ease}.non-critical.loaded{opacity:1}
  </style>

  <!-- Analytics scripts now loaded dynamically for better performance -->
  <!-- See src/utils/performanceAnalytics.ts for implementation -->



  <!-- AnimeJS now loaded dynamically when needed - see dynamicImports.ts -->

  <!-- Spline Viewer - Conditionally loaded only when needed -->
  <!-- Moved to SplineViewer component to avoid loading on all pages -->

  <!-- Global error handler for Spline viewer -->
  <script nonce="webcraftdev-2025">
    // Catch any unhandled errors from Spline viewer
    window.addEventListener('error', function(event) {
      if (event.filename && event.filename.includes('spline-viewer')) {
        event.preventDefault(); // Prevent the error from showing in console
        return true;
      }
    });

    // Catch unhandled promise rejections from Spline viewer
    window.addEventListener('unhandledrejection', function(event) {
      if (event.reason && (
        event.reason.message?.includes('spline') ||
        event.reason.stack?.includes('spline-viewer') ||
        event.reason.toString().includes('Missing property')
      )) {
        event.preventDefault(); // Prevent the error from showing in console
      }
    });

    // Enhanced console filtering for WebGL and Spline errors
    const originalConsoleError = console.error;
    const originalConsoleWarn = console.warn;
    const originalConsoleLog = console.log;

    // WebGL error patterns to suppress
    const webglErrorPatterns = [
      'GL_INVALID_VALUE',
      'GL_INVALID_FRAMEBUFFER_OPERATION',
      'glTexStorage2D',
      'glClear',
      'glClearBufferfv',
      'glDrawElements',
      'glDrawArrays',
      'Framebuffer is incomplete',
      'Texture dimensions must all be greater than zero',
      'Attachment has zero size',
      'WebGL: too many errors',
      'WebGL-0x'
    ];

    // Override console methods to suppress WebGL warnings
    console.warn = function(...args) {
      const message = args.join(' ');
      if (!webglErrorPatterns.some(pattern => message.includes(pattern))) {
        originalConsoleWarn.apply(console, args);
      }
    };

    console.error = function(...args) {
      const message = args.join(' ');
      if (!webglErrorPatterns.some(pattern => message.includes(pattern))) {
        originalConsoleError.apply(console, args);
      }
    };

    // Override WebGL context creation to add error suppression
    const originalGetContext = HTMLCanvasElement.prototype.getContext;
    HTMLCanvasElement.prototype.getContext = function(contextType, ...args) {
      const context = originalGetContext.call(this, contextType, ...args);

      if (contextType === 'webgl' || contextType === 'webgl2' || contextType === 'experimental-webgl') {
        if (context) {
          // Suppress WebGL errors at the source
          const originalGetError = context.getError;
          context.getError = function() {
            const error = originalGetError.call(this);
            // Silently consume WebGL errors during navigation/initialization
            return context.NO_ERROR;
          };
        }
      }

      return context;
    };

    // Global error handler for WebGL errors during navigation
    window.addEventListener('error', function(event) {
      if (event.message && (
        event.message.includes('WebGL') ||
        event.message.includes('GL_INVALID') ||
        event.message.includes('Framebuffer') ||
        event.message.includes('glClear') ||
        event.message.includes('glDraw') ||
        event.message.includes('Texture dimensions')
      )) {
        event.preventDefault();
        event.stopPropagation();
        return false;
      }
    }, true);

    // Suppress unhandled promise rejections related to WebGL
    window.addEventListener('unhandledrejection', function(event) {
      if (event.reason && event.reason.message && (
        event.reason.message.includes('WebGL') ||
        event.reason.message.includes('GL_INVALID') ||
        event.reason.message.includes('Framebuffer')
      )) {
        event.preventDefault();
        return false;
      }
    });

    // Navigation-aware WebGL error suppression
    let isNavigating = false;
    let navigationTimeout;

    // Detect navigation start
    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;

    history.pushState = function(...args) {
      isNavigating = true;
      clearTimeout(navigationTimeout);
      navigationTimeout = setTimeout(() => { isNavigating = false; }, 2000);
      return originalPushState.apply(this, args);
    };

    history.replaceState = function(...args) {
      isNavigating = true;
      clearTimeout(navigationTimeout);
      navigationTimeout = setTimeout(() => { isNavigating = false; }, 2000);
      return originalReplaceState.apply(this, args);
    };

    // Listen for popstate (back/forward navigation)
    window.addEventListener('popstate', function() {
      isNavigating = true;
      clearTimeout(navigationTimeout);
      navigationTimeout = setTimeout(() => { isNavigating = false; }, 2000);
    });

    // Detect clicks on navigation links for immediate suppression
    document.addEventListener('click', function(event) {
      const target = event.target.closest('a');
      if (target && target.href && target.href.includes(window.location.origin)) {
        isNavigating = true;
        clearTimeout(navigationTimeout);
        navigationTimeout = setTimeout(() => { isNavigating = false; }, 3000);
      }
    });

    // Start with navigation suppression active
    isNavigating = true;
    setTimeout(() => { isNavigating = false; }, 2000);

    // Override WebGL context methods during navigation
    const originalWebGLMethods = {};

    function suppressWebGLDuringNavigation() {
      if (isNavigating && window.WebGLRenderingContext) {
        const methods = ['clear', 'drawElements', 'drawArrays', 'texStorage2D', 'clearBufferfv'];
        methods.forEach(method => {
          if (!originalWebGLMethods[method] && WebGLRenderingContext.prototype[method]) {
            originalWebGLMethods[method] = WebGLRenderingContext.prototype[method];
            WebGLRenderingContext.prototype[method] = function(...args) {
              try {
                return originalWebGLMethods[method].apply(this, args);
              } catch (e) {
                // Silently ignore WebGL errors during navigation
                return;
              }
            };
          }
        });
      }
    }

    // Apply suppression periodically during navigation
    setInterval(suppressWebGLDuringNavigation, 100);

    console.error = function(...args) {
      const message = args.join(' ');
      if (message.includes('Missing property') ||
          message.includes('spline-viewer') ||
          message.includes('WebGL') ||
          message.includes('GL_INVALID') ||
          message.includes('GL_INVALID_VALUE') ||
          message.includes('GL_INVALID_FRAMEBUFFER_OPERATION') ||
          message.includes('glTexStorage2D') ||
          message.includes('glClear') ||
          message.includes('glDrawElements') ||
          message.includes('glDrawArrays') ||
          message.includes('glClearBufferfv') ||
          message.includes('Framebuffer') ||
          message.includes('Texture dimensions must all be greater than zero') ||
          message.includes('Attachment has zero size') ||
          message.includes('Framebuffer is incomplete') ||
          message.includes('too many errors') ||
          message.includes('no more errors will be reported') ||
          message.includes('[.WebGL-') ||
          message.includes('Understand this warning') ||
          message.includes('Understand this error') ||
          message.includes('Spline Runtime') ||
          message.includes('.splinecode file is more recent') ||
          isNavigating) { // Suppress ALL errors during navigation
        return; // Completely suppress these messages
      }
      originalConsoleError.apply(console, args);
    };

    console.warn = function(...args) {
      const message = args.join(' ');
      if (message.includes('Missing property') ||
          message.includes('spline-viewer') ||
          message.includes('WebGL') ||
          message.includes('GL_INVALID') ||
          message.includes('GL_INVALID_VALUE') ||
          message.includes('GL_INVALID_FRAMEBUFFER_OPERATION') ||
          message.includes('glTexStorage2D') ||
          message.includes('glClear') ||
          message.includes('glDrawElements') ||
          message.includes('glDrawArrays') ||
          message.includes('glClearBufferfv') ||
          message.includes('Framebuffer') ||
          message.includes('Texture dimensions must all be greater than zero') ||
          message.includes('Attachment has zero size') ||
          message.includes('Framebuffer is incomplete') ||
          message.includes('too many errors') ||
          message.includes('no more errors will be reported') ||
          message.includes('[.WebGL-') ||
          message.includes('Understand this warning') ||
          message.includes('Understand this error') ||
          message.includes('Spline Runtime') ||
          message.includes('.splinecode file is more recent') ||
          isNavigating) { // Suppress ALL warnings during navigation
        return; // Completely suppress these messages
      }
      originalConsoleWarn.apply(console, args);
    };

    // Also intercept any console.log that might contain Spline warnings
    console.log = function(...args) {
      const message = args.join(' ');
      if (message.includes('Spline Runtime') ||
          message.includes('.splinecode file is more recent')) {
        return; // Completely suppress these messages
      }
      originalConsoleLog.apply(console, args);
    };

    // Override the specific Spline logging function if it exists
    setTimeout(() => {
      if (window.console && window.console.groupCollapsed) {
        const originalGroupCollapsed = console.groupCollapsed;
        console.groupCollapsed = function(...args) {
          const message = args.join(' ');
          if (message.includes('Spline Runtime') ||
              message.includes('.splinecode')) {
            return; // Suppress Spline group logs
          }
          originalGroupCollapsed.apply(console, args);
        };
      }

      // Try to intercept Spline's internal logging by checking for spline-viewer elements
      const splineViewers = document.querySelectorAll('spline-viewer');
      splineViewers.forEach(viewer => {
        if (viewer.shadowRoot) {
          // Try to suppress internal Spline logging
          const originalFetch = window.fetch;
          window.fetch = function(...args) {
            const url = args[0];
            if (typeof url === 'string' && url.includes('.splinecode')) {
              // Suppress any fetch-related logging for splinecode files
              return originalFetch.apply(this, args).catch(err => {
                // Silently handle splinecode fetch errors
                return Promise.resolve(new Response());
              });
            }
            return originalFetch.apply(this, args);
          };
        }
      });
    }, 2000);
  </script>
  <script type="module" crossorigin src="/assets/index-BWidIZti.js"></script>
  <link rel="modulepreload" crossorigin href="/assets/zip-vendor-CQfOIJNR.js">
  <link rel="stylesheet" crossorigin href="/assets/index-D9SRwGFb.css">
</head>

<body>
  <!-- Google Tag Manager (noscript) -->
  <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MKHWFG6Z"
  height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->

  <!-- Facebook Pixel (noscript) -->
  <noscript>
    <img height="1" width="1" style="display:none"
         src="https://www.facebook.com/tr?id=752245637314757&ev=PageView&noscript=1"/>
  </noscript>

  <div id="root"></div>
  <div id="cursor-dot" class="cursor-dot"></div>
  <div id="cursor-outline" class="cursor-outline"></div>
</body>

</html>