What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report

Agility CMS

Rendering Agility's GEO Fields with @agility/geo

From the editor's tab to server-rendered JSON-LD: the data shape, the minimal Next.js and plain JavaScript code, and how the graph merges.

Joel Varty

Joel Varty

CTO

SEODeveloperDeveloper ToolsSEO
From the GEO tab to the HTML: page.geo from the Fetch API, built by @agility/geo into one JSON-LD graph

Starting today, every page, dynamic page and dynamic-page item in Agility CMS has a GEO tab, and its fields reach your site through the Fetch API. Two open source packages render them: @agility/geo 0.1.2 builds one schema.org JSON-LD graph per page in any JavaScript runtime, and @agility/nextjs 16.1.0 adds <AgilityJsonLd /> and generateAgilityMetadata() for the App Router. The Agility Next.js starter already uses both.

Why this replaces Additional Header Markup

Structured data used to mean editors pasting a <script type="application/ld+json"> block into Additional Header Markup. Nothing validated it, nothing merged it with what the site generated, and our App Router starters used to drop it. The new fields are typed data instead of an HTML string, so your code owns the markup and editors own the facts.

How it works, end to end

Four sources feed one page's structured data: site settings, the GEO fields, a dynamic item's own fields, and the Components on the page

  1. An editor fills the tab: a Page Type (schema.org), a Summary, and JSON-LD schema that's validated as they type. Two AI settings sit beside them.
  2. Agility stores it and serves it on the page and on the item: Fetch API (REST and GraphQL), Management API, the MCP server and the CLI all carry it. Nothing is generated inside Agility.
  3. Your site builds the graph when it renders the page, from those fields, the content itself and your site settings.
  4. The JSON-LD is in the server-rendered HTML, so crawlers that don't run JavaScript see it, and audit tools read it without JavaScript rendering on.

The data looks like this (each property is omitted when it isn't set):

page.geo = {
	schemaType: "AboutPage", // WebPage, AboutPage, ContactPage, CollectionPage, FAQPage, ItemPage, ProfilePage
	summary: "An answer-first, plain-text summary.",
	structuredData: { /* the editor's JSON-LD: a node, an array, or an @graph */ },
	ai: { llmsInclude: "inherit", crawl: "inherit" },
}

A dynamic item has its own item.geo with the same shape. Page Type and the AI settings inherit from the dynamic page; Summary and JSON-LD schema never do, so posts don't all share one summary.

Next.js: the minimal code

If your site started from the Next.js starter, its latest version already renders both. On your own App Router site:

npm install @agility/nextjs@16.1.0 @agility/geo
// app/[...slug]/page.tsx
import { AgilityJsonLd, generateAgilityMetadata } from "@agility/nextjs/geo"

export async function generateMetadata(props: PageProps): Promise<Metadata> {
	const agilityData = await getAgilityPage(props)
	return generateAgilityMetadata({
		page: agilityData.page,
		item: agilityData.dynamicPageItem,
		url: pageUrl,
		locale,
		title: agilityData.sitemapNode?.title,
		titleTemplate: "%s | Example",
	})
}

// inside the page component
<AgilityJsonLd
	page={agilityData.page}
	item={agilityData.dynamicPageItem}
	url={pageUrl}
	path={agilityData.sitemapNode?.path}
	locale={locale}
	sitemap={sitemapFlat}
	siteSettings={siteSettings}
/>

generateAgilityMetadata() returns only what the page sets (title, description with Summary as the fallback, canonical, robots, hreflang alternates), so merge it over your defaults. Canonical, robots, SEO title and share image come from seo.canonicalUrl, seo.robots, seo.title and seo.ogImage. In this release those are API-only: the Manager App has no field for them, so editors can't set them, and you or an agent set them through the Management API, MCP or the CLI. <AgilityJsonLd /> is a component because Next.js metadata can't emit <script> tags, and it renders on the server.

Any other JavaScript framework

@agility/geo has no dependencies and runs in Node.js, edge functions or the browser. Build the graph where you render the page on the server, and put the tag in the head:

import { buildJsonLd, jsonLdScriptTag } from "@agility/geo"

const graph = buildJsonLd({ page, item, url, locale, sitemap, siteSettings })
const tag = jsonLdScriptTag(graph) // <script type="application/ld+json">…</script>

Always use jsonLdScriptTag or serializeJsonLd, never JSON.stringify. Editors write the Summary and JSON-LD schema, and escaping stops text such as </script> breaking out of the tag.

The graph merges by @id

buildJsonLd() builds one @graph in this order: Organization and WebSite from your site settings; the WebPage (typed by Page Type, described by Summary) and a BreadcrumbList from the sitemap; then the editor's JSON-LD schema; then your transform, which runs last.

  • A node whose @id is already in the graph is merged into it. Its properties win, and arrays are replaced.
  • A node with a new @id, or none, is added.

So an editor can correct a generated value by reusing its @id (<page URL>#webpage, https://www.example.com/#organization). Use transform to build nodes from content you already have, such as a BlogPosting from a post's title, date and author, or questions from an FAQ Component on an FAQPage. Build from fields, not pasted copies, and the markup follows the content when it changes. The docs have transform helpers for both.

What's stored but inert

  • AI Crawlers (geo.ai.crawl) and Include in llms.txt (geo.ai.llmsInclude) are saved and served, but block and list nothing until your own robots.txt and llms.txt routes read them. Builders for both are planned.
  • Site settings aren't in the product yet. Pass siteSettings from your own Site Settings item or from config.
  • There's no preview of the final JSON-LD in Agility. Check the rendered page with the Rich Results Test, or run validateJsonLd() on the built graph in your tests.
  • Only items in a list that backs a dynamic page get the tab.

Learn more

Frequently asked questions

Does anything change on my site when the tab appears?

No. Existing Additional Header Markup keeps rendering, and nothing new reaches a page until you adopt the packages. When you do, move pasted JSON-LD into the JSON-LD schema field and clear the old one, or the page describes the same thing twice.

Do I need @agility/content-fetch to use @agility/geo?

No. The package reads plain objects, so any Fetch API client works. With @agility/content-fetch, use 2.0.14 or later for the geo types.

Will FAQPage markup get me FAQ rich results?

Probably not. Google limited those to a few government and health sites in 2023. The markup still describes the page accurately.