# hreflang Checker & Return-Tag Validator · SiteVerdict (`siteverdict/siteverdict-hreflang-validator`) Actor

Audit international SEO: crawl a site, read hreflang from HTML, HTTP headers and XML sitemaps, verify return tags across domains, x-default, ISO language/region codes, canonical and noindex conflicts. Matrix report, CSV/PDF, monitoring and alerts.

- **URL**: https://apify.com/siteverdict/siteverdict-hreflang-validator.md
- **Developed by:** [Aman Thakran](https://apify.com/siteverdict) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## SiteVerdict — hreflang Validator & Return-Tag Matrix

**Find out exactly why Google shows the wrong country version of your pages.** The validator crawls your international site and reads hreflang from all three places Google accepts: `<head>` link tags, HTTP `Link` headers and XML sitemaps. It then fetches every alternate, including those on other domains, and checks each pair. The result is a **return-tag matrix** with an issue list that says what to fix and where.

Who it is for: international SEO teams, agencies running multi-country sites, developers of localised templates, and AI agents (MCP-ready).

### Three ways people use it

1. **International audit.** Get one matrix per hreflang cluster (pages × language codes) with ✓ return / ✗ no return per cell. `MATRIX-CSV` drops straight into a spreadsheet.
2. **Release guard.** Schedule it with `monitoring` on and get a Slack/webhook alert when a deploy breaks return tags, adds an invalid code or points hreflang at redirects/404s.
3. **ccTLD / subdomain setups.** Alternates on other domains (example.de, fr.example.com) are fetched politely to verify return tags across properties.

### What it checks

| Issue | Severity |
|---|---|
| Invalid code: not ISO 639-1 / ISO 3166-1, `en-UK`, `en_GB`, `jp`, `gb`, `es-419`, `en-EU`, unknown scripts. Includes plain-English explanations ("UK is not an ISO country code; use en-GB") | high |
| Missing return tag (the alternate does not link back), including across domains | high |
| Same code pointing to different URLs | high |
| hreflang target is 4xx/5xx/unreachable, noindex, or canonicalises elsewhere | high |
| hreflang `<link>` inside `<body>` (ignored by Google) | high |
| Relative or invalid hreflang URLs | high |
| hreflang target redirects | medium |
| Page with hreflang is not canonical | medium |
| hreflang target blocked by robots.txt | medium |
| HTML/header and sitemap annotations disagree | medium |
| Missing self-reference | medium |
| Missing x-default | low |
| `<html lang>` does not match the hreflang language | low |
| Cluster pages list different sets | low |
| Return tag could not be verified (alternate not fetched; listed, never counted as an error) | info |

### Output

- Dataset views: **Issues** (one row per problem, with fix), **Clusters (matrix)** and **Summary**.
- Key-value store: `REPORT` (HTML with issues + matrix per cluster), `REPORT-PDF`, `MATRIX-CSV` (cluster, page, hreflang, target, returnTag), `ISSUES-CSV`, `SUMMARY`.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| `page-checked` | $0.002 | Each page or alternate URL fetched (200) and analysed |
| `site-report` | $0.02 | Once per site: matrix, HTML/PDF/CSV reports, monitoring diff, alert |

Examples: 5 languages × 40 pages (200 URLs) ≈ **$0.42**. 2,000 URLs ≈ **$4.02**. Robots-blocked or failed URLs and sample mode are free.

### FAQ

**Do I need hreflang in both HTML and the sitemap?** No. Google accepts either. If you use both, they must agree, and we flag pages where they don't.

**Why is "uk" valid?** `uk` is Ukrainian. If you meant the United Kingdom, use `en-GB`. We add a note so the mistake is visible.

**Does it render JavaScript?** No. Google strongly recommends hreflang in the server HTML, headers or sitemap, which is what this tool reads.

### Limits

- 5,000 pages and 5,000 alternate fetches per site; 20 sites per run.
- Sitemaps up to 20,000 URLs are read.
- Return tags for alternates beyond the limit are reported as "unverified".

### Sources & compliance

- Rules follow Google Search Central "Tell Google about localized versions of your page" (developers.google.com/search/docs/specialty/international/localized-versions, read 11 Oct 2026). Code lists are ISO 639-1, ISO 15924 and ISO 3166-1 alpha-2 (facts).
- Data comes from the public pages, headers and sitemaps of the sites you check. They are fetched with the honest user agent `SiteVerdictBot/1.0 (+aman.thakran@bharatfare.com)`, and robots.txt is obeyed on every host. The tool never bypasses bot protection. Fetching is SSRF-safe.
- No personal data is collected. Under UK GDPR/PECR you are the controller of your audit, so only check sites you own or may audit. The webhook URL is a secret input.

# Changelog

This Actor's version history is a separate document: https://apify.com/siteverdict/siteverdict-hreflang-validator/changelog.md

# Actor input Schema

## `startUrls` (type: `array`):

Start URL of each site (max 20).

## `crawl` (type: `boolean`):

Follow internal links and hreflang alternates. Off = only the start URLs (plus their alternates).

## `maxPagesPerSite` (type: `integer`):

Upper limit of pages crawled per site.

## `maxDepth` (type: `integer`):

How deep to follow links.

## `useSitemap` (type: `boolean`):

Parse xhtml:link alternates in the sitemap and compare them with the pages.

## `checkAlternates` (type: `boolean`):

Fetch alternate URLs that were not crawled (including other domains) so return tags can be verified.

## `maxAlternateChecks` (type: `integer`):

Cap on extra alternate URLs fetched.

## `includePatterns` (type: `array`):

Globs or /regex/.

## `excludePatterns` (type: `array`):

Globs or /regex/.

## `includeSubdomains` (type: `boolean`):

Treat fr.example.com etc. as the same site when crawling.

## `maxConcurrency` (type: `integer`):

Parallel lanes (each host still rate-limited).

## `requestDelayMs` (type: `integer`):

Polite spacing per host. robots.txt Crawl-delay is honoured when it is larger.

## `requestTimeoutSecs` (type: `integer`):

Per request.

## `monitoring` (type: `boolean`):

Snapshot per site in the named store 'siteverdict-hreflang-history'; reports new and fixed issues.

## `projectName` (type: `string`):

Optional stable name used to store the monitoring snapshot, e.g. 'client-acme'. Defaults to the site host.

## `alertWebhookUrl` (type: `string`):

Optional. Slack incoming-webhook URL or any https:// endpoint that receives a JSON POST when monitoring finds new problems. Stored encrypted as an Apify secret, never logged or written to the output.

## `sampleMode` (type: `boolean`):

Run on a built-in demo with planted problems. No network requests and nothing charged. Use it to see the output format.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.apple.com/uk/"
    }
  ],
  "crawl": true,
  "maxPagesPerSite": 20,
  "maxDepth": 10,
  "useSitemap": true,
  "checkAlternates": true,
  "maxAlternateChecks": 500,
  "includeSubdomains": false,
  "maxConcurrency": 2,
  "requestDelayMs": 500,
  "requestTimeoutSecs": 20,
  "monitoring": true,
  "sampleMode": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `files` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://www.apple.com/uk/"
        }
    ],
    "maxPagesPerSite": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("siteverdict/siteverdict-hreflang-validator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [{ "url": "https://www.apple.com/uk/" }],
    "maxPagesPerSite": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("siteverdict/siteverdict-hreflang-validator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://www.apple.com/uk/"
    }
  ],
  "maxPagesPerSite": 20
}' |
apify call siteverdict/siteverdict-hreflang-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,siteverdict/siteverdict-hreflang-validator"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/OxlHjWlsnO6nNMLAf/builds/KFr4UYfjMgNV0B0FB/openapi.json
