Skip to content

Custom-built storefront

You build your own storefront. Shopify may sit behind it as the commerce backend, or there may be no commerce platform at all and your catalogue is simply your own pages. Both are supported, and they differ in exactly one place — where your catalogue comes from — which you settle once in the dashboard.

Build with agents
Add Geoffy to my storefront: geoffy.ai/docs/SKILL.md
Or paste the whole prompt — for an agent that cannot fetch a URL
Add Geoffy to your storefront
---
name: geoffy-headless
description: Add Geoffy's AI-discovery surfaces to a custom-built Next.js or Astro storefront — the product widget, schema.org data, llms.txt, plain-text product twins, buying guides and crawler rules, all served from the merchant's own domain.
---

# Add Geoffy to a custom-built storefront

Geoffy publishes a discovery layer for a product catalogue — structured data, plain-text
twins of every product, buying guides, `llms.txt` — served from **the merchant's own
domain**, so the citations it earns are theirs.

You are adding the integration. Read all of this before you touch a file.

---

## What you can do, and what you cannot

**Say this to the user before you start.** It prevents the failure that otherwise wastes
an hour at the end.

You can do: install the package, write every route and component, merge `robots.txt`, and
verify the markup is server-rendered.

You **cannot** do, because they need a human or a live site:

| Blocker | Why |
|---|---|
| Getting a `GEOFFY_SITE_KEY` | Needs a Geoffy account with a connected site |
| Proving domain ownership | Geoffy fetches their **live, public https** site. Localhost cannot satisfy it |
| Generating and publishing content | Costs money. That is the user's decision, not yours |

⚠️ **Until ownership is verified and something is published, the integration renders
NOTHING.** Before verification every Geoffy endpoint returns 404. After verification, a
product with nothing published answers `published: false`, and the component renders nothing
for it. Both are correct behaviour, not bugs. Do not "fix" working code because nothing
appears. See *Critical rules*.

---

## Before you start

Show the user this checklist and **wait for a yes**:

1. This adds Geoffy to your **custom-built storefront** (your own Next.js or Astro app).
   If you run a themed Shopify store or WordPress, this is the wrong integration — use
   your Geoffy dashboard instead.
2. You have a `GEOFFY_SITE_KEY` from your site's settings in Geoffy. If not, get one
   first: it is free, and nothing below works without it.
3. Your site is deployed at a public https domain, or will be. Geoffy verifies ownership
   by fetching it.
4. I will add routes and one component, and modify your `robots.txt`. I will not change
   your product page's existing markup.

If they have no site key, stop. Point them at <https://geoffy.ai/docs/headless> and offer
to continue once they have one.

---

## Step 1: Read the project before you ask anything

Derive everything you can. Only ask about what you genuinely cannot determine — see
Step 2.

| Determine | How |
|---|---|
| Framework | `next` or `astro` in `package.json` dependencies |
| Package manager | Lockfile: `bun.lock` → bun, `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, else npm |
| Next.js router | `app/` directory → App Router. **Pages Router is not supported** — stop and say so |
| Source root | `src/app` and `src/pages` are as common as the root forms. Match what exists |
| TypeScript | `tsconfig.json` present |
| Product route | Glob for a dynamic segment under a products-ish path, e.g. `app/**/products/[*]/page.tsx`, `src/pages/**/products/[*].astro` |
| Existing canonical | Does the product page set `alternates.canonical` (Next) or `<link rel="canonical">` (Astro)? |
| Existing Product JSON-LD | Grep the product page for `"@type": "Product"` (or `"@type":"Product"`) or an imported helper that builds one (`productJsonLd`, `ProductJsonLd`, …). Note whether it carries an `@id` |
| Existing robots | `app/robots.ts`, `app/robots.txt/route.ts`, `src/pages/robots.txt.ts`, or `public/robots.txt` |
| Astro render mode | `output` in `astro.config.*`, and whether an adapter is configured |
| Locales | A `[lang]`/`[locale]` segment, or an i18n config |
| Commerce backend | Shopify client packages in `package.json` (`@shopify/storefront-api-client`, `shopify-buy`, …) |

Report what you found in two or three lines. Do not ask the user to confirm things you
just read off disk.

---

## Step 2: Ask the open questions — once, together, with a recommendation

Some choices change the code and **cannot** be derived. Ask about those, and only those.

**Ask them all in ONE message, before writing any code.** Do not interleave questions with
edits: the user answers once and you work uninterrupted, and they can see the whole shape
of the change before it starts.

For each question: state what you found, recommend one option **with the reason from their
codebase**, and list the real alternatives. Format:

```
Q1. <the question>

    Found: <evidence from their project>
    Recommend: <option> — <why, given that evidence>
    Alternatives: <option> — <when that is the right call>
```

End with: *"Reply with your choices, or say 'recommended' to take all of them."*

### The questions that are worth asking

Ask a question only if the trigger applies. Skip the rest silently.

**Q. Which kind of site is this?** — *always ask, unless Shopify packages make it obvious.*
Decides what `handle` means and how their catalogue reaches Geoffy.
- *Shopify behind your storefront* — products already synced; `handle` is the Shopify
  product handle.
- *No commerce platform* — Geoffy crawls their product pages; `handle` is the segment
  their URL pattern captures, shown in the dashboard.
- Recommend the first if you found a Shopify client, the second otherwise, and say which
  package you saw.

**Q. Is `<found path>` the product page?** — *ask when zero or multiple candidates match.*
With one confident match, say which file you will edit and move on.

**Q. Where should the widget appear?** — *always ask.* It is a visual decision on their
page and you should not make it silently. Name the components you found in their product
page and recommend a position — usually after the description and before reviews or
related products. Say that the position is entirely theirs and Geoffy only checks that it
is present.

**Q. Your Astro site is `output: 'static'`. Switch to on-demand rendering?** — *Astro only,
when static.* A static build **cannot serve the `/apps/geoffy` namespace at all**, and its
content only updates on a rebuild.
- *Add an adapter and serve on demand* — recommend this if their catalogue changes;
  they get the namespace and hourly refresh.
- *Stay static* — recommend if the catalogue is genuinely fixed. Then **skip Step 6
  entirely** and tell them their guides and product twins stay on Geoffy's domain rather
  than theirs.
- This one has a real cost either way. Do not decide it for them.

**Q. Mount the revalidate route?** — *Next.js only.* Publishing reaches their pages within
an hour without it; this makes it seconds. Recommend yes — it is one file (code in Step 5,
*The revalidate route*) — and note it needs a secret from their Geoffy settings.

**Q. Your site has locales. Which one is Geoffy publishing for?** — *only when you found a
locale segment.* Geoffy publishes against **one canonical URL per product**, in one
language. Recommend passing both `locale` (the locale the route renders) and `canonicalUrl`.
Then the component renders only on the published locale and nothing on the others, so
mounting it everywhere is safe. On the other locales it makes no request to Geoffy. This
needs `@geoffy/headless` 0.2.0 or later; older versions ignore `locale`.

  After mounting, **verify this on a non-canonical locale directly** rather than trusting
  it from reading the code — `curl` the same product on a locale that is *not* the
  published one and confirm no `data-geoffy` markup and no Geoffy `Product` node appear.
  The safety mechanism depends on the `canonicalUrl` you pass actually differing per
  locale (i.e. on a real per-locale canonical, not one URL reused across all of them); if
  it does not differ, every locale looks like a match and the published locale's widget
  and JSON-LD render under every translation. Passing `locale` catches that case too, as long
  as the published content's language differs from the page's. This has been seen to slip
  through in practice, so treat it as unverified until you have curled it.

**Q. You already have `<robots file>`. Replace it with a route that merges Geoffy's rules?**
— *only when a robots file exists.* Their own rules are preserved either way; you are
changing how the file is produced. Recommend yes, and show them the merged result before
deleting anything.

**Q. Your product page already emits a `Product` JSON-LD node. What happens to it?** —
*only when Step 1 found one.* This is not a decision to make silently, and the answer is
almost never "remove it" — see *Two `Product` nodes on one page* below for the full
reasoning. Format it like the others:

```
Found: <file/helper> emits its own `Product` node<, with @id <value>> for <fields it carries
        — price, offers, availability, sku, brand, …>.
Recommend: Keep it exactly as it is. Geoffy's node is a supplement, not a replacement — it
        never emits offers/price/availability, so the native node is the page's ONLY source
        for those. Two Product nodes with different @ids is valid schema.org, not duplicate
        data.
Alternatives: none that are safe. Commenting the native node out to "avoid duplication" is
        the one thing to actively avoid — it removes commerce data Geoffy will never supply.
```

Do not ask this as an open question the way the others are asked — state the recommendation
and proceed unless the user pushes back. There is no legitimate alternative today.

### Do not ask about

Anything in the Step 1 table. Package manager, framework, router, TypeScript, file
locations, whether they use `src/` — read it. A question you could have answered yourself
reads as not having looked.

---

## Step 3: Install

Use their package manager.

```bash
npm install @geoffy/headless      # or pnpm add / yarn add / bun add
```

Node 18 or newer. ESM only. React is an optional peer, needed only for the Next.js entry.

---

## Step 4: Environment

```bash
# .env — from their site's settings in Geoffy. Public; safe in a build.
GEOFFY_SITE_KEY=hs_live_...
```

Never invent a key. If they have not given you one, stop and ask.

Next.js only, if they said yes to the revalidate route:

```bash
GEOFFY_REVALIDATE_SECRET=...
```

Leave `GEOFFY_ORIGIN` unset unless the user is pointing the build at something other than
`https://api.geoffy.ai` — most commonly a locally-run Geoffy API during development against
that codebase, not something an ordinary merchant integration needs:

```bash
GEOFFY_ORIGIN=http://localhost:3000
```

It is real but undocumented on the public site — it exists as a code comment in the
package's client, not in any guide a merchant would read. Only reach for it when the user
tells you they are developing against a non-production Geoffy API.

Add both `GEOFFY_SITE_KEY` and `GEOFFY_REVALIDATE_SECRET` to `.env.example` if one exists.
`GEOFFY_ORIGIN` does not belong there — it is a local escape hatch, not a value a real
deployment should ever set. Never commit real values.

---

## Step 5: The product page

Mount at the position they chose in Step 2. Pass the canonical they already build — do not
construct a second copy of that logic.

**Next.js (App Router):**

```tsx
import { GeoffyProduct } from "@geoffy/headless/next";

// inside the product page component, where they chose:
<GeoffyProduct
  siteKey={process.env.GEOFFY_SITE_KEY!}
  handle={handle}
  locale={locale}            // only if the site has locales: the locale this route renders
  canonicalUrl={canonical}
/>
```

It is an async **server component**. It renders the widget and the structured data
together, and renders nothing when Geoffy has nothing published or is unreachable. Mount it
on every product page: a page for an unpublished product makes no request to Geoffy.

**Astro:**

```astro
---
import { getGeoffyProductMarkup } from "@geoffy/headless/astro";

const geoffy = await getGeoffyProductMarkup(
  { siteKey: import.meta.env.GEOFFY_SITE_KEY },
  handle,
  { canonicalUrl: canonical, locale }, // locale: only if the site has locales
);
---

{geoffy && <Fragment set:html={geoffy.jsonLdScript} slot="head" />}
{geoffy && <div data-geoffy-widget set:html={geoffy.widgetHtml} />}
```

`slot="head"` only reaches the head if their `Layout.astro` declares `<slot name="head" />`
inside its `<head>`. **Check.** If it does not, either add it or say the structured data
will render in the body — which still works.

If the product page has no canonical at all, add one. Without it Geoffy refuses to publish,
and in the Next.js App Router a missing `generateMetadata` means every product page
inherits the layout's metadata and claims to be the home page.

---

### The revalidate route (Next.js only, if the user said yes in Step 2)

What it is: Geoffy **calls this**, not the other way round. Whenever a product or a guide
changes, it POSTs an HMAC-signed request to `https://<verified-domain>/api/geoffy/revalidate`
so the merchant's ISR cache purges within seconds instead of on its own schedule
(`revalidateSeconds`, default one hour). It is purely an optimisation — losing it degrades
"seconds" back to "within the hour", nothing more, so a missed call is never the reason
content fails to go live.

Every publish, a product or a guide, also refreshes `llms.txt`, `llms-full.txt` and
`agents.md`. Before `@geoffy/headless` 0.2.0 a guide publish did not, and a new guide stayed
missing from `llms.txt` for up to the revalidate window. If the project pins an older
version, upgrade it.

```ts
// app/api/geoffy/revalidate/route.ts   <- this exact path; Geoffy calls it, not you
import { revalidateTag } from "next/cache";
import { createGeoffyRevalidateRoute } from "@geoffy/headless/next";

export const POST = createGeoffyRevalidateRoute({
  secret: process.env.GEOFFY_REVALIDATE_SECRET!,
  revalidateTag,
});
```

Pass `revalidateTag` **bare, straight from `next/cache`**, and do not wrap it. Next 16 takes a
second argument — a cache-life profile — and **the package supplies it**: `{ expire: 0 }`, which
is Next's own guidance for an invalidation arriving from outside a Server Action, where
`updateTag` is unavailable. The config type takes that second parameter as optional, so Next
15's one-argument `revalidateTag` and Next 16's two-argument one are both assignable bare.

Do not add a profile of your own here. `"max"`, which Next's examples recommend, means the first
visitor after a publish is still served the **pre-publish** page while the refresh runs behind
it — the exact delay this route exists to remove. If you are on `@geoffy/headless` 0.1.0 or
earlier the package still makes the deprecated one-argument call, which Next treats as
`{ expire: 0 }` anyway; upgrade the package rather than wrapping the function.

- **The path is part of the contract.** Geoffy builds the address it calls from the domain
  verified in Step 11 — there is no field anywhere to override it. Mounting the handler
  anywhere else means nothing ever calls it, silently, because a missed invalidation is
  survivable and is never reported as a publish failure.
- `GEOFFY_REVALIDATE_SECRET` is issued by Geoffy and shown on the site's settings page —
  copy it, never invent it (see Step 4).
- If `next build` reports a type error assigning `revalidateTag`, that is a real signal the
  installed `@geoffy/headless` version predates the Next 16 fix — upgrade the package rather
  than reaching for a wrapper. A wrapper compiles, so it hides the version skew instead of
  reporting it.

---

### Two `Product` nodes on one page — and why that is correct, not a bug to fix

A storefront that already renders its own `Product` JSON-LD is common, and adding
`<GeoffyProduct>` puts a **second** `Product`-typed node on the same page. The instinct is
to treat this as duplicate data and remove one of them. Do not act on that instinct.

**Geoffy's node is a content supplement, never a commerce record.** By design it carries
its own `@id` — `<canonical-url>#geoffy-enrichment` — and it never emits `offers`, `price`,
`priceCurrency`, or `availability`. Those fields belong to the storefront's own node, which
is live and changes on every sale; Geoffy's is generated periodically and would go stale
the moment it tried to own a price. So the native node is not redundant with Geoffy's — it
is the page's **only** source for anything commerce-shaped. Comment it out, and the page
still has a Product node, but one that can no longer say what the product costs or whether
it is in stock.

Two `Product`-typed nodes with **different** `@id`s is valid schema.org and unambiguous to
a parser — each node identifies something distinct. This is a settled design decision on
Geoffy's side: an earlier version tried to make the two
nodes merge by reusing the native node's `@id`, and Google's Rich Results Test showed it
does not merge same-`@id` nodes across separate `<script>` tags — it surfaced Geoffy's node
as a second, nameless product instead, which is worse than two clearly separate nodes.

**On a custom-built storefront, the two nodes cannot be linked yet, and that is expected.**
Geoffy's server-side emitter supports an `about: { "@id": nativeNodeId }` field that
associates its node with the storefront's own — but that requires the platform to tell
Geoffy what the native node's `@id` is, and today that detection only exists for the
Shopify and WooCommerce publish paths (theme inspection / WooCommerce's deterministic
`${permalink}#product`). The headless/custom-storefront publish path has no equivalent
step, and `@geoffy/headless`'s `fetchGeoffyProduct` has no option to pass a native `@id`
through either. So on this integration Geoffy's node will stand alone, with no `about`
link — that is the current, correct behaviour of the package, not something to route
around by deleting the other node.

So, on finding an existing `Product` node:

- **Leave it exactly as it is.** Mount `<GeoffyProduct>` alongside it; do not comment out,
  delete, or modify the native node or its imports.
- **Never merge the two by hand** — do not copy fields from one into the other, and do not
  give Geoffy's script tag the native node's `@id`. Both repeat the mistake described
  above in a new shape.
- If the user is uneasy about two `Product` nodes, the correct next step is validating the
  rendered page with Google's Rich Results Test or the [schema.org
  validator](https://validator.schema.org/) with **both** nodes present — not removing
  one on a guess.
- Say plainly, in your handback, that the storefront's own commerce fields (price,
  availability, offers) are unaffected and continue to be owned by the native node.

---

## Step 6: The site-wide files

Three files, one line of logic each. Match their source layout.

**Next.js** — `app/llms.txt/route.ts`, and the same for `llms-full.txt` and `agents.md`:

```ts
import { createGeoffyTextRoute } from "@geoffy/headless/next";
export const GET = createGeoffyTextRoute({ siteKey: process.env.GEOFFY_SITE_KEY! }, "llms.txt");
```

**Astro** — `src/pages/llms.txt.ts`, and the same two siblings:

```ts
import { createGeoffyTextEndpoint } from "@geoffy/headless/astro";
export const GET = createGeoffyTextEndpoint(
  { siteKey: import.meta.env.GEOFFY_SITE_KEY },
  "llms.txt",
);
```

Write **all three**. They serve one document under three names because three conventions
exist; mounting one leaves agents looking for the others with nothing.

---

## Step 7: The Geoffy namespace

One catch-all route puts every remaining Geoffy surface on their domain — plain-text
product twins, buying guides, and a sitemap of both. Without it that content is served from
Geoffy's domain, and the citations it earns go to Geoffy rather than to them.

**Next.js** — `app/apps/geoffy/[...path]/route.ts`:

```ts
import { createGeoffyProxyRoute } from "@geoffy/headless/next";
export const GET = createGeoffyProxyRoute({ siteKey: process.env.GEOFFY_SITE_KEY! });
```

**Astro** — `src/pages/apps/geoffy/[...path].ts`:

```ts
import { createGeoffyProxyEndpoint } from "@geoffy/headless/astro";

export const prerender = false;   // REQUIRED — the build fails without it
export const GET = createGeoffyProxyEndpoint({ siteKey: import.meta.env.GEOFFY_SITE_KEY });
```

Skip this step entirely if they chose to stay static in Step 2.

---

## Step 8: Crawler rules

**Append** Geoffy's block to their `robots.txt`. Never replace their file.

```ts
import { fetchGeoffyText } from "@geoffy/headless";

const YOUR_RULES = `User-Agent: *
Disallow: /admin
`;
const YOUR_GLOBAL_RECORDS = `Host: https://yourdomain.com
Sitemap: https://yourdomain.com/sitemap.xml
`;

export async function GET() {
  const geoffyRules = await fetchGeoffyText(
    { siteKey: process.env.GEOFFY_SITE_KEY! },
    "robots-rules.txt",
  );
  // null means Geoffy was unreachable — serve THEIR rules anyway.
  const body = [YOUR_RULES, geoffyRules ?? "", YOUR_GLOBAL_RECORDS].filter(Boolean).join("\n");
  return new Response(body, {
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
}
```

Carry across the rules from whatever they had before, verbatim. Then delete the old file —
`app/robots.ts`, or `public/robots.txt`, which wins over any route and would silently keep
serving the old content.

**Order matters.** Geoffy's block contains a `User-agent` group, so it belongs *with their
other groups*, and their global records (`Host:`, `Sitemap:`) go **last**.

---

## Step 9: The IndexNow key

Their Geoffy settings show an IndexNow key. Save it at the site root, named after itself:

```
public/{key}.txt        # contents: the key, nothing else
```

The namespace only authorises what sits under it, so without this file every index ping
for a **product page** is discarded — silently, with nothing failing anywhere. Ask them for
the key; do not guess one.

---

## Step 10: Verify what you can

**Every check Geoffy runs against the merchant's own site — ownership verification, the
namespace-mount probe, the post-publish presence check, and the `/ingest` crawl — refuses
anything that is not a public https address.** `localhost`, private/reserved addresses, and
non-default ports are all rejected outright, deliberately, with no override. So none of
those checks can ever pass against `localhost:3000`, whatever port the dev server uses —
this is not a bug to work around, and there is no local-only escape hatch. To test any of
it before deploying, put a public https tunnel in front of the dev server (e.g.
`cloudflared tunnel --url http://localhost:<port>`) and use the tunnel's hostname
everywhere the site's own domain would go — as the site address entered in Geoffy, and as
the value the product page's canonical is built from. If those two disagree, publishing
succeeds but the page is held at `canonical_broken`.

Build first. Then, against their dev server, tunnel, or deployment:

```bash
curl -s <their product URL> | grep -c 'data-geoffy'
curl -si <their domain>/apps/geoffy/sitemap.xml | head -20
curl -s <their domain>/llms.txt | head -20
```

The first is the one that matters: greater than zero means the markup is in the
**server-rendered** HTML. Browser devtools cannot tell you this — they show the page after
JavaScript has run, which is precisely what most AI crawlers never see.

**Zero is expected** if ownership is not yet verified or nothing is published. Say so
plainly rather than treating it as a failure.

**A `curl` against the running dev server can also lie, in the other direction — reporting
stale state as if it were current.** Next.js's dev fetch cache lives in `.next/dev/cache`
(not `.next/cache`, which clearing does nothing to fix) and can hold the proxy's outbound
fetch for up to `revalidateSeconds` (defaults to one hour). So a product that was just
published, or a namespace mount that was just confirmed, can still read as absent for up to
an hour after it changed. Before concluding anything about Geoffy's state from a local
fetch: `rm -rf .next/dev/cache` and restart the dev server, or pass a short
`revalidateSeconds` (for example `10`) to the helpers while you integrate.

---

## Step 11: Hand back

Tell the user exactly what remains, because none of it is yours to do:

1. **Deploy.** Geoffy verifies against the live site, not localhost.
2. **Prove ownership** — serve the token at
   `/.well-known/geoffy-site-verification.txt` and run the check in Geoffy.
3. **Generate and publish** in the dashboard. This is the step that costs money.
4. **Watch the state.** Products stay pending until Geoffy fetches the live page and finds
   the markup. `code_not_added` means it is not there or renders on the client;
   `stale_version` clears itself within the hour; `canonical_broken` means the page does
   not name itself canonical.

Link them to <https://geoffy.ai/docs/headless> for the reasoning behind any of it.

---

## Critical rules

- **Never render Geoffy content on the client.** No `next/script` with a client strategy,
  no `onMount` injection, no client component. It will look right in a browser and be
  invisible to the crawlers this exists for.
- **Never sanitise `widgetHtml`.** Its CSS travels inside it as a `<style>` element; a
  sanitiser that strips that leaves the widget unstyled on a live site.
- **Never remove, comment out, or modify a storefront's existing `Product` JSON-LD to make
  room for Geoffy's.** Geoffy's node is a supplement and by design never carries
  `offers`/`price`/`availability` — the native node is the page's only source for those.
  Two `Product` nodes with distinct `@id`s on one page is valid and expected. See *Two
  `Product` nodes on one page* above.
- **Never test ownership verification, the namespace probe, or the presence check against
  `localhost`.** They refuse any non-public-https address unconditionally — this needs a
  tunnel, not a workaround.
- **Never use a `next.config.js` rewrite for the root files.** A catch-all segment answers
  `/llms.txt` with the home page and HTTP **200**, which a crawler reads as success.
- **Never omit `export const prerender = false`** on the Astro namespace route. The build
  fails outright.
- **Never replace the user's `robots.txt`.** Append, and carry their rules across verbatim.
- **Never change the mount path from `/apps/geoffy`.** Geoffy probes that exact path.
- **Never invent a site key, an IndexNow key, or a product handle.** Ask.
- **Never commit `GEOFFY_REVALIDATE_SECRET`.**
- **Never conclude the integration is broken because nothing renders.** Check whether
  ownership is verified and anything is published first. A 404 from Geoffy before
  verification, and `published: false` for an unpublished product after it, are documented
  states, not defects in your work.
- **Never claim you have finished the setup.** You finished the *code*. Verification and
  publishing are the user's, and saying otherwise leaves them thinking it is live when it
  is not.

It works out your framework, your router and your product page from your project, asks only what it genuinely cannot determine, and writes the integration. It hands back at the two steps that need you: proving you own the domain, and publishing.

Each guide is complete on its own. Pick one and you will not need to come back here.

SurfaceWhere it appears
The visible Geoffy widgetOn your product page, where you place the component
schema.org product dataOn the same page, server-rendered
llms.txt, llms-full.txt, agents.mdAt the root of your own domain
A plain-text version of each productOn your own domain, under /apps/geoffy/
Your buying guides, and their plain-text twinsOn your own domain, under /apps/geoffy/
A sitemap of the Geoffy content on your siteOn your own domain, under /apps/geoffy/
Crawler rules for AI agentsLines you merge into your own robots.txt
Search-engine index pingsSent by Geoffy when you publish

Everything is server-rendered. That is not a preference — AI crawlers largely do not run JavaScript, so anything added to the page after hydration is invisible to the systems this is for.