No commerce platform
Your products live in your own CMS, a database, or a folder of MDX. There is no commerce platform to sync from, so Geoffy builds its catalogue by reading your own pages.
The code on your product page is identical to the Shopify case. What differs is where
handle comes from and what you do in the dashboard first.
Step one is in the dashboard, not in your editor
Section titled “Step one is in the dashboard, not in your editor”-
Geoffy reads your sitemap and proposes URL patterns that look like product pages. You need a working
sitemap.xmlbefore this — if you do not have one, that is the first thing to build. -
You confirm the pattern, for example
/products/:handle. The segment it captures becomes each product’s handle. -
Geoffy crawls the matching pages and records what it could read about each product.
-
You review what it found — per product, including the canonical URL each page declares. This is where a broken canonical shows up — before it reaches anything published.
-
You publish the products worth publishing.
Make your pages readable first
Section titled “Make your pages readable first”The crawl reads what your HTML says. So the quality of what Geoffy has to work with is decided by your product page, before any of this runs:
| Give it | Why |
|---|---|
A real <h1> with the product name | It is the strongest signal of what the page is about |
A <link rel="canonical"> naming this product | Without it the product is unpublishable — canonical_broken |
| Product facts as text, not only in images | A specification table in a JPEG is invisible |
Your own Product structured data, if you have it | Geoffy supplements it rather than duplicating it |
The product page
Section titled “The product page”import { notFound } from "next/navigation";import { GeoffyProduct } from "@geoffy/headless/next";import { getProduct } from "@/lib/catalogue";
const canonical = (handle: string) => `https://yourdomain.com/products/${handle}`;
export async function generateMetadata({ params }) { const { handle } = await params; const product = await getProduct(handle); if (!product) return {}; return { title: product.name, alternates: { canonical: canonical(handle) }, };}
export default async function ProductPage({ params }) { const { handle } = await params; const product = await getProduct(handle); if (!product) notFound();
return ( <article> <h1>{product.name}</h1> <ProductBody product={product} />
{/* `handle` is the segment your URL pattern captured — the same value in your route, your data layer and Geoffy's catalogue. */} <GeoffyProduct siteKey={process.env.GEOFFY_SITE_KEY!} handle={handle} canonicalUrl={canonical(handle)} /> </article> );}When your catalogue changes
Section titled “When your catalogue changes”A new product is not in Geoffy until it is crawled. Re-run the crawl from the dashboard after you add products; the pattern is already saved, so it is one action.
Removing a product from your site does not un-publish it. Unpublish it in Geoffy too, or
its plain-text twin stays reachable under /apps/geoffy/llm/{handle}.md on your own
domain.
The rest of the setup is unchanged
Section titled “The rest of the setup is unchanged”Everything else — the root files, the namespace, robots.txt, the IndexNow key — is identical. Follow the quickstart.