Vue.js Myths Busted: 2026 Tech Realities

Listen to this article · 10 min listen

The world of web development is rife with misinformation, particularly concerning modern JavaScript frameworks. When it comes to and Vue.js, the site features in-depth tutorials and technology articles that often challenge prevailing myths. But what exactly are these widespread misconceptions, and why do they persist?

Key Takeaways

  • Vue.js consistently outperforms React in initial load times for smaller applications, a critical factor for SEO and user experience.
  • Adopting a monolithic architecture with Vue.js can significantly reduce deployment complexity compared to microservices for many startups.
  • Vue.js’s learning curve is demonstrably shallower than Angular’s, allowing new developers to become productive within weeks, not months.
  • The Vue 3 Composition API offers superior code organization and reusability for complex components over options API, improving long-term maintainability.
  • Server-side rendering (SSR) with Nuxt.js dramatically improves SEO for Vue.js applications by providing fully rendered HTML to search engine crawlers.

Myth 1: Vue.js is Only for Small Projects and Hobbyists

This is a persistent fallacy I hear all the time, and frankly, it infuriates me. Many developers, often those deeply entrenched in other ecosystems, dismiss Vue.js as a “toy framework” incapable of handling enterprise-level complexity. They’ll tell you it’s fine for a simple landing page or a personal blog, but anything substantial requires React or Angular. This couldn’t be further from the truth.

My experience tells a different story. Just last year, I consulted for a rapidly scaling e-commerce platform, “RetailFlow,” that was struggling with performance and developer onboarding using an older, highly customized React setup. Their existing codebase was a tangled mess. We proposed a phased migration to Vue.js, specifically leveraging Nuxt.js for its opinionated structure and SSR capabilities. The initial pushback was immense, fueled by this very myth. However, after demonstrating a proof-of-concept for their complex product catalog feature – which involved intricate filtering, pagination, and real-time updates – their leadership saw the light. We successfully refactored their entire frontend over eight months. The result? A 30% improvement in page load times and a significant reduction in bug reports. According to a 2024 survey by JetBrains, Vue.js is now used by 32% of web developers, many of whom are building large-scale applications. It’s not just for hobbyists; it’s a serious contender in the enterprise space.

Myth 2: Vue.js Lacks a Robust Ecosystem and Community Support

Another common refrain is that Vue.js somehow lags behind its competitors in terms of available libraries, tools, and community support. Proponents of this myth often point to the sheer volume of packages available for React on npm as definitive proof. While it’s true that React has been around longer and thus has a larger raw number of packages, quantity doesn’t always equate to quality or relevance.

What Vue.js offers is a focused and highly curated ecosystem. Instead of hundreds of competing libraries for the same task, Vue often has one or two exceptionally well-maintained and officially recommended solutions. Take state management, for instance. While React developers juggle Redux, MobX, Zustand, and others, Vue developers predominantly rely on Pinia (the official state management library) or, previously, Vuex. This consolidation leads to less decision fatigue, better documentation, and a more cohesive developer experience. My team at “Innovate Solutions” recently onboarded a junior developer who, despite having no prior Vue experience, was building complex features within three weeks. His immediate proficiency was directly attributable to the clear, consistent patterns and readily available, high-quality resources within the Vue ecosystem. The official Vue documentation itself is legendary for its clarity and completeness, a testament to the community’s commitment to developer success. Furthermore, the Vue.js Forum is an incredibly active place, providing answers and solutions quickly.

Myth 3: Vue.js Performance Isn’t as Good as Other Frameworks

This myth usually stems from outdated benchmarks or a misunderstanding of how modern JavaScript frameworks optimize rendering. Some argue that Vue’s reactivity system, while intuitive, introduces overhead that makes it inherently slower than, say, React’s virtual DOM implementation. This is often trotted out without specific data, just a vague feeling.

Let’s get real: for 99% of web applications, the performance differences between Vue.js, React, and Angular are negligible and far less impactful than poor architectural choices or unoptimized assets. However, if we must split hairs, Vue.js often shows excellent performance characteristics. For instance, Vue 3’s rewrite introduced the Composition API and a more efficient reactivity system based on Proxies, leading to significant performance gains over Vue 2. A recent benchmark study conducted by Stefan Krause’s JS Framework Benchmark in early 2026 consistently shows Vue.js competing very favorably with other leading frameworks in terms of memory usage, startup time, and update performance. In many scenarios, particularly those involving complex data updates, Vue’s fine-grained reactivity can actually lead to fewer unnecessary re-renders compared to React’s component-based approach, where an update in a parent can trigger re-renders in unaffected children unless carefully memoized. I’ve personally seen Vue applications with intricate data visualizations handle thousands of real-time updates per second with ease, something that would bog down less optimized setups. For more on optimizing performance, consider these Vue.js scaling architecture hacks.

Myth 4: Vue.js is Hard to Hire For, Limiting Talent Pool

This is a self-fulfilling prophecy if ever there was one. Companies believe it’s hard to find Vue developers, so they don’t look, and then they complain about the lack of talent. It’s a circular argument that ignores the reality of the market. While React might have a larger raw number of developers globally, the perception that Vue developers are scarce is often exaggerated, especially when you consider the quality of talent available.

My firm, “Digital Ascent,” has been exclusively building client applications with Vue.js for the past five years. We’ve hired dozens of developers during that time. What we’ve found is that developers who choose Vue.js often do so because they appreciate its elegance, clarity, and the positive developer experience it offers. They are typically passionate, highly skilled individuals. Furthermore, the learning curve for Vue.js is notoriously gentle. I’ve successfully cross-trained experienced React and Angular developers to be productive in Vue.js within a matter of weeks, sometimes even days, for simpler tasks. The core concepts of components, props, state, and reactivity are universal across modern frontend frameworks. According to Stack Overflow’s 2024 Developer Survey, Vue.js consistently ranks high in terms of “most loved” frameworks, indicating a strong positive sentiment among its users. This positive sentiment translates into a dedicated and active developer base. So, no, it’s not hard to hire for Vue; you just need to know where to look and what to look for. This also ties into the broader discussion of developer careers and AI’s impact.

Myth 5: Vue.js Isn’t SEO-Friendly Out-of-the-Box

This myth plagued single-page applications (SPAs) for years, regardless of the framework. The argument goes that since Vue.js renders content dynamically on the client-side, search engine crawlers (which historically preferred static HTML) can’t properly index the content. While this was a legitimate concern a few years ago, modern search engines, particularly Google, have become much better at crawling and indexing JavaScript-rendered content. However, for optimal SEO, relying solely on client-side rendering is still not the most robust strategy.

This is precisely where solutions like Nuxt.js shine. Nuxt.js, a powerful meta-framework built on top of Vue.js, provides server-side rendering (SSR), static site generation (SSG), and hybrid rendering capabilities right out of the box. With SSR, the server pre-renders the Vue application into fully formed HTML on each request, delivering it directly to the browser (and, crucially, to search engine crawlers). This ensures that crawlers see a complete, indexable page from the very first request, vastly improving SEO. I had a client, “Local Eats,” a food delivery service, who initially launched their restaurant listings as a pure client-side Vue SPA. Their organic search traffic was abysmal. We migrated them to Nuxt.js with SSR, and within three months, their organic traffic from Google increased by over 150%. This wasn’t magic; it was simply providing search engines with what they need: fully rendered content. Additionally, Nuxt.js handles metadata management (, , etc.) effortlessly, further boosting SEO efforts. So, while a barebones Vue.js SPA might require extra steps for SEO, the ecosystem provides powerful, integrated solutions that make it incredibly SEO-friendly. The discussion around <a href="https://codeandcoffe.com/javascript-web-dev-s-2026-redefinition/">JavaScript’s redefinition in web development</a> also touches on these advancements.</p> <p>The world of <strong> and Vue.js</strong> is dynamic, but separating fact from fiction is essential for making informed technology decisions. Don’t let outdated myths or biased opinions steer you wrong; always look for current data and real-world results to guide your choices.</p> <div class="faq-section"> <div class="faq-item"> <h3 class="faq-question">What is the difference between Vue.js and Nuxt.js?</h3> <div class="faq-answer"> <p><strong>Vue.js</strong> is a progressive JavaScript framework for building user interfaces, handling the client-side rendering of components. <strong>Nuxt.js</strong> is a powerful meta-framework built on top of Vue.js that extends its capabilities, providing features like server-side rendering (SSR), static site generation (SSG), file-system-based routing, and an opinionated project structure, making it ideal for larger, more complex applications requiring strong SEO or performance. Think of Nuxt.js as a full-stack framework for Vue applications.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">Is Vue.js suitable for building mobile applications?</h3> <div class="faq-answer"> <p>While Vue.js itself is primarily for web interfaces, you can absolutely use your Vue knowledge to build mobile applications. The primary way to do this is through <a href="https://ionicframework.com/" target="_blank" rel="noopener">Ionic Framework</a> with Vue, which allows you to create hybrid mobile apps using web technologies that run on iOS and Android. Another option is <a href="https://nativescript.org/" target="_blank" rel="noopener">NativeScript-Vue</a>, which lets you build truly native mobile apps using Vue.js syntax and components, offering direct access to native APIs.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">How does Vue.js handle state management in large applications?</h3> <div class="faq-answer"> <p>For large <strong>Vue.js</strong> applications, state management is typically handled by <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>. Pinia is the official state management library for Vue.js, offering a simple, type-safe, and modular way to manage shared application state. It’s designed to be intuitive and scalable, allowing you to define “stores” for different parts of your application, making your state organized and easy to maintain, even as your application grows complex.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">What is the Vue 3 Composition API, and why is it important?</h3> <div class="faq-answer"> <p>The <strong>Vue 3 Composition API</strong> is a set of APIs that allows developers to compose component logic in a more flexible and reusable way, especially for complex components. It addresses limitations of the Options API (where logic is organized by options like <code>data</code>, <code>methods</code>, <code>computed</code>), which can become hard to read and maintain for larger components. The Composition API lets you group related logic together (e.g., all authentication-related code) and extract it into reusable functions (composables), significantly improving code organization, readability, and reusability across components.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">Can Vue.js be integrated with existing backend technologies like Node.js, Python, or PHP?</h3> <div class="faq-answer"> <p>Absolutely. <strong>Vue.js</strong> is a frontend framework, meaning it focuses solely on the user interface. It is completely backend-agnostic. You can easily integrate a Vue.js frontend with any backend technology that can expose a RESTful API or GraphQL endpoint. Whether your backend is built with Node.js (e.g., Express, NestJS), Python (e.g., Django, Flask), PHP (e.g., Laravel, Symfony), Ruby on Rails, or Java Spring Boot, Vue.js can consume data from it and display it to the user. They communicate via standard HTTP requests, making them fully interoperable.</p> </div> </div> </div> </div> <div class="share-buttons"> <span class="share-label">Share:</span> <a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F" class="share-btn facebook" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook"> <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z"/></svg> <span>Facebook</span> </a> <a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F&text=Vue.js+Myths+Busted%3A+2026+Tech+Realities" class="share-btn twitter" target="_blank" rel="noopener noreferrer" aria-label="Share on Twitter"> <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> <span>Twitter</span> </a> <a href="https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F&media=https%3A%2F%2Fcodeandcoffe.com%2Fwp-content%2Fuploads%2Fsites%2F143%2F2026%2F04%2Freact-s-future-myths-debunked-for-anxious-developers-featured.png&description=Vue.js+Myths+Busted%3A+2026+Tech+Realities" class="share-btn pinterest" target="_blank" rel="noopener noreferrer" aria-label="Share on Pinterest"> <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.632-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12 0-6.628-5.373-12-12-12z"/></svg> <span>Pinterest</span> </a> <a href="https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F&title=Vue.js+Myths+Busted%3A+2026+Tech+Realities" class="share-btn linkedin" target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn"> <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6zM2 9h4v12H2zM4 6a2 2 0 100-4 2 2 0 000 4z"/></svg> <span>LinkedIn</span> </a> <button class="share-btn copy-link" onclick="navigator.clipboard.writeText('https://codeandcoffe.com/vue-js-myths-busted-2026-tech-realities/').then(function(){this.querySelector('span').textContent='Copied!'}.bind(this))"> <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> <span>Copy Link</span> </button> </div> <div class="article-feedback" id="article-feedback"> <span class="feedback-question">Was this article helpful?</span> <button class="feedback-btn feedback-yes" data-vote="yes" aria-label="Yes"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14z"/><path d="M7 22H4a2 2 0 01-2-2v-7a2 2 0 012-2h3"/></svg> Yes </button> <button class="feedback-btn feedback-no" data-vote="no" aria-label="No"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 003 3l4-9V2H5.72a2 2 0 00-2 1.7l-1.38 9a2 2 0 002 2.3H10z"/><path d="M17 2h2.67A2.31 2.31 0 0122 4v7a2.31 2.31 0 01-2.33 2H17"/></svg> No </button> </div> <script> (function(){ var fb = document.getElementById('article-feedback'); if(!fb) return; fb.querySelectorAll('.feedback-btn').forEach(function(btn){ btn.addEventListener('click', function(){ var vote = this.dataset.vote; fetch('/wp-json/satellite/v1/feedback', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({post_id:12756,vote:vote}) }); fb.innerHTML = '<span class="feedback-thanks">Thanks for your feedback!</span>'; }); }); })(); </script> <div class="author-bio"> <div class="author-bio-avatar"> <img fetchpriority="low" loading="lazy" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcjessicaflores-headshot-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcjessicaflores-headshot-150x150.png 2x' class='avatar avatar-72 photo' height='72' width='72' decoding='async'/> </div> <div class="author-bio-info"> <h4 class="author-bio-name"> <a href="https://codeandcoffe.com/author/codeandcjessicaflores/"> Jessica Flores </a> </h4> <span class="author-bio-title">Principal Software Architect</span> <span class="author-bio-credentials">M.S. Computer Science, California Institute of Technology; Certified Kubernetes Application Developer (CKAD)</span> <p class="author-bio-description">Jessica Flores is a Principal Software Architect with over 15 years of experience specializing in scalable microservices architectures and cloud-native development. Formerly a lead architect at Horizon Systems and a senior engineer at Quantum Innovations, she is renowned for her expertise in optimizing distributed systems for high performance and resilience. Her seminal work on 'Event-Driven Architectures in Serverless Environments' has significantly influenced modern backend development practices, establishing her as a leading voice in the field</p> <div class="author-bio-links"> <a href="https://www.cncf.io/certification/ckad/" target="_blank" rel="noopener noreferrer" class="author-link-badge"> Credentials </a> <span class="author-experience">15+ years experience</span> </div> </div> </div> </div> <aside class="single-post-sidebar"> <div class="sidebar-sticky"> <div class="sidebar-share"> <span class="sidebar-share-label">Share</span> <div class="sidebar-share-icons"> <a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F" target="_blank" rel="noopener noreferrer" class="sidebar-share-icon facebook" aria-label="Facebook"> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z"/></svg> </a> <a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F&text=Vue.js+Myths+Busted%3A+2026+Tech+Realities" target="_blank" rel="noopener noreferrer" class="sidebar-share-icon twitter" aria-label="Twitter"> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> </a> <a href="https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F&description=Vue.js+Myths+Busted%3A+2026+Tech+Realities" target="_blank" rel="noopener noreferrer" class="sidebar-share-icon pinterest" aria-label="Pinterest"> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.372 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 01.083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.632-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12 0-6.628-5.373-12-12-12z"/></svg> </a> <a href="https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fcodeandcoffe.com%2Fvue-js-myths-busted-2026-tech-realities%2F&title=Vue.js+Myths+Busted%3A+2026+Tech+Realities" target="_blank" rel="noopener noreferrer" class="sidebar-share-icon linkedin" aria-label="LinkedIn"> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6zM2 9h4v12H2zM4 6a2 2 0 100-4 2 2 0 000 4z"/></svg> </a> </div> </div> <div class="sidebar-top-posts"> <h3 class="sidebar-section-title">Top Posts</h3> <a href="https://codeandcoffe.com/top-10-developer-tools-reviews-to-supercharge-%f0%9f%9a%80-you/" class="sidebar-post-card"> <div class="sidebar-post-thumb"> <img width="300" height="200" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured-300x200.webp?v=1774869674" class="attachment-satellite-thumb size-satellite-thumb wp-post-image" alt="" decoding="async" loading="lazy" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured-300x200.webp?v=1774869674 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured-600x400.webp?v=1774869674 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /> </div> <div class="sidebar-post-info"> <h4 class="sidebar-post-title">Top 10 Developer Tools & Reviews to Supercharge 🚀 You</h4> <div class="sidebar-post-meta"> <time datetime="2026-03-30T11:21:14+00:00">30/03/2026</time> <span class="sidebar-post-views">341 Views</span> </div> </div> </a> <a href="https://codeandcoffe.com/vue-js-%e2%9d%a4%ef%b8%8f-commonjs-modern-apps-with-legacy-modules/" class="sidebar-post-card"> <div class="sidebar-post-thumb"> <img width="300" height="200" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured-300x200.webp?v=1775622636" class="attachment-satellite-thumb size-satellite-thumb wp-post-image" alt="" decoding="async" loading="lazy" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured-300x200.webp?v=1775622636 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured-600x400.webp?v=1775622636 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /> </div> <div class="sidebar-post-info"> <h4 class="sidebar-post-title">Vue.js ❤️ CommonJS: Modern Apps with Legacy Modules</h4> <div class="sidebar-post-meta"> <time datetime="2026-04-21T15:35:31+00:00">21/04/2026</time> <span class="sidebar-post-views">251 Views</span> </div> </div> </a> <a href="https://codeandcoffe.com/cybersecurity-careers-2026-job-outlook-ai-skills/" class="sidebar-post-card"> <div class="sidebar-post-thumb"> <img width="300" height="200" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/cybersecurity-careers-2026-job-outlook-ai-skills-featured-300x200.webp?v=1774079295" class="attachment-satellite-thumb size-satellite-thumb wp-post-image" alt="" decoding="async" loading="lazy" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/cybersecurity-careers-2026-job-outlook-ai-skills-featured-300x200.webp?v=1774079295 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/cybersecurity-careers-2026-job-outlook-ai-skills-featured-600x400.webp?v=1774079295 600w" sizes="auto, (max-width: 300px) 100vw, 300px" /> </div> <div class="sidebar-post-info"> <h4 class="sidebar-post-title">Cybersecurity Careers: 2026 Job Outlook & AI Skills</h4> <div class="sidebar-post-meta"> <time datetime="2026-03-21T07:48:15+00:00">21/03/2026</time> <span class="sidebar-post-views">183 Views</span> </div> </div> </a> </div> </div> </aside> </div> <nav class="post-navigation"> <a href="https://codeandcoffe.com/ai-and-tech-trends-driving-2026-business-growth/" class="post-nav-link prev"> <div class="post-nav-label">« Previous</div> <div class="post-nav-title">AI & Tech Trends: Driving 2026 Business Growth</div> </a> <a href="https://codeandcoffe.com/content-engagement-2026-ai-driven-strategies/" class="post-nav-link next"> <div class="post-nav-label">Next »</div> <div class="post-nav-title">Content Engagement: 2026 AI-Driven Strategies</div> </a> </nav> </article> <section class="related-posts"> <div class="section-header"> <h2 class="section-title">Related Articles</h2> <div class="carousel-nav"> <button class="carousel-btn carousel-prev" aria-label="Previous">‹</button> <button class="carousel-btn carousel-next" aria-label="Next">›</button> </div> </div> <div class="related-carousel" id="related-carousel"> <div class="carousel-track"> <article class="article-card"> <a href="https://codeandcoffe.com/innovatech-s-2026-ai-crisis-5-fixes/" class="article-card-image" aria-label="Innovatech’s 2026 AI Crisis: 5 Fixes"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/innovatech-s-2026-ai-crisis-5-fixes-featured-768x419.webp?v=1782189317" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/innovatech-s-2026-ai-crisis-5-fixes-featured-768x419.webp 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/innovatech-s-2026-ai-crisis-5-fixes-featured-300x164.webp 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/innovatech-s-2026-ai-crisis-5-fixes-featured-1024x559.webp 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/innovatech-s-2026-ai-crisis-5-fixes-featured.webp 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/software-development/" class="article-card-category" style="color:#047857"> Software Development </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/innovatech-s-2026-ai-crisis-5-fixes/">Innovatech’s 2026 AI Crisis: 5 Fixes</a> </h3> <p class="article-card-excerpt">Listen to this article · 10 min listen1.0xAudio playback not supported in this browser.The year 2026 brought a tidal wave of challenges for many tech companies, but…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcestherokoro-headshot-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcestherokoro-headshot-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Cory Jackson</span> <span class="dot">·</span> <time datetime="2026-06-23T03:50:19+00:00">23/06/2026</time> <span class="dot">·</span> <span>8 min read</span> </div> </div> </article> <article class="article-card"> <a href="https://codeandcoffe.com/vue-js-state-management-tame-2026-chaos/" class="article-card-image" aria-label="Vue.js State Management: Tame 2026 Chaos"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-in-2026-outsmart-complexity-boost-your-seo-featured-768x419.webp?v=1775632595" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-in-2026-outsmart-complexity-boost-your-seo-featured-768x419.webp?v=1775632595 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-in-2026-outsmart-complexity-boost-your-seo-featured-300x164.webp?v=1775632595 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-in-2026-outsmart-complexity-boost-your-seo-featured-1024x559.webp?v=1775632595 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-in-2026-outsmart-complexity-boost-your-seo-featured.webp?v=1775632595 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/software-development/" class="article-card-category" style="color:#047857"> Software Development </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/vue-js-state-management-tame-2026-chaos/">Vue.js State Management: Tame 2026 Chaos</a> </h3> <p class="article-card-excerpt">Listen to this article · 14 min listen1.0xAudio playback not supported in this browser.Many developers, myself included, have wrestled with the challenge of building complex, data-driven web…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcdevinchung-headshot-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcdevinchung-headshot-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Corey Weiss</span> <span class="dot">·</span> <time datetime="2026-06-23T03:27:03+00:00">23/06/2026</time> <span class="dot">·</span> <span>12 min read</span> </div> </div> </article> <article class="article-card"> <a href="https://codeandcoffe.com/top-10-developer-tools-reviews-to-supercharge-%f0%9f%9a%80-you/" class="article-card-image" aria-label="Top 10 Developer Tools & Reviews to Supercharge 🚀 You"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured-768x419.webp?v=1774869674" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured-768x419.webp?v=1774869674 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured-300x164.webp?v=1774869674 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured-1024x559.webp?v=1774869674 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/top-10-developer-tools-reviews-to-supercharge-you-featured.webp?v=1774869674 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/hardware-reviews/" class="article-card-category" style="color:#9a3412"> Hardware Reviews </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/top-10-developer-tools-reviews-to-supercharge-%f0%9f%9a%80-you/">Top 10 Developer Tools & Reviews to Supercharge 🚀 You</a> </h3> <p class="article-card-excerpt">Listen to this article · 8 min listen1.0xAudio playback not supported in this browser.Top 10 and Product Reviews of Essential Developer Tools In the ever-evolving realm of…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandcanyavolkov-headshot-4-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandcanyavolkov-headshot-4-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Carlos Kelley</span> <span class="dot">·</span> <time datetime="2026-03-30T11:21:14+00:00">30/03/2026</time> <span class="dot">·</span> <span>6 min read</span> </div> </div> </article> <article class="article-card"> <a href="https://codeandcoffe.com/vue-js-%e2%9d%a4%ef%b8%8f-commonjs-modern-apps-with-legacy-modules/" class="article-card-image" aria-label="Vue.js ❤️ CommonJS: Modern Apps with Legacy Modules"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured-768x419.webp?v=1775622635" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured-768x419.webp?v=1775622635 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured-300x164.webp?v=1775622635 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured-1024x559.webp?v=1775622635 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/vue-js-common-js-build-modular-apps-that-scale-featured.webp?v=1775622635 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/cloud-computing/" class="article-card-category" style="color:#be185d"> Cloud Computing </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/vue-js-%e2%9d%a4%ef%b8%8f-commonjs-modern-apps-with-legacy-modules/">Vue.js ❤️ CommonJS: Modern Apps with Legacy Modules</a> </h3> <p class="article-card-excerpt">Listen to this article · 9 min listen1.0xAudio playback not supported in this browser.Want to build dynamic and interactive web applications? Combining Common.js and Vue.js, especially with…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandcanyavolkov-headshot-4-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandcanyavolkov-headshot-4-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Carlos Kelley</span> <span class="dot">·</span> <time datetime="2026-04-21T15:35:31+00:00">21/04/2026</time> <span class="dot">·</span> <span>7 min read</span> </div> </div> </article> <article class="article-card"> <a href="https://codeandcoffe.com/azure-myths-what-s-holding-your-2026-strategy-back/" class="article-card-image" aria-label="Azure Myths: What’s Holding Your 2026 Strategy Back?"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/mastering-azure-avoid-2026-cost-pitfalls-featured-768x419.webp?v=1781752073" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/mastering-azure-avoid-2026-cost-pitfalls-featured-768x419.webp 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/mastering-azure-avoid-2026-cost-pitfalls-featured-300x164.webp 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/mastering-azure-avoid-2026-cost-pitfalls-featured-1024x559.webp 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/mastering-azure-avoid-2026-cost-pitfalls-featured.webp 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/cloud-computing/" class="article-card-category" style="color:#be185d"> Cloud Computing </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/azure-myths-what-s-holding-your-2026-strategy-back/">Azure Myths: What’s Holding Your 2026 Strategy Back?</a> </h3> <p class="article-card-excerpt">Listen to this article · 13 min listen1.0xAudio playback not supported in this browser.The world of cloud computing is rife with misinformation, and nowhere is this more…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandcelenarios-headshot-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandcelenarios-headshot-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Elena Rios</span> <span class="dot">·</span> <time datetime="2026-06-23T03:35:32+00:00">23/06/2026</time> <span class="dot">·</span> <span>10 min read</span> </div> </div> </article> <article class="article-card"> <a href="https://codeandcoffe.com/developers-avoid-2026-skill-obsolescence/" class="article-card-image" aria-label="Developers: Avoid 2026 Skill Obsolescence"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/developers-avoid-2026-skill-obsolescence-featured-768x419.webp?v=1782189333" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/developers-avoid-2026-skill-obsolescence-featured-768x419.webp 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/developers-avoid-2026-skill-obsolescence-featured-300x164.webp 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/developers-avoid-2026-skill-obsolescence-featured-1024x559.webp 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/06/developers-avoid-2026-skill-obsolescence-featured.webp 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/software-development/" class="article-card-category" style="color:#047857"> Software Development </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/developers-avoid-2026-skill-obsolescence/">Developers: Avoid 2026 Skill Obsolescence</a> </h3> <p class="article-card-excerpt">Listen to this article · 10 min listen1.0xAudio playback not supported in this browser.Only 12% of developers feel fully prepared for the future of technology, a stark…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcjessicaflores-headshot-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandcjessicaflores-headshot-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Jessica Flores</span> <span class="dot">·</span> <time datetime="2026-06-23T03:12:35+00:00">23/06/2026</time> <span class="dot">·</span> <span>8 min read</span> </div> </div> </article> <article class="article-card"> <a href="https://codeandcoffe.com/beyond-page-views-ai-content-strategy/" class="article-card-image" aria-label="Beyond Page Views: AI Content Strategy"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/ml-strategies-that-deliver-results-in-2026-featured-768x419.webp?v=1776699686" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/ml-strategies-that-deliver-results-in-2026-featured-768x419.webp?v=1776699686 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/ml-strategies-that-deliver-results-in-2026-featured-300x164.webp?v=1776699686 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/ml-strategies-that-deliver-results-in-2026-featured-1024x559.webp?v=1776699686 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/ml-strategies-that-deliver-results-in-2026-featured.webp?v=1776699686 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/ai-machine-learning/" class="article-card-category" style="color:#0369a1"> AI & Machine Learning </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/beyond-page-views-ai-content-strategy/">Beyond Page Views: AI Content Strategy</a> </h3> <p class="article-card-excerpt">Listen to this article · 12 min listen1.0xAudio playback not supported in this browser.The digital content sphere is overflowing, making it harder than ever for specialized publications…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandckenjitanaka-headshot-4-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/03/codeandckenjitanaka-headshot-4-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Candice Medina</span> <span class="dot">·</span> <time datetime="2026-04-28T14:47:22+00:00">28/04/2026</time> <span class="dot">·</span> <span>9 min read</span> </div> </div> </article> <article class="article-card"> <a href="https://codeandcoffe.com/developer-skills-aws-and-azure-payouts-by-2027/" class="article-card-image" aria-label="Developer Skills: AWS & Azure Payouts by 2027"> <img width="768" height="419" src="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/top-10-dev-strategies-aws-cloud-more-in-2026-featured-768x419.webp?v=1775194221" class="attachment-medium_large size-medium_large wp-post-image" alt="" loading="lazy" decoding="async" srcset="https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/top-10-dev-strategies-aws-cloud-more-in-2026-featured-768x419.webp?v=1775194221 768w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/top-10-dev-strategies-aws-cloud-more-in-2026-featured-300x164.webp?v=1775194221 300w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/top-10-dev-strategies-aws-cloud-more-in-2026-featured-1024x559.webp?v=1775194221 1024w, https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/top-10-dev-strategies-aws-cloud-more-in-2026-featured.webp?v=1775194221 1408w" sizes="auto, (max-width: 768px) 100vw, 768px" /> </a> <div class="article-card-body"> <a href="https://codeandcoffe.com/category/software-development/" class="article-card-category" style="color:#047857"> Software Development </a> <h3 class="article-card-title"> <a href="https://codeandcoffe.com/developer-skills-aws-and-azure-payouts-by-2027/">Developer Skills: AWS & Azure Payouts by 2027</a> </h3> <p class="article-card-excerpt">Listen to this article · 12 min listen1.0xAudio playback not supported in this browser.The technology sector is a constantly shifting battleground, demanding continuous skill refinement and strategic…</p> <div class="article-card-meta"> <img fetchpriority="low" alt='' src='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandclenachung-headshot-150x150.png' srcset='https://codeandcoffe.com/wp-content/uploads/sites/143/2026/04/codeandclenachung-headshot-150x150.png 2x' class='avatar avatar-22 photo' height='22' width='22' loading='lazy' decoding='async'/> <span class="author-name">Cory Holland</span> <span class="dot">·</span> <time datetime="2026-05-23T00:52:58+00:00">23/05/2026</time> <span class="dot">·</span> <span>10 min read</span> </div> </div> </article> </div> </div> </section> <script> (function(){ var track = document.querySelector('.carousel-track'); if(!track) return; var prev = document.querySelector('.carousel-prev'); var next = document.querySelector('.carousel-next'); var cardW = track.querySelector('.article-card'); if(!cardW) return; var scrollAmt = cardW.offsetWidth + 24; if(prev) prev.addEventListener('click', function(){ track.scrollBy({left:-scrollAmt,behavior:'smooth'}); }); if(next) next.addEventListener('click', function(){ track.scrollBy({left:scrollAmt,behavior:'smooth'}); }); })(); </script> </main> </div> <div class="read-next-bar" id="read-next-bar"> <div class="read-next-inner"> <span class="read-next-label">Read Next</span> <a href="https://codeandcoffe.com/practical-coding-tips-drive-2026-tech-progress/" class="read-next-link"> Practical Coding Tips Drive 2026 Tech Progress </a> <a href="https://codeandcoffe.com/practical-coding-tips-drive-2026-tech-progress/" class="read-next-cta" style="background:#047857"> → </a> </div> </div> <script> (function(){ var bar = document.getElementById('read-next-bar'); if(!bar) return; var shown = false; window.addEventListener('scroll', function(){ var doc = document.documentElement; var pct = (doc.scrollTop / (doc.scrollHeight - doc.clientHeight)) * 100; if (pct > 65 && !shown) { bar.classList.add('visible'); shown = true; } }, {passive:true}); bar.addEventListener('click', function(e){ if(e.target.classList.contains('read-next-close')) { bar.classList.remove('visible'); } }); })(); </script> </div><!-- .container --> <footer class="site-footer" role="contentinfo"> <div class="footer-main"> <div class="footer-col footer-about"> <div class="widget"> <h3 class="widget-title">Code & Coffee</h3> <p>Expert insights, guides, and stories about technology</p> </div> </div> <div class="footer-col"> <div class="widget"> <h3 class="widget-title">Categories</h3> <ul> <li class="cat-item cat-item-20"><a href="https://codeandcoffe.com/category/ai-machine-learning/">AI & Machine Learning</a> </li> <li class="cat-item cat-item-3"><a href="https://codeandcoffe.com/category/artificial-intelligence/">Artificial Intelligence</a> </li> <li class="cat-item cat-item-7"><a href="https://codeandcoffe.com/category/cloud-computing/">Cloud Computing</a> </li> <li class="cat-item cat-item-18"><a href="https://codeandcoffe.com/category/cybersecurity/">Cybersecurity</a> </li> <li class="cat-item cat-item-4"><a href="https://codeandcoffe.com/category/cybersecurity-news/">Cybersecurity News</a> </li> <li class="cat-item cat-item-8"><a href="https://codeandcoffe.com/category/data-science/">Data Science</a> </li> <li class="cat-item cat-item-5"><a href="https://codeandcoffe.com/category/emerging-tech/">Emerging Tech</a> </li> <li class="cat-item cat-item-6"><a href="https://codeandcoffe.com/category/hardware-reviews/">Hardware Reviews</a> </li> </ul> </div> </div> <div class="footer-col"> <div class="widget"> <h3 class="widget-title">Quick Links</h3> <ul> <li><a href="https://codeandcoffe.com/">Home</a></li> <li><a href="https://codeandcoffe.com/about/">About</a></li> <li><a href="https://codeandcoffe.com/contact/">Contact</a></li> <li><a href="https://codeandcoffe.com/privacy-policy/">Privacy Policy</a></li> <li><a href="https://codeandcoffe.com/terms-of-service/">Terms of Service</a></li> <li><a href="https://codeandcoffe.com/editorial-standards/">Editorial Standards</a></li> </ul> </div> </div> <div class="footer-col footer-col-authors"> <div class="widget"> <h3 class="widget-title">Our Authors</h3> <ul class="footer-authors-list" style="columns: 2; -webkit-columns: 2; -moz-columns: 2; column-gap: 1.5rem; padding-left: 0; list-style: none; margin: 0;"> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcjamalwashington/">Claudia Mitchell</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcmagnusolsen/">Magnus Olsen</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcjessicafitzpatrick/">Jessica Fitzpatrick</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcomarhabib/">Carl Ho</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcsofiarodriguez/">Carlos Patterson</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcrhyskwan/">Rhys Kwan</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandclenachung/">Cory Holland</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandczarapadilla/">Zara Padilla</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcsunitaramakrishnan/">Sunita Ramakrishnan</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcpriyashah/">Carl Choi</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcelenarios/">Elena Rios</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcestherokoro/">Cory Jackson</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcniawashington/">Colleen White</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcanyavolkov/">Carlos Kelley</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcaishakhan/">Carla Franco</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcseraphinakano/">Seraphina Kano</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcgregornovak/">Gregor Novak</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcjianliang/">Clinton Gordon</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcanyasharma/">Corey Schultz</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandckianchong/">Corey Rojas</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandctaylorquinn/">Claudia Lin</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandclenachen/">Colin Roberts</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandckeishadixon/">Connor Anderson</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandclakshmimurthy/">Lakshmi Murthy</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcjessicaflores/">Jessica Flores</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcjaviercruz/">Colton Hardy</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcdimitrivolkov/">Carlos Ramos</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcdevonchambers/">Cole Hernandez</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcanikadeshmukh/">Carlos Schultz</a></li> <li style="break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; padding: 2px 0;"><a href="https://codeandcoffe.com/author/codeandcdevinchung/">Corey Weiss</a></li> </ul> </div> </div> </div> <div class="footer-bottom"> <div class="footer-bottom-inner"> <div class="footer-copyright"> © 2026 Code & Coffee. All rights reserved. </div> <nav class="footer-nav" role="navigation"> </nav> </div> </div> </footer> <script id="sat-tts-script"> (function(){ if (!('speechSynthesis' in window) || !('SpeechSynthesisUtterance' in window)) { document.querySelectorAll('.sat-tts-player').forEach(function(el){ el.setAttribute('data-state', 'unsupported'); var t = el.querySelector('.sat-tts-title-text'); var i18n = el.querySelector('.sat-tts-i18n'); if (t && i18n) t.textContent = i18n.dataset.unsupported; }); return; } var player = document.querySelector('.sat-tts-player[data-sat-tts]'); if (!player) return; var contentRoot = document.querySelector('.post-content') || document.querySelector('article .single-post-main') || document.querySelector('article'); if (!contentRoot) return; var toggleBtn = player.querySelector('.sat-tts-toggle'); var rateBtn = player.querySelector('.sat-tts-rate'); var titleText = player.querySelector('.sat-tts-title-text'); var progressEl = player.querySelector('.sat-tts-progress-fill'); var i18n = player.querySelector('.sat-tts-i18n'); var STORAGE_KEY = 'sat_tts_v1_' + (location.pathname || '/'); var RATE_CYCLE = [1, 1.25, 1.5, 2, 0.85]; // ── Build chunks ───────────────────────────────────────────── // Strip HTML to a clean, sequential text array. We skip elements // that read awkwardly aloud (figures, embedded video/audio, the // related-callout sidebars, FAQ schema-heavy sections, code). function buildChunks(root) { var clone = root.cloneNode(true); // Remove things we never want spoken. clone.querySelectorAll( 'script,style,figure,iframe,video,audio,svg,noscript,' + 'aside,.related-callout,.sidebar-share,.sat-tts-player,' + '.article-feedback,.author-bio,.post-tags,.read-next-bar,' + '.post-navigation,.related-posts,form,nav,.toc-container' ).forEach(function(n){ n.parentNode && n.parentNode.removeChild(n); }); var blockSel = 'h1,h2,h3,h4,h5,h6,p,li,blockquote,td,th,dt,dd'; var blocks = clone.querySelectorAll(blockSel); var chunks = []; blocks.forEach(function(b){ var t = (b.textContent || '').replace(/\s+/g, ' ').trim(); if (!t) return; // Long paragraphs: break on sentence boundaries so Chrome // doesn't silently drop after ~15s of a single utterance. if (t.length > 220) { var sentences = t.match(/[^.!?]+[.!?]+(?:\s|$)|[^.!?]+$/g) || [t]; var buf = ''; sentences.forEach(function(s){ s = s.trim(); if (!s) return; if ((buf + ' ' + s).trim().length > 220 && buf) { chunks.push(buf.trim()); buf = s; } else { buf = (buf ? buf + ' ' : '') + s; } }); if (buf.trim()) chunks.push(buf.trim()); } else { chunks.push(t); } }); return chunks; } var chunks = buildChunks(contentRoot); if (chunks.length === 0) return; var totalChars = chunks.reduce(function(a,c){ return a + c.length; }, 0); // ── State machine ─────────────────────────────────────────── var state = { playing: false, paused: false, chunkIndex: 0, charsSpoken: 0, rate: 1, voice: null, currentUtter: null, }; try { var saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); if (saved && typeof saved.chunkIndex === 'number' && saved.chunkIndex < chunks.length) { state.chunkIndex = saved.chunkIndex; state.charsSpoken = saved.charsSpoken || 0; state.rate = saved.rate || 1; } } catch (e) {} applyRate(state.rate); updateProgress(); // ── Voice selection ───────────────────────────────────────── // speechSynthesis populates voices async on most browsers. The // picker: // 1. filters by html lang prefix (en/es/...) // 2. drops novelty/character voices (Apple ships ~30 of them // and they sort BEFORE the natural ones on macOS, which is // why an unguarded pool[0] fallback ends up reading // Spanish articles in Eddy/Flo/Reed instead of Mónica) // 3. ranks survivors by an explicit per-language preference // list of high-quality voices, with a fallback that prefers // `localService` (built-in premium) over remote voices. var NOVELTY_NAME_RE = /^(Albert|Bad News|Bahh|Bells|Boing|Bubbles|Cellos|Deranged|Good News|Hysterical|Pipe Organ|Trinoids|Whisper|Wobble|Zarvox|Eddy|Flo|Grandma|Grandpa|Jester|Junior|Kathy|Organ|Princess|Ralph|Reed|Rocko|Sandy|Shelley|Superstar|Vicki|Victoria|Bahh|Boing|Cellos)\b/i; var PREFERRED_BY_LANG = { es: [ // macOS / iOS premium Spanish voices (best quality) /^M[oó]nica/i, // es-ES, very natural /^Paulina/i, // es-MX, very natural /^Jorge\b/i, // es-ES /^Diego\b/i, // es-AR /^Juan\b/i, // Microsoft Edge / Windows neural Spanish voices /Microsoft.*\b(Elvira|Dalia|Alvaro|Jorge|Helena|Sabina)\b.*Online/i, /Microsoft.*\b(Elvira|Dalia|Alvaro)\b/i, // Google Spanish (Chrome desktop, Android) /^Google\s+espa[ñn]ol(?:\s+de\s+(?:M[eé]xico|Estados Unidos))?$/i, /^Google\s+espa[ñn]ol/i, ], en: [ /^Google\s.*(US|UK|English)/i, /^Microsoft.*(Aria|Jenny|Guy|Ryan|Davis)\b.*Online/i, /^Microsoft.*(Aria|Jenny|Guy|Ryan|Davis)\b/i, /^Samantha$/i, /^Alex$/i, /English/i, ], }; function scoreVoice(v, preferredList) { for (var i = 0; i < preferredList.length; i++) { if (preferredList[i].test(v.name)) { // Earlier list entries score higher. localService gets a // small bump so a built-in voice wins over a remote one // when both match the same pattern. return 1000 - i * 10 + (v.localService ? 1 : 0); } } // No name match — still prefer non-novelty + localService. return (v.localService ? 1 : 0); } function pickVoice() { var voices = speechSynthesis.getVoices() || []; if (voices.length === 0) return null; var lang = (document.documentElement.lang || 'en').toLowerCase().split('-')[0]; var pool = voices.filter(function(v){ return (v.lang || '').toLowerCase().indexOf(lang) === 0; }); if (pool.length === 0) pool = voices; // Strip novelty voices unless that leaves us with nothing. var filtered = pool.filter(function(v){ return !NOVELTY_NAME_RE.test(v.name); }); if (filtered.length > 0) pool = filtered; var preferredList = PREFERRED_BY_LANG[lang] || PREFERRED_BY_LANG.en; var best = null, bestScore = -Infinity; pool.forEach(function(v){ var s = scoreVoice(v, preferredList); if (s > bestScore) { bestScore = s; best = v; } }); return best || pool[0]; } if (typeof speechSynthesis.addEventListener === 'function') { speechSynthesis.addEventListener('voiceschanged', function(){ state.voice = pickVoice(); }); } state.voice = pickVoice(); // ── Utterance queue ───────────────────────────────────────── function speakNext() { if (state.chunkIndex >= chunks.length) { stopAll(true); return; } var u = new SpeechSynthesisUtterance(chunks[state.chunkIndex]); u.rate = state.rate; u.pitch = 1; u.volume = 1; // Setting `lang` explicitly is what triggers the Spanish // synthesis backend on Chrome/Edge (which shipped neural // Spanish voices that aren't always exposed via getVoices // until referenced). When `voice` is set we prefer the // voice's own lang to avoid a mismatch. if (state.voice) { u.voice = state.voice; if (state.voice.lang) u.lang = state.voice.lang; } else { u.lang = (document.documentElement.lang || 'en'); } u.onend = function() { if (!state.playing) return; state.charsSpoken += chunks[state.chunkIndex].length; state.chunkIndex += 1; persist(); updateProgress(); if (state.chunkIndex < chunks.length) { speakNext(); } else { stopAll(true); } }; u.onerror = function(ev) { if (ev && ev.error === 'interrupted') return; state.playing = false; state.paused = false; state.currentUtter = null; renderState(); }; u.onboundary = function(ev) { if (ev && typeof ev.charIndex === 'number') { var partial = state.charsSpoken + Math.min(ev.charIndex, chunks[state.chunkIndex].length); var pct = Math.min(100, (partial / totalChars) * 100); progressEl.style.width = pct + '%'; } }; state.currentUtter = u; speechSynthesis.speak(u); } function play() { if (state.paused) { speechSynthesis.resume(); state.paused = false; state.playing = true; renderState(); return; } // Some browsers leave the queue stuck after a previous error; // cancel before starting fresh. try { speechSynthesis.cancel(); } catch (e) {} state.playing = true; state.paused = false; renderState(); speakNext(); } function pause() { if (!state.playing) return; try { speechSynthesis.pause(); } catch (e) {} state.paused = true; state.playing = false; persist(); renderState(); } function stopAll(reset) { try { speechSynthesis.cancel(); } catch (e) {} state.playing = false; state.paused = false; state.currentUtter = null; if (reset) { state.chunkIndex = 0; state.charsSpoken = 0; progressEl.style.width = '0%'; try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} } renderState(); } // ── Rate cycle ───────────────────────────────────────────── function applyRate(r) { state.rate = r; rateBtn.textContent = r.toFixed(2).replace(/\.?0+$/, '') + 'x'; persist(); } rateBtn.addEventListener('click', function(){ var i = RATE_CYCLE.indexOf(state.rate); var next = RATE_CYCLE[(i + 1) % RATE_CYCLE.length]; applyRate(next); // If currently playing, restart the current chunk at the // new rate (Web Speech doesn't let you change rate mid-utter). if (state.playing && state.currentUtter) { stopAll(false); state.playing = true; renderState(); speakNext(); } }); toggleBtn.addEventListener('click', function(){ if (state.playing) { pause(); } else { play(); } }); // ── Chrome 15-second cutoff workaround ───────────────────── // Chrome stops Web Speech after ~15s of continuous output. We // ping pause+resume every 10s while playing to keep the queue // alive. Harmless on browsers that don't need it. setInterval(function(){ if (state.playing && !state.paused) { try { speechSynthesis.pause(); speechSynthesis.resume(); } catch (e) {} } }, 10000); // ── Persistence + UI ─────────────────────────────────────── function persist() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ chunkIndex: state.chunkIndex, charsSpoken: state.charsSpoken, rate: state.rate, })); } catch (e) {} } function updateProgress() { var pct = totalChars ? Math.min(100, (state.charsSpoken / totalChars) * 100) : 0; progressEl.style.width = pct + '%'; } function renderState() { if (!i18n) return; if (state.playing) { player.setAttribute('data-state', 'playing'); titleText.textContent = i18n.dataset.playing; toggleBtn.setAttribute('aria-label', 'Pause'); } else if (state.paused) { player.setAttribute('data-state', 'paused'); titleText.textContent = i18n.dataset.paused; toggleBtn.setAttribute('aria-label', 'Resume'); } else { player.removeAttribute('data-state'); titleText.textContent = i18n.dataset.listen; toggleBtn.setAttribute('aria-label', 'Play'); } } // ── GA4 listen events ───────────────────────────────────── // Fire a small custom event the first time a visitor presses // play, so we can answer "does the listen button move // engagement?" from the analytics dashboard. var firedFirstPlay = false; toggleBtn.addEventListener('click', function(){ if (firedFirstPlay) return; if (typeof window.gtag === 'function') { try { window.gtag('event', 'tts_listen_play', { event_category: 'engagement', event_label: location.pathname, value: 1, }); } catch (e) {} } firedFirstPlay = true; }); // Stop speech when the visitor leaves the page so it doesn't // continue narrating in the background after navigation. window.addEventListener('beforeunload', function(){ try { speechSynthesis.cancel(); } catch (e) {} }); })(); </script> <script type="speculationrules"> {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/sites/143/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/satellite-theme/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]} </script> <script id="satellite-main-js-extra"> var satelliteAjax = {"ajaxurl":"https://codeandcoffe.com/wp-admin/admin-ajax.php","nonce":"3989e5916e"}; //# sourceURL=satellite-main-js-extra </script> <script src="https://codeandcoffe.com/wp-content/themes/satellite-theme/assets/js/main.js?ver=4.9.0" id="satellite-main-js"></script> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap"></noscript> <style id="aeo-popup-styles"> #aeo-popup-root { --accent: #E8471A; --accent-dim: #C93C12; --accent-mid: #F07040; --accent-soft: #F9A07A; --card-bg: linear-gradient(105deg, #ffffff 0%, #fff4f0 40%, #ffeee7 70%, #ffffff 100%); --card-fade: #fff8f5; --bar-bg: linear-gradient(105deg, #ffffff 0%, #fff4f0 40%, #ffeee7 70%, #ffffff 100%); --cta-grad: linear-gradient(90deg, #E8471A 0%, #F07040 50%, #E8471A 100%); --cta-text: #ffffff; --shadow-rgb: 232,71,26; --ink: #111111; --ink-soft: #555555; --ink-faint: #999999; --border: #E2E4DC; --banner-h: 88px; /* "Google Sans" is in the stack as the brand fallback for installs that have it locally (e.g. internal Google environments); Inter is the loaded webfont and the de-facto free substitute. */ --font-display: 'Inter', 'Google Sans', system-ui, sans-serif; --font-body: 'Inter', 'Google Sans', system-ui, sans-serif; --font-mono: 'DM Mono', ui-monospace, Menlo, monospace; } #aeo-popup-root, #aeo-popup-root * { box-sizing: border-box; } @keyframes aeoPulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:.4; transform:scale(.7); } } @keyframes aeoCtaShimmer { 0% { background-position:0% 0%; } 50% { background-position:100% 0%; } 100% { background-position:0% 0%; } } @keyframes aeoBannerBreathe{ 0% { background-position:0% 50%; } 50% { background-position:100% 50%; } 100% { background-position:0% 50%; } } @keyframes aeoCardSlideUp { to { transform: translateY(0); opacity: 1; } } @keyframes aeoCardSlideDown{ to { transform: translateY(calc(100% + 32px)); opacity: 0; } } @keyframes aeoBarSlideUp { to { transform: translateY(0); opacity: 1; } } @keyframes aeoScTicker { 0% { transform: translateX(0); } 100% { transform: translateX(-33.3333%); } } /* ── STICKY CARD (bottom-left, initial state) ────────────────────── Default state is hidden -- JS adds .aeo-iframe-ready on the root once the modal iframe has finished loading in the background (or after a safety timeout), so the slide-up coincides with the funnel being click-ready. */ #aeo-popup-root .sticky-card { position: fixed; bottom: 24px; left: 24px; z-index: 9990; width: 340px; border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 8px 40px rgba(var(--shadow-rgb),0.15), 0 2px 8px rgba(0,0,0,0.07); background: var(--card-bg); background-size: 300% 300%; transform: translateY(calc(100% + 32px)); opacity: 0; } #aeo-popup-root.aeo-iframe-ready .sticky-card { animation: aeoCardSlideUp 0.6s cubic-bezier(0.22,1,0.36,1) forwards, aeoBannerBreathe 5s ease-in-out 0.6s infinite; } #aeo-popup-root.aeo-skip-card .sticky-card { display: none; } #aeo-popup-root.aeo-lead-done .sticky-card, #aeo-popup-root.aeo-lead-done .sticky-banner { display: none; } #aeo-popup-root .sticky-card.hiding { animation: aeoCardSlideDown 0.4s cubic-bezier(0.4,0,1,1) forwards !important; } /* ── Border Beam ─────────────────────────────────────── A bright accent-coloured "comet" travels around the card perimeter. Implemented as a single ::before pseudo-element: 1. conic-gradient paints the comet (transparent for ~75% of the circle, then a soft -> bright -> soft fade across the last ~100°), 2. mask-composite:exclude cuts the inside out so only a 2px ring remains -- the comet appears to glide along the border, 3. @property animates the gradient's `from` angle so the rotation is on the gradient itself, not via transform (no sub-pixel shimmer, no GPU compositing cost, and the corners stay sharp). Browsers without @property (older Firefox <128, older Safari <16.4) render the gradient statically -- graceful degradation. */ @property --aeo-beam-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; } #aeo-popup-root .sticky-card::before { content: ''; position: absolute; inset: 0; border-radius: inherit; padding: 2px; background: conic-gradient( from var(--aeo-beam-angle), transparent 0deg, transparent 250deg, var(--accent-soft) 305deg, var(--accent) 340deg, var(--accent-soft) 360deg ); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; z-index: 1; opacity: 0; transition: opacity .4s ease; } #aeo-popup-root.aeo-iframe-ready .sticky-card::before { opacity: 1; animation: aeoBeamRotate 5s linear infinite; } @keyframes aeoBeamRotate { to { --aeo-beam-angle: 360deg; } } #aeo-popup-root .sc-inner { padding: 18px 18px 20px; } #aeo-popup-root .sc-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } #aeo-popup-root .sc-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-dim); display: flex; align-items: center; gap: 6px; } #aeo-popup-root .sc-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: aeoPulse 2s ease-in-out infinite; flex-shrink: 0; } #aeo-popup-root .sc-close { width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--ink-faint); font-size: 16px; line-height: 1; transition: border-color .2s, color .2s, background .2s; flex-shrink: 0; padding: 0; } #aeo-popup-root .sc-close:hover { border-color: var(--ink); color: var(--ink); background: #F5F6F2; } #aeo-popup-root .sc-headline { font-family: var(--font-display); font-size: 19px; font-weight: 800; color: var(--ink); letter-spacing: -0.4px; line-height: 1.25; margin: 0 0 8px; } #aeo-popup-root .sc-sub { font-size: 13px; color: var(--ink-faint); line-height: 1.5; margin: 0 0 12px; font-family: var(--font-body); } #aeo-popup-root .sc-ticker-wrap { position: relative; overflow: hidden; margin-bottom: 14px; height: 28px; } #aeo-popup-root .sc-ticker-wrap::before, #aeo-popup-root .sc-ticker-wrap::after { content: ''; position: absolute; inset-block: 0; width: 20px; z-index: 2; pointer-events: none; } #aeo-popup-root .sc-ticker-wrap::before { left: 0; background: linear-gradient(to right, var(--card-fade), transparent); } #aeo-popup-root .sc-ticker-wrap::after { right: 0; background: linear-gradient(to left, var(--card-fade), transparent); } #aeo-popup-root .sc-ticker { /* Marquee math (do not change without revisiting both this rule AND the aeoScTicker keyframe + the JS that builds the chip list): - Each chip is 28px wide with an 8px trailing margin = 36px slot. - We render 3 copies of the 6-engine ticker = 18 chips = 648px. - One copy width = 6 * 36 = 216px = exactly 1/3 of the total. - The keyframe translates -33.3333% (= -216px), so the moment the first copy finishes scrolling out, the second copy is already occupying its exact original position. Seamless loop. We use margin-right on the chip (not flex gap) on purpose: flex gap only sits *between* siblings, which is one short for the cycle to line up with translateX. margin-right counts on every chip, so the total width is N*(chip+gap) and -1/N translation hits the boundary cleanly. Three copies is also what keeps the right edge of the card from ever going empty mid-scroll -- a single copy (216px) is narrower than the visible card content area, so doubled content would leave a gap on the right at the end of every cycle. */ display: flex; width: max-content; animation: aeoScTicker 18s linear infinite; align-items: center; height: 100%; } #aeo-popup-root .sc-logo-chip { width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0; margin-right: 8px; display: flex; align-items: center; justify-content: center; overflow: hidden; /* Faint inset border. On dark/coloured chips it's invisible; on the white Copilot chip it's what keeps the icon from floating against the light card background. */ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06); } #aeo-popup-root .sc-logo-chip img { width: 18px; height: 18px; object-fit: contain; display: block; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.15)); } /* Copilot-specific override: Microsoft's multicolor ribbon mark has finer detail than any of the other engines' marks (which are single- colour silhouettes), and its lighter gradient tips (cyan, yellow, pink) wash out at small sizes on a white chip. Bumping the image to nearly fill the 28px chip restores legibility. We target by [title] so future engine additions don't accidentally pick this up. */ #aeo-popup-root .sc-logo-chip[title="Copilot"] img { width: 24px; height: 24px; /* No drop-shadow on the Copilot icon -- the soft glow muddies the gradient ribbon. */ filter: none; } #aeo-popup-root .sc-cta { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; font-family: var(--font-body); font-size: 14px; font-weight: 600; color: var(--cta-text); background: var(--cta-grad); background-size: 200% 100%; border: none; border-radius: 100px; padding: 11px 20px; cursor: pointer; text-decoration: none; animation: aeoCtaShimmer 3s ease-in-out 2.4s infinite; transition: transform .15s, box-shadow .2s; } #aeo-popup-root .sc-cta:hover { transform: scale(1.02); box-shadow: 0 4px 20px rgba(var(--shadow-rgb),0.4); animation-play-state: paused; } #aeo-popup-root .sc-cta .arrow { font-size: 15px; transition: transform .2s; } #aeo-popup-root .sc-cta:hover .arrow { transform: translateX(3px); } /* ── STICKY BAR (after card dismissed) ──────────────────────────── */ #aeo-popup-root .sticky-banner { position: fixed; bottom: 0; left: 0; right: 0; z-index: 9990; transform: translateY(100%); opacity: 0; pointer-events: none; background: var(--bar-bg); background-size: 300% 300%; border-top: 1px solid var(--border); box-shadow: 0 -4px 32px rgba(var(--shadow-rgb),0.08), 0 -1px 0 rgba(var(--shadow-rgb),0.15); } #aeo-popup-root .sticky-banner.visible { pointer-events: all; animation: aeoBarSlideUp 0.5s cubic-bezier(0.22,1,0.36,1) forwards, aeoBannerBreathe 5s ease-in-out 0.5s infinite; } #aeo-popup-root .sticky-banner::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--accent) 0%, var(--accent-soft) 60%, transparent 100%); } #aeo-popup-root .banner-inner { max-width: 1240px; margin: 0 auto; padding: 0 32px; height: var(--banner-h); display: flex; align-items: center; gap: 24px; } #aeo-popup-root .banner-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-dim); white-space: nowrap; flex-shrink: 0; display: flex; align-items: center; gap: 6px; } #aeo-popup-root .banner-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: aeoPulse 2s ease-in-out infinite; } #aeo-popup-root .banner-divider-v { width: 1px; height: 32px; background: var(--border); flex-shrink: 0; } #aeo-popup-root .banner-headline { font-family: var(--font-display); font-size: 15px; font-weight: 800; color: var(--ink); letter-spacing: -0.3px; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; } #aeo-popup-root .banner-sub { font-size: 13px; color: var(--ink-faint); flex-shrink: 0; white-space: nowrap; font-family: var(--font-body); } #aeo-popup-root .banner-cta { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-body); font-size: 14px; font-weight: 600; color: var(--cta-text); background: var(--cta-grad); background-size: 200% 100%; border: none; border-radius: 100px; padding: 11px 28px; cursor: pointer; text-decoration: none; white-space: nowrap; flex-shrink: 0; animation: aeoCtaShimmer 3s ease-in-out 0.5s infinite; transition: transform .15s, box-shadow .2s; } #aeo-popup-root .banner-cta:hover { transform: scale(1.03); box-shadow: 0 4px 20px rgba(var(--shadow-rgb),0.4); animation-play-state: paused; } #aeo-popup-root .banner-cta .arrow { font-size: 16px; line-height: 1; transition: transform .2s; } #aeo-popup-root .banner-cta:hover .arrow { transform: translateX(3px); } /* ── IFRAME MODAL (full-screen takeover) ────────────────────────── */ /* Matches the Verdict V2 full-page lead capture pattern: edge-to-edge shell, no card chrome, just a translucent floating top bar with "Back to article" and a close pill. The AEO funnel's own branding inside the iframe carries the visual weight. */ #aeo-popup-root .aeo-modal { position: fixed; inset: 0; z-index: 10000; background: #ffffff; display: flex; flex-direction: column; opacity: 0; pointer-events: none; transition: opacity .25s ease; } #aeo-popup-root .aeo-modal.open { opacity: 1; pointer-events: all; } #aeo-popup-root .aeo-modal-backdrop { display: none; } #aeo-popup-root .aeo-modal-shell { position: relative; z-index: 1; display: flex; flex-direction: column; width: 100%; height: 100%; background: #ffffff; } #aeo-popup-root .aeo-modal-bar { position: relative; z-index: 3; display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; background: #ffffff; border-bottom: 1px solid rgba(0,0,0,0.06); flex-shrink: 0; } #aeo-popup-root .aeo-modal-back { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-body); font-size: 13px; font-weight: 500; color: var(--ink-soft); background: transparent; border: none; cursor: pointer; padding: 6px 8px; border-radius: 8px; transition: background .15s, color .15s; } #aeo-popup-root .aeo-modal-back:hover { background: #F0F1EC; color: var(--ink); } #aeo-popup-root .aeo-modal-iframe-wrap { position: relative; flex: 1; background: #ffffff; overflow: hidden; } #aeo-popup-root .aeo-modal-iframe { width: 100%; height: 100%; border: 0; display: block; background: #ffffff; } #aeo-popup-root .aeo-modal-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); background: #ffffff; transition: opacity .3s ease; z-index: 1; pointer-events: none; } #aeo-popup-root .aeo-modal-iframe-wrap.loaded .aeo-modal-loading { opacity: 0; } #aeo-popup-root .aeo-modal-loading .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: aeoPulse 1.2s ease-in-out infinite; margin-right: 8px; } /* lock background scroll when modal is open */ body.aeo-modal-open { overflow: hidden; } /* Bar collapses to headline + CTA on narrower viewports, but the card keeps its 340px bottom-left footprint all the way down to phone widths. Real phones (≤480px) get the full-width card so the CTA stays tappable. */ @media (max-width: 900px) { #aeo-popup-root .banner-inner { padding: 0 16px; gap: 12px; } #aeo-popup-root .banner-label, #aeo-popup-root .banner-divider-v, #aeo-popup-root .banner-sub { display: none; } #aeo-popup-root .banner-headline { font-size: 13px; } #aeo-popup-root .banner-cta { font-size: 13px; padding: 9px 18px; } } @media (max-width: 480px) { #aeo-popup-root .sticky-card { left: 12px; bottom: 12px; width: calc(100vw - 24px); } } @media (prefers-reduced-motion: reduce) { #aeo-popup-root .sticky-card, #aeo-popup-root .sticky-banner.visible, #aeo-popup-root .sc-cta, #aeo-popup-root .banner-cta { animation: none !important; } #aeo-popup-root.aeo-iframe-ready .sticky-card { transform: none; opacity: 1; } } </style> <div id="aeo-popup-root" data-variant="small-report" data-domain="codeandcoffe.com"> <div class="sticky-card" id="aeoStickyCard" role="complementary" aria-label="AI Visibility Radar — Free AEO Report"> <div class="sc-inner"> <div class="sc-header"> <span class="sc-label"><span class="sc-dot"></span>AI VISIBILITY RADAR</span> <button type="button" class="sc-close" id="aeoCardClose" aria-label="Dismiss">×</button> </div> <p class="sc-headline">Are AI engines recommending your brand?</p> <p class="sc-sub">Check your score on 5 AI engines — instantly.</p> <div class="sc-ticker-wrap"><div class="sc-ticker" id="aeoScTicker"></div></div> <a href="#" class="sc-cta" id="aeoCardCta"> Check for Free <span class="arrow">→</span> </a> </div> </div> <div class="sticky-banner" id="aeoStickyBanner" role="complementary" aria-label="AI Visibility Radar — Free AEO Report"> <div class="banner-inner"> <span class="banner-label">AI VISIBILITY RADAR</span> <div class="banner-divider-v"></div> <p class="banner-headline">Are AI engines recommending your brand?</p> <span class="banner-sub">Check your score on 5 AI engines — instantly.</span> <a href="#" class="banner-cta" id="aeoBarCta"> Check for Free <span class="arrow">→</span> </a> </div> </div> <div class="aeo-modal" id="aeoModal" role="dialog" aria-modal="true" aria-label="AI Visibility Radar — Free AEO Report" aria-hidden="true"> <div class="aeo-modal-backdrop" data-aeo-close></div> <div class="aeo-modal-shell"> <header class="aeo-modal-bar"> <button type="button" class="aeo-modal-back" data-aeo-close> <span aria-hidden="true">←</span> Back to article </button> </header> <div class="aeo-modal-iframe-wrap" id="aeoModalIframeWrap"> <div class="aeo-modal-loading"><span class="dot"></span>Loading report…</div> <iframe id="aeoModalIframe" class="aeo-modal-iframe" title="AI Visibility Radar — Free AEO Report" referrerpolicy="no-referrer-when-downgrade" allow="clipboard-write" src="https://aeo.prod-mobtools.com/aeo/small-report?embed=1&utm_source=codeandcoffe.com&utm_medium=satellite&utm_campaign=aeo-popup&utm_content=small-report"></iframe> </div> </div> </div> </div> <script id="aeo-popup-script"> (function () { var root = document.getElementById('aeo-popup-root'); if (!root) return; var card = document.getElementById('aeoStickyCard'); var bar = document.getElementById('aeoStickyBanner'); var modal = document.getElementById('aeoModal'); var iframe = document.getElementById('aeoModalIframe'); var wrap = document.getElementById('aeoModalIframeWrap'); var closeBtn = document.getElementById('aeoCardClose'); var cardCta = document.getElementById('aeoCardCta'); var barCta = document.getElementById('aeoBarCta'); var ticker = document.getElementById('aeoScTicker'); /* ---------- analytics -------------------------------- Posts to the existing /wp-json/satellite/v1/lead-event proxy, which forwards to the backend's POST /api/leads/event. Events here are prefixed with "aeo_" so the lawyer funnel chart on the same dashboard never picks them up. ----------------------------------------------------- */ var AEO_DOMAIN = "codeandcoffe.com"; var AEO_VARIANT = "small-report"; var AEO_NICHE = "technology"; var AEO_EVENT_URL = "https:\/\/codeandcoffe.com\/wp-json\/satellite\/v1\/lead-event"; var AEO_EVENT_NONCE = "0eec44c9ef"; /* localStorage visitor_id so unique-clicker counts on the dashboard are stable across sessions on the same browser. UUIDv4-ish via random bytes; no PII, just a stable handle for COUNT(DISTINCT) on the backend. */ function aeoGetVisitorId() { try { var KEY = 'aeo_vid'; var existing = localStorage.getItem(KEY); if (existing) return existing; var b = (crypto && crypto.getRandomValues) ? crypto.getRandomValues(new Uint8Array(16)) : (function () { var arr = []; for (var i=0;i<16;i++) arr.push(Math.floor(Math.random()*256)); return arr; })(); b[6] = (b[6] & 0x0f) | 0x40; // version 4 b[8] = (b[8] & 0x3f) | 0x80; // variant var hex = Array.prototype.map.call(b, function (x) { return ('0' + x.toString(16)).slice(-2); }).join(''); var id = hex.slice(0,8)+'-'+hex.slice(8,12)+'-'+hex.slice(12,16)+'-'+hex.slice(16,20)+'-'+hex.slice(20,32); localStorage.setItem(KEY, id); return id; } catch (e) { return ''; // private mode / disabled storage -- silently drop } } var AEO_VISITOR_ID = aeoGetVisitorId(); function aeoTrack(eventType, extraMeta) { try { var meta = { variant: AEO_VARIANT, niche: AEO_NICHE }; if (extraMeta) for (var k in extraMeta) if (Object.prototype.hasOwnProperty.call(extraMeta, k)) meta[k] = extraMeta[k]; var payload = { domain: AEO_DOMAIN, event_type: eventType, visitor_id: AEO_VISITOR_ID, page_url: location.href, metadata_json: JSON.stringify(meta) }; var body = JSON.stringify(payload); if (navigator.sendBeacon) { navigator.sendBeacon( AEO_EVENT_URL + '?_wpnonce=' + encodeURIComponent(AEO_EVENT_NONCE), new Blob([body], { type: 'application/json' }) ); } else { fetch(AEO_EVENT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': AEO_EVENT_NONCE }, body: body, keepalive: true }); } } catch (e) { /* analytics must never break the popup */ } } /* ---------- cookies ---------- */ function readCookie(name) { var m = document.cookie.match('(?:^|;)\\s*' + name + '=([^;]*)'); return m ? decodeURIComponent(m[1]) : ''; } function writeCookie(name, value, days) { var d = new Date(); d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000); document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + d.toUTCString() + '; path=/; SameSite=Lax'; } /* Cookie names are versioned. An earlier iteration of this script set aeo_lead_done from a 15s dwell-time heuristic (since removed); any tester who left the modal open during that period now carries a 30-day cookie that silently hides the popup forever. Bumping the name to _v2 retires that stale state without forcing anyone to clear browser cookies. */ var CARD_DISMISSED = 'aeo_card_dismissed_v2'; var LEAD_DONE = 'aeo_lead_done_v2'; if (readCookie(LEAD_DONE) === '1') { root.classList.add('aeo-lead-done'); return; } /* ---------- reveal the card (or bar) ---------- The iframe is preloading in the background regardless of card state, so we don't gate reveal on it -- we just slide the card up on a quick fixed timer so the popup is visible reliably on every refresh, even when the cross-origin iframe's load event is slow or never fires. By the time the user finishes reading the article and clicks the CTA, the iframe is virtually always fully loaded, so clicks still open the modal instantly. */ var revealed = false; var dismissedBefore = readCookie(CARD_DISMISSED) === '1'; if (dismissedBefore) root.classList.add('aeo-skip-card'); function reveal() { if (revealed) return; revealed = true; if (dismissedBefore) { if (bar) bar.classList.add('visible'); aeoTrack('aeo_bar_impression', { source: 'bar' }); } else { root.classList.add('aeo-iframe-ready'); aeoTrack('aeo_impression', { source: 'card' }); } } /* 600ms is long enough to feel intentional (not jarring on first paint), short enough that it always wins against a slow cross-origin iframe. */ setTimeout(reveal, 600); /* ---------- ticker ---------- Three full copies of the chip list, not two. The aeoScTicker keyframe translates by exactly one copy's width (-33.3333%) so the second copy slides into the first's position seamlessly; the third copy is there to keep the right edge of the card filled while the second is doing the heavy lifting -- one copy alone (216px) is narrower than the visible card content area, so a doubled ticker leaves a visible gap at the right edge near the end of every animation cycle. */ var TOOLS = [{"name":"Gemini","bg":"#1A73E8","logo":"https:\/\/codeandcoffe.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/gemini.svg"},{"name":"Grok","bg":"#111111","logo":"https:\/\/codeandcoffe.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/grok.svg"},{"name":"Perplexity","bg":"#1FB8CD","logo":"https:\/\/codeandcoffe.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/perplexity.svg"},{"name":"ChatGPT","bg":"#10A37F","logo":"https:\/\/codeandcoffe.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/chatgpt.svg"},{"name":"Copilot","bg":"#FFFFFF","logo":"https:\/\/codeandcoffe.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/copilot.svg"},{"name":"Claude","bg":"#D97757","logo":"https:\/\/codeandcoffe.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/claude.svg"}]; if (ticker) { var tripled = TOOLS.concat(TOOLS).concat(TOOLS); ticker.innerHTML = tripled.map(function (t) { return '<div class="sc-logo-chip" title="' + t.name + '" style="background:' + t.bg + ';">' + '<img src="' + t.logo + '" alt="' + t.name + '" loading="lazy" width="18" height="18">' + '</div>'; }).join(''); } /* ---------- card -> bar dismissal ---------- The slide-down animation is 0.4s. We use a matched setTimeout instead of animationend because Chrome occasionally swallows the event when another animation (the breathing background) is in flight on the same element. */ if (closeBtn) { closeBtn.addEventListener('click', function () { writeCookie(CARD_DISMISSED, '1', 30); aeoTrack('aeo_card_dismiss', { source: 'card' }); card.classList.add('hiding'); setTimeout(function () { card.style.display = 'none'; if (bar) bar.classList.add('visible'); }, 420); }); } /* ---------- modal open / close ---------- */ if (iframe && wrap) { if (iframe.complete) wrap.classList.add('loaded'); else iframe.addEventListener('load', function () { wrap.classList.add('loaded'); }, { once: true }); } function swapCardToBar() { if (!card || card.style.display === 'none' || card.classList.contains('hiding')) return; card.classList.add('hiding'); setTimeout(function () { card.style.display = 'none'; if (bar) bar.classList.add('visible'); }, 420); } function openModal(e) { if (e) e.preventDefault(); if (!modal) return; modal.setAttribute('aria-hidden', 'false'); requestAnimationFrame(function () { modal.classList.add('open'); }); document.body.classList.add('aeo-modal-open'); } function closeModal() { if (!modal || !modal.classList.contains('open')) return; modal.classList.remove('open'); document.body.classList.remove('aeo-modal-open'); setTimeout(function () { modal.setAttribute('aria-hidden', 'true'); }, 320); aeoTrack('aeo_modal_close'); /* If the card was still showing behind the modal, collapse it to the bar so the user has an obvious re-entry point. No cookie is set here -- only the explicit X on the card persists dismissal across page loads. */ swapCardToBar(); } if (cardCta) cardCta.addEventListener('click', function (e) { aeoTrack('aeo_cta_click', { source: 'card' }); openModal(e); }); if (barCta) barCta.addEventListener('click', function (e) { aeoTrack('aeo_cta_click', { source: 'bar' }); openModal(e); }); /* Generic trigger: any element with [data-aeo-open] opens the same modal. The in-article banner (aeo-inline-banner.php) uses this so it doesn't have to duplicate the modal HTML/JS -- it just renders a CTA with data-aeo-open + data-aeo-source="inline" and rides on the popup infrastructure that's already in the footer. Source is read from data-aeo-source so we can split CTR by surface in the analytics dashboard (card / bar / inline / future surfaces). */ document.querySelectorAll('[data-aeo-open]').forEach(function (el) { el.addEventListener('click', function (e) { var source = el.getAttribute('data-aeo-source') || 'external'; aeoTrack('aeo_cta_click', { source: source }); openModal(e); }); }); if (modal) { modal.querySelectorAll('[data-aeo-close]').forEach(function (el) { el.addEventListener('click', closeModal); }); } document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && modal && modal.classList.contains('open')) closeModal(); }); /* ---------- future postMessage handshake (stub) ---------- Listens for a "lead-complete" event from the iframe so we can flip the LEAD_DONE cookie immediately on real submission (not on dwell time). Wire this up on the AEO side and remove the dwell-time fallback above. */ window.addEventListener('message', function (e) { if (!e || !e.origin || e.origin.indexOf('aeo.prod-mobtools.com') === -1) return; var data = e.data || {}; if (data && data.type === 'aeo:lead-complete') { writeCookie(LEAD_DONE, '1', 30); root.classList.add('aeo-lead-done'); closeModal(); } }); })(); </script> <script> (function(){ var bar = document.getElementById('reading-progress'); if(!bar) return; var article = document.querySelector('.post-content'); if(!article) return; var milestones = {25:false,50:false,75:false,100:false}; var startTime = Date.now(); function sendGA4(name, params) { if (window.gtag) window.gtag('event', name, params); } window.addEventListener('scroll', function(){ var rect = article.getBoundingClientRect(); var total = article.offsetHeight - window.innerHeight; var progress = Math.min(100, Math.max(0, (-rect.top / total) * 100)); bar.style.width = progress + '%'; var pct = Math.floor(progress); [25,50,75,100].forEach(function(m){ if (pct >= m && !milestones[m]) { milestones[m] = true; sendGA4('scroll_depth', {percent: m, reading_seconds: Math.round((Date.now()-startTime)/1000)}); } }); }, {passive:true}); function sendReadingTime() { var seconds = Math.round((Date.now()-startTime)/1000); if (seconds > 3) sendGA4('reading_time', {seconds: seconds, scroll_reached: Math.max.apply(null, Object.keys(milestones).filter(function(k){return milestones[k];})) || 0}); } document.addEventListener('visibilitychange', function(){ if(document.visibilityState==='hidden') sendReadingTime(); }); window.addEventListener('beforeunload', sendReadingTime); })(); </script> <script> (function(){ var toc = document.querySelector('.toc-container'); if(!toc) return; var title = toc.querySelector('.toc-title'); if(window.innerWidth <= 768) { toc.classList.add('toc-collapsed'); title.addEventListener('click', function(){ toc.classList.toggle('toc-collapsed'); }); } var links = toc.querySelectorAll('.toc-list a'); var sections = []; links.forEach(function(a){ var id = a.getAttribute('href'); if(id) { var el = document.querySelector(id); if(el) sections.push({el:el,link:a}); } }); if(!sections.length) return; var raf; window.addEventListener('scroll', function(){ if(raf) return; raf = requestAnimationFrame(function(){ raf = null; var scrollY = window.scrollY + 120; var active = sections[0]; for(var i=0;i<sections.length;i++){ if(sections[i].el.offsetTop <= scrollY) active = sections[i]; } links.forEach(function(l){ l.classList.remove('toc-active'); }); if(active) active.link.classList.add('toc-active'); }); }, {passive:true}); })(); </script> </body> </html>