Connect: Nuxt
Connect a Nuxt site. Server routes talk to our API with the key; the pages fetch those routes, so the key never reaches the browser.
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_KEYis read on the server and sent asAuthorization: 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
/blogand/blog-sitemap.xmltoo. - 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
Nuxt 4. The key lives in server-only runtime config and is used only in server routes, so it never reaches the browser.
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Server-only, and empty here on purpose: set NUXT_BLOGSFORSEO_API_KEY (and optionally
// NUXT_BLOGSFORSEO_API_URL) when the server starts. Never write process.env.… here: Nuxt
// would copy the key into .output at build time.
blogsforseoApiKey: "",
blogsforseoApiUrl: "https://blogsforseo.com",
},
});
// server/utils/blogsforseo.ts
/** Asks the Content API on every request. A 404 from the API is a 404 here; any other error is a 500. */
export async function bfs(path: string, query: Record<string, string | number> = {}) {
const { blogsforseoApiKey: key, blogsforseoApiUrl } = useRuntimeConfig();
if (!key) throw createError({ statusCode: 500, statusMessage: "BLOGSFORSEO_API_KEY is not set" });
const url = new URL(blogsforseoApiUrl.replace(/\/$/, "") + "/api/v1" + path);
for (const [name, value] of Object.entries(query)) url.searchParams.set(name, String(value));
const response = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal: AbortSignal.timeout(10_000) });
if (response.status === 404) throw createError({ statusCode: 404, statusMessage: "Not found" });
if (!response.ok) throw createError({ statusCode: 500, statusMessage: `Blogs for SEO API answered ${response.status}` });
return response;
}
/** ?page= as a whole number from 1; anything else is page 1. */
export function pageNumber(raw: unknown): number {
return typeof raw === "string" && /^[1-9]\d{0,5}$/.test(raw) ? Number(raw) : 1;
}
// server/api/blog/posts.get.ts
export default defineEventHandler(async (event) => (await bfs("/html/posts", { page: pageNumber(getQuery(event).page) })).json());
// server/api/blog/[slug].get.ts
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, "slug") ?? "";
if (!/^[a-z0-9-]+$/.test(slug)) throw createError({ statusCode: 404, statusMessage: "Not found" });
return (await bfs("/html/posts/" + slug)).json();
});
// server/routes/blog-sitemap.xml.ts (server/routes/llms.txt.ts is the same with "/llms.txt" and text/plain)
export default defineEventHandler(async (event) => {
setHeader(event, "Content-Type", "application/xml");
return (await bfs("/sitemap", { format: "xml" })).text();
});
<!-- app/pages/blog/[slug].vue (blog/index.vue fetches /api/blog/posts with ?page= and shows body the same way) -->
<script setup lang="ts">
const slug = String(useRoute().params.slug);
if (!/^[a-z0-9-]+$/.test(slug)) throw createError({ statusCode: 404, fatal: true });
const { data, error } = await useFetch(`/api/blog/${slug}`);
if (error.value) throw createError({ statusCode: error.value.statusCode ?? 500, fatal: true });
// Nuxt builds <head> from data, so use `meta` (the same values as `head`).
const meta = data.value!.meta;
useSeoMeta({
title: meta.title,
description: meta.description,
ogType: "article",
ogTitle: meta.openGraph.title,
ogDescription: meta.openGraph.description,
ogUrl: meta.openGraph.url ?? undefined,
ogImage: meta.openGraph.image?.url,
articlePublishedTime: meta.openGraph.publishedTime,
articleModifiedTime: meta.openGraph.modifiedTime,
twitterCard: meta.twitter.card,
twitterTitle: meta.twitter.title,
twitterDescription: meta.twitter.description,
twitterImage: meta.twitter.image ?? undefined,
});
useHead({ link: meta.canonical ? [{ rel: "canonical", href: meta.canonical }] : [] });
</script>
<template>
<div v-html="data?.body" />
</template>
- The page fetches your own server route (
/api/blog/...), never our API: the key stays on the server. Nuxt adds the post body a second time in the page's hydration data; that's expected, and search engines read the rendered HTML. - IndexNow key file: save
public/<your IndexNow key>.txtcontaining just the key. - robots.txt: add the sitemap line from your dashboard (
Sitemap: https://<your domain>/blog-sitemap.xml) topublic/robots.txt. - Run it:
nuxt build, thenNUXT_BLOGSFORSEO_API_KEY=… node .output/server/index.mjs.
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.