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.
Options every helper takes
Section titled “Options every helper takes”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.
@geoffy/headless/next
Section titled “@geoffy/headless/next”GeoffyProduct
Section titled “GeoffyProduct”<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.
createGeoffyTextRoute(options, name)
Section titled “createGeoffyTextRoute(options, name)”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.
createGeoffyProxyRoute(options)
Section titled “createGeoffyProxyRoute(options)”export const GET = createGeoffyProxyRoute({ siteKey });The /apps/geoffy/[...path] catch-all. Query strings are not forwarded.
createGeoffyRevalidateRoute(config)
Section titled “createGeoffyRevalidateRoute(config)”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.
@geoffy/headless
Section titled “@geoffy/headless”The framework-agnostic client. You will use fetchGeoffyText for
robots.txt; the rest is what a Nuxt or SvelteKit adapter would be
built on.
| Export | Signature | Notes |
|---|---|---|
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) => string | Escapes < the way Next’s own example does |
compareCanonical | (page, artifact) => "match" | "mismatch" | "unknown" | Path only, not origin |
skippedMarker | (reason, detail) => string | The inert data-geoffy-skipped comment |
handleGeoffyProxy | (options, requestUrl) => Promise<Response> | What the proxy route wraps |
resolveProxyPath | (requestUrl) => string | null | null for anything outside the namespace |
DEFAULT_GEOFFY_ORIGIN | "https://api.geoffy.ai" | |
PROXY_PREFIX | "/apps/geoffy" | The mount path is fixed; see the namespace |
GeoffyProductArtifact
Section titled “GeoffyProductArtifact”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;}Not published yet
Section titled “Not published yet”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.
Runtime
Section titled “Runtime”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.