Listen to this article · 12 min listen

Mastering digital visibility requires precision, and a core component of that is showcasing specific tactics like keyword research within your broader marketing strategy. It’s the bedrock of attracting the right audience, ensuring your content resonates with genuine user intent. But how do you go from vague ideas to concrete, actionable keyword strategies that drive measurable results?

Key Takeaways

  • Identify high-volume, low-competition keywords using a blend of seed keywords and competitor analysis within tools like Ahrefs.
  • Prioritize long-tail keywords (4+ words) that reflect specific user intent, as they typically convert at a higher rate due to their specificity.
  • Map your chosen keywords to distinct content types and stages of the buyer’s journey to create a structured content calendar.
  • Implement on-page SEO best practices by strategically placing primary and secondary keywords in titles, headings, and the first 100 words of your content.
  • Track keyword performance using Google Analytics 4 and Google Search Console to refine your strategy quarterly and adapt to search trend shifts.

1. Kickstarting with Seed Keywords and Brainstorming

Every effective keyword strategy begins with a solid foundation: your seed keywords. These are the broad terms that define your business or product. Think about what your ideal customer would type into a search engine if they had no idea your specific brand existed. For example, if you sell artisanal coffee beans, your seed keywords might be “coffee beans,” “specialty coffee,” or “buy coffee online.”

I always start with a simple spreadsheet. List everything that comes to mind – products, services, common problems your product solves, industry jargon, and even competitor names. Don’t censor yourself here; the goal is quantity. After this initial brain dump, I categorize them roughly. For a recent client, a boutique bakery in Atlanta’s Virginia-Highland neighborhood specializing in gluten-free pastries, our initial seed keywords looked like “gluten-free bakery Atlanta,” “dairy-free treats,” “vegan cakes Virginia-Highland,” and “best pastries Atlanta GA.” This broad stroke gives you a starting point for more granular research.

Pro Tip: Look beyond obvious terms. Consider the problems your product solves. If you sell noise-cancelling headphones, “focus at work” or “study without distractions” could be valuable seed concepts, even if they don’t explicitly contain “headphones.”

2. Leveraging Keyword Research Tools for Data-Driven Insights

Once you have your seed list, it’s time to bring in the heavy artillery: dedicated keyword research tools. My go-to is Ahrefs. It provides a wealth of data that goes far beyond basic search volume. I’ll walk you through a typical workflow.

First, navigate to the “Keywords Explorer” in Ahrefs. Enter one of your seed keywords, for example, “specialty coffee beans.”

Next, ensure your target country is selected (e.g., United States). Click “Search.”

You’ll immediately see key metrics like Search Volume, Keyword Difficulty (KD), and a list of related keywords. Here’s where the magic starts. I typically filter for:

  • Volume: Minimum 500 searches per month (this can vary based on niche, but it’s a good starting point for most businesses).
  • Keyword Difficulty: Maximum 30. While higher KD scores aren’t impossible, for a beginner’s guide, we’re aiming for quicker wins.
  • Word Count: At least 3 words. This helps us find more specific, long-tail opportunities.

Now, look at the “Matching terms” report. This is a goldmine. You’ll find variations, questions, and related phrases. Export this list. I also repeat this process with the “Questions” report to uncover intent-rich queries like “how to brew specialty coffee” or “where to buy fair trade coffee.”

Another excellent tool for understanding user intent is the “People Also Ask” section directly on Google’s search results page. Just type in your seed keyword and scroll down. These questions are direct insights into what users want to know, and they often make fantastic blog post topics.

Common Mistake: Focusing solely on high-volume keywords. A keyword with 100,000 searches per month and a KD of 90 is often a waste of effort for new sites. A keyword with 1,000 searches and a KD of 15 is a much better bet for initial traction. It’s about finding the right balance of volume and attainability.

3. Analyzing Competitor Keywords

Why reinvent the wheel when your competitors have already done some of the heavy lifting? Understanding what keywords drive traffic to your rivals is an invaluable tactic. Using Ahrefs again, go to the “Site Explorer” and enter a competitor’s domain (e.g., “starbucks.com” if you’re a coffee shop, though they’re a huge player, so pick someone closer to your size). Navigate to the “Organic keywords” report.

This report shows you every keyword your competitor ranks for, along with their position, search volume, and estimated traffic. I filter this list by Position: Top 10. These are the keywords they’re actively winning on. Look for keywords that are relevant to your business but perhaps you haven’t considered. You might find niche terms or long-tail variations that they’ve successfully captured. I had a client, a local real estate agent in Buckhead, Atlanta, whose main competitor was ranking for “luxury condos Buckhead with skyline views.” My client hadn’t even considered that specific phrase, but it was driving qualified leads for their competitor. We immediately integrated it into their content plan.

Pro Tip: Don’t just copy. Use competitor data as inspiration. Analyze why they rank for certain terms. Is their content exceptionally detailed? Do they have strong backlinks? This analysis informs your own content strategy.

4. Intent-Based Keyword Grouping and Content Mapping

Now you have a massive list of potential keywords. The next step is to organize them based on user intent. This is where many beginners falter, treating all keywords equally. They’re not. Some keywords indicate a user is just starting their research (informational intent), while others show they’re ready to buy (transactional intent).

I categorize keywords into four main intent types:

  1. Informational: “How to make pour over coffee,” “benefits of organic coffee.”
  2. Navigational: “Starbucks near me,” “Ahrefs login.”
  3. Commercial Investigation: “Best coffee grinders 2026,” “specialty coffee bean reviews.”
  4. Transactional: “Buy Ethiopian Yirgacheffe coffee,” “discount coffee beans.”

Once grouped, I map these keywords to specific content types and stages of the buyer’s journey. Informational keywords are perfect for blog posts, guides, and FAQs. Commercial investigation keywords suit comparison articles, reviews, and detailed product descriptions. Transactional keywords are for product pages, service pages, and landing pages designed for conversion.

For example, if I’m working with a SaaS company selling project management software, “what is agile methodology” (informational) would map to a blog post, “Asana vs. Monday.com” (commercial investigation) would be a comparison page, and “project management software pricing” (transactional) would go to their pricing page. This structured approach ensures every piece of content serves a purpose and targets a specific audience need. I firmly believe that this mapping is the single most important step after gathering the keywords. Without it, you’re just writing content aimlessly.

5. Implementing On-Page SEO with Your Keywords

Having a brilliant keyword list is useless if you don’t actually use it. This step is about strategically weaving your chosen keywords into your content and website structure. For every page or post, you should have one primary keyword and 2-3 secondary keywords.

Here’s my checklist for on-page implementation:

  • Page Title ( tag):</strong> Your primary keyword absolutely must be here, ideally toward the beginning. Keep it under 60 characters for optimal display in search results.</li> <li><strong>Meta Description:</strong> Include your primary and at least one secondary keyword. This doesn’t directly impact ranking but influences click-through rates. Make it compelling!</li> <li><strong>URL Slug:</strong> Keep it short, descriptive, and include your primary keyword. For example, <code>yourdomain.com/specialty-coffee-beans</code>.</li> <li><strong>H1 Heading:</strong> Your page’s main heading should contain your primary keyword. There should only be one H1 per page.</li> <li><strong>H2/H3 Headings:</strong> Use secondary keywords and variations naturally in your subheadings. This breaks up content and signals topic relevance to search engines.</li> <li><strong>First 100 Words:</strong> Your primary keyword should appear within the first paragraph.</li> <li><strong>Body Content:</strong> Distribute your primary and secondary keywords naturally throughout the text. Don’t keyword stuff – write for humans first, search engines second. Aim for a natural density, typically around 0.5-1.5% for your primary keyword.</li> <li><strong>Image Alt Text:</strong> Describe your images using relevant keywords. This helps with image search and accessibility.</li> <li><strong>Internal Linking:</strong> Link to other relevant pages on your site using anchor text that includes your target keywords for the linked page.</li> </ul> <p>I once worked on a local business site for a plumbing service in Sandy Springs. Their original service page for “water heater repair” had a title “Our Services.” We changed it to “<strong>Expert Water Heater Repair Sandy Springs GA</strong> | Fast & Reliable Service” and saw a 30% increase in organic traffic to that page within three months. It wasn’t rocket science; it was simply aligning the page with what people were actually searching for.</p> <div class="callout"> <strong>Common Mistake:</strong> Keyword stuffing. This is an outdated and harmful practice. Repeating a keyword unnaturally will hurt your rankings and annoy your readers. Focus on natural language. Google is smart enough to understand synonyms and related concepts. </div> <aside class="related-callout"><span class="related-callout-label">Related Reading</span><a href="https://ppcgrowthstudio.com/google-ads-2026-bridging-pro-and-newbie-gaps/">Google Ads 2026: Bridging Pro & Newbie Gaps</a></p> <p class="related-callout-excerpt">Discover how keyword strategies are crucial for both SEO and effective Google Ads campaigns.</p> </aside> <h2 id="section-7">6. Monitoring and Adapting Your Strategy</h2> <p>Keyword research isn’t a one-and-done task. The digital landscape is constantly shifting. New trends emerge, search intent evolves, and competitors adjust their strategies. You need to monitor your performance and adapt accordingly.</p> <p>My essential tools for monitoring are <a href="https://analytics.google.com/analytics/web/" target="_blank" rel="noopener">Google Analytics 4 (GA4)</a> and <a href="https://search.google.com/search-console/about" target="_blank" rel="noopener">Google Search Console (GSC)</a>.</p> <p>In GSC, navigate to “Performance” > “Search results.” Here you can see which queries your site is appearing for, your average position, clicks, and impressions. Filter by “Queries” and sort by “Impressions” to identify terms you’re showing up for but not getting many clicks. These might be opportunities to improve your title tags and meta descriptions. Also, look at “Pages” to see which pages are performing best and which might need a content refresh or more keyword targeting.</p> <p>GA4 provides deeper insights into user behavior after they land on your site. You can see engagement rates, conversions, and bounce rates for traffic coming from specific keywords. If a keyword is bringing in a lot of traffic but users are bouncing immediately, it might indicate a mismatch between their intent and your content. Perhaps you need to refine the content or target a different keyword.</p> <p>I review keyword performance quarterly. If I see a drop in rankings for a key term, I investigate. Has a competitor published something better? Has Google updated its algorithm? Sometimes it’s as simple as refreshing statistics or adding a new section to an old blog post. According to a <a href="https://blog.hubspot.com/marketing/blogging-statistics" target="_blank" rel="noopener">HubSpot report</a>, updating and republishing old blog posts with new content and keywords can increase organic traffic by as much as 106%.</p> <p>Mastering keyword research is a continuous journey, not a destination. By systematically identifying, analyzing, implementing, and monitoring your keywords, you build a robust foundation for all your digital marketing efforts, ensuring you connect with your audience exactly when they’re looking for you. For more insights on <a href="https://ppcgrowthstudio.com/marketing-roi-data-driven-growth-in-2026/">data-driven growth in 2026</a>, consider how your keyword strategy feeds into your overall <a href="https://ppcgrowthstudio.com/ppc-roas-2026-data-driven-growth-for-ads/">PPC ROAS data-driven growth</a>. Also, ensuring your <a href="https://ppcgrowthstudio.com/master-2026-conversion-tracking-gtm-to-meta/">conversion tracking is mastered for 2026</a> is essential to measure the impact of your keyword efforts.</p> <div class="faq-section"> <div class="faq-item"> <h3 class="faq-question">How often should I do keyword research?</h3> <div class="faq-answer"> <p>While the initial deep dive might be annual, I recommend a quarterly review of your existing keywords and a refresh of your strategy. The search landscape changes rapidly, and staying current with trends and competitor movements is essential for sustained visibility.</p> </p></div> </p></div> <div class="faq-item"> <h3 class="faq-question">What’s the difference between short-tail and long-tail keywords?</h3> <div class="faq-answer"> <p><strong>Short-tail keywords</strong> are typically 1-2 words, very broad, and have high search volume but also high competition (e.g., “coffee”). <strong>Long-tail keywords</strong> are 3+ words, more specific, have lower search volume but much higher conversion rates due to their clear user intent (e.g., “best organic fair trade coffee beans for espresso machine”). I always prioritize long-tail keywords for targeted traffic.</p> </p></div> </p></div> <div class="faq-item"> <h3 class="faq-question">Can I do keyword research without paid tools?</h3> <div class="faq-answer"> <p>Yes, you can. While paid tools like Ahrefs or <a href="https://www.semrush.com/" target="_blank" rel="noopener">Semrush</a> offer unparalleled depth, you can start with free resources. <a href="https://ads.google.com/home/tools/keyword-planner/" target="_blank" rel="noopener">Google Keyword Planner</a> (requires a Google Ads account), Google Search Console, and the “People Also Ask” section on Google search results pages are excellent starting points for identifying relevant terms and understanding basic search volume.</p> </p></div> </p></div> <div class="faq-item"> <h3 class="faq-question">Should I target keywords with zero search volume?</h3> <div class="faq-answer"> <p>Generally, no. If a keyword shows zero search volume in a reputable tool, it means virtually no one is searching for it. While there are edge cases for extremely niche or emerging terms, focusing your efforts on keywords with at least some measurable volume is a more effective use of your resources.</p> </p></div> </p></div> <div class="faq-item"> <h3 class="faq-question">How do I know if a keyword is too competitive for my website?</h3> <div class="faq-answer"> <p>Keyword Difficulty (KD) scores from tools like Ahrefs are a good indicator. For newer sites or those with fewer backlinks, targeting keywords with a KD score below 30 is a smart move. As your site gains authority, you can gradually aim for more competitive terms. Also, manually check the top 10 results for a keyword – if they’re all massive brands with millions of backlinks, it’s likely too tough for now.</p> </p></div> </p></div> </div> </div> <div class="t1-feedback" id="t1-feedback"> <span class="t1-feedback-q">Was this article helpful?</span> <button type="button" data-vote="yes">Yes</button> <button type="button" data-vote="no">No</button> </div> <script> (function(){ var fb = document.getElementById('t1-feedback'); if(!fb) return; fb.querySelectorAll('button').forEach(function(btn){ btn.addEventListener('click', function(){ fetch('/wp-json/satellite/v1/feedback', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({post_id:18476, vote:this.dataset.vote})}).catch(function(){}); fb.innerHTML = '<span class="t1-feedback-thanks">Thanks for your feedback!</span>'; }); }); })(); </script> <div class="t1-author-bio"> <div><img fetchpriority="low" loading="lazy" alt='' src='https://secure.gravatar.com/avatar/b7fd50a9d0c5d6e20947d500964c66c1b6cc7b7c674729f45a2759f1a382ac3d?s=84&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/b7fd50a9d0c5d6e20947d500964c66c1b6cc7b7c674729f45a2759f1a382ac3d?s=168&d=mm&r=g 2x' class='avatar avatar-84 photo' height='84' width='84' decoding='async'/></div> <div> <h3 class="t1-author-name"><a href="https://ppcgrowthstudio.com/author/ppcgrowteditorialteam/">Editorial Team</a></h3> <p class="t1-author-desc">The editorial team behind PPC Growth Studio.</p> </div> </div> </div> </div> <section class="t1-related"> <div class="t1-section-header"><h2 class="t1-section-title">Related Articles</h2></div> <div class="t1-grid t1-grid-3"> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/microsoft-advertising-s-2025-surge-15-3-market-share/" aria-label="Microsoft Advertising’s 2025 Surge: 15.3% Market Share"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/microsoft-advertising-boost-roas-in-2026-featured-600x400.webp?v=1785974954" class="attachment-satellite-card size-satellite-card wp-post-image" alt="Microsoft Advertising’s 2025 Surge: 15.3% Market Share" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/microsoft-advertising-boost-roas-in-2026-featured-600x400.webp 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/microsoft-advertising-boost-roas-in-2026-featured-300x200.webp 300w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/microsoft-advertising-boost-roas-in-2026-featured-400x267.webp 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739"> Digital Marketing </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> <time datetime="2026-08-12T06:14:27+00:00">Aug 12, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/microsoft-advertising-s-2025-surge-15-3-market-share/">Microsoft Advertising’s 2025 Surge: 15.3% Market Share</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/ppc-roi-stop-losing-1-5-billion-in-2026/" aria-label="PPC ROI: Stop Losing $1.5 Billion in 2026"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/ppc-roi-23-wasted-spend-in-2026-featured-600x400.webp?v=1786275264" class="attachment-satellite-card size-satellite-card wp-post-image" alt="PPC ROI: Stop Losing $1.5 Billion in 2026" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/ppc-roi-23-wasted-spend-in-2026-featured-600x400.webp 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/ppc-roi-23-wasted-spend-in-2026-featured-300x200.webp 300w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/ppc-roi-23-wasted-spend-in-2026-featured-400x267.webp 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739"> Digital Marketing </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> <time datetime="2026-08-12T05:03:11+00:00">Aug 12, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/ppc-roi-stop-losing-1-5-billion-in-2026/">PPC ROI: Stop Losing $1.5 Billion in 2026</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/bid-management-15-conversion-boost-by-2026/" aria-label="Bid Management: 15% Conversion Boost by 2026"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/bid-management-15-conversion-boost-by-2026-featured-600x400.webp?v=1786502282" class="attachment-satellite-card size-satellite-card wp-post-image" alt="Bid Management: 15% Conversion Boost by 2026" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/bid-management-15-conversion-boost-by-2026-featured-600x400.webp 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/bid-management-15-conversion-boost-by-2026-featured-300x200.webp 300w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/08/bid-management-15-conversion-boost-by-2026-featured-400x267.webp 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739"> Digital Marketing </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> <time datetime="2026-08-12T02:08:02+00:00">Aug 12, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/bid-management-15-conversion-boost-by-2026/">Bid Management: 15% Conversion Boost by 2026</a> </h3> </div> </article> </div> </section> <section class="t1-related"> <div class="t1-section-header"><h2 class="t1-section-title">Popular Posts</h2></div> <div class="t1-grid t1-grid-3"> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026/" aria-label="A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237" class="attachment-satellite-card size-satellite-card wp-post-image" alt="A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-300x200.webp?v=1776360237 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/customer-experience/" style="background:#e8543f"> Customer Experience </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> <time datetime="2026-04-16T16:06:03+00:00">Apr 16, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026/">A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/ppc-a-b-testing-case-study-175-conversion-lift/" aria-label="PPC A/B Testing Case Study: 175% Conversion Lift"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/ppc-a-b-testing-case-study-175-conversion-lift-featured-600x400.webp?v=1773385149" class="attachment-satellite-card size-satellite-card wp-post-image" alt="PPC A/B Testing Case Study: 175% Conversion Lift" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/ppc-a-b-testing-case-study-175-conversion-lift-featured-600x400.webp?v=1773385149 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/ppc-a-b-testing-case-study-175-conversion-lift-featured-300x200.webp?v=1773385149 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/campaign-analysis/" style="background:#db3d67"> Campaign Analysis </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> <time datetime="2026-03-13T06:59:09+00:00">Mar 13, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/ppc-a-b-testing-case-study-175-conversion-lift/">PPC A/B Testing Case Study: 175% Conversion Lift</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/microsoft-ads-2026-will-ai-copilot-steal-your-job/" aria-label="Microsoft Ads 2026: Will AI Copilot Steal Your Job?"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/ai-kills-a-b-testing-ad-copy-in-2026-featured-600x400.webp?v=1775749299" class="attachment-satellite-card size-satellite-card wp-post-image" alt="Microsoft Ads 2026: Will AI Copilot Steal Your Job?" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/ai-kills-a-b-testing-ad-copy-in-2026-featured-600x400.webp?v=1775749299 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/ai-kills-a-b-testing-ad-copy-in-2026-featured-300x200.webp?v=1775749299 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/customer-experience/" style="background:#e8543f"> Customer Experience </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> <time datetime="2026-04-19T19:52:43+00:00">Apr 19, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/microsoft-ads-2026-will-ai-copilot-steal-your-job/">Microsoft Ads 2026: Will AI Copilot Steal Your Job?</a> </h3> </div> </article> </div> </section> </div> </article> </div><!-- /.t1-site-main --> <footer class="t1-footer" role="contentinfo"> <div class="t1-container"> <div class="t1-footer-grid"> <div class="t1-footer-col"> <h4 class="t1-footer-title">About PPC Growth Studio</h4> <p class="t1-footer-about">Expert insights, guides, and stories about marketing</p> </div> <div class="t1-footer-col"> <h4 class="t1-footer-title">Latest Articles</h4> <div class="t1-footer-post"> <a class="t1-pill" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739">Digital Marketing</a> <a class="t1-footer-post-title" href="https://ppcgrowthstudio.com/microsoft-advertising-s-2025-surge-15-3-market-share/">Microsoft Advertising’s 2025 Surge: 15.3% Market Share</a> <div class="t1-footer-post-date">Aug 12, 2026</div> </div> <div class="t1-footer-post"> <a class="t1-pill" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739">Digital Marketing</a> <a class="t1-footer-post-title" href="https://ppcgrowthstudio.com/ppc-roi-stop-losing-1-5-billion-in-2026/">PPC ROI: Stop Losing $1.5 Billion in 2026</a> <div class="t1-footer-post-date">Aug 12, 2026</div> </div> </div> <div class="t1-footer-col"> <h4 class="t1-footer-title">Quick Links</h4> <ul class="t1-footer-links"> <li><a href="https://ppcgrowthstudio.com/about/">About</a></li><li><a href="https://ppcgrowthstudio.com/contact/">Contact</a></li><li><a href="https://ppcgrowthstudio.com/privacy-policy/">Privacy Policy</a></li><li><a href="https://ppcgrowthstudio.com/terms-of-service/">Terms of Service</a></li><li><a href="https://ppcgrowthstudio.com/category/ai-agent-attribution/">AI Agent Attribution</a></li><li><a href="https://ppcgrowthstudio.com/category/digital-marketing/">Digital Marketing</a></li><li><a href="https://ppcgrowthstudio.com/category/campaign-analysis/">Campaign Analysis</a></li><li><a href="https://ppcgrowthstudio.com/category/marketing-analytics/">Marketing Analytics</a></li> </ul> </div> <div class="t1-footer-col"> <h4 class="t1-footer-title">Trending</h4> <a class="t1-footer-feature" href="https://ppcgrowthstudio.com/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026/"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237" class="attachment-satellite-card size-satellite-card wp-post-image" alt="A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-300x200.webp?v=1776360237 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> <span class="t1-footer-feature-shade"></span> <span class="t1-footer-feature-content"> <span class="t1-pill" style="background:#e8543f">Customer Experience</span> <span class="t1-footer-feature-title">A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)</span> </span> </a> </div> </div> <div class="t1-footer-bottom"> <div class="t1-footer-copy">© 2026 <strong>PPC Growth Studio</strong>. All rights reserved.</div> <form class="t1-footer-subscribe" id="t1-subscribe" data-t1-cta-trigger> <input type="email" name="email" placeholder="Enter your email address" aria-label="Enter your email address" /> <button type="submit" class="t1-subscribe">Subscribe</button> </form> </div> </div> </footer> <script> (function(){ // Route SUBSCRIBE / footer form / sticky CTA into the existing lead-popup // mu-plugins. Graceful fallback: scroll to the footer subscribe field. function openLead(source){ try { if (window.LawyerLeadFormV2 && window.LawyerLeadFormV2.open) { window.LawyerLeadFormV2.open(source); return true; } if (window.AeoLeadPopup && window.AeoLeadPopup.open) { window.AeoLeadPopup.open(source); return true; } if (window.ExitIntentPopup && window.ExitIntentPopup.open) { window.ExitIntentPopup.open(source); return true; } document.dispatchEvent(new CustomEvent('satellite:open-lead-popup', {detail: {source: source}})); return true; } catch(e){ return false; } } document.querySelectorAll('[data-t1-cta-trigger]').forEach(function(el){ el.addEventListener(el.tagName === 'FORM' ? 'submit' : 'click', function(e){ e.preventDefault(); if (!openLead('t1_cta')) { var f = document.getElementById('t1-subscribe'); if (f) f.scrollIntoView({behavior:'smooth', block:'center'}); } }); }); })(); </script> <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/37/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/satellite-theme1/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]} </script> <script src="https://ppcgrowthstudio.com/wp-content/themes/satellite-theme1/assets/main.js?ver=1.1.5" id="satellite-theme1-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="ppcgrowthstudio.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=ppcgrowthstudio.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 = "ppcgrowthstudio.com"; var AEO_VARIANT = "small-report"; var AEO_NICHE = "marketing"; var AEO_EVENT_URL = "https:\/\/ppcgrowthstudio.com\/wp-json\/satellite\/v1\/lead-event"; var AEO_EVENT_NONCE = "8598abee5f"; /* 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:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/gemini.svg"},{"name":"Grok","bg":"#111111","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/grok.svg"},{"name":"Perplexity","bg":"#1FB8CD","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/perplexity.svg"},{"name":"ChatGPT","bg":"#10A37F","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/chatgpt.svg"},{"name":"Copilot","bg":"#FFFFFF","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/copilot.svg"},{"name":"Claude","bg":"#D97757","logo":"https:\/\/ppcgrowthstudio.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 id="satellite-cta-fallback">(function(){ function ready(fn){ if(document.readyState!=='loading'){fn();} else {document.addEventListener('DOMContentLoaded',fn);} } ready(function(){ document.querySelectorAll('[class*="-sticky-cta"]').forEach(function(el){ var b = el.querySelector('[class*="-sticky-cta-btn"]'); if (b && /subscribe/i.test(b.textContent)) { el.remove(); } }); }); document.addEventListener('satellite:open-lead-popup', function(){ var has = (window.LawyerLeadFormV2&&window.LawyerLeadFormV2.open)||(window.AeoLeadPopup&&window.AeoLeadPopup.open)||(window.ExitIntentPopup&&window.ExitIntentPopup.open); if (!has) { window.location.href = 'https://ppcgrowthstudio.com/contact/'; } }); })();</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}); } 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 && title) { 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>