Skip to content
NagentNagent
Log inSign upHire your AI team
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.

  1. Issue a key. Your team creates a site key in Nagent and hands it to you.
  2. Store it on the server. It goes in a server environment variable, never in browser code.
  3. Read the route. Your server asks Nagent for the live fixes for a page.
  4. 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:

  1. Open Settings, then Integrations, then the Site connection card.
  2. Click Issue key.
  3. 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_ or VITE_.
  • 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"
    }
  ]
}
FieldMeaning
pagesEvery page with at least one live fix, sorted by url. Empty when nothing is live.
pathThe page path, with any query string. No trailing slash, except / for the home page.
urlThe full page address as Nagent audited it: lower-case host, no trailing slash, no #fragment.
titleThe approved page title. Present only when a title fix is live.
descriptionThe approved meta description. Present only when a description fix is live.
h1The approved main heading. Present only when a heading fix is live.
jsonLdAn array of strings, each a complete JSON-LD document. Always present, often empty.
updatedAtWhen the most recent fix on this page was approved (ISO 8601, UTC).
  • Every field except path, url, jsonLd and updatedAt is optional. Keep your own value whenever one is missing.
  • Each jsonLd entry 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 in JSON.stringify again. 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.example and www.retail-1.example, the same path on each is a separate entry and ?path=/pricing returns both. Match on url to tell them apart.

Status codes

StatusWhenWhat to do
200The key is valid.Use pages, which may be empty.
401No 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.
500A 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 generateMetadata and 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 siteFix from a shared layout or dynamic route with the current path, or read the route once without ?path= and look pages up by path.

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 jsonLd entry as it arrives. Passing it through esc_html or wp_json_encode breaks 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_title and wpseo_metadesc; Rank Math uses rank_math/frontend/title and rank_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:

  1. Do not issue a site key.
  2. In Nagent, open the fix on the Proposals page and copy the title, description or JSON-LD.
  3. Paste it into the platform's own SEO fields or custom code area.
  4. 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

ParameterMeaning
pagePage number, from 1. Default 1.
per_pagePosts per page, 1 to 100. Default 25.
tagOnly posts with this tag.
categoryOnly posts in this category.
fields=previewLeave 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

FieldMeaning
nameThe post title.
slugThe post slug; full_slug is blog/<slug>.
uuidThe post's stable id.
published_at, created_at, updated_atISO 8601 timestamps.
tag_listThe post tags.
content.shortDescriptionThe excerpt.
content.metaTitle, content.metaDescriptionThe post's own SEO title and description, when set.
content.coverImageThe cover image URL, or an empty string.
content.date, content.readTimePublication date, and reading time in minutes as a string.
content.category, content.tagsThe category, and the tags as one comma-separated string.
content.contentThe 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 seeLikely causeFix
Every read is 401The 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 emptyNothing 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 showYour 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 pageAn 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 pageThe 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.