Monitoring

Analytics Setup

# Analytics & Monitoring Setup Playbook

Production-tested patterns for GA4, Google Search Console, IndexNow, and optional privacy-first analytics. Based on the ActiveWizards Astro site with 147 pages generating organic traffic.


Table of Contents

  • GA4 Setup
  • GA4 Custom Events
  • Google Search Console
  • Sitemap Configuration
  • IndexNow
  • robots.txt
  • OG Tags and Twitter Cards
  • Core Web Vitals
  • Cloudflare Web Analytics
  • Umami Self-Hosted

  • 1. GA4 Setup

    Create GA4 property

  • Go to analytics.google.com
  • Admin > Create Property
  • Property name: "My Site"
  • Time zone and currency: match your business
  • Create a Web data stream: enter your domain
  • Copy the Measurement ID: G-XXXXXXXXXX
  • Install in BaseLayout

    Add the GA4 snippet to your in BaseLayout.astro:

    ``astro

    `

    Production tip: Conditional loading

    To avoid tracking development traffic:

    `astro

    {import.meta.env.PROD && (

    <>

    )}

    `

    Or better: always load it but filter by hostname in GA4's data stream settings.


    2. GA4 Custom Events

    Contact form generate_lead event

    Fire after successful form submission:

    `js

    // In your form's success handler

    if (typeof gtag === 'function') {

    gtag('event', 'generate_lead', {

    event_category: 'contact',

    event_label: 'contact_form',

    });

    }

    `

    CTA click tracking

    `js

    // Track CTA button clicks

    document.querySelectorAll('[data-track-cta]').forEach((btn) => {

    btn.addEventListener('click', () => {

    gtag('event', 'cta_click', {

    event_category: 'engagement',

    event_label: btn.getAttribute('data-track-cta'),

    });

    });

    });

    `

    Newsletter signup

    `js

    gtag('event', 'sign_up', {

    method: 'newsletter',

    });

    `

    Common event patterns

    Event NameCategoryWhen
    generate_leadcontactForm submitted successfully
    cta_clickengagementAny CTA button clicked
    sign_upconversionNewsletter signup
    file_downloadengagementPDF/resource downloaded
    outbound_clickengagementExternal link clicked

    GA4 Conversions

    Mark events as conversions in GA4:

  • Admin > Events
  • Find your event (e.g., generate_lead)
  • Toggle "Mark as conversion"

  • 3. Google Search Console

    Verify property

    Method 1: DNS TXT record (recommended for Cloudflare)
  • Go to search.google.com/search-console
  • Add Property > Domain property: mysite.com
  • Copy the TXT record value
  • In Cloudflare DNS: Add TXT record
  • - Type: TXT

    - Name: @

    - Content: google-site-verification=XXXXXXXXXXXXXXXX

    - TTL: Auto

  • Click Verify in GSC (may take a few minutes)
  • Method 2: HTML meta tag (simpler but URL-prefix only)

    `html

    `

    Submit sitemap

  • GSC > Sitemaps
  • Enter: https://mysite.com/sitemap-index.xml
  • Submit
  • The @astrojs/sitemap integration generates the sitemap automatically at build time.

    URL inspection

    After deploying new content:

  • GSC > URL Inspection
  • Paste the new URL
  • Click "Request Indexing"
  • Monitor performance

    Key GSC reports:

    • Performance: Clicks, impressions, CTR, average position
    • Coverage: Indexed pages, errors, warnings
    • Core Web Vitals: LCP, FID/INP, CLS
    • Links: Internal and external link counts

    Pull GSC data programmatically

    `python

    # Example: Pull GSC data via API (requires OAuth2 setup)

    from googleapiclient.discovery import build

    from google.oauth2.credentials import Credentials

    creds = Credentials(token='...', refresh_token='...', ...)

    service = build('searchconsole', 'v1', credentials=creds)

    response = service.searchanalytics().query(

    siteUrl='sc-domain:mysite.com',

    body={

    'startDate': '2025-01-01',

    'endDate': '2025-03-01',

    'dimensions': ['query', 'page'],

    'rowLimit': 1000,

    }

    ).execute()

    for row in response.get('rows', []):

    print(row['keys'][0], row['clicks'], row['impressions'], row['position'])

    `


    4. Sitemap Configuration

    Install and configure

    `bash

    npm install @astrojs/sitemap

    `

    In astro.config.mjs:

    `js

    import sitemap from '@astrojs/sitemap';

    export default defineConfig({

    site: 'https://mysite.com', // REQUIRED for sitemap

    integrations: [

    sitemap(),

    ],

    });

    `

    Output

    The integration generates at build time:

    • dist/sitemap-index.xml — index pointing to individual sitemaps
    • dist/sitemap-0.xml — actual URLs

    Exclude pages

    `js

    sitemap({

    filter: (page) =>

    !page.includes('/admin/') &&

    !page.includes('/draft/'),

    }),

    `

    Custom change frequency and priority

    `js

    sitemap({

    serialize(item) {

    if (item.url.includes('/blog/')) {

    item.changefreq = 'weekly';

    item.priority = 0.7;

    }

    if (item.url === 'https://mysite.com/') {

    item.changefreq = 'daily';

    item.priority = 1.0;

    }

    return item;

    },

    }),

    `


    5. IndexNow

    IndexNow notifies search engines (Bing, Yandex, Naver) about new or updated pages for faster indexing.

    Setup

  • Generate a key (any UUID or random string):
  • `bash

    python -c "import uuid; print(uuid.uuid4().hex)"

    # e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

    `

  • Create key file at public/{key}.txt with the key as its content:
  • `

    a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

    `

  • Submit URLs via API:
  • `python

    import httpx

    INDEXNOW_KEY = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

    SITE_URL = "https://mysite.com"

    def submit_urls(urls: list[str]):

    """Submit URLs to IndexNow for immediate indexing."""

    payload = {

    "host": "mysite.com",

    "key": INDEXNOW_KEY,

    "keyLocation": f"{SITE_URL}/{INDEXNOW_KEY}.txt",

    "urlList": urls,

    }

    resp = httpx.post(

    "https://api.indexnow.org/indexnow",

    json=payload,

    headers={"Content-Type": "application/json"},

    )

    print(f"IndexNow response: {resp.status_code}")

    return resp.status_code

    # Submit after publishing new content

    submit_urls([

    "https://mysite.com/blog/new-article/",

    "https://mysite.com/blog/updated-article/",

    ])

    `

    Batch submission after deployment

    `bash

    # Generate URL list from sitemap and submit

    python -c "

    import xml.etree.ElementTree as ET

    import httpx

    tree = ET.parse('dist/sitemap-0.xml')

    urls = [loc.text for loc in tree.iter('{http://www.sitemaps.org/schemas/sitemap/0.9}loc')]

    print(f'Found {len(urls)} URLs')

    resp = httpx.post('https://api.indexnow.org/indexnow', json={

    'host': 'mysite.com',

    'key': 'YOUR_KEY',

    'keyLocation': 'https://mysite.com/YOUR_KEY.txt',

    'urlList': urls[:10000],

    })

    print(f'Submitted: {resp.status_code}')

    "

    `


    6. robots.txt

    Automatic with astro-robots-txt

    `bash

    npm install astro-robots-txt

    `

    `js

    // astro.config.mjs

    import robotsTxt from 'astro-robots-txt';

    export default defineConfig({

    site: 'https://mysite.com',

    integrations: [

    robotsTxt(),

    ],

    });

    `

    This generates:

    `

    User-agent: *

    Allow: /

    Sitemap: https://mysite.com/sitemap-index.xml

    `

    Manual alternative

    Create public/robots.txt:

    `

    User-agent: *

    Allow: /

    # Block admin/draft pages

    Disallow: /admin/

    Disallow: /draft/

    # Block search results pages

    Disallow: /search?

    Sitemap: https://mysite.com/sitemap-index.xml

    `

    Block AI crawlers (optional)

    `

    # Block AI training crawlers

    User-agent: GPTBot

    Disallow: /

    User-agent: ChatGPT-User

    Disallow: /

    User-agent: CCBot

    Disallow: /

    User-agent: anthropic-ai

    Disallow: /

    User-agent: Google-Extended

    Disallow: /

    `


    7. OG Tags and Twitter Cards

    SEO component implementation

    The SEO component (see ASTRO_SETUP.md) handles all meta tags:

    `astro

    {publishedAt && }

    {updatedAt && }

    `

    OG image requirements

    • Size: 1200 x 630 pixels
    • Format: PNG or JPG (PNG preferred for text-heavy images)
    • File size: Under 1MB
    • Location: public/images/brand/og-default.png for site-wide default
    • Per-page OG images: set ogImage in frontmatter

    Validation tools


    8. Core Web Vitals

    Targets

    MetricGoodNeeds ImprovementPoor
    LCP (Largest Contentful Paint)< 2.5s2.5s - 4.0s> 4.0s
    INP (Interaction to Next Paint)< 200ms200ms - 500ms> 500ms
    CLS (Cumulative Layout Shift)< 0.10.1 - 0.25> 0.25

    How to achieve good scores with Astro

    LCP optimization:
    • Preload hero fonts with
    • Use loading="eager" on above-the-fold images
    • Use loading="lazy" on below-the-fold images
    • Avoid render-blocking CSS imports (use for fonts)
    • astro-compress reduces HTML/CSS size
    INP optimization:
    • Minimal client-side JavaScript (Astro ships zero JS by default)
    • Defer non-critical scripts
    • Use IntersectionObserver for scroll animations (not scroll events)
    CLS optimization:
    • Set explicit width and height on all tags
    • Reserve space for dynamic content (e.g., newsletter form)
    • Avoid inserting content above existing content after page load
    • Use font-display: swap for web fonts

    Monitoring tools

  • PageSpeed Insights: pagespeed.web.dev
  • GSC Core Web Vitals report: Shows real-user data
  • Chrome DevTools > Lighthouse: Local testing
  • web-vitals library: For custom RUM (Real User Monitoring)
  • `html

    `


    9. Cloudflare Web Analytics

    Free, privacy-first analytics built into Cloudflare. No JS snippet needed if the site is proxied through CF.

    Enable

  • CF Dashboard > Your Site > Analytics & Logs > Web Analytics
  • Toggle "Enable Web Analytics"
  • Done — it works automatically via CF proxy, no code changes needed
  • Or use the JS beacon (for non-proxied sites)

    `html

    `

    What it provides

    • Page views, unique visitors, top pages
    • Country, device type, browser breakdown
    • Core Web Vitals (real user data)
    • No cookies, no personal data collection
    • GDPR-compliant without consent banners

    Limitations

    • No custom events
    • No conversion tracking
    • No funnel analysis
    • Data retained for ~6 months
    Recommendation: Use CF Web Analytics alongside GA4. CF for privacy-friendly overview, GA4 for conversion tracking and custom events.

    10. Umami Self-Hosted

    For privacy-first analytics with more features than CF Web Analytics, self-host Umami.

    When to adopt

    • When traffic exceeds 1000 sessions/month (enough data for meaningful analysis)
    • When you need custom events without GA4's privacy concerns
    • When GDPR compliance without consent banners is required

    Docker deployment

    `yaml

    # docker-compose.yml

    version: '3'

    services:

    umami:

    image: ghcr.io/umami-software/umami:postgresql-latest

    ports:

    - "3000:3000"

    environment:

    DATABASE_URL: postgresql://umami:password@db:5432/umami

    DATABASE_TYPE: postgresql

    APP_SECRET: your-random-secret-here

    depends_on:

    - db

    db:

    image: postgres:15

    environment:

    POSTGRES_DB: umami

    POSTGRES_USER: umami

    POSTGRES_PASSWORD: password

    volumes:

    - umami-db-data:/var/lib/postgresql/data

    volumes:

    umami-db-data:

    `

    Install tracking script

    `html

    `

    Custom events

    `js

    // Track events

    umami.track('contact_form_submit', { company: 'Acme Corp' });

    umami.track('cta_click', { location: 'hero' });

    `


    Analytics Setup Checklist

    GA4

    • [ ] GA4 property created
    • [ ] Measurement ID obtained (G-XXXXXXXXXX)
    • [ ] Snippet installed in BaseLayout
    • [ ] generate_lead event fires on contact form success
    • [ ] Event marked as conversion in GA4 admin
    • [ ] Internal traffic filtered (by IP or hostname)

    Google Search Console

    • [ ] Property verified (DNS TXT record)
    • [ ] Sitemap submitted
    • [ ] Initial URL inspection for key pages

    Sitemap

    • [ ] @astrojs/sitemap installed and configured
    • [ ] site set in astro.config.mjs
    • [ ] Sitemap accessible at /sitemap-index.xml

    IndexNow

    • [ ] Key generated
    • [ ] Key file deployed to public/
    • [ ] Submission script tested

    robots.txt

    • [ ] Generated or manually created
    • [ ] Sitemap URL referenced
    • [ ] Admin/draft pages blocked

    OG/Twitter

    • [ ] Default OG image created (1200x630)
    • [ ] SEO component renders all meta tags
    • [ ] Validated with Facebook/Twitter/LinkedIn debuggers

    Core Web Vitals

    • [ ] LCP < 2.5s (check with PageSpeed Insights)
    • [ ] INP < 200ms
    • [ ] CLS < 0.1
    • [ ] Fonts preloaded with font-display: swap`
    • [ ] Images have explicit width/height