Several languages on one site
Take a school that serves every programme page in German and in English:
| Language | Page | Handle |
|---|---|---|
| German | /de/programmes/data-science | data-science |
| English | /programmes/data-science | data-science |
| German | /de/programmes/psychologie | psychologie |
| English | /programmes/psychology | psychology |
Some slugs are translated, and many are not. That is fine. Geoffy publishes content per
page, against that page’s own canonical URL, and writes it in the language the page
declares. /de/programmes/data-science and /programmes/data-science are two pages to
Geoffy, with two sets of content, even though both end in data-science.
Two pages with the same handle need @geoffy/headless 0.3.5 or later, and each page must
pass its own canonicalUrl. That is how the component tells the two pages apart.
This recipe is for a site with no commerce platform behind it, where Geoffy reads your own pages. With Shopify behind your storefront, Geoffy publishes each Shopify product against one canonical page, built from your path template.
Step one is in the dashboard: one URL pattern per language
Give Geoffy a pattern for each language. Each pattern reads the pages of one language:
/de/programmes/{handle}/programmes/{handle}If your programmes sit one level deeper, give each level its own pattern, with the level written out:
/de/programmes/bachelor/{handle}/de/programmes/master/{handle}/programmes/bachelor/{handle}/programmes/master/{handle}Prefer that over a named segment such as /de/programmes/{level}/{handle}. A named segment
matches every page of that shape, so it also reads a page that is not a programme, such as
/de/programmes/dual/partners. The review step lists every page the crawl found, with its
handle and its canonical URL. Check that list before you publish anything.
The page
Section titled “The page”Call the helper on every language’s page, and pass that page’s own values: the handle your URL pattern captures, the locale the route renders, and the canonical URL the page declares.
---import Layout from "@/layouts/Layout.astro";import ProgrammeBody from "@/components/ProgrammeBody.astro";import { getGeoffyContentMarkup } from "@geoffy/headless/astro";
export const prerender = false;
const ORIGIN = "https://school.example";const { lang, slug } = Astro.params;
// This site serves English without a prefix and every other language under its own.// Build the canonical the same way your site already does; do not keep a second copy.const canonical = lang === "en" ? `${ORIGIN}/programmes/${slug}` : `${ORIGIN}/${lang}/programmes/${slug}`;
const geoffy = await getGeoffyContentMarkup( { siteKey: import.meta.env.GEOFFY_SITE_KEY }, slug!, { canonicalUrl: canonical, locale: lang },);---
<Layout lang={lang}> <link rel="canonical" href={canonical} slot="head" /> {geoffy && <Fragment set:html={geoffy.jsonLdScript} slot="head" />}
<article> <ProgrammeBody lang={lang} slug={slug} /> {geoffy && <div data-geoffy-widget set:html={geoffy.widgetHtml} />} </article></Layout>getGeoffyContentMarkup is getGeoffyProductMarkup under a name for a page that is not a
product. Use whichever reads truthfully on your site.
| Value | Why |
|---|---|
handle | The segment your URL pattern captured. The same value can occur in several languages |
canonicalUrl | Selects this page’s content. It must be the page’s own canonical, so it differs per language |
locale | A second check: content in another language than the page is never shown |
What each page gets
| Page | Widget and structured data | Plain-text copy |
|---|---|---|
/de/programmes/data-science | The German content | /apps/geoffy/llm/de/programmes/data-science.md |
/programmes/data-science | The English content | /apps/geoffy/llm/programmes/data-science.md |
/de/programmes/psychologie | The German content | /apps/geoffy/llm/de/programmes/psychologie.md |
| A page with nothing published | Nothing | No copy (404) |
Publishing one page never changes what another page shows, in whichever order you publish.
Each plain-text copy lives at its page’s own path under /apps/geoffy/llm/, and names its
page as canonical in a Link header. The Geoffy sitemap on your domain lists every copy.
Publish the languages you want
Each page is generated and published on its own, in the language its page declares. Publish the German pages first and the English ones later if that suits you. A page you have not published renders nothing.
Check it
Run these against your deployed site:
# Each page carries its own markup, in the server-rendered HTMLcurl -s https://school.example/de/programmes/data-science | grep -c 'data-geoffy'curl -s https://school.example/programmes/data-science | grep -c 'data-geoffy'
# Each plain-text copy names its own page as canonicalcurl -s -D - -o /dev/null https://school.example/apps/geoffy/llm/de/programmes/data-science.md | grep -i '^link'# link: <https://school.example/de/programmes/data-science>; rel="canonical"Both counts are greater than zero once both pages are published. A count of zero on a page
you published means the package is older than 0.3.5, or the page did not pass its own
canonicalUrl.
With an older package
Before 0.3.5 the package asks Geoffy by handle alone. Two pages with one handle then share one answer: the page published last. The other page renders nothing. Upgrade the package; your code does not change.
On a page that declines, the helper returns null, so the {geoffy && …} guards render
nothing. There is no marker on this side.
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. If there is no commerce platform behind your site, read no commerce platform for how Geoffy reads your pages.