# 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
1. GA4 Setup
Create GA4 property
G-XXXXXXXXXXInstall 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 && (
<>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
>
)}
`
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 Name | Category | When |
generate_lead | contact | Form submitted successfully |
cta_click | engagement | Any CTA button clicked |
sign_up | conversion | Newsletter signup |
file_download | engagement | PDF/resource downloaded |
outbound_click | engagement | External link clicked |
GA4 Conversions
Mark events as conversions in GA4:
)3. Google Search Console
Verify property
Method 1: DNS TXT record (recommended for Cloudflare) - Type: TXT
- Name: @
- Content: google-site-verification=XXXXXXXXXXXXXXXX
- TTL: Auto
`html
`
Submit sitemap
The @astrojs/sitemap integration generates the sitemap automatically at build time.
URL inspection
After deploying new content:
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
`bash
python -c "import uuid; print(uuid.uuid4().hex)"
# e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
`
with the key as its content:`
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
`
`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
- Facebook: developers.facebook.com/tools/debug
- Twitter: cards-dev.twitter.com/validator
- LinkedIn: linkedin.com/post-inspector
- General: opengraph.xyz
8. Core Web Vitals
Targets
| Metric | Good | Needs Improvement | Poor |
| LCP (Largest Contentful Paint) | < 2.5s | 2.5s - 4.0s | > 4.0s |
| INP (Interaction to Next Paint) | < 200ms | 200ms - 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | < 0.1 | 0.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
- Minimal client-side JavaScript (Astro ships zero JS by default)
- Defer non-critical scripts
- Use IntersectionObserver for scroll animations (not scroll events)
- Set explicit width
andheighton alltags
- 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
`html
`
9. Cloudflare Web Analytics
Free, privacy-first analytics built into Cloudflare. No JS snippet needed if the site is proxied through CF.
Enable
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
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 inastro.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