Skip to content

The product page

app/[lang]/products/[handle]/page.tsx
import { GeoffyProduct } from "@geoffy/headless/next";
export default async function ProductPage({ params }) {
const { lang, handle } = await params;
return (
<>
<YourProductUI handle={handle} />
<GeoffyProduct
siteKey={process.env.GEOFFY_SITE_KEY!}
handle={handle}
locale={lang}
canonicalUrl={`https://yourdomain.com/${lang}/products/${handle}`}
/>
</>
);
}

The file path above is an example, not a requirement. Your product route can be as deep as you like: nothing in this package reads the URL, because you pass handle explicitly.

Your setuphandle is
Shopify behind your storefrontthe Shopify product handle, the same value you already query the Storefront API with
No commerce platformthe segment your product URL pattern captured. Geoffy shows you the handle it recorded next to each crawled product, so you can copy it rather than guess

See which kind of site is yours.

canonicalUrl is optional and worth passing

Section titled “canonicalUrl is optional and worth passing”

Geoffy publishes content per page, against that page’s canonical URL. Pass canonicalUrl so Geoffy answers for this page. Two pages may share a handle, such as an untranslated slug in two locales or two categories that end in the same word. This needs @geoffy/headless 0.3.5 or later.

Without canonicalUrl, Geoffy answers by handle alone with the page published last. On another page with the same handle, that can be the wrong content.

If Geoffy has nothing published for this page, the component renders nothing. When it declines because the content belongs to another page, or is in another language, it also leaves an inert data-geoffy-skipped marker for diagnosis from view-source.

Pass the same value you already build for generateMetadata rather than a second copy of the logic.

If your site serves more than one language

Section titled “If your site serves more than one language”

Pass locale and canonicalUrl. Each published locale renders its own content; a page with nothing published renders nothing.

<GeoffyProduct
siteKey={process.env.GEOFFY_SITE_KEY!}
handle={handle}
locale={lang} // the locale this route renders, e.g. "sr" or "de-DE"
canonicalUrl={canonical} // this page's own canonical
/>

If the published content has a different language from the page, the component declines without a product request to Geoffy when the published-page list is available. Only a clear language mismatch hides content; an unknown language does not.

Several locales under one Geoffy site work when each locale is its own page. Give Geoffy a URL pattern per locale, then publish the pages you want. Each page gets content in its own language. Pass that page’s own canonicalUrl and locale to select its content and check its language.

Two locales may share a handle when you pass canonicalUrl. This needs @geoffy/headless 0.3.5 or later. Older versions show a shared handle on one page only.

A worked example, with the same slug in two languages: several languages on one site.

<GeoffyProduct> renders nothing when Geoffy has nothing published for the handle, when Geoffy is unreachable or erroring, and when locale or canonicalUrl does not match.

A product with nothing published is a normal state, not an error, and a page for it makes no request to Geoffy. So you can mount the component on every product page, published or not.

That is deliberate and absolute: your storefront is your business, and if we are having a bad minute the correct behaviour is for your product page to render exactly as it would have without us. The fetch carries its own timeout for the same reason: a revalidation that never settles holds a worker, which is worse than one that fails.