Browse documentation
Connect your site
How your website reads the search and AI visibility fixes your team approves in Nagent, and your published blog posts, with one server-side key.
Fixes your team approves, live on your own site
Your team approves search and AI visibility fixes in Nagent: page titles, meta descriptions, main headings and structured data. This guide shows how your website reads those fixes and puts them on its pages. You wire it in once. After that, every fix your team approves appears on the site, and a fix they withdraw comes off it.
- Issue a key. Your team creates a site key in Nagent and hands it to you.
- Store it on the server. It goes in a server environment variable, never in browser code.
- Read the route. Your server asks Nagent for the live fixes for a page.
- Render the tags. Put the title, description, heading and JSON-LD into the HTML you send.
The examples use retail-1.example as your site and https://nagent.ai as the
Nagent address. Use the address shown on your Site connection card if it
differs.
What your site receives
For each page that has an approved fix, Nagent can supply:
- a page title, for the
<title>tag; - a meta description;
- a main heading, the page's H1;
- one or more JSON-LD blocks for
<script type="application/ld+json">tags.
A page appears only when at least one fix for it is live, and each field appears only when a fix for that field is live. Everything else on the page stays exactly as you built it.
Get a key
Someone on your team with settings access issues the key in Nagent:
- Open Settings, then Integrations, then the Site connection card.
- Click Issue key.
- Copy the key straight away. It is shown once. Nagent keeps only a fingerprint of it, so nobody at Nagent can read it back.
Two more buttons appear once a key exists:
- Rotate key issues a new key and stops the old one at once. The site shows no fixes until the new key is deployed, so rotate when you are ready to deploy it. A lost key is replaced by rotating.
- Revoke key stops the key and issues nothing in its place. Approved fixes are kept and come back as soon as a new key is issued and deployed.
Fixes approved before any key exists are not lost. They go live as soon as a key is issued and your site reads it.
Keep the key secret
Anyone holding the key can read your approved fixes and your workspace's published blog posts.
- Store it in a server-side environment variable, for example
NAGENT_SITE_KEY. - Never put it in a variable your framework exposes to the browser, such as one
starting with
NEXT_PUBLIC_orVITE_. - Never call the routes from browser code. Call them from your server, your build or an edge function. The routes send no CORS headers, so browsers block such a call anyway.
- Do not commit the key to your repository.
Read your fixes
GET https://nagent.ai/api/cms/site-fixes
Authorization: Bearer <your site key>
Add ?path= to read one page, for example ?path=/pricing. It accepts a path
(/pricing), the same path with a trailing slash (/pricing/), or an absolute
URL, of which only the path is used. Letter case matters, and a query string is
part of the path: ?path=/pricing does not match a page stored as
/pricing?plan=team. Encode the value with encodeURIComponent when it
contains ? or &. A path with no live fixes returns an empty list, not an
error.
Response
{
"pages": [
{
"path": "/pricing",
"url": "https://www.retail-1.example/pricing",
"title": "Pricing and plans | Retail One",
"description": "Compare plans, see what each includes, and start free.",
"h1": "Plans for every size of store",
"jsonLd": [
"{\"@context\":\"https://schema.org\",\"@type\":\"Organization\"}"
],
"updatedAt": "2026-09-29T10:15:00.000Z"
}
]
}
| Field | Meaning |
|---|---|
pages | Every page with at least one live fix, sorted by url. Empty when nothing is live. |
path | The page path, with any query string. No trailing slash, except / for the home page. |
url | The full page address as Nagent audited it: lower-case host, no trailing slash, no #fragment. |
title | The approved page title. Present only when a title fix is live. |
description | The approved meta description. Present only when a description fix is live. |
h1 | The approved main heading. Present only when a heading fix is live. |
jsonLd | An array of strings, each a complete JSON-LD document. Always present, often empty. |
updatedAt | When the most recent fix on this page was approved (ISO 8601, UTC). |
- Every field except
path,url,jsonLdandupdatedAtis optional. Keep your own value whenever one is missing. - Each
jsonLdentry is a string, ready to place inside a script tag as it is. Do not parse and re-serialise it, and do not wrap it inJSON.stringifyagain. Any<inside it is already written as<, so a stray</script>in a page title cannot end your tag early. A page may carry several entries, for example an organisation block and a breadcrumb block. - The newest approval wins. If fixes for the same field were approved at different times, the page shows the most recent one.
- Pages are keyed by their full URL. If your site answers on two hosts, such as
retail-1.exampleandwww.retail-1.example, the same path on each is a separate entry and?path=/pricingreturns both. Match onurlto tell them apart.
Status codes
| Status | When | What to do |
|---|---|---|
| 200 | The key is valid. | Use pages, which may be empty. |
| 401 | No Authorization header, a header that is not Bearer <key>, or a key that is wrong, rotated or revoked. Carries WWW-Authenticate: Bearer and is never cached. | Check the key and the environment variable. A rotated key stops working at once. |
| 500 | A fault on Nagent's side. | Not a key problem, so do not rotate. Render the page with your own values and try again on the next request. |
Caching
A successful answer carries Cache-Control: private, max-age=300 and
Vary: Authorization. Fixes change only when someone approves or withdraws
one, so reading the route at most every five minutes is plenty, and a change in
Nagent shows on your site within about five minutes.
Cache the answer on your own server, keyed on your key. Never put it in a shared CDN or proxy cache keyed only on the URL: the answer belongs to one workspace.
Connect your stack
Pick the section that matches how your pages are built. Nagent never writes into your CMS; your site reads the fixes and your CMS content stays as it is.
Next.js (App Router)
Read the route on the server, use it in generateMetadata for the title and
description, then render the H1 and JSON-LD in the page. If the route fails or
is slow, the page falls back to its own values and still renders.
// lib/site-fixes.ts
import 'server-only';
export interface SiteFix {
path: string;
url: string;
title?: string;
description?: string;
h1?: string;
jsonLd: string[];
updatedAt: string;
}
const NAGENT = 'https://nagent.ai';
const SITE_ORIGIN = 'https://www.retail-1.example';
export async function siteFix(path: string): Promise<SiteFix | null> {
const key = process.env.NAGENT_SITE_KEY;
if (!key) return null;
try {
const res = await fetch(
`${NAGENT}/api/cms/site-fixes?path=${encodeURIComponent(path)}`,
{
headers: { Authorization: `Bearer ${key}` },
next: { revalidate: 300 },
signal: AbortSignal.timeout(3000),
},
);
if (!res.ok) return null;
const { pages } = (await res.json()) as { pages: SiteFix[] };
const onThisHost = pages.find(p => p.url.startsWith(SITE_ORIGIN));
return onThisHost ?? pages[0] ?? null;
} catch {
return null;
}
}
// app/pricing/page.tsx
import type { Metadata } from 'next';
import { siteFix } from '@/lib/site-fixes';
const DEFAULTS = {
title: 'Pricing',
description: 'Plans and prices.',
h1: 'Pricing',
};
export async function generateMetadata(): Promise<Metadata> {
const fix = await siteFix('/pricing');
return {
title: fix?.title ?? DEFAULTS.title,
description: fix?.description ?? DEFAULTS.description,
};
}
export default async function PricingPage() {
const fix = await siteFix('/pricing');
return (
<>
{fix?.jsonLd.map((ld, i) => (
<script
key={i}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: ld }}
/>
))}
<h1>{fix?.h1 ?? DEFAULTS.h1}</h1>
{/* the rest of your page */}
</>
);
}
- Next.js shares one fetch between
generateMetadataand the page when the URL and options match, so the route is read once per render. next: { revalidate: 300 }matches the route's five minute cache, held in your own server's data cache.- For many pages, call
siteFixfrom a shared layout or dynamic route with the current path, or read the route once without?path=and look pages up bypath.
WordPress
WordPress runs PHP on every page, so it can read the route on the server. Add
the key to wp-config.php:
define('NAGENT_SITE_KEY', 'your site key');
Then add this to a small plugin or your theme's functions.php:
function nagent_site_fix() {
static $fix = null;
static $done = false;
if ($done) return $fix;
$done = true;
if (!defined('NAGENT_SITE_KEY')) return null;
$path = wp_parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/';
$cache_key = 'nagent_fix_' . md5($path);
$cached = get_transient($cache_key);
if ($cached !== false) return $fix = ($cached ?: null);
$res = wp_remote_get(
'https://nagent.ai/api/cms/site-fixes?path=' . rawurlencode($path),
[
'headers' => ['Authorization' => 'Bearer ' . NAGENT_SITE_KEY],
'timeout' => 3,
]
);
if (is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200) {
return null;
}
$pages = json_decode(wp_remote_retrieve_body($res), true)['pages'] ?? [];
$origin = untrailingslashit(strtolower(home_url()));
$page = null;
foreach ($pages as $p) {
if (strpos($p['url'], $origin) === 0) { $page = $p; break; }
}
$page = $page ?? ($pages[0] ?? null);
set_transient($cache_key, $page ?: '', 5 * MINUTE_IN_SECONDS);
return $fix = $page;
}
add_filter('pre_get_document_title', function ($title) {
return nagent_site_fix()['title'] ?? $title;
}, 20);
add_action('wp_head', function () {
$fix = nagent_site_fix();
if (!$fix) return;
if (!empty($fix['description'])) {
echo '<meta name="description" content="'
. esc_attr($fix['description']) . '">' . "\n";
}
foreach ($fix['jsonLd'] as $ld) {
echo '<script type="application/ld+json">' . $ld . '</script>' . "\n";
}
}, 1);
- A failed or slow read returns nothing and the page renders with its own values. Only a successful answer is cached, for five minutes, in a transient.
- Print each
jsonLdentry as it arrives. Passing it throughesc_htmlorwp_json_encodebreaks it. - If an SEO plugin such as Yoast or Rank Math writes the title and description,
feed the fix through its filters instead of the two hooks above, so each page
has one title and one description. Yoast uses
wpseo_titleandwpseo_metadesc; Rank Math usesrank_math/frontend/titleandrank_math/frontend/description. - The main heading is printed by your theme's templates. Where the template prints the page heading, print the fix when there is one:
echo esc_html(nagent_site_fix()['h1'] ?? get_the_title());
Headless CMS with your own front end
If your content lives in a headless CMS such as Contentful, Sanity, Strapi or Storyblok and a front end you control renders the pages, wire the route into that front end as the Next.js section shows. The approved fix takes the place of the CMS value when the page renders. Your editors keep working in the CMS as before. While a fix is live it wins; once it is withdrawn, the CMS value shows again.
Single-page apps (for example Vite and React)
Search crawlers, and most AI crawlers in particular, read the HTML your server sends, and many do not run JavaScript. A title or JSON-LD block set in the browser after load is invisible to them, so put the fixes into the HTML your build or server produces:
- At build time. Read the route in a prerender step or a small script that
runs before
vite build, and write the tags into each page's HTML. New fixes then appear after the next build, so rebuild on a schedule if they should appear without a manual deploy. - On the server or at the edge. If your host can run code in front of your static files (server rendering, an edge function or a worker), read the route there, keep the key in that environment, and insert the tags before the HTML is sent.
Reading the route from browser code is not an option: it would hand the key to every visitor, and browsers block the call.
Shopify, Webflow, Wix and Squarespace
These builders do not let you run your own code on the server, so the site cannot read the route with a secret key, and the key must never go into theme JavaScript. For a site like this:
- Do not issue a site key.
- In Nagent, open the fix on the Proposals page and copy the title, description or JSON-LD.
- Paste it into the platform's own SEO fields or custom code area.
- Click Mark applied on the finding instead of approving it. Approving puts a fix on the connection, which this site does not read. The next audit checks the change is on the page.
Your blog posts
The same key reads the published blog posts in your Nagent workspace.
GET https://nagent.ai/api/cms/blogs
GET https://nagent.ai/api/cms/blogs/<slug>
Authorization: Bearer <your site key>
With a valid key, both routes return only your workspace's published posts,
never drafts and never another workspace's posts, with
Cache-Control: private, max-age=300 and Vary: Authorization. A missing,
wrong, rotated or revoked key returns 401, as on the fixes route.
The list
| Parameter | Meaning |
|---|---|
page | Page number, from 1. Default 1. |
per_page | Posts per page, 1 to 100. Default 25. |
tag | Only posts with this tag. |
category | Only posts in this category. |
fields=preview | Leave out each post body, for listing pages. |
Posts are newest first. total counts every published post matching the
filters, across all pages.
{
"stories": [ { "slug": "...", "name": "...", "content": { } } ],
"cv": 1790000000000,
"rels": [],
"links": [],
"total": 42,
"page": 1,
"per_page": 25
}
A single post
GET /api/cms/blogs/<slug> returns
{ "story": { ... }, "cv": ..., "rels": [], "links": [] }, or 404 with
{ "error": "Blog not found" } when no published post has that slug. Each read
of a single post counts as one view in Nagent, so cache it on your server
rather than reading it on every visit.
Fields of a post
| Field | Meaning |
|---|---|
name | The post title. |
slug | The post slug; full_slug is blog/<slug>. |
uuid | The post's stable id. |
published_at, created_at, updated_at | ISO 8601 timestamps. |
tag_list | The post tags. |
content.shortDescription | The excerpt. |
content.metaTitle, content.metaDescription | The post's own SEO title and description, when set. |
content.coverImage | The cover image URL, or an empty string. |
content.date, content.readTime | Publication date, and reading time in minutes as a string. |
content.category, content.tags | The category, and the tags as one comma-separated string. |
content.content | The body as rich text (ProseMirror JSON). On the single post route it is an HTML string for posts written as HTML. With fields=preview it is an empty document. |
A single post also carries content.authorName, content.authorEmail and
content.authors, each author with name, role and avatarUrl.
Check the connection
From a terminal on your server:
curl -i https://nagent.ai/api/cms/site-fixes \
-H "Authorization: Bearer $NAGENT_SITE_KEY"
A working key answers like this:
HTTP/1.1 200 OK
cache-control: private, max-age=300
vary: Authorization
content-type: application/json
{"pages":[]}
{"pages":[]} means the key works and nothing is live yet. Ask your team to
approve a fix, and it appears on the next read. Nagent can see that a key has
been issued but not whether your site reads it, so tell your team once the site
is connected.
Troubleshooting
| What you see | Likely cause | Fix |
|---|---|---|
| Every read is 401 | The key is missing from the server environment, has a stray space or quote, or was rotated. | Print the length of the variable on the server (never the value). If the key was rotated, deploy the new one. |
pages is empty | Nothing is approved yet, or ?path= does not match the stored path. | Read without ?path= and compare. Case matters, and a query string is part of the path. |
| An approved fix does not show | Your cache or the route's five minute cache still holds the older answer. | Wait five minutes, or clear your own server cache. |
| Two titles or two descriptions on a WordPress page | An SEO plugin writes its own tags as well. | Feed the fix through the plugin's filters, as shown in the WordPress section. |
| The JSON-LD shows as text or breaks the page | The entry was escaped or re-serialised. | Print each jsonLd string exactly as received inside the script tag. |
Questions about a fix itself, such as why it was suggested or what it changes, go to the person on your team who approves fixes in Nagent.
