Skip to content

Vercel Integration

Vercel’s Edge Middleware runs before your app on Vercel’s CDN, making it the perfect place to intercept bot requests and route them through SeoRend.

Bot → Vercel Edge Middleware → SeoRend API → Rendered HTML
User → Vercel Edge Middleware → Your App (unchanged)

The middleware runs in under 1ms for regular users (just a header check) and only adds latency for bots.

  • Vercel project (any framework: Next.js, SvelteKit, Nuxt, Astro, etc.)
  • SeoRend API key from app.seorend.com
  1. In your Vercel project:

    1. Go to Settings → Environment Variables
    2. Add SEOREND_API_KEY with your API key as the value
    3. Set scope to Production (and Preview if needed)
  2. Create middleware.ts in the root of your project (next to package.json):

    middleware.ts
    import { NextRequest, NextResponse } from 'next/server';
    const BOT_PATTERNS = [
    // Google
    'googlebot', 'googleother', 'storebot-google', 'google-inspectiontool',
    'google-extended', 'google-cloudvertexbot', 'google-favicon',
    'apis-google', 'adsbot-google', 'mediapartners-google', 'feedfetcher-google', 'google page speed',
    // Bing / Microsoft
    'bingbot', 'bingpreview', 'msnbot', 'adidxbot', 'microsoftpreview',
    // Yandex
    'yandexbot', 'yandeximages', 'yandexvideo', 'yandexmobilebot', 'yandexmetrika', 'yandexaccessibilitybot', 'yabrowser',
    // Baidu & other search
    'baiduspider', 'duckduckbot', 'duckassistbot', 'applebot', 'naver',
    'sogouspider', '360spider', 'coccoc', 'seznambot', 'qwantbot',
    'ecosia', 'yahoo', 'exabot', 'petalbot', 'bravebot',
    // AI / LLM crawlers
    'gptbot', 'oai-searchbot', 'chatgpt-user', 'chatgpt',
    'claudebot', 'claude-web', 'claude-user', 'claude-searchbot', 'anthropic-ai',
    'perplexitybot', 'perplexity-user', 'amazonbot', 'ccbot',
    'meta-externalagent', 'meta-externalfetcher',
    'mistralai-user', 'cohere-ai', 'ai2bot',
    'diffbot', 'deepseekbot', 'firecrawlagent', 'kagi-fetcher',
    'bytespider', 'tiktokspider',
    'xai-crawler', 'timpibot', 'anchor browser', 'novellum ai crawl', 'proratainc',
    // Social media / link previews
    'facebookexternalhit', 'facebookcatalog', 'facebookbot', 'facebookplatform', 'instagram',
    'twitterbot', 'linkedinbot', 'pinterestbot', 'pinterest',
    'slackbot', 'slack-imgproxy', 'discordbot', 'telegrambot', 'whatsapp',
    'redditbot', 'snapchat', 'vkshare',
    'tumblr', 'flipboard', 'outbrain', 'embedly', 'bitlybot', 'quora link preview', 'line', 'viber',
    // SEO tools
    'semrushbot', 'splitsignalbot', 'ahrefsbot', 'ahrefssiteaudit',
    'mj12bot', 'rogerbot', 'dotbot', 'chrome-lighthouse', 'screaming frog',
    'oncrawlbot', 'botifybot', 'deepcrawl', 'lumar', 'dataforseobot', 'serpstatbot', 'w3c_validator',
    // Internet Archive
    'ia_archiver', 'archive.org_bot',
    ];
    const STATIC_EXTENSIONS = /\.(js|css|png|jpg|jpeg|gif|ico|svg|webp|avif|woff2?|ttf|eot|pdf|zip|mp4|mp3|wasm|map)$/i;
    function isBot(userAgent: string): boolean {
    const ua = userAgent.toLowerCase();
    return BOT_PATTERNS.some(p => ua.includes(p));
    }
    export async function middleware(request: NextRequest) {
    const ua = request.headers.get('user-agent') || '';
    const { pathname } = request.nextUrl;
    // Skip non-GET, static assets, API routes, and internal Next.js paths
    if (
    request.method !== 'GET' ||
    STATIC_EXTENSIONS.test(pathname) ||
    pathname.startsWith('/api/') ||
    pathname.startsWith('/_next/') ||
    pathname.startsWith('/__')
    ) {
    return NextResponse.next();
    }
    // Loop guard
    if (request.headers.get('x-seorend-processed') === '1') {
    return NextResponse.next();
    }
    if (!isBot(ua)) {
    return NextResponse.next();
    }
    const apiKey = process.env.SEOREND_API_KEY;
    if (!apiKey) {
    console.error('SEOREND_API_KEY not set');
    return NextResponse.next();
    }
    try {
    const renderRes = await fetch('https://render.seorend.com/v1/render', {
    method: 'POST',
    headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
    url: request.url,
    userAgent: ua,
    }),
    });
    if (!renderRes.ok) {
    // Fallback to origin on any error
    return NextResponse.next();
    }
    const data = await renderRes.json();
    return new NextResponse(data.html, {
    status: data.status || 200,
    headers: {
    'Content-Type': 'text/html; charset=utf-8',
    'X-Seorend-Cache': data.cache?.hit ? 'HIT' : 'MISS',
    },
    });
    } catch {
    return NextResponse.next();
    }
    }
    export const config = {
    matcher: ['/((?!_next/static|_next/image|favicon).*)'],
    };
  3. Terminal window
    vercel --prod

    Or push to your Git branch connected to Vercel — it deploys automatically.

Terminal window
# Should return X-Seorend-Cache header
curl -s -I -A "Googlebot/2.1" https://your-app.vercel.app/
# Should NOT have X-Seorend headers
curl -s -I -A "Mozilla/5.0 Chrome/120" https://your-app.vercel.app/

Middleware not running: ensure middleware.ts is in the project root (not inside src/)

Environment variable not found: Edge Middleware does NOT have access to server-side env vars by default — go to Vercel Settings → Environment Variables and confirm SEOREND_API_KEY is set for the correct environment.

413 / body too large: Vercel Edge responses are limited to 4MB. For very large pages this won’t be an issue in practice, but worth knowing.