Skip to main content
8 min read

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, no cross-origin block
  • 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

The proxied script stops honouring Do Not Track

Every bundle served through /api/_z/script.js ships with ignoreDNT enabled, so a visitor whose browser sends the legacy Do Not Track signal is still tracked on a proxied site, whether or not you asked for that. Global Privacy Control is never overridden and is still honoured. If Do Not Track matters for your audience, serve the standard script from api.zenovay.com instead. See Privacy-first.


Before You Start

Make sure you have:

  • An Astro project (v2.0+ recommended)
  • Your Zenovay tracking code (found under Domains → your domain → General)
  • 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

The Tracking script card showing the script snippet with HTML, React, Next.js, and First-Party tabs
Find your tracking code in the First-Party tab under Domains → your domain → General.

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

TerminalBash
npm install @astrojs/vercel
astro.config.mjsJavaScript
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
output: 'server', // or 'hybrid' for partial SSR
adapter: vercel(),
});

For Netlify

TerminalBash
npm install @astrojs/netlify
astro.config.mjsJavaScript
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';

export default defineConfig({
output: 'server',
adapter: netlify(),
});

For Cloudflare

TerminalBash
npm install @astrojs/cloudflare
astro.config.mjsJavaScript
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
output: 'server',
adapter: cloudflare(),
});

For Node.js

TerminalBash
npm install @astrojs/node
astro.config.mjsJavaScript
import { 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:

src/pages/api/_z/[...path].tsTypeScript
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:

src/layouts/Layout.astroASTRO
---
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:

TerminalBash
# Build the project
npm run build

# Deploy (depends on your adapter)
# Vercel: vercel deploy
# Netlify: netlify deploy --prod
# Cloudflare: wrangler pages deploy dist

If most of your site is static but you need the proxy, use hybrid mode:

astro.config.mjsJavaScript
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:

src/pages/api/_z/[...path].tsTypeScript
// 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

  1. Visit your deployed site
  2. Open DevTools (F12)
  3. Go to the Network tab
  4. Reload the page
  5. Look for /api/_z/script.js

You should see:

  • Status: 200
  • Domain: Your domain
  • Response: JavaScript code

Test in Firefox

  1. Open Firefox
  2. Settings → Privacy & Security → Enhanced Tracking Protection: Strict
  3. Visit your site
  4. Verify the script loads successfully

Troubleshooting

404 Error on API Route

Cause: SSR not enabled or adapter not configured.

Solution:

  1. Make sure you have output: 'server' or output: 'hybrid' in astro.config.mjs
  2. Verify you have an adapter installed and configured
  3. 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:

  1. Check the server logs for detailed error messages
  2. Make sure TypeScript types are correct
  3. Verify all imports are available

Script Loads But No Data

Cause: Tracking code mismatch.

Solution:

  1. Verify data-tracking-code matches your code in the Zenovay dashboard
  2. Check browser console for JavaScript errors
  3. 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

vercel.jsonJSON
{
"rewrites": [
  { "source": "/api/_z/:path*", "destination": "https://api.zenovay.com/fp/:path*" }
]
}

Netlify

public/_redirectsTEXT
/api/_z/*  https://api.zenovay.com/fp/:splat  200

Cloudflare Pages

Use Cloudflare Pages Functions instead - see the Cloudflare Pages guide.


Complete Example

Project StructureTEXT
my-astro-site/
├── astro.config.mjs
├── src/
│   ├── layouts/
│   │   └── Layout.astro
│   └── pages/
│       ├── index.astro
│       └── api/
│           └── _z/
│               └── [...path].ts
└── package.json
astro.config.mjsJavaScript
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
output: 'hybrid',
adapter: vercel(),
});
src/layouts/Layout.astroASTRO
---
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 = false is set (for hybrid mode)
  • Script tag uses /api/_z/script.js (not the direct Zenovay URL)
  • data-tracking-code attribute 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

Was this page helpful?