First-Party Tracking with Astro
Set up a first-party proxy in your Astro project using API routes. Works with both static and SSR modes.
Difficulty: Easy - Add one API route file to your project.
Why This Works (Same-Origin)
This approach uses /api/_z/script.js which is on the same origin as your website. Firefox ETP and other tracking protections only block cross-origin requests.
- Browser sees:
yourdomain.com/api/_z/script.js→ Same origin ✅ - Astro's API route proxies the request to Zenovay server-side (browser never sees this)
- All tracking protection is bypassed because the request is first-party
Before You Start
Make sure you have:
- An Astro project (v2.0+ recommended)
- Your Zenovay tracking code (found in Dashboard → Your Site → Settings)
- SSR adapter installed (for Vercel, Netlify, Cloudflare, Node, etc.)
Your Tracking Code Format
Your tracking code looks like: ZV_XXXXXXXXXX
- Starts with
ZV_ - Followed by 10 characters (letters and numbers)
- CASE-SENSITIVE - copy it exactly
Example: ZV_Q8U0GYD70WR
Static sites: If you're using output: 'static', Astro API routes won't work. Use your hosting platform's proxy instead (Vercel rewrites, Netlify redirects, etc.).
Step 1: Enable SSR (if not already)
First, make sure your Astro project has SSR enabled with an adapter.
For Vercel
npm install @astrojs/vercelimport { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server', // or 'hybrid' for partial SSR
adapter: vercel(),
});For Netlify
npm install @astrojs/netlifyimport { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server',
adapter: netlify(),
});For Cloudflare
npm install @astrojs/cloudflareimport { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare(),
});For Node.js
npm install @astrojs/nodeimport { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone'
}),
});Step 2: Create the Proxy API Route
Create a catch-all API route that proxies requests to Zenovay:
import type { APIRoute } from 'astro'
export const ALL: APIRoute = async ({ params, request }) => {
// Build the path from the catch-all parameter
const path = params.path || ''
const url = new URL(request.url)
const targetUrl = `https://api.zenovay.com/fp/${path}${url.search}`
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
})
}
try {
// Get the real client IP for accurate geolocation
const clientIP = request.headers.get('x-forwarded-for')?.split(',')[0]
|| request.headers.get('cf-connecting-ip')
|| request.headers.get('x-real-ip')
|| ''
// Build proxy headers
const headers = new Headers()
headers.set('Content-Type', request.headers.get('Content-Type') || 'application/json')
headers.set('X-Zenovay-Real-IP', clientIP)
// Forward the request
const response = await fetch(targetUrl, {
method: request.method,
headers: headers,
body: request.method !== 'GET' ? await request.text() : undefined,
})
// Build response with CORS headers
const responseHeaders = new Headers(response.headers)
responseHeaders.set('Access-Control-Allow-Origin', '*')
return new Response(response.body, {
status: response.status,
headers: responseHeaders,
})
} catch (error) {
console.error('Proxy error:', error)
return new Response(JSON.stringify({ error: 'Proxy error' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
})
}
}Why [...path].ts? The spread syntax creates a catch-all route that captures multiple path segments. This means /api/_z/script.js, /api/_z/e/CODE, and /api/_z/settings/CODE all route to this single file.
Step 3: Add the Tracking Script
Add the tracking script to your layout:
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<!-- Zenovay Analytics (First-Party Proxy) -->
<script defer data-tracking-code="YOUR_TRACKING_CODE" src="/api/_z/script.js"></script>
</head>
<body>
<slot />
</body>
</html>Important: Replace YOUR_TRACKING_CODE with your actual tracking code from the Zenovay dashboard.
Step 4: Deploy
Deploy your Astro project to your chosen platform:
# Build the project
npm run build
# Deploy (depends on your adapter)
# Vercel: vercel deploy
# Netlify: netlify deploy --prod
# Cloudflare: wrangler pages deploy distHybrid Mode (Recommended)
If most of your site is static but you need the proxy, use hybrid mode:
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'hybrid', // Static by default, SSR for specific routes
adapter: vercel(),
});Then mark the API route for server rendering:
// Force server rendering for this route
export const prerender = false;
import type { APIRoute } from 'astro'
export const ALL: APIRoute = async ({ params, request }) => {
// ... rest of the proxy code
}Verify It's Working
Check the Network Tab
- Visit your deployed site
- Open DevTools (F12)
- Go to the Network tab
- Reload the page
- Look for
/api/_z/script.js
You should see:
- Status: 200
- Domain: Your domain
- Response: JavaScript code
Test in Firefox
- Open Firefox
- Settings → Privacy & Security → Enhanced Tracking Protection: Strict
- Visit your site
- Verify the script loads successfully
Troubleshooting
404 Error on API Route
Cause: SSR not enabled or adapter not configured.
Solution:
- Make sure you have
output: 'server'oroutput: 'hybrid'inastro.config.mjs - Verify you have an adapter installed and configured
- For hybrid mode, add
export const prerender = false;to the API route
500 Error
Cause: Syntax error or runtime issue in the API route.
Solution:
- Check the server logs for detailed error messages
- Make sure TypeScript types are correct
- Verify all imports are available
Script Loads But No Data
Cause: Tracking code mismatch.
Solution:
- Verify
data-tracking-codematches your code in the Zenovay dashboard - Check browser console for JavaScript errors
- Ensure the domain is registered in Zenovay
Geolocation Wrong
Cause: Client IP not being forwarded correctly.
Solution: The API route tries multiple headers. Make sure your platform forwards one of:
x-forwarded-for(most common)cf-connecting-ip(Cloudflare)x-real-ip(nginx)
Static Site Alternative
If you're using output: 'static', you can't use API routes. Instead, use your hosting platform's proxy:
Vercel
{
"rewrites": [
{ "source": "/api/_z/:path*", "destination": "https://api.zenovay.com/fp/:path*" }
]
}Netlify
/api/_z/* https://api.zenovay.com/fp/:splat 200Cloudflare Pages
Use Cloudflare Pages Functions instead - see the Cloudflare Pages guide.
Complete Example
my-astro-site/
├── astro.config.mjs
├── src/
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ ├── index.astro
│ └── api/
│ └── _z/
│ └── [...path].ts
└── package.jsonimport { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'hybrid',
adapter: vercel(),
});---
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{title}</title>
<script defer data-tracking-code="YOUR_TRACKING_CODE" src="/api/_z/script.js"></script>
</head>
<body>
<slot />
</body>
</html>Final Checklist
Before you're done, verify ALL of these:
- Astro SSR is enabled (or using static site platform rewrites)
- API route file exists at
src/pages/api/_z/[...path].ts -
export const prerender = falseis set (for hybrid mode) - Script tag uses
/api/_z/script.js(not the direct Zenovay URL) -
data-tracking-codeattribute contains your correct tracking code - Project is deployed (not just running locally)
- Tested in Firefox with Enhanced Tracking Protection set to Strict
- Visits appearing in Zenovay dashboard
Next Steps
- Custom Events - Track user interactions
- Visitor Identification - Link analytics to users
- Troubleshooting - More help with issues