Skip to content

The package

Two entry points matter to a Next.js app: @geoffy/headless/next for the framework adapters, and @geoffy/headless for the framework-agnostic client underneath them.

interface GeoffyClientOptions {
siteKey: string; // from Geoffy settings; public, addresses rather than authorises
origin?: string; // overrides GEOFFY_ORIGIN and the default, for this call
timeoutMs?: number; // how long before giving up and rendering without Geoffy
revalidateSeconds?: number; // how long your framework may reuse a cached copy
}

timeoutMs is deliberately short. These run during a build or an ISR revalidation, and the fallback — render without the widget — is cheap, while a stalled build is not.

revalidateSeconds is the backstop that makes the design self-correcting: if our purge call to your site is lost, the page still refreshes within this window on its own.

<GeoffyProduct siteKey={string} handle={string} canonicalUrl?={string} locale?={string} />

An async server component. Renders the widget and the structured data together, at the point you mount it. Renders nothing — no error, no fallback — when Geoffy has nothing published for the handle, is unreachable, when locale names a different language from the published content, or when canonicalUrl does not match the page Geoffy published against. In the last two cases it leaves an inert data-geoffy-skipped marker.

locale is the language this route renders, for example sr or de-DE. Only the language part is compared, so en-GB content renders on an en-US page. A value that is not a language tag is treated as unknown, and the component renders.

Accepts every GeoffyClientOptions field as a prop.

Pages with nothing to show cost no request

Section titled “Pages with nothing to show cost no request”

The component reads a small list of the products Geoffy has published, with the language and the path of each. It is cached with your root files and refreshed on every publish. With that list, the component declines a product page without asking Geoffy about the product: an unpublished product, a page in another language, or a page whose path is not the published one. There is nothing to configure.

If the list cannot be read, the component asks about the product directly, as older versions do. A problem on Geoffy’s side never hides a published product.

export const GET = createGeoffyTextRoute({ siteKey }, "llms.txt");

name is "llms.txt", "llms-full.txt" or "agents.md". Returns a route handler. Answers 503 with Retry-After when Geoffy is degraded, never a fabricated 404.

export const GET = createGeoffyProxyRoute({ siteKey });

The /apps/geoffy/[...path] catch-all. Query strings are not forwarded.

export const POST = createGeoffyRevalidateRoute({ secret, revalidateTag });

You pass revalidateTag in rather than the package importing it, so the package never depends on next/cache directly. Verifies the request signature for you. A product publish refreshes that product’s page; every publish, a guide included, refreshes llms.txt, llms-full.txt and agents.md.

Pass it bare. config.revalidateTag is typed (tag: string, profile?: string | { expire?: number }) => void, so both Next 15’s and Next 16’s version are assignable unwrapped, and the route supplies Next 16’s profile itself — { expire: 0 }, so the next request after a publish gets the new content instead of being served the old one while the refresh runs. Why.

The framework-agnostic client. You will use fetchGeoffyText for robots.txt; the rest is what a Nuxt or SvelteKit adapter would be built on.

ExportSignatureNotes
fetchGeoffyText(options, name) => Promise<string | null>null means unreachable. Accepts "robots-rules.txt", which the route factories do not
fetchGeoffyProduct(options, handle) => Promise<GeoffyProductArtifact | null>The raw artifact. null also when nothing is published for the handle
serializeJsonLd(node: unknown) => stringEscapes < the way Next’s own example does
compareCanonical(page, artifact) => "match" | "mismatch" | "unknown"Path only, not origin
skippedMarker(reason, detail) => stringThe inert data-geoffy-skipped comment
handleGeoffyProxy(options, requestUrl) => Promise<Response>What the proxy route wraps
resolveProxyPath(requestUrl) => string | nullnull for anything outside the namespace
DEFAULT_GEOFFY_ORIGIN"https://api.geoffy.ai"
PROXY_PREFIX"/apps/geoffy"The mount path is fixed; see the namespace
interface GeoffyProductArtifact {
handle: string;
canonicalUrl: string; // the URL Geoffy published against
jsonLd: unknown; // schema.org Product node
widgetHtml: string; // styles included — do not sanitise them out
publishedAt: string | null;
}

When your site is verified but nothing is published for a handle, Geoffy answers with a successful { "handle": "blue-widget", "published": false }, not an error. Your framework can cache it like any other answer, and publishing the product purges it through the revalidate route. fetchGeoffyProduct returns null for it, so the component renders nothing.

Node 18 or newer — the package uses global fetch, AbortController and crypto.subtle. ESM only. React is an optional peer dependency, needed only for the /next entry point.