blogsforseo

Connect: SvelteKit

Connect a SvelteKit site. +page.server.ts load functions and +server.ts routes fetch from our API on the server.

This is the simple way: we send finished HTML with the SEO done, and your site shows it in its own design. Using a coding agent? Copy the prompt from your dashboard's Integration page instead: it has your values filled in and the same steps for this framework.

How it works

  • The key comes from the environment. BLOGSFORSEO_API_KEY is read on the server and sent as Authorization: Bearer <key>. It's never in the code, the templates or the browser.
  • Every visit asks our API. The pages are rendered on each request, never saved at build time, so new posts, edits and removals show up on their own. The one rule for caching: nothing may keep our API's answer for more than 5 minutes. If you add a cache, keep it at 300 seconds or less, and check your CDN rules on /blog and /blog-sitemap.xml too.
  • A 404 from us is a 404 from you. An unknown or removed post must answer a real 404, not an empty page. Any other error fails the request, so your framework shows its normal error page.

Match the routes to your post URL pattern in the dashboard: these use /blog and /blog/{slug}. In this guide, {API} means https://blogsforseo.com/api/v1.

Set it up

SvelteKit 3 with the Node adapter. The key is a private environment variable read when the server starts, never baked into the build.

// vite.config.js
import adapter from "@sveltejs/adapter-node";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";

export default defineConfig({ plugins: [sveltekit({ adapter: adapter() })] });
// src/env.ts
import { defineEnvVars } from "@sveltejs/kit/env";

// Server-only, read when the app starts. Optional here because SvelteKit also reads these during
// `vite build`; the API helper below fails clearly if the key is missing at runtime.
export const variables = defineEnvVars({
  BLOGSFORSEO_API_KEY: { schema: (value) => value ?? "" },
  BLOGSFORSEO_API_URL: { schema: (value) => value || "https://blogsforseo.com" },
});
// src/params.ts: post slugs only; anything else is a 404 without asking the API.
import { defineParams } from "@sveltejs/kit/params";

export const params = defineParams({ slug: (param) => (/^[a-z0-9-]+$/.test(param) ? param : undefined) });
// src/lib/server/blogsforseo.ts (imported as #lib/server/blogsforseo: add "imports": { "#lib/*": "./src/lib/*" } to package.json)
import { BLOGSFORSEO_API_KEY, BLOGSFORSEO_API_URL } from "$app/env/private";
import { error } from "@sveltejs/kit";

const API = BLOGSFORSEO_API_URL.replace(/\/$/, "") + "/api/v1";

/** Asks the Content API on every request. A 404 from the API is this page's 404; any other error is a 500. */
export async function bfs(path: string, query: Record<string, string | number> = {}): Promise<Response> {
  if (!BLOGSFORSEO_API_KEY) throw new Error("BLOGSFORSEO_API_KEY is not set");
  const url = new URL(API + path);
  for (const [name, value] of Object.entries(query)) url.searchParams.set(name, String(value));
  const response = await fetch(url, { headers: { Authorization: `Bearer ${BLOGSFORSEO_API_KEY}` }, signal: AbortSignal.timeout(10_000) });
  if (response.status === 404) error(404, "Not found");
  if (!response.ok) throw new Error(`Blogs for SEO API answered ${response.status} for ${path}`);
  return response;
}

/** ?page= as a whole number from 1; anything else is page 1. */
export function pageNumber(url: URL): number {
  const page = url.searchParams.get("page") ?? "";
  return /^[1-9]\d{0,5}$/.test(page) ? Number(page) : 1;
}
// src/routes/blog/+page.server.ts
import { bfs, pageNumber } from "#lib/server/blogsforseo";

export const load = async ({ url }) => (await bfs("/html/posts", { page: pageNumber(url) })).json() as Promise<{ body: string }>;
// src/routes/blog/[slug=slug]/+page.server.ts
import { bfs } from "#lib/server/blogsforseo";

export const load = async ({ params }) => (await bfs("/html/posts/" + params.slug)).json() as Promise<{ head: string; body: string }>;
<!-- src/routes/blog/[slug=slug]/+page.svelte (blog/+page.svelte is the same with only {@html data.body}) -->
<script lang="ts">
  let { data } = $props();
</script>

<svelte:head>{@html data.head}</svelte:head>
{@html data.body}
// src/routes/blog-sitemap.xml/+server.ts (src/routes/llms.txt/+server.ts is the same with "/llms.txt" and text/plain)
import { bfs } from "#lib/server/blogsforseo";

export const GET = async () => new Response(await (await bfs("/sitemap", { format: "xml" })).text(), { headers: { "Content-Type": "application/xml" } });
  • IndexNow key file: save static/<your IndexNow key>.txt containing just the key.
  • robots.txt: add the sitemap line from your dashboard (Sitemap: https://<your domain>/blog-sitemap.xml) to static/robots.txt.
  • Run it: vite build, then BLOGSFORSEO_API_KEY=… node build.

The advanced way

Fetch /posts and /posts/{slug} instead of the /html endpoints, and build the page from the fields in the Content API reference, following the advanced guide. bodyHtml is already sanitised: print it raw. When you put jsonLd in a <script type="application/ld+json">, escape <:

JSON.stringify(post.jsonLd).replace(/</g, "\\u003c")

Check it

Run these against your site (local or live). -A GPTBot asks the way an AI crawler does, without JavaScript:

curl -A GPTBot https://<your domain>/blog                 # lists posts, or "No posts yet"
curl -A GPTBot https://<your domain>/blog/<a post slug>   # <h1>, article text, canonical link, application/ld+json
curl -i https://<your domain>/blog/not-a-real-post        # HTTP 404
curl https://<your domain>/blog-sitemap.xml               # the same posts as GET {API}/sitemap
curl https://<your domain>/llms.txt                       # plain text
curl https://<your domain>/<your IndexNow key>.txt        # just the key
curl https://<your domain>/robots.txt                     # has the Sitemap: line

Then check it stays fresh: the number of posts on /blog matches total from the API. Finish with the simple guide's dashboard steps: check the post address, run the health check and connect Search Console.