How to Capture Screenshots of Hreflang Landing Pages for International SEO
Capture consistent screenshots of locale-specific landing pages, inspect Google’s rendered view when needed, and validate hreflang separately.
Capture each locale-specific landing page from its explicit URL, using the same viewport, scale, capture scope, and page state for a fair visual comparison. A screenshot records how a page appeared; it does not prove that the hreflang annotations or alternate URL set are correct. Validate those separately.
For a visual record, a browser screenshot is usually the right starting point. Capture the initial viewport to compare first impressions, a full-page image when below-the-fold content matters, or a selected element when the evidence concerns one region. When you need Google’s inspection rendering, run a successful live URL Inspection test in Search Console and save its rendered screenshot. Record which renderer produced each image.
1. Build a locale URL manifest
List the intended language or language-region target and the exact landing-page URL for every variant. Google recommends distinct URLs for language versions and hreflang annotations that associate the versions. Do not rely on browser language, cookies, or automatic redirection to expose every version: Googlebot generally does not vary its location or send an Accept-Language header to discover locale-adaptive content. Capture the explicit URLs directly. Google’s guidance on multi-regional and multilingual sites explains these signals.
locale,url,language_region,notes
en-US,https://example.com/en-us/products/,en-US,US English
en-GB,https://example.com/en-gb/products/,en-GB,UK English
fr-FR,https://example.com/fr-fr/produits/,fr-FR,French for France
x-default,https://example.com/language/,fallback selector
Replace these example URLs with the actual canonical landing pages you intend to compare. Include x-default only when you have a genuine fallback destination, such as a language-selector page. In the manifest, note the audience, expected visible language, and any material state needed to reach the page, such as authentication or a consent choice. This is a reproducibility aid, not an SEO audit.
2. Choose the screenshot scope and keep it consistent
| Capture scope | Use it for | Keep consistent |
|---|---|---|
| Viewport | Initial screen, headline, hero, navigation, consent state | Viewport width and height, device scale, scroll position |
| Full page | Below-the-fold content, footer, long landing-page structure | Full-page mode, viewport and page state |
| Element | A particular content block, locale selector, price or navigation region | CSS selector and element visibility |
Playwright supports viewport, full-page, and selected-element screenshots, with CSS-pixel or device-pixel scaling. For visual comparisons, choose one mode and one scale for every locale. A CSS-pixel image preserves the browser CSS coordinate system; device-pixel scaling can produce a higher-resolution image. Record the choice so a difference in image dimensions is not mistaken for a layout difference. See the Playwright screenshot documentation.
3. Capture with Playwright
The following runnable Node.js example reads a CSV manifest, opens each explicit URL, waits for the page to settle, and saves one full-page PNG per locale. It uses a fixed viewport and CSS-pixel scale. Install Playwright and its Chromium browser first:
npm init -y
npm install playwright
npx playwright install chromium
Save the manifest above as locales.csv, then save this script as capture.mjs and run node capture.mjs. The CSV reader here expects simple comma-separated rows without embedded commas or quoted fields.
import { chromium } from 'playwright';
import { readFile, mkdir } from 'node:fs/promises';
const csv = await readFile('locales.csv', 'utf8');
const [header, ...lines] = csv.trim().split(/\r?\n/);
const columns = header.split(',').map(value => value.trim());
const rows = lines.filter(Boolean).map(line => {
const values = line.split(',').map(value => value.trim());
return Object.fromEntries(columns.map((key, index) => [key, values[index] ?? '']));
});
await mkdir('screenshots', { recursive: true });
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
locale: 'en-US'
});
try {
for (const row of rows) {
if (!row.locale || !row.url) throw new Error(`Missing locale or URL: ${JSON.stringify(row)}`);
const page = await context.newPage();
try {
const response = await page.goto(row.url, { waitUntil: 'networkidle', timeout: 60000 });
if (!response) throw new Error(`No main-document response for ${row.url}`);
const status = response.status();
if (status >= 400) throw new Error(`HTTP ${status} for ${row.url}`);
await page.screenshot({
path: `screenshots/${row.locale}.png`,
fullPage: true,
animations: 'disabled'
});
console.log(`${row.locale}\t${status}\t${row.url}`);
} catch (error) {
console.error(`${row.locale}\tFAILED\t${row.url}\t${error.message}`);
} finally {
await page.close();
}
}
} finally {
await context.close();
await browser.close();
}
Using networkidle is a convenient default for pages whose requests settle, but analytics, chat, or other persistent requests can prevent it from firing. If that happens, use domcontentloaded or load, then wait for a page-specific selector or a short, deliberate delay. A selector wait is more reliable when you know which content marks the page as ready.
Viewport and element variants
For a viewport capture, change fullPage: true to fullPage: false. To capture a specific element, wait for and screenshot its selector:
const target = page.locator('[data-testid="localized-hero"]');
await target.waitFor({ state: 'visible', timeout: 15000 });
await target.screenshot({ path: `screenshots/${row.locale}-hero.png` });
Choose a selector present on every locale page. If markup differs by locale, maintain an explicit selector mapping and record it alongside the manifest rather than silently capturing a different region.
Locale state and consent
The sample sets the browser locale to en-US to make the run’s environment explicit; it still visits each URL directly. If the page varies based on browser locale, cookies, geolocation, or authentication, set and document that state intentionally. Use a separate browser context or state file for each materially different state so one locale’s cookies do not leak into another capture. Decide whether the consent banner is part of the evidence: preserve it consistently when documenting first-visit experience, or use a consistent accepted state when comparing page content after consent.
4. Compare the evidence fairly
- Confirm each screenshot filename maps to the exact URL and locale in the manifest.
- Compare pages captured at the same viewport, scale, scope, scroll position, and browser state.
- Check whether visible language, navigation, currency or regional details, and the language switcher fit the URL’s intended audience.
- Record differences that could come from renderer behavior, blocked resources, delayed content, or a consent state rather than assuming they are localization defects.
- Keep the original files and manifest together so another person can reproduce the comparison.
Google says it determines page language from visible content, not the hreflang or HTML lang attribute. Those annotations associate locale variants; they do not make the visible page language correct. See Google’s multilingual site guidance.
5. Inspect Google’s rendered view when relevant
To see what Google’s URL Inspection tool rendered, open Search Console, run a live test for the exact locale URL, and view the screenshot after a successful test. Google does not offer this screenshot in the indexed-URL view, and it may be unavailable if the fetch or test fails. The result documents Google’s inspection rendering; it is not a replacement for a user-browser capture. Google notes that differences can occur when resources are blocked to its inspection tool. See URL Inspection documentation.
Save the URL and the test context with the image, and label the renderer clearly, for example fr-FR-google-live.png. If the image differs from Playwright, check resource access and rendering state before concluding that the locale page itself is broken.
6. Validate hreflang independently
Visual evidence cannot establish that the alternate annotations are complete or reciprocal. Google supports hreflang in HTML, HTTP headers, or XML sitemaps. Whichever method you use, verify that each version lists itself and all alternate versions, that return links are present, and that language and optional region codes are valid. Check that every annotated URL resolves to the intended page. Use x-default for a fallback when no listed language or region matches; it is particularly useful for a language-selector page. The authoritative requirements are in Google’s localized-versions documentation.
Keep the checks distinct: screenshots answer “what did this renderer show at this URL and state?”; hreflang inspection answers “are the intended URL variants connected correctly?” Both are useful, and one cannot substitute for the other.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Two locale URLs show the same language | Redirect or adaptation uses cookies, IP location, or browser signals | Open explicit locale URLs; record redirects and state; verify the final URL and visible content. |
| Script times out waiting for network idle | Persistent analytics, chat, or streaming requests | Wait for domcontentloaded or load, then wait for a content selector or bounded delay. |
| Screenshot is blank or missing key content | Capture began before client-rendered content appeared, or the selector did not match | Wait for the relevant visible selector; check navigation errors and selector presence before capture. |
| Different image dimensions across locales | Viewport, full-page length, device scale, or responsive breakpoint differs | Fix viewport and scale; keep capture scope constant. Different full-page heights can be valid if page content length differs. |
| Search Console screenshot is unavailable | Indexed view used, live test unsuccessful, or Google could not fetch the page | Run a live inspection test and resolve fetch failures; screenshot availability depends on a successful test. |
| Google render differs from browser render | Resources are blocked or the inspection renderer has a different state | Inspect resource access and compare while clearly labeling the renderer and capture conditions. |
| Consent banner appears in only some images | Shared cookies, different context, or locale-specific consent behavior | Use isolated contexts and a stated consent policy; capture the same first-visit or accepted state. |
| HTTP error or redirect loop | Wrong URL, access restriction, locale redirect, or site response issue | Check the manifest URL and response status; use the intended accessible URL and document required auth or state. |
8. Performance, reliability, and cost
For a small set of pages, sequential browser captures are simple and make failures easy to associate with a URL. For larger sets, limit concurrency to avoid overloading the site or the capture machine, and retry only transient navigation failures with a bounded retry count. Preserve a per-URL success or failure record. A full-page screenshot can consume more memory and take longer than a viewport capture, especially on very long pages; use the narrowest scope that answers the comparison question.
Browser automation has setup and runtime costs: install and maintain a browser, allocate compute, and manage credentials or state if pages require them. Search Console provides an inspection rendering for the specific URL when the live test succeeds; it is not a batch screenshot workflow. The cost of running local automation depends on your infrastructure, and no fixed benchmark is implied here.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. For a quick capture of a locale URL, replace the example URL with the explicit landing-page URL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/fr-fr/produits/ -o fr-FR.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/fr-fr/produits/"},
timeout=90,
)
r.raise_for_status()
open("fr-FR.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/fr-fr/produits/'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('fr-FR.webp', Buffer.from(await res.arrayBuffer())));
See the ScreenshotNeo API documentation for the available capture options. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers say which page verdict and billing status applied. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month with no card.
FAQ
Does a screenshot prove hreflang is correct?
No. It shows rendered appearance. Inspect the annotations and complete alternate URL set separately.
Should I capture a redirected locale URL or its destination?
Record the requested URL and final destination. If the destination is not the intended locale page, investigate the redirect and capture the explicit URL that represents the intended variant.
Can one locale screenshot represent all users in that language?
No. Region, browser state, personalization, and page changes can affect what appears. Tie each image to its exact URL and capture conditions.
Does the Google screenshot show the indexed page?
The rendered screenshot described here is from a successful live URL Inspection test; Google does not provide it in the indexed-URL view.


