Pular para o conteúdo principal
7 min de leitura

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

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

Esta página foi útil?