Astro Proxy Setup
Configure Astro middleware to proxy Rybbit tracking requests
Astro's middleware feature makes it simple to proxy Rybbit tracking. This guide shows how to set up server-side request proxying in your Astro application.
Overview
Astro middleware allows you to intercept and modify requests before they're handled, making it perfect for creating a transparent analytics proxy.
What you'll achieve:
- Proxy Rybbit endpoints through your Astro site
- Forward necessary headers for accurate tracking
- Simple configuration in
astro.config.mjs - Support all Rybbit features
Prerequisites
- Astro 2.0 or later (with SSR or hybrid rendering enabled)
- Your Rybbit instance URL:
- Cloud hosted:
https://app.rybbit.io - Self-hosted: Your instance URL
- Cloud hosted:
- Your Rybbit site ID
This guide requires SSR (Server-Side Rendering) or hybrid mode. Static sites cannot proxy requests server-side. For static sites on platforms like Vercel or Netlify, use their platform-specific proxy configurations.
Implementation
Enable SSR in Astro
Update astro.config.mjs to enable SSR:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: node({
mode: 'standalone',
}),
});Install the Node adapter if not already installed:
npm install @astrojs/nodeConfigure Environment Variables
Create or update .env:
# .env
RYBBIT_HOST=https://app.rybbit.io
# For self-hosted: RYBBIT_HOST=https://analytics.yourcompany.comCreate Middleware
Create src/middleware.ts (or .js for JavaScript):
// src/middleware.ts
import type { MiddlewareHandler } from 'astro';
const RYBBIT_HOST = import.meta.env.RYBBIT_HOST || 'https://app.rybbit.io';
export const onRequest: MiddlewareHandler = async (context, next) => {
const { request } = context;
const url = new URL(request.url);
// Check if this is an analytics request
if (url.pathname.startsWith('/analytics/')) {
return proxyToRybbit(request, url.pathname);
}
// Continue with normal request handling
return next();
};
async function proxyToRybbit(request: Request, pathname: string): Promise<Response> {
try {
// Map paths
let rybbitPath = pathname.replace('/analytics/', '/api/');
// Special case for site config
if (pathname.startsWith('/analytics/site/')) {
rybbitPath = pathname.replace('/analytics/', '/');
}
const rybbitUrl = `${RYBBIT_HOST}${rybbitPath}`;
// Get client IP from headers
const forwardedFor = request.headers.get('x-forwarded-for');
const clientIp = forwardedFor ? forwardedFor.split(',')[0].trim() : '127.0.0.1';
// Forward request
const proxyHeaders = new Headers(request.headers);
proxyHeaders.set('X-Real-IP', clientIp);
proxyHeaders.set('X-Forwarded-For', clientIp);
const response = await fetch(rybbitUrl, {
method: request.method,
headers: proxyHeaders,
body: request.method !== 'GET' ? await request.text() : undefined,
});
// Return proxied response
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} catch (error) {
console.error('Rybbit proxy error:', error);
return new Response('Analytics proxy error', { status: 500 });
}
}For JavaScript projects:
// src/middleware.js
const RYBBIT_HOST = import.meta.env.RYBBIT_HOST || 'https://app.rybbit.io';
export const onRequest = async (context, next) => {
const { request } = context;
const url = new URL(request.url);
if (url.pathname.startsWith('/analytics/')) {
return proxyToRybbit(request, url.pathname);
}
return next();
};
async function proxyToRybbit(request, pathname) {
try {
let rybbitPath = pathname.replace('/analytics/', '/api/');
if (pathname.startsWith('/analytics/site/')) {
rybbitPath = pathname.replace('/analytics/', '/');
}
const rybbitUrl = `${RYBBIT_HOST}${rybbitPath}`;
const forwardedFor = request.headers.get('x-forwarded-for');
const clientIp = forwardedFor ? forwardedFor.split(',')[0].trim() : '127.0.0.1';
const proxyHeaders = new Headers(request.headers);
proxyHeaders.set('X-Real-IP', clientIp);
proxyHeaders.set('X-Forwarded-For', clientIp);
const response = await fetch(rybbitUrl, {
method: request.method,
headers: proxyHeaders,
body: request.method !== 'GET' ? await request.text() : undefined,
});
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} catch (error) {
console.error('Rybbit proxy error:', error);
return new Response('Analytics proxy error', { status: 500 });
}
}Update Your Layout
Add the tracking script to your layout:
---
// src/layouts/Layout.astro
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<!-- Rybbit Analytics -->
<script src="/analytics/script.js" async data-site-id="YOUR_SITE_ID"></script>
</head>
<body>
<slot />
</body>
</html>Start Development Server
npm run devVerify the Setup
- Open your site in a browser with Developer Tools
- Check Network tab: Requests should go to
/analytics/* - Verify in Rybbit dashboard: Data should appear
How It Works
Astro middleware intercepts requests before they're processed:
- Request to
/analytics/script.jsenters middleware - Middleware proxies to
https://app.rybbit.io/api/script.js - Client IP headers are preserved
- Response is returned to browser
Advanced Configuration
Caching with Astro
Add caching for scripts:
// src/middleware.ts
const cache = new Map<string, { response: Response; timestamp: number }>();
const CACHE_TTL = 3600000; // 1 hour in milliseconds
async function proxyToRybbit(request: Request, pathname: string): Promise<Response> {
// Cache GET requests for scripts
if (request.method === 'GET' && pathname.endsWith('.js')) {
const cached = cache.get(pathname);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.response.clone();
}
}
// ... proxy logic
// Cache the response
if (request.method === 'GET' && pathname.endsWith('.js')) {
cache.set(pathname, {
response: response.clone(),
timestamp: Date.now(),
});
}
return response;
}Conditional Proxying
Only proxy in production:
export const onRequest: MiddlewareHandler = async (context, next) => {
const { request } = context;
const url = new URL(request.url);
// Only proxy in production
if (import.meta.env.PROD && url.pathname.startsWith('/analytics/')) {
return proxyToRybbit(request, url.pathname);
}
return next();
};Error Logging
Add detailed error logging:
async function proxyToRybbit(request: Request, pathname: string): Promise<Response> {
try {
// ... proxy logic
} catch (error) {
const errorDetails = {
message: error instanceof Error ? error.message : 'Unknown error',
pathname,
method: request.method,
timestamp: new Date().toISOString(),
};
console.error('Rybbit proxy error:', errorDetails);
// Return error response
return new Response(JSON.stringify({ error: 'Analytics proxy error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}Troubleshooting
Middleware not running
Problem: Requests bypass middleware.
Solution:
- Ensure SSR or hybrid mode is enabled in
astro.config.mjs - Verify middleware file is in
src/middleware.ts(not in subdirectory) - Check adapter is installed:
npm install @astrojs/node
Static build issues
Problem: Build fails or proxy doesn't work in production.
Solution: Middleware requires server-side rendering. Ensure:
// astro.config.mjs
export default defineConfig({
output: 'server', // Required for middleware
adapter: node({
mode: 'standalone',
}),
});Environment variables not loading
Problem: RYBBIT_HOST is undefined.
Solution:
- Check
.envfile is in project root - Use
import.meta.env.RYBBIT_HOST(notprocess.env) - For production, set environment variables in your hosting platform
Deployment
Vercel
Deploy with Vercel adapter:
npm install @astrojs/vercel// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel(),
});Netlify
Deploy with Netlify adapter:
npm install @astrojs/netlify// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify(),
});Node.js Server
For self-hosting:
npm run build
node ./dist/server/entry.mjsRelated Resources
- Astro Integration Guide - General Astro integration
- Tracking Script Documentation - Script configuration
- Astro Middleware - Official Astro docs