SEO by Role 11 min read

Lighthouse GitHub Actions: Automated SEO and Performance Monitoring

Automate Lighthouse audits with GitHub Actions to catch SEO and performance regressions before deployment. Learn setup, configuration, and alert workflows.

V
Victor Romo
|

Lighthouse GitHub Actions: Automated SEO and Performance Monitoring

Quick Summary

- What this covers: Automate Lighthouse audits with GitHub Actions to catch SEO and performance regressions before deployment. Learn setup, configuration, and alert workflows.

- Who it's for: SEO practitioners at every career stage

- Key takeaway: Read the first section for the core framework, then use the specific tactics that match your situation.

Lighthouse audits web pages for performance, accessibility, SEO, and best practices. Running Lighthouse manually before every deploy doesn't scale. GitHub Actions automates Lighthouse audits on every pull request, catching regressions before they reach production.

This guide shows how to integrate Lighthouse with GitHub Actions, configure SEO-focused audits, and set up alerting for performance and SEO issues.

Why Automate Lighthouse Audits

Manual Lighthouse audits face three problems:

Inconsistency: Developers forget to run audits, or only audit on desktop, or skip audits when rushing to deploy. Late detection: Discovering a performance regression after deployment means rollback complexity, angry users, and lost rankings. No historical tracking: Manual audits produce one-time scores with no trend data to identify gradual degradation. Automated Lighthouse via GitHub Actions solves these: Consistency: Every pull request runs audits automatically, no human memory required. Early detection: Regressions get caught during code review, before merging to main. Failed audits block merges. Historical tracking: Store audit results in artifacts or external services to track performance trends over time. SEO-specific use cases:
  • Catch meta tag removals or canonical tag errors before deployment
  • Detect structured data breakage or schema.org validation errors
  • Monitor crawlability issues (broken internal links, robots.txt changes)
  • Track Core Web Vitals trends (LCP, CLS, FID) across releases

Setting Up Lighthouse GitHub Actions

Basic Workflow Configuration

Create a GitHub Actions workflow file at .github/workflows/lighthouse.yml:

``yaml

name: Lighthouse CI

on:

pull_request:

branches:

- main

jobs:

lighthouse:

runs-on: ubuntu-latest

steps:

- name: Checkout code

uses: actions/checkout@v3

- name: Install Node.js

uses: actions/setup-node@v3

with:

node-version: '18'

- name: Install dependencies

run: npm install

- name: Build site

run: npm run build

- name: Run Lighthouse

uses: treosh/lighthouse-ci-action@v9

with:

urls: |

https://example.com

https://example.com/blog

https://example.com/products

uploadArtifacts: true

temporaryPublicStorage: true

` What this does:
  • Triggers on pull requests to main
  • Checks out code
  • Installs dependencies and builds the site
  • Runs Lighthouse against specified URLs
  • Uploads audit results as artifacts
  • URL targeting: Test critical pages—homepage, top landing pages, key product pages. Auditing 5-10 pages catches most issues.

    Lighthouse CI Server Setup (Advanced)

    For persistent storage and historical trends, use Lighthouse CI Server.

    Benefits:
    • Stores audit history across all branches and PRs
    • Provides web UI for comparing runs
    • Enables trend analysis (performance over time)
    • Supports assertions (fail builds if scores drop below thresholds)
    Setup:
  • Deploy Lighthouse CI Server (Heroku, Vercel, or self-hosted Docker container)
  • Update workflow to post results to the server:
  • `yaml
    • name: Run Lighthouse CI
    uses: treosh/lighthouse-ci-action@v9

    with:

    urls: |

    https://example.com

    https://example.com/blog

    serverBaseUrl: https://your-lhci-server.herokuapp.com

    serverToken: ${{ secrets.LHCISERVERTOKEN }}

    `
  • Add assertions in a lighthouserc.json config:
  • `json

    {

    "ci": {

    "collect": {

    "numberOfRuns": 3

    },

    "assert": {

    "assertions": {

    "categories:performance": ["error", {"minScore": 0.9}],

    "categories:seo": ["error", {"minScore": 0.95}],

    "categories:accessibility": ["warn", {"minScore": 0.9}]

    }

    },

    "upload": {

    "target": "lhci",

    "serverBaseUrl": "https://your-lhci-server.herokuapp.com",

    "token": "YOUR_TOKEN"

    }

    }

    }

    ` Assertions:
    • categories:performance requires performance score ≥90
    • categories:seo requires SEO score ≥95
    • categories:accessibility warns if accessibility <90 but doesn't fail
    Pull requests that fail assertions get blocked from merging.

    SEO-Focused Lighthouse Configuration

    Lighthouse's SEO audit checks 16+ factors. Customize which failures block deployments.

    SEO Audit Categories

    Lighthouse SEO audits include:

    Document structure:
    • <code> tag exists and isn't empty</li> <li>Meta description exists</li> <li>Page has valid </code><html lang><code> attribute</li> </ul> <strong>Crawlability:</strong> <ul class="space-y-2 my-6 text-text-muted"><li></code>robots.txt<code> is valid</li> <li>Page is mobile-friendly</li> <li>Links have descriptive text (no "click here")</li> <li>Links are crawlable (use </code><a href><code>, not JavaScript click handlers)</li> </ul> <strong>Structured data:</strong> <ul class="space-y-2 my-6 text-text-muted"><li>Structured data is valid (schema.org compliance)</li> </ul> <strong>Image optimization:</strong> <ul class="space-y-2 my-6 text-text-muted"><li>Images have </code>alt<code> attributes</li> <li>Images use appropriate sizes</li> </ul> <strong>Mobile usability:</strong> <ul class="space-y-2 my-6 text-text-muted"><li>Viewport meta tag exists</li> <li>Font sizes are legible on mobile</li> <li>Tap targets are appropriately sized (44x44px minimum)</li> </ul> <h3 id="custom-seo-assertions" class="text-xl font-semibold mt-8 mb-4">Custom SEO Assertions</h3> <p>Target critical SEO issues in </code>lighthouserc.json<code>:</p> </code>`<code>json <p>{</p> <p>"ci": {</p> <p>"assert": {</p> <p>"assertions": {</p> <p>"categories:seo": ["error", {"minScore": 0.95}],</p> <p>"document-title": "error",</p> <p>"meta-description": "error",</p> <p>"link-text": "warn",</p> <p>"crawlable-anchors": "error",</p> <p>"image-alt": "warn",</p> <p>"hreflang": "error",</p> <p>"canonical": "error"</p> <p>}</p> <p>}</p> <p>}</p> <p>}</p> </code>`<code> <strong>Critical errors (block merge):</strong> <ul class="space-y-2 my-6 text-text-muted"><li>Missing </code><title><code> or meta description</li> <li>Invalid canonical tags or hreflang</li> <li>Links that aren't crawlable</li> </ul> <strong>Warnings (don't block, but flag):</strong> <ul class="space-y-2 my-6 text-text-muted"><li>Generic link text ("click here", "read more")</li> <li>Missing alt attributes</li> </ul> <h3 id="validating-structured-data" class="text-xl font-semibold mt-8 mb-4">Validating Structured Data</h3> <p>Lighthouse validates structured data but doesn't exhaustively check schema.org compliance. Add a separate step for schema validation:</p> </code>`<code>yaml <ul class="space-y-2 my-6 text-text-muted"><li>name: Validate Structured Data</li> </ul> run: | <p>npx structured-data-testing-tool https://example.com</p> </code>`<code> <p>Alternatively, integrate <strong>Google's Rich Results Test</strong> via API or scraping.</p> <h2 id="core-web-vitals-tracking" class="text-2xl sm:text-3xl font-bold mb-6">Core Web Vitals Tracking</h2> <p>Lighthouse measures <strong>Core Web Vitals</strong> (LCP, CLS, FID/TBT). Track these over time to identify regressions.</p> <h3 id="web-vitals-assertions" class="text-xl font-semibold mt-8 mb-4">Web Vitals Assertions</h3> <p>Set thresholds in </code>lighthouserc.json<code>:</p> </code>`<code>json <p>{</p> <p>"ci": {</p> <p>"assert": {</p> <p>"assertions": {</p> <p>"largest-contentful-paint": ["error", {"maxNumericValue": 2500}],</p> <p>"cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}],</p> <p>"total-blocking-time": ["error", {"maxNumericValue": 300}]</p> <p>}</p> <p>}</p> <p>}</p> <p>}</p> </code>`<code> <strong>Thresholds:</strong> <ul class="space-y-2 my-6 text-text-muted"><li><strong>LCP:</strong> ≤2.5s (good), 2.5-4.0s (needs improvement), >4.0s (poor)</li> <li><strong>CLS:</strong> ≤0.1 (good), 0.1-0.25 (needs improvement), >0.25 (poor)</li> <li><strong>TBT (proxy for FID):</strong> ≤300ms (good), 300-600ms (needs improvement), >600ms (poor)</li> </ul> <p>Pull requests that exceed thresholds fail, forcing developers to optimize before merging.</p> <h3 id="tracking-trends" class="text-xl font-semibold mt-8 mb-4">Tracking Trends</h3> <p>Store Lighthouse scores in a database or external service to track trends.</p> <strong>Option 1: Lighthouse CI Server</strong> (built-in trending) <strong>Option 2: Post results to analytics platform</strong> </code>`<code>yaml <ul class="space-y-2 my-6 text-text-muted"><li>name: Post Lighthouse results to analytics</li> </ul> run: | <p>curl -X POST https://analytics.example.com/lighthouse \</p> <p>-H "Content-Type: application/json" \</p> <p>-d '{"score": ${{ steps.lighthouse.outputs.performance }}, "timestamp": "$(date -Iseconds)"}'</p> </code>`<code> <strong>Option 3: Store results in Google Sheets or Airtable</strong> <p>Use a GitHub Action like </code>googleapis/sheets-action<code> to append scores to a tracking sheet.</p> <h2 id="handling-dynamic-and-authenticated-pages" class="text-2xl sm:text-3xl font-bold mb-6">Handling Dynamic and Authenticated Pages</h2> <p>Lighthouse audits require publicly accessible URLs. Staging environments or authenticated pages need special handling.</p> <h3 id="auditing-staging-environments" class="text-xl font-semibold mt-8 mb-4">Auditing Staging Environments</h3> <p>Deploy PR branches to preview environments (Vercel, Netlify, or custom staging), then audit those URLs.</p> <strong>Example with Vercel:</strong> </code>`<code>yaml <ul class="space-y-2 my-6 text-text-muted"><li>name: Deploy to Vercel</li> </ul> uses: amondnet/vercel-action@v20 <p>with:</p> <p>vercel-token: ${{ secrets.VERCEL_TOKEN }}</p> <p>vercel-org-id: ${{ secrets.VERCEL<em>ORG</em>ID }}</p> <p>vercel-project-id: ${{ secrets.VERCEL<em>PROJECT</em>ID }}</p> <ul class="space-y-2 my-6 text-text-muted"><li>name: Run Lighthouse on preview</li> </ul> uses: treosh/lighthouse-ci-action@v9 <p>with:</p> <p>urls: ${{ steps.vercel.outputs.preview-url }}</p> </code>`<code> <strong>This audits the deployed preview, catching issues before production.</strong> <h3 id="auditing-authenticated-pages" class="text-xl font-semibold mt-8 mb-4">Auditing Authenticated Pages</h3> <p>Lighthouse can't audit pages behind login by default. Two workarounds:</p> <strong>Option 1: Bypass authentication for CI</strong> <p>Create a temporary token or bypass parameter that allows Lighthouse to access authenticated pages during CI runs. Restrict this to CI IP addresses.</p> <strong>Option 2: Puppeteer login flow</strong> <p>Use Puppeteer to log in before running Lighthouse:</p> </code>`<code>javascript <p>const puppeteer = require('puppeteer');</p> <p>const lighthouse = require('lighthouse');</p> <p>(async () => {</p> <p>const browser = await puppeteer.launch();</p> <p>const page = await browser.newPage();</p> <p>// Login</p> <p>await page.goto('https://example.com/login');</p> <p>await page.type('#username', 'test-user');</p> <p>await page.type('#password', 'test-password');</p> <p>await page.click('#login-button');</p> <p>await page.waitForNavigation();</p> <p>// Run Lighthouse</p> <p>const result = await lighthouse('https://example.com/dashboard', {</p> <p>port: new URL(browser.wsEndpoint()).port,</p> <p>output: 'json'</p> <p>});</p> <p>console.log(result.lhr.categories.seo.score);</p> <p>await browser.close();</p> <p>})();</p> </code>`<code> <h2 id="alerting-on-regressions" class="text-2xl sm:text-3xl font-bold mb-6">Alerting on Regressions</h2> <p>GitHub Actions can send alerts when audits fail.</p> <h3 id="slack-notifications" class="text-xl font-semibold mt-8 mb-4">Slack Notifications</h3> <p>Post Lighthouse failures to Slack:</p> </code>`<code>yaml <ul class="space-y-2 my-6 text-text-muted"><li>name: Notify Slack on failure</li> </ul> if: failure() <p>uses: slackapi/slack-github-action@v1</p> <p>with:</p> <p>webhook-url: ${{ secrets.SLACK_WEBHOOK }}</p> <p>payload: |</p> <p>{</p> <p>"text": "Lighthouse audit failed on PR #${{ github.event.pull_request.number }}",</p> <p>"blocks": [</p> <p>{</p> <p>"type": "section",</p> <p>"text": {</p> <p>"type": "mrkdwn",</p> <p>"text": "Performance score dropped below threshold. Review: ${{ github.event.pull<em>request.html</em>url }}"</p> <p>}</p> <p>}</p> <p>]</p> <p>}</p> </code>`<code> <h3 id="email-notifications" class="text-xl font-semibold mt-8 mb-4">Email Notifications</h3> <p>Use GitHub Actions' built-in email notifications or a custom action to send email alerts on failures.</p> <h3 id="pr-comments" class="text-xl font-semibold mt-8 mb-4">PR Comments</h3> <p>Post Lighthouse results directly to pull requests:</p> </code>`<code>yaml <ul class="space-y-2 my-6 text-text-muted"><li>name: Comment Lighthouse results on PR</li> </ul> uses: actions/github-script@v6 <p>with:</p> <p>script: |</p> <p>github.rest.issues.createComment({</p> <p>issue_number: context.issue.number,</p> <p>owner: context.repo.owner,</p> <p>repo: context.repo.repo,</p> <p>body: 'Lighthouse Audit Results:\n- Performance: 92\n- SEO: 98\n- Accessibility: 95'</p> <p>});</p> </code>`<code> <h2 id="example-full-lighthouse-github-actions-workflow" class="text-2xl sm:text-3xl font-bold mb-6">Example: Full Lighthouse GitHub Actions Workflow</h2> </code>`<code>yaml <p>name: Lighthouse CI</p> <p>on:</p> <p>pull_request:</p> <p>branches:</p> <p>- main</p> <p>jobs:</p> <p>lighthouse:</p> <p>runs-on: ubuntu-latest</p> <p>steps:</p> <p>- name: Checkout code</p> <p>uses: actions/checkout@v3</p> <p>- name: Setup Node.js</p> <p>uses: actions/setup-node@v3</p> <p>with:</p> <p>node-version: '18'</p> <p>- name: Install dependencies</p> <p>run: npm install</p> <p>- name: Build site</p> <p>run: npm run build</p> <p>- name: Run Lighthouse CI</p> <p>uses: treosh/lighthouse-ci-action@v9</p> <p>with:</p> <p>urls: |</p> <p>http://localhost:3000</p> <p>http://localhost:3000/blog</p> <p>http://localhost:3000/products</p> <p>uploadArtifacts: true</p> <p>configPath: './lighthouserc.json'</p> <p>- name: Comment results on PR</p> <p>uses: actions/github-script@v6</p> <p>with:</p> <p>script: |</p> <p>const fs = require('fs');</p> <p>const results = JSON.parse(fs.readFileSync('.lighthouseci/manifest.json'));</p> <p>github.rest.issues.createComment({</p> <p>issue_number: context.issue.number,</p> <p>owner: context.repo.owner,</p> <p>repo: context.repo.repo,</p> body: </code>Lighthouse Results:\n- SEO: ${results[0].summary.seo <em> 100}\n- Performance: ${results[0].summary.performance </em> 100}<code> <p>});</p> <p>- name: Notify Slack on failure</p> <p>if: failure()</p> <p>uses: slackapi/slack-github-action@v1</p> <p>with:</p> <p>webhook-url: ${{ secrets.SLACK_WEBHOOK }}</p> <p>payload: |</p> <p>{</p> <p>"text": "Lighthouse audit failed for PR #${{ github.event.pull_request.number }}"</p> <p>}</p> </code>`<code> <strong>Take Action: Give Your AI a Memory</strong> <p>Everything above gets easier when your AI already knows your business. The <a href="/setup.html" class="text-accent hover:text-accent-light">Claude Code + Obsidian setup</a> builds persistent, file-based memory so context compounds instead of evaporating between sessions.</p> <h2 id="key-recap" class="text-2xl sm:text-3xl font-bold mb-6">Key Recap</h2> <ul class="space-y-2 my-6 text-text-muted"><li><strong>Why Automate Lighthouse Audits:</strong> Manual Lighthouse audits face three problems:</li> <li><strong>Setting Up Lighthouse GitHub Actions:</strong> Create a GitHub Actions workflow file at .github/workflows/lighthouse.yml:</li> <li><strong>SEO-Focused Lighthouse Configuration:</strong> Lighthouse's SEO audit checks 16+ factors.</li> <li><strong>Core Web Vitals Tracking:</strong> Lighthouse measures Core Web Vitals (LCP, CLS, FID/TBT).</li> <li><strong>Handling Dynamic and Authenticated Pages:</strong> Lighthouse audits require publicly accessible URLs.</li> <li><strong>Alerting on Regressions:</strong> GitHub Actions can send alerts when audits fail.</li> </ul> <h2 id="faq" class="text-2xl sm:text-3xl font-bold mb-6">FAQ</h2> <strong>Does Lighthouse GitHub Actions work for server-side rendered apps?</strong> <p>Yes, but you must build and serve the app before running Lighthouse. Use </code>npm run build && npm run start<code> to serve a local instance, then audit </code>http://localhost:3000<code>.</p> <strong>Can I audit mobile and desktop separately?</strong> <p>Yes. Configure separate Lighthouse runs with different emulation settings in </code>lighthouserc.json<code>:</p> </code>`<code>json <p>{</p> <p>"ci": {</p> <p>"collect": {</p> <p>"settings": {</p> <p>"emulatedFormFactor": "mobile"</p> <p>}</p> <p>}</p> <p>}</p> <p>}</p> </code>`<code> <p>Run the workflow twice with different configs, or use matrix builds to test both.</p> <strong>How do I prevent flaky Lighthouse scores?</strong> <p>Run Lighthouse multiple times per URL and average scores. Set </code>numberOfRuns: 3<code> in </code>lighthouserc.json<code> to run 3 audits per URL and use the median score.</p> <strong>What if Lighthouse fails due to third-party script issues?</strong> <p>Lighthouse audits the entire page, including third-party scripts. If a third-party script causes failures, either fix the issue with the vendor, remove the script, or adjust assertions to warn instead of error.</p> <strong>Can I run Lighthouse on every commit, not just PRs?</strong> <p>Yes, but this consumes more CI minutes. Use triggers like </code>push: branches: [main]<code> to audit on every commit to main. Most teams audit PRs only to save resources.</p> <strong>How do I benchmark against competitors?</strong> <p>Add competitor URLs to the </code>urls` list. Lighthouse audits any publicly accessible URL. Track your scores vs. competitors to identify gaps.</p> <hr class="my-8 border-border"> <h2 id="when-this-approach-isn-t-right" class="text-2xl sm:text-3xl font-bold mb-6">When This Approach Isn't Right</h2> <p>This guidance may not fit if:</p> <ul class="space-y-2 my-6 text-text-muted"><li><strong>You're brand new to SEO.</strong> Some frameworks here assume working knowledge of crawling, indexing, and ranking fundamentals. Start with the basics first — this article builds on them.</li> <li><strong>Your site has fewer than 50 indexed pages.</strong> Some strategies (like cannibalization audits or hub-and-spoke restructuring) require a minimum content base. Focus on content creation before optimization.</li> <li><strong>You're working on a site with active penalties.</strong> Manual actions require a different playbook. Resolve the penalty first, then apply these optimization frameworks.</li> </ul> <hr class="my-8 border-border"> <h2 id="frequently-asked-questions" class="text-2xl sm:text-3xl font-bold mb-6">Frequently Asked Questions</h2> <h3 id="is-this-relevant-to-my-specific-seo-role" class="text-xl font-semibold mt-8 mb-4">Is this relevant to my specific SEO role?</h3> <p>This article addresses patterns that apply across SEO specializations. Whether you manage technical SEO, content strategy, or client-facing audits, the frameworks here adapt to your workflow. Role-specific implementation details are called out where they diverge.</p> <h3 id="how-do-i-prioritize-these-recommendations" class="text-xl font-semibold mt-8 mb-4">How do I prioritize these recommendations?</h3> <p>Start with the diagnostic framework in the first section to identify which recommendations match your current situation. Not everything applies to every site. Prioritize by expected impact relative to implementation effort — the article flags which tactics are quick wins versus long-term investments.</p> <h3 id="can-i-share-this-with-my-team-or-clients" class="text-xl font-semibold mt-8 mb-4">Can I share this with my team or clients?</h3> <p>Yes. The frameworks are designed to be communicable. The comparison tables and checklists work well in client presentations or team documentation. Adapt the specific numbers to your data when presenting recommendations.</p> </div> <!-- CTA Section --> <section class="my-16 p-8 sm:p-10 bg-gradient-to-br from-accent/20 via-surface to-surface-light rounded-2xl border border-accent/20 text-center"> <h2 class="text-2xl sm:text-3xl font-bold mb-4"> Your AI Has Amnesia. Here's the Fix. </h2> <p class="text-lg text-text-muted mb-6 max-w-xl mx-auto"> $997. 90 minutes. One file that gives Claude permanent memory of your business, your clients, and your preferences. </p> <ul class="text-left max-w-md mx-auto mb-8 space-y-3"> <li class="flex items-start gap-3"> <svg class="w-5 h-5 text-accent mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/> </svg> <span class="text-text-muted">Personal CLAUDE.md file built for your specific context</span> </li> <li class="flex items-start gap-3"> <svg class="w-5 h-5 text-accent mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/> </svg> <span class="text-text-muted">Obsidian vault structure optimized for AI retrieval</span> </li> <li class="flex items-start gap-3"> <svg class="w-5 h-5 text-accent mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/> </svg> <span class="text-text-muted">Claude Code configuration and hook scripts</span> </li> <li class="flex items-start gap-3"> <svg class="w-5 h-5 text-accent mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/> </svg> <span class="text-text-muted">Live 90-minute walkthrough of the entire system</span> </li> </ul> <a href="https://scalewithsearch.com/#offer" class="cta-primary relative inline-flex items-center justify-center px-8 py-4 bg-accent hover:bg-accent-light text-white text-lg font-semibold rounded-xl transition-colors"> Get Your Setup - $997 </a> <p class="text-text-dim text-sm mt-4">Pays for itself in the first week.</p> </section> </article> </div> </main> <!-- Footer --> <footer class="bg-void border-t border-border py-12"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6"> <div> <p class="text-lg font-semibold text-text mb-1">Search your thoughts. Scale your output.</p> <p class="text-sm text-text-dim">© 2026 AI First Search. All rights reserved.</p> </div> <nav class="flex flex-wrap gap-6 text-sm text-text-muted"> <a href="/how-to-make-ai-remember-you" class="hover:text-text transition-colors">The Guide</a> <a href="/about" class="hover:text-text transition-colors">About</a> <a href="https://scalewithsearch.com/#offer" class="hover:text-text transition-colors">Get Setup</a> <a href="/pages/privacy" class="hover:text-text transition-colors">Privacy</a> </nav> </div> </div> </footer> <script> // Reading Progress const progressBar = document.getElementById('reading-progress'); window.addEventListener('scroll', () => { const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const progress = (scrollTop / docHeight) * 100; progressBar.style.width = `${progress}%`; }); // Mobile Menu Toggle const mobileMenuBtn = document.getElementById('mobile-menu-btn'); const mobileMenu = document.getElementById('mobile-menu'); mobileMenuBtn.addEventListener('click', () => { mobileMenu.classList.toggle('hidden'); }); // Copy Code Button function copyCode(button) { const codeBlock = button.closest('.code-block'); const code = codeBlock.querySelector('code').innerText; navigator.clipboard.writeText(code).then(() => { button.textContent = 'Copied!'; button.classList.add('copied'); setTimeout(() => { button.textContent = 'Copy'; button.classList.remove('copied'); }, 2000); }); } </script> </body> </html>