If you run a website that serves more than one country, sooner or later you hit the same quiet problem: content meant for one market ends up on another market’s pages. A US product page written in British English with prices in pounds. German copy sitting on the page that was supposed to be Austrian. Rest-of-world boilerplate on a page that should have been localised for a specific country.
I work in iGaming, so my worked example below is a multi-market casino, and the mismatch I kept finding was Australian content on New Zealand pages. But nothing here is casino-specific, and you do not need to know anything about gambling to use it. The exact same setup works for an e-commerce store with country subfolders, a SaaS site with localised landing pages, a travel site, a marketplace, anything that serves different content to different geographies. If your site has a /de/, a /fr/ and an /en-us/, or country subdomains, this is for you. Swap my Australia and New Zealand for whatever markets you run.
Nobody creates these mismatches on purpose. They happen through copy-paste, through templates, through a page cloned from another market and never fully rewritten, through a translation step that got skipped. And they are invisible until you go looking. The page loads, the layout is fine, the offer is there. You only notice it is wrong when you actually read the words and realise they are talking to the wrong audience. On a handful of pages you can do that by hand. On a few thousand you cannot.
So I built a check that does it during a crawl. It uses Screaming Frog to crawl the site and Gemini to read each page and tell me whether the content matches the market that page is supposed to serve. While I was at it I added two more things: a plain copywriter’s verdict on each page, and a separate check for internal links that leak across locales. Everything runs in one crawl, and on the cheapest Gemini model it costs next to nothing.
This post is the full write-up of how it is wired together. I have used dummy domains throughout (example.com, example5.com), so swap in your own.
The setup at a glance
My example site uses path-based locales:
/en-au/for Australia/en-nz/for New Zealand- everything at the root (no locale segment) is treated as rest of world
Your scheme will be different, and that is fine. It might be /en-us/ and /en-gb/, or /de/ /fr/ /it/, or country subdomains. The logic below only cares that the market is encoded somewhere in the URL in a predictable way. Adjust the patterns to match yours.
Three moving parts do the work:
- A small custom JavaScript snippet that reads the URL and outputs the expected market for each page.
- A Gemini prompt that reads the page content and checks it against that expected market, and also gives a quick quality verdict.
- A second custom JavaScript snippet that flags internal links pointing into a different locale than the page they sit on.
You need a Screaming Frog licence (the free version will not run custom JavaScript or the AI integration) and a Gemini API key from Google AI Studio. That is it.
A word on the model, because it moved
I originally set this up on gemini-2.5-flash-lite because it was the cheapest thing going. Halfway through, Google closed it to new projects ahead of its retirement, and I got a lovely 404 - model no longer available to new users. If you hit that, do not fight it, just move to a current-generation Lite model.
As of the end of July 2026 I am using gemini-3.1-flash-lite, which is the cheapest current model and has a free-tier row. If that ever refuses to attach on your key, gemini-3.5-flash-lite is the generally-available fallback and only marginally more expensive. The Lite models are the right pick here for two reasons: price, and the fact that they default to minimal “thinking”, so you are not silently billed for reasoning tokens on what is really a classification job.
One habit worth keeping: check the exact model ID against your own AI Studio dashboard rather than trusting any blog (including this one). The names and free-tier status have been changing every few weeks.
Before you crawl anything: exclude the thin templated pages
This is the tip I wish someone had given me first. Most large sites have an enormous number of near-identical, templated pages that carry no unique, market-specific content. On a casino it is often the individual game pages: same template, same boilerplate, a title and a thumbnail, multiplied by thousands. On an e-commerce store it is product variants and filtered category permutations. On a listings site it is the individual listings. Whatever your equivalent is, crawling all of it means thousands of URLs, thousands of Gemini calls, and a mountain of results that tell you nothing.
So I excluded them before starting. On the free tier this matters even more, because you are capped at roughly a thousand requests a day, and you do not want to burn the whole quota on pages that were never going to have a localisation problem worth finding.
Use Config > Exclude and add a regex that matches the path you want to skip, something like:
https://example\.com/.*/games/.*
Adjust to whatever your throwaway URLs actually look like. The point is to spend your crawl (and your tokens) on pages that have real, market-specific copy: landing pages, category pages, promotions, checkout and payment info, about, terms. Those are where localisation mistakes actually live.
Part 1: the market extraction
This is a custom JavaScript snippet. Go to Config > Custom > Custom JavaScript, click Add, name it Market, and set the content type to text/html.
function marketFromUrl() {
const p = location.pathname.toLowerCase();
if (/\/en-nz(\/|$)/.test(p)) return seoSpider.data('New Zealand');
if (/\/en-au(\/|$)/.test(p)) return seoSpider.data('Australia');
return seoSpider.data('Rest of world');
}
return marketFromUrl();
That is the whole thing. It runs on every page, reads that page’s own path, and outputs the market. The regex (\/|$) is there so it matches both /en-nz/some-page and a bare /en-nz at the end of the path. To adapt it, change the locale segments and the labels to your own, and add a line per market.
The reason I do this in JavaScript and not in the AI prompt is important. The expected market is a fact you already know from the URL. It is not something you want an AI to guess. If you ask the model “what country is this page for”, it will start reasoning from vocabulary in the content, and that is exactly the trap I fell into (more on that below). Deriving the market deterministically from the URL is free, instant, and always right. The AI only gets involved to check whether the content lives up to it.
Part 2: the Gemini prompt
Go to Config > API Access > AI, pick Gemini, paste your API key on the Account Information tab, set the model, and click Connect. On the Advanced tab, set the requests per minute. On the free tier I keep it around 10, comfortably under the limit so I do not trigger throttling errors mid-crawl.
Then, on the Prompt Configuration tab, add a prompt. To feed it the market value from Part 1, click the cog on the prompt, tick Advanced Prompt, and add two targets: the Market extraction and Page Text. Screaming Frog will insert tokens for these, something like {CUSTOM_JAVASCRIPT1|} for the extraction and {PAGE_TEXT} for the content. These tokens are placeholders, and where you put them is where that data gets injected, so put the market token inline where you refer to the expected market, and put the page text token at the very bottom, so the model reads all the instructions before it meets the page body.
You will also need Store HTML enabled (Config > Spider > Extraction) because the prompt uses Page Text, and JavaScript rendering on (Config > Spider > Rendering) because the custom JS needs it.
Here is my prompt. It is tuned to my case, Australia versus New Zealand, so read it as a worked example rather than something to paste verbatim. Replace the market token with whatever name Screaming Frog gives your Market extraction:
You are reviewing a page from a multi-market website. Do TWO separate jobs and
report both.
The expected market for THIS page is: {market}
JOB 1 - Localisation check.
Find concrete evidence the content belongs to a DIFFERENT market than expected.
Currency handling (important):
- A bare dollar sign or the word "dollars" is NEUTRAL. Locals in both Australia
and New Zealand just say "dollars" or "$" and mean their own currency, so it
proves nothing. Ignore it.
- An explicit foreign currency IS a decisive mismatch signal: euro / EUR,
British pounds / GBP, US dollars / USD, Canadian dollars / CAD, and similar.
- Explicit "AUD" vs "NZD" is a valid signal when written out, but do not expect
it and do not treat its absence as anything.
Other decisive signals:
- Place names: Australia, Australian, NSW, Sydney, Melbourne vs New Zealand,
Auckland, Wellington, Christchurch
- Regulators/law: ACMA and the Interactive Gambling Act = Australia; DIA and the
Gambling Act 2003 = New Zealand
- Phone codes (+61 Australia, +64 New Zealand) and domains (.com.au vs .co.nz)
- Banks or payment methods tied to one country
Neutral, ignore completely:
- A bare "$" or the word "dollars"
- Shared industry terms used in both markets
- British-style spelling (shared by both)
- Generic terminology
If the expected market is "Rest of world", flag content specific to a single
named country (especially Australia); euro is acceptable for Rest of world.
JOB 2 - Copy quality.
Judge the body copy as a conversion copywriter: clarity, persuasiveness,
structure, and strength of the offer and call to action. Give a one-word rating
and one short instruction on the single most useful thing to do next.
Return strictly, nothing else:
VERDICT: MATCH | MISMATCH | UNCLEAR
LOOKS_LIKE: <market the content actually targets, or none>
EVIDENCE: <exact words or phrases, max 20 words>
QUALITY: weak | ok | good
ACTION: <one short instruction, max 12 words>
Adapting the prompt to your own site
The structure is what matters, not my specific signals. To point this at your markets, replace the signal lists with whatever actually distinguishes them:
- Currency: which currencies are neutral on your pages and which are a dead giveaway. On a US-vs-UK site, a pound sign on a US page is the tell.
- Language and spelling variants: en-US vs en-GB (color vs colour, “fall” vs “autumn”), formal vs informal address in German (du vs Sie), European vs Latin American Spanish, and so on.
- Place names, regions, cities.
- Legal, tax and regulatory terms: VAT vs GST vs sales tax, GDPR references, country-specific consumer law.
- Local payment methods, carriers, phone formats, address formats, and date formats (MM/DD vs DD/MM is a classic).
Two principles carry over no matter what industry you are in. First, do not ask the model to classify the market from scratch, tell it the expected market and have it hunt only for evidence of a different one. Second, explicitly list the terms that look like signals but are actually shared across your markets, so it stops treating them as evidence. That second one is the whole game, as you will see next.
The mistakes that shaped this prompt
The prompt did not start this clean. It got there by being wrong in instructive ways, and the lessons are the useful part. They also happen to be the bits that transfer directly to your site, whatever it sells.
Do not let the model classify the market from scratch. My first version asked it to identify the country from the content. It told me a New Zealand page was Australian because it saw a particular gambling term, and that term is used in both countries. The moment you ask a model to classify, it grabs whatever vocabulary is lying around and treats it as evidence, even when it proves nothing. The fix was to flip the whole task: you already know the expected market, so the model’s only job is to hunt for positive evidence of a different one. Shared terms then sit harmlessly in the middle, because they point at no specific market. Your site will have its own version of that shared word, a term or a brand that exists across several of your markets, and it will trip a classifier every time. Name it in the neutral list.
Currency is not what you think. I had the prompt treating A$ and NZ$ as the giveaway. Turns out people in both markets just write “$” and “dollars” and mean their own currency. The explicit prefix is rare in real copy. So a bare dollar is neutral, and the actual signal is a foreign currency: a euro on an Australian page is a dead giveaway that you are looking at generic content that never got localised. The same logic applies to any market pair that shares a currency symbol or a language.
Shared spelling will not save you. Australia and New Zealand both use British-style spelling, so it cannot separate them, exactly the way US and Canadian English overlap heavily. If two of your markets share a language variant, spelling is useless as a signal and you have to lean on the things that genuinely differ: place names, regulators, phone codes, local domains, payment methods.
Make it show its work. The EVIDENCE line forcing the model to quote the exact words it flagged is doing real work. When you sort the results, a bare MISMATCH is a leap of faith, but MISMATCH ... EVIDENCE: "prices shown in euro" you can trust at a glance. And keeping UNCLEAR as an allowed verdict means a thin page with no signals either way lands in UNCLEAR instead of being forced into a false MATCH. Those UNCLEAR pages are often the ones too generic to be properly localised for anyone, which is its own useful finding.
The QUALITY and ACTION lines are the bonus. I wanted a quick copywriter’s gut call on every page, nothing fancy, just “weak”, “ok” or “good” and a one-line nudge like “tighten intro, lead with the offer”. If you find the model parks everything on “ok”, switch it to a 1-to-5 scale, which pushes it to actually discriminate.
Part 3: the cross-locale link check
Separate concern, separate snippet. This one has nothing to do with the AI. The rule is simple: a page under /en-au/ should only link to other /en-au/ pages, an /en-nz/ page only to /en-nz/, and a root (rest-of-world) page should not link into any locale folder. Cross-locale internal links leak equity and confuse geotargeting, and hreflang does not fix sloppy internal linking. This is just as relevant to an e-commerce site whose UK pages accidentally link into the US store.
Another custom JavaScript snippet, named Locale link check, content type text/html:
function localeLinkCheck() {
// Base domains that count as internal (mirrors / CDN). Subdomains like
// www. are handled automatically below.
const INTERNAL_BASES = ['example.com', 'example5.com'];
// Also treat numbered variants (example2.com, example12.com, ...) as internal,
// with or without a subdomain such as www.
const INTERNAL_PATTERN = /(^|\.)example\d*\.com$/i;
const isInternal = (host) => {
const h = host.toLowerCase();
if (INTERNAL_BASES.some(b => h === b || h.endsWith('.' + b))) return true;
return INTERNAL_PATTERN.test(h);
};
const bucket = (pathname) => {
const p = pathname.toLowerCase();
if (/\/en-nz(\/|$)/.test(p)) return 'en-nz';
if (/\/en-au(\/|$)/.test(p)) return 'en-au';
return 'row';
};
const here = new URL(location.href);
const currentBucket = bucket(here.pathname);
const mismatches = [];
for (const a of document.querySelectorAll('a[href]')) {
let u;
try { u = new URL(a.getAttribute('href'), location.href); }
catch (e) { continue; }
if (u.protocol !== 'http:' && u.protocol !== 'https:') continue;
if (!isInternal(u.hostname)) continue;
if (bucket(u.pathname) !== currentBucket) {
mismatches.push(u.origin + u.pathname);
}
}
const unique = [...new Set(mismatches)];
if (unique.length === 0) return seoSpider.data('OK (' + currentBucket + ')');
const shown = unique.slice(0, 25).join(' | ');
const extra = unique.length > 25 ? ' ...+' + (unique.length - 25) + ' more' : '';
return seoSpider.data('MISMATCH (' + currentBucket + ', ' + unique.length + '): ' + shown + extra);
}
return localeLinkCheck();
A clean page returns OK (en-au). A leaky one returns MISMATCH (en-nz, 3): https://... | https://... with the offending targets listed, so you can fix them without re-crawling. As with the market snippet, change the bucket logic to match your own locale segments.
Two things bit me here, and they will probably bite you too:
Your CDN or mirror domains are not “internal” as far as JavaScript is concerned. Screaming Frog might be configured to crawl example5.com as part of the site, but this snippet has its own idea of internal based purely on hostname. If a mirror domain is not in the allowlist, links to it get dropped and the check looks clean when it is not. That is what INTERNAL_BASES and the INTERNAL_PATTERN regex are for. The pattern is handy if you run a numbered domain rotation, since (^|\.)example\d*\.com$ matches example.com, example5.com, example12.com and their www. variants in one line.
The www. trap. I tested on example.com and it worked, then tested on www.example.com and it suddenly found nothing. The links resolved to the www. host, which my first allowlist did not cover. The endsWith('.' + b) check and the (^|\.) in the pattern fix that by accepting any subdomain.
If it ever returns OK when you can see cross-locale links on the page, dump what the snippet actually sees by temporarily returning document.querySelectorAll('a[href]') mapped to .href. Either the links are not real anchors (JS-driven buttons that navigate on click), or their hostname is not in your allowlist. Usually the latter.
Expect language switchers and global footers to throw false positives, since those cross-locale links are often intentional. Once you see the pattern, scope the scan to the main content by swapping the query for (document.querySelector('main') || document.body).querySelectorAll('a[href]'), or skip a known switcher block with if (a.closest('.lang-switcher, footer')) continue;.
Running it
With all three in place, enable JavaScript rendering and Store HTML, point Screaming Frog at the site, and go. Test each piece first with the built-in testers (the play icon on the prompt, the JS tester on the snippets) against one known page per market before committing to a full crawl. It saves you from discovering a wiring mistake three thousand URLs in.
When it finishes you get three columns working together:
- Market, the expected market per URL.
- AI verdict,
MATCH/MISMATCH/UNCLEARplus evidence, and the quality read. - Locale link check,
OKor the list of leaking links.
Filter the AI column for MISMATCH first. That is your list of pages serving the wrong market, finite and specific. Then skim UNCLEAR for pages too generic to belong anywhere. Then work the link check for the internal leaks.
Why bother
Because this is the kind of QA that never gets done otherwise. It is too boring to do by hand and too fuzzy for a normal crawl rule to catch. “Does this page’s copy actually talk to the country it is meant for” is a judgement call, and until recently judgement calls did not scale. Now they cost a fraction of a cent each and run while you get a coffee. That is true whether you run a casino, a shop, or a hundred localised landing pages for a SaaS product.
It is also a nice reminder that the AI is best used as the narrow, fuzzy component inside a mostly deterministic pipeline. The URL logic, the link parsing, the market derivation, all of that is plain code that is free and exact. The model only does the one thing code cannot: read the words and tell you if they feel wrong for the audience. Keep that division of labour and these builds stay cheap, fast and trustworthy.