Integrations/Lovable
Back to integrations
Lovable
LovableLive
lovable.dev · ~5 minutes

Lovable + xSeek: A blog that AI agents discover, read, and cite

Ship a Lovable site that pulls SEO/GEO articles from xSeek and tracks AI bots server-side. Drop in a /blogs SSR section, a sitemap.xml, and detection inside server.ts — no manual content sync.

AI site builderSSR blogBot tracking

Overview

Lovable is an AI website builder that scaffolds production-grade React apps (TanStack Start on Cloudflare Workers by default). The xSeek integration covers both directions: outbound, your Lovable site server-fetches published articles from xSeek's Content Studio API and renders them under /blogs and /blogs/{slug} with SSR + a sitemap.xml. Inbound, the same server.ts handler detects AI bot User-Agents on every request and posts visits to xSeek's ai_visits endpoint — same pattern as the Next.js middleware integration, adapted to TanStack Start's fetch entry.

×Client-side JS

What you'd miss

Google Analytics, Plausible, Fathom — none of them fire for GPTBot, ClaudeBot, or Perplexity. Client scripts never run for crawlers that don't execute JS.

xSeek · Server entry + SSR blog

What you capture

Every bot hit, server-side, with the exact user-agent, URL, and timestamp — before the page even renders. No sampling, no blind spots.

How it works

3 steps
01

Paste the xSeek prompt into Lovable to scaffold /blogs, /blogs/{slug}, and /sitemap.xml — all SSR.

02

Add XSEEK_API_KEY (articles:read + ai_visits:push) and XSEEK_WEBSITE_ID as server environment variables.

03

Wrap the fetch handler in server.ts to detect AI bots and post visits to xSeek before serving the response.

Key benefits

Articles flow from xSeek Content Studio to your Lovable site automatically

Server-rendered HTML — AI agents see the full article on first request, not after JS hydration

AI bot visits tracked from the same server.ts entry that serves the site

Next step

Start tracking AI bots in under five minutes.

Pick your stack, follow the guide, watch the data land. No credit card, no call required to start.

Copy & paste

Two snippets to ship.

The first adds a blog that pulls articles from xSeek. The second tracks AI agents visiting your site. Paste, set three keys, deploy.

01

Add a blog connected to xSeek

Paste into Lovable chat

Your articles published in xSeek appear automatically on your site. No manual sync.

Build a server-rendered blog section for my site with three routes that pull articles from the xSeek API.

## CRITICAL — non-negotiable

- ALL routes (including /blogs, /blogs/:slug, /sitemap.xml) MUST be server-side rendered. The HTML returned to the browser MUST already contain the article content — never load it after JS hydration.
- The xSeek API key MUST stay on the server. Never expose it to the browser.
- The xSeek API has NO CORS — fetch must happen server-side regardless.

## Routes

### /blogs — list of articles (SSR)
- Server-fetches all published articles from the xSeek API
- Each card shows: title (h2), meta description (truncated 2 lines), published date ("May 15, 2026"), "Read article →" link to /blogs/{slug}
- Sort by publishedAt descending (newest first)
- Layout: single column on mobile, 2-column grid above md
- Empty state: "No articles published yet."
- <title>: "Blog"
- <meta name="description">: "Latest articles."
- Cache the response for 5 minutes

### /blogs/:slug — single article (SSR)
- Server-fetches one article by its slug
- Layout: max-width 720px, centered, line-height 1.7
- Top: small "← Back to blog" link
- Header: title (h1), meta description (muted lead paragraph), published date (mono font)
- Body: render contentMarkdown using react-markdown + remark-gfm
- Style markdown: h2/h3 with margin-top, paragraphs, ul/ol, blockquote (border-l + italic), inline code (bg-gray-100 mono), code blocks (bg-gray-900 light), links (underlined + accent)
- <title>: article title
- <meta name="description">: article metaDescription
- Inject the article's schemaMarkup JSON-LD into <head> — it's a pre-serialized JSON string, wrap it in <script type="application/ld+json">{schemaMarkup}</script> WITHOUT double-encoding
- 404 if slug not found
- Cache the response for 5 minutes

### /sitemap.xml — XML sitemap (SSR)
- Server-fetches the full list of published articles
- Returns Content-Type: application/xml
- One <url> entry per published article: <loc>{SITE_ORIGIN}/blogs/{slug}</loc>, <lastmod>{lastmod}</lastmod>, <changefreq>weekly</changefreq>
- IMPORTANT: publishedAt can be null on freshly-published articles whose snapshot row didn't write. Use a fallback chain so the sitemap never crashes: const lastmod = (article.publishedAt ?? article.createdAt ?? new Date().toISOString()).split('T')[0]
- Also include an entry for /blogs (the index)
- Cache for 1 hour

## API integration

### Environment variables (server-only)

- XSEEK_API_KEY — Bearer token with articles:read privilege
- XSEEK_WEBSITE_ID — the website UUID
- SITE_ORIGIN — your site's canonical origin (e.g. https://yoursite.com)

### Endpoint 1 — list published articles (used by /blogs AND /sitemap.xml)

GET https://www.xseek.io/api/v1/websites/{XSEEK_WEBSITE_ID}/articles?view=public&status=published&pageSize=100
Authorization: Bearer {XSEEK_API_KEY}

Response shape:
{
  "success": true,
  "data": {
    "articles": [
      {
        "id": "uuid",
        "title": "Best AI visibility tools 2026",
        "slug": "best-ai-visibility-tools-2026",
        "metaDescription": "Side-by-side comparison.",
        "status": "published",
        "publishedAt": "2026-05-15T10:00:00Z"
      }
    ],
    "pagination": { "page": 1, "pageSize": 100, "total": 47, "totalPages": 1 }
  }
}

### Endpoint 2 — get article by slug

GET https://www.xseek.io/api/v1/websites/{XSEEK_WEBSITE_ID}/articles/by-slug/{slug}?view=public
Authorization: Bearer {XSEEK_API_KEY}

Response shape:
{
  "success": true,
  "data": {
    "id": "uuid",
    "title": "Best AI visibility tools 2026",
    "slug": "best-ai-visibility-tools-2026",
    "metaDescription": "Side-by-side comparison.",
    "contentMarkdown": "# Heading\n\nMarkdown body...",
    "schemaMarkup": "{\"@context\":\"https://schema.org\",\"@type\":\"Article\",...}",
    "status": "published",
    "publishedAt": "2026-05-15T10:00:00Z"
  }
}

If the slug returns 404, render the 404 page.

## Implementation notes

- Install: react-markdown, remark-gfm
- Date formatter: new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(publishedAt)) → "May 15, 2026"
- Use semantic HTML: <article>, <header>, <time dateTime={publishedAt}>
- The schemaMarkup is already a JSON string — do NOT JSON.stringify it again
- For the sitemap response: set Content-Type: application/xml explicitly
- Also register /sitemap.xml in robots.txt: add a line "Sitemap: https://yoursite.com/sitemap.xml"

## Verification

After build:
- View page source on /blogs — article titles must be in raw HTML, not loaded by JS
- View page source on /blogs/{any-slug} — article content must be in raw HTML
- curl /sitemap.xml should return XML
- XSEEK_API_KEY must NOT appear in browser DevTools Network or source
02

Track AI agents visiting your site

Paste into Lovable chat

See which AI assistants read your pages, and which ones recommend you. One paste, no extra latency.

Add server-side AI bot tracking to my Lovable site so I can see which AI assistants (ChatGPT, Claude, Perplexity, Gemini, Copilot, etc.) read my pages and which ones recommend me.

Replace the contents of src/server.ts with the code below. The tracking runs inside the TanStack Start fetch handler — every incoming request is checked against a list of known AI bot User-Agents, and matching visits are POSTed to xSeek before the request is handed to the existing server handler. The POST is fire-and-forget via ctx.waitUntil() so it adds no latency to the response.

If src/server.ts already has custom logic beyond the default TanStack Start entry, preserve it. Just ensure the xSeek bot-tracking call runs FIRST inside the fetch handler, before any other request processing.

```ts
import "./lib/error-capture";

import { consumeLastCapturedError } from "./lib/error-capture";
import { renderErrorPage } from "./lib/error-page";

type ServerEntry = {
  fetch: (request: Request, env: unknown, ctx: unknown) => Promise<Response> | Response;
};

interface Env {
  XSEEK_API_KEY?: string;
  XSEEK_WEBSITE_ID?: string;
}

// ── xSeek AI bot tracking ──────────────────────────────────────────────────
// Known AI bot User-Agent patterns. Synced from xSeek's public catalog —
// refresh from https://www.xseek.io/api/v1/robots when new crawlers ship.
const XSEEK_BOTS: { name: string; pattern: RegExp }[] = [
  { name: "anthropic-ai", pattern: /anthropic-ai/i },
  { name: "claudebot", pattern: /ClaudeBot/i },
  { name: "claude-web", pattern: /claude-web/i },
  { name: "claude-user", pattern: /Claude-User/i },
  { name: "claude-searchbot", pattern: /Claude-SearchBot/i },
  { name: "claude-code", pattern: /claude-code\//i },
  { name: "perplexitybot", pattern: /PerplexityBot/i },
  { name: "perplexity-user", pattern: /Perplexity-User/i },
  { name: "grokbot", pattern: /GrokBot(?!.*DeepSearch)/i },
  { name: "grok-search", pattern: /xAI-Grok/i },
  { name: "grok-deepsearch", pattern: /Grok-DeepSearch/i },
  { name: "GPTBot", pattern: /GPTBot/i },
  { name: "chatgpt-user", pattern: /ChatGPT-User/i },
  { name: "oai-searchbot", pattern: /OAI-SearchBot/i },
  { name: "google-extended", pattern: /Google-Extended/i },
  { name: "Google-Agent", pattern: /Google-Agent/i },
  { name: "applebot", pattern: /Applebot(?!-Extended)/i },
  { name: "applebot-extended", pattern: /Applebot-Extended/i },
  { name: "meta-external", pattern: /meta-externalagent/i },
  { name: "meta-externalfetcher", pattern: /meta-externalfetcher/i },
  { name: "bingbot", pattern: /Bingbot(?!.*AI)/i },
  { name: "bingpreview", pattern: /bingbot.*Chrome/i },
  { name: "microsoftpreview", pattern: /MicrosoftPreview/i },
  { name: "cohere-ai", pattern: /cohere-ai/i },
  { name: "cohere-training-data-crawler", pattern: /cohere-training-data-crawler/i },
  { name: "youbot", pattern: /YouBot/i },
  { name: "duckassistbot", pattern: /DuckAssistBot/i },
  { name: "semanticscholarbot", pattern: /SemanticScholarBot/i },
  { name: "ccbot", pattern: /CCBot/i },
  { name: "ai2bot", pattern: /AI2Bot/i },
  { name: "ai2bot-dolma", pattern: /AI2Bot-Dolma/i },
  { name: "aihitbot", pattern: /aiHitBot/i },
  { name: "amazonbot", pattern: /Amazonbot/i },
  { name: "novaact", pattern: /NovaAct/i },
  { name: "brightbot", pattern: /Brightbot/i },
  { name: "bytespider", pattern: /Bytespider/i },
  { name: "tiktokspider", pattern: /TikTokSpider/i },
  { name: "cotoyogi", pattern: /Cotoyogi/i },
  { name: "crawlspace", pattern: /Crawlspace/i },
  { name: "pangubot", pattern: /PanguBot/i },
  { name: "petalbot", pattern: /PetalBot/i },
  { name: "sidetrade-indexer", pattern: /Sidetrade indexer bot/i },
  { name: "timpibot", pattern: /Timpibot/i },
  { name: "omgili", pattern: /omgili/i },
  { name: "omgilibot", pattern: /omgilibot/i },
  { name: "webzio-extended", pattern: /Webzio-Extended/i },
  { name: "baiduspider", pattern: /Baiduspider/i },
  { name: "mistralai-user", pattern: /MistralAI-User/i },
];

function trackXseekBot(request: Request, env: Env): Promise<unknown> | null {
  if (!env.XSEEK_API_KEY || !env.XSEEK_WEBSITE_ID) return null;

  const ua = request.headers.get("user-agent") ?? "";
  const bot = XSEEK_BOTS.find((b) => b.pattern.test(ua));
  if (!bot) return null;

  const url = new URL(request.url);
  const ip =
    request.headers.get("cf-connecting-ip") ??
    request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
    "";

  return fetch(
    `https://www.xseek.io/api/v1/websites/${env.XSEEK_WEBSITE_ID}/ai-visits`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.XSEEK_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        botName: bot.name,
        userAgent: ua,
        url: url.pathname + url.search,
        ip,
        referer: request.headers.get("referer") ?? "",
      }),
      signal: AbortSignal.timeout(3000),
    },
  ).catch((err) => {
    console.error("[xseek] bot tracking failed:", err);
  });
}
// ────────────────────────────────────────────────────────────────────────────

let serverEntryPromise: Promise<ServerEntry> | undefined;

async function getServerEntry(): Promise<ServerEntry> {
  if (!serverEntryPromise) {
    serverEntryPromise = import("@tanstack/react-start/server-entry").then(
      (m) => (m.default ?? m) as ServerEntry,
    );
  }
  return serverEntryPromise;
}

async function normalizeCatastrophicSsrResponse(response: Response): Promise<Response> {
  if (response.status < 500) return response;
  const contentType = response.headers.get("content-type") ?? "";
  if (!contentType.includes("application/json")) return response;

  const body = await response.clone().text();
  if (!body.includes('"unhandled":true') || !body.includes('"message":"HTTPError"')) {
    return response;
  }

  console.error(consumeLastCapturedError() ?? new Error(`h3 swallowed SSR error: ${body}`));
  return new Response(renderErrorPage(), {
    status: 500,
    headers: { "content-type": "text/html; charset=utf-8" },
  });
}

export default {
  async fetch(request: Request, env: unknown, ctx: unknown) {
    // Fire-and-forget bot tracking. ctx.waitUntil keeps the Worker alive
    // long enough for the POST to finish, but doesn't block the response.
    const trackPromise = trackXseekBot(request, env as Env);
    if (
      trackPromise &&
      ctx &&
      typeof (ctx as { waitUntil?: (p: Promise<unknown>) => void }).waitUntil === "function"
    ) {
      (ctx as { waitUntil: (p: Promise<unknown>) => void }).waitUntil(trackPromise);
    }

    try {
      const handler = await getServerEntry();
      const response = await handler.fetch(request, env, ctx);
      return await normalizeCatastrophicSsrResponse(response);
    } catch (error) {
      console.error(error);
      return new Response(renderErrorPage(), {
        status: 500,
        headers: { "content-type": "text/html; charset=utf-8" },
      });
    }
  },
};
```

Required environment variables (add in Lovable → Project Settings → Environment Variables):
- XSEEK_API_KEY — must include both articles:read and ai_visits:push privileges
- XSEEK_WEBSITE_ID — find it in your xSeek dashboard URL

After deploying, verify with a curl using a bot User-Agent:

curl -A "Mozilla/5.0 (compatible; GPTBot/1.0)" https://yoursite.com/

The visit should appear in xSeek → AI Visits within ~30 seconds.
Three keys to set in Lovable

XSEEK_API_KEY, XSEEK_WEBSITE_ID, and SITE_ORIGIN — all from your xSeek dashboard. Add them in Lovable → Project Settings → Environment Variables.