The package
Three entry points matter to an Astro app: @geoffy/headless/astro for the framework
adapters, @geoffy/headless for the framework-agnostic client underneath them, and
@geoffy/headless/astro-middleware for the optional AI visit
tracking.
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; // Next.js only; accepted and ignored in Astro}timeoutMs is deliberately short: the fallback (render without the widget) is cheap,
while a stalled build is not.
A missing or empty siteKey, handle or file name makes no request. The helper returns “no
content” as if nothing were published, and the server log says so once:
@geoffy/headless: GEOFFY_SITE_KEY is not set, so Geoffy content is skipped. Set it to the site key from your Geoffy settings.@geoffy/headless/astro
Section titled “@geoffy/headless/astro”getGeoffyProductMarkup(options, handle, extra?)
Section titled “getGeoffyProductMarkup(options, handle, extra?)”const geoffy = await getGeoffyProductMarkup( { siteKey }, handle, { canonicalUrl, locale },);extra takes canonicalUrl and locale, both optional. locale is the language this page
renders, for example sr or de-DE. Only the language part is compared, so en-GB content
matches an en-US page. A value that is not a language tag is treated as unknown, and the
helper returns the content.
Resolves to null, or to:
interface GeoffyProductMarkup { jsonLdScript: string; // a complete <script type="application/ld+json"> element widgetHtml: string; // the visible widget, styles included artifact: GeoffyProductArtifact;}Both strings are pre-rendered HTML, so both need set:html. null covers every case
(nothing published, Geoffy unreachable, an error, a locale in a different language from the
published content, or a canonicalUrl that does not match), so your {geoffy && …} guards
handle every one of them.
artifact is the raw data behind the two strings, if you want to render your own markup
instead.
Pages with nothing to show cost no request
Section titled “Pages with nothing to show cost no request”The helper reads a small list of the products Geoffy has published, with the language and the
path of each, and keeps it in memory for 30 seconds. With that list, it returns null for an
unpublished product, a page in another language, or a page whose path is not the published
one, without asking Geoffy about the product. A newly published product appears within
those 30 seconds, or at your next build on a static site. There is nothing to configure.
If the list cannot be read, the helper asks about the product directly, as older versions do. A problem on Geoffy’s side never hides a published product.
createGeoffyTextEndpoint(options, name, endpoint?)
Section titled “createGeoffyTextEndpoint(options, name, endpoint?)”export const GET = createGeoffyTextEndpoint({ siteKey }, "llms.txt");name is "llms.txt", "llms-full.txt" or "agents.md". It refuses
"robots-rules.txt": that is a fragment you append to your own file, not a whole file to
serve. Answers 503 with Retry-After when Geoffy is degraded, never a fabricated 404.
endpoint takes one option, forwardCrawler, off by default. When it is on, the endpoint
tells Geoffy which AI agent asked for the file, by its agent name and never a raw user agent.
It has an effect only on an endpoint rendered on request (export const prerender = false):
a prerendered file has no request to read.
createGeoffyProxyEndpoint(options)
Section titled “createGeoffyProxyEndpoint(options)”export const prerender = false;export const GET = createGeoffyProxyEndpoint({ siteKey });The /apps/geoffy/[...path] catch-all. prerender = false is required: without it
astro build fails outright. Query strings are not forwarded. It tells Geoffy which AI
agent asked for a file, by the agent’s name only; a browser’s user agent is never passed on.
@geoffy/headless/astro-middleware
Section titled “@geoffy/headless/astro-middleware”createGeoffyAiVisitMiddleware(options)
Section titled “createGeoffyAiVisitMiddleware(options)”export const onRequest = createGeoffyAiVisitMiddleware({ siteKey: import.meta.env.GEOFFY_SITE_KEY, secret: import.meta.env.GEOFFY_REVALIDATE_SECRET,});Optional. Reports the AI crawlers that read your pages and the shoppers who arrive from an
AI assistant; every other request makes no network call. Returns an onRequest handler that
calls next() once and returns its result. The report is handed to context.waitUntil, or
to locals.runtime.ctx.waitUntil, when your adapter provides one. Pages prerendered at build
time never reach middleware, so they are not counted.
Takes siteKey, secret (default GEOFFY_REVALIDATE_SECRET from process.env), origin,
clientIp (which here also receives the middleware context), exclude, sample and
timeoutMs. With no secret, nothing is sent. AI visit
tracking has the table and what each report carries.
Also exported: AI_ASSISTANT_HOSTS and BUILT_IN_AI_AGENT_SIGNATURES, the lists it matches
against.
@geoffy/headless
Section titled “@geoffy/headless”The framework-agnostic client. You will use fetchGeoffyText for
robots.txt; the rest is what another framework’s adapter would be
built on.
| Export | Signature | Notes |
|---|---|---|
fetchGeoffyText | (options, name, request?) => Promise<string | null> | null means unreachable. Accepts "robots-rules.txt", which the endpoint factory does not. Pass request to tell Geoffy which AI agent asked |
fetchGeoffyProduct | (options, handle) => Promise<GeoffyProductArtifact | null> | The raw artifact. null also when nothing is published for the handle |
serializeJsonLd | (node: unknown) => string | Escapes < safely for inline script |
compareCanonical | (page, artifact) => "match" | "mismatch" | "unknown" | Path only, not origin |
fetchGeoffyManifest | (options, memoMs?) => Promise<GeoffyManifest | null> | The list of published products, with the language and path of each. null on any failure |
decideFromManifest | (manifest, handle, page?) => ManifestDecision | Whether a page should ask for a product, from the manifest alone. reason is "not-published", "language-mismatch" or "canonical-mismatch" |
handleGeoffyProxy | (options, request) => Promise<Response> | What the proxy endpoint wraps. Takes the Request, not its URL |
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.
fetchGeoffyProduct returns null for it, so your guards render 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 and is not needed for the /astro
entry point.