Skip to content

SvelteKit Integration

SvelteKit’s server hooks (hooks.server.ts) are the ideal integration point for SeoRend. The handle hook intercepts every request before it reaches your routes — perfect for bot detection and prerendering.

Bot → SvelteKit handle hook → SeoRend API → Rendered HTML
User → SvelteKit handle hook → Your SvelteKit App
  • SvelteKit project with @sveltejs/adapter-node or any SSR adapter
  • SeoRend API key from app.seorend.com
  1. .env
    SEOREND_API_KEY=sk_live_your_key_here

    For production, set it in your hosting environment variables.

  2. Create src/hooks.server.ts (or add to your existing file):

    src/hooks.server.ts
    import type { Handle } from '@sveltejs/kit';
    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_EXT = /\.(js|css|png|jpg|jpeg|gif|ico|svg|webp|avif|woff2?|ttf|eot|pdf|zip|mp4|wasm|map)$/i;
    function isBot(userAgent: string): boolean {
    const ua = userAgent.toLowerCase();
    return BOT_PATTERNS.some(p => ua.includes(p));
    }
    const API_KEY = process.env.SEOREND_API_KEY;
    export const handle: Handle = async ({ event, resolve }) => {
    const { request } = event;
    const ua = request.headers.get('user-agent') ?? '';
    const url = new URL(request.url);
    // Only intercept GET requests for non-static paths
    if (
    request.method !== 'GET' ||
    STATIC_EXT.test(url.pathname) ||
    url.pathname.startsWith('/_app/') ||
    url.pathname.startsWith('/api/') ||
    request.headers.get('x-seorend-processed') === '1'
    ) {
    return resolve(event);
    }
    if (!isBot(ua) || !API_KEY) {
    return resolve(event);
    }
    try {
    const renderResponse = await fetch('https://render.seorend.com/v1/render', {
    method: 'POST',
    headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({
    url: request.url,
    userAgent: ua,
    }),
    });
    if (!renderResponse.ok) {
    return resolve(event);
    }
    const data = await renderResponse.json();
    return new Response(data.html, {
    status: data.status ?? 200,
    headers: {
    'Content-Type': 'text/html; charset=utf-8',
    'X-Seorend-Cache': data.cache?.hit ? 'HIT' : 'MISS',
    },
    });
    } catch {
    // Network error → serve normally
    return resolve(event);
    }
    };
  3. If you already have a handle hook, use SvelteKit’s sequence helper:

    src/hooks.server.ts
    import { sequence } from '@sveltejs/kit/hooks';
    import type { Handle } from '@sveltejs/kit';
    const seorend: Handle = async ({ event, resolve }) => {
    // ... (seorend hook code from step 2)
    };
    const auth: Handle = async ({ event, resolve }) => {
    // ... your existing auth hook
    return resolve(event);
    };
    // SeoRend MUST come first — before auth, i18n, etc.
    export const handle = sequence(seorend, auth);
  4. Build and start your app:

    Terminal window
    npm run build
    SEOREND_API_KEY=sk_live_... node build

    Or use your preferred adapter (node, vercel, netlify, cloudflare).

Terminal window
npm run dev
Terminal window
# Should return X-Seorend-Cache
curl -s -I -A "Googlebot/2.1" http://localhost:5173/
# Should NOT have X-Seorend-* headers
curl -s -I -A "Mozilla/5.0 Chrome/120" http://localhost:5173/

Hook not running: ensure the file is at src/hooks.server.ts (not hooks.ts or hooks.client.ts).

process.env.SEOREND_API_KEY is undefined: the hooks.server.ts file runs on the server, so process.env works. But with Vite, you need to expose it — either use $env/static/private or ensure the variable is set in your server environment.

Using SvelteKit’s env system:

import { SEOREND_API_KEY } from '$env/static/private';