# Hreflang Migration Validator (`kingii98/hreflang-migration-validator`) Actor

Validate hreflang annotations after a domain or site migration: locale syntax, duplicates, target health, canonicals, x-default, self-references, reciprocal return links, and old-domain references.

- **URL**: https://apify.com/kingii98/hreflang-migration-validator.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 page validateds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Hreflang Migration Validator

Validate hreflang annotations after a domain or site migration. Given the destination pages — as one XML sitemap or an explicit URL list — the Actor fetches each page once, parses its `link rel="alternate" hreflang` declarations, and validates them: locale syntax against a deterministic maintained ISO/BCP 47-narrow set, duplicate locale declarations, target URL status and redirect behavior, canonical conflicts, `x-default` coverage, self-references, reciprocal return links between audited pages, and references to pre-migration domains.

This is a migration and international-SEO validator, not a generic crawler: it fetches only the input pages and their declared hreflang targets — each at most once, within hard caps — and never follows links into recursive discovery.

The package reuses the safe HTTP, SSRF validation, and redirect-chain helpers of the `sitemap_health_monitor` and `migration_qa_auditor` packages in this repository; those packages and their tests are unchanged.

### What it checks

- Locale syntax of every `hreflang` value: `lang`, `lang-REGION`, and `x-default` forms only, with the language in the maintained ISO 639-1 set and the region in the ISO 3166-1 alpha-2 or UN M.49 sets (so `en-US` and `es-419` pass, `en-XX` and `english` fail)
- Duplicate locale declarations on the same page, after normalization (`EN-us` and `en-US` collide)
- Relative `href` targets, resolved against the page's final URL
- Target URL health: bounded redirect chains with every hop validated as a public address, 4xx/5xx responses, redirect loops, over-long chains, redirects without `Location`, and redirected targets (hreflang targets should be final URLs)
- Canonical conflicts: a page or target whose canonical URL differs from its final URL
- `x-default` coverage per page (gated by `requireXDefault`)
- Self-reference: the page's own final URL present in its hreflang set (gated by `requireSelfReference`)
- Reciprocal return links: when a target is itself an audited page, that page must declare a link back (gated by `checkReciprocal`)
- Old-domain references: any hreflang target on a domain listed in `oldDomains` or its subdomains
- Caps and truncation: per-page target cap, total target cap, and page cap are enforced and reported

### Input

Provide exactly one of `sitemapUrl` or `urls`.

```json
{
  "sitemapUrl": "https://new.example.com/sitemap.xml",
  "oldDomains": ["old-example.com"],
  "maxPages": 100,
  "maxTargetsPerPage": 20,
  "maxTotalTargets": 5000,
  "concurrency": 10,
  "timeoutSecs": 20,
  "maxRedirects": 5,
  "requireXDefault": true,
  "requireSelfReference": true,
  "checkReciprocal": true
}
```

```json
{
  "urls": ["https://example.com/en/pricing", "https://example.com/de/preise"],
  "oldDomains": ["old-example.com"]
}
```

| Field | Description |
|---|---|
| `sitemapUrl` | Public HTTP(S) XML sitemap of destination pages. Nested sitemap indexes and gzip payloads are supported. |
| `urls` | Explicit page URL list, used instead of `sitemapUrl`. Entries are normalized and deduplicated; lists longer than `maxPages` are rejected before any network work. |
| `oldDomains` | Optional pre-migration domains. Targets on these domains or their subdomains are flagged. Entries must be bare domain names. |
| `maxPages` | Hard cap on audited pages. Default 100; range 1-1,000. |
| `maxTargetsPerPage` | Hard cap on hreflang declarations validated per page; extras are skipped and flagged. Default 20; range 1-100. |
| `maxTotalTargets` | Hard cap on unique target URLs fetched per run; targets beyond the cap are reported as `target-not-checked`. Default 5,000. |
| `concurrency` | Concurrent page or target fetches. Default 10; range 1-30. |
| `timeoutSecs` | Per-request timeout. Default 20 seconds; range 2-60. |
| `maxRedirects` | Maximum redirect hops followed per page or target. Default 5; range 0-10. |
| `requireXDefault` | Flag pages without an `x-default` declaration. Enabled by default. |
| `requireSelfReference` | Flag pages whose hreflang set omits the page itself. Enabled by default. |
| `checkReciprocal` | Flag non-reciprocal links between audited pages. Enabled by default. |

Invalid combinations (both or neither source mode, empty lists, URLs with credentials or non-HTTP schemes, malformed old domains, out-of-range numbers) fail fast with a clear validation error before any network work.

### Output

Every run writes one `summary` record, one `page-result` record per audited input page, and one `hreflang-result` record per validated declaration to the default dataset.

Summary:

```json
{
  "recordType": "summary",
  "checkedAt": "2026-08-07T09:15:00+00:00",
  "pagesChecked": 2,
  "hreflangChecked": 6,
  "invalidLocales": 1,
  "duplicateLocales": 1,
  "brokenTargets": 1,
  "redirectedTargets": 1,
  "missingReciprocal": 1,
  "missingSelfReference": 0,
  "missingXDefault": 0,
  "oldDomainReferences": 1,
  "canonicalConflicts": 0,
  "fetchErrors": 0,
  "errors": 1,
  "pagesTruncated": false,
  "targetsTruncated": false,
  "perPageTargetsTruncated": 0
}
```

Per-page result:

```json
{
  "recordType": "page-result",
  "pageUrl": "https://example.com/en/pricing",
  "finalUrl": "https://example.com/en/pricing",
  "status": 200,
  "canonicalUrl": "https://example.com/en/pricing",
  "declaredTargets": 3,
  "xDefault": true,
  "selfReference": true,
  "issueCodes": [],
  "error": null,
  "checkedAt": "2026-08-07T09:15:00+00:00"
}
```

Per-hreflang result:

```json
{
  "recordType": "hreflang-result",
  "pageUrl": "https://example.com/en/pricing",
  "locale": "de-DE",
  "targetUrl": "https://example.com/de/preise",
  "finalUrl": "https://example.com/de/preise",
  "status": 200,
  "validLocale": true,
  "selfReference": false,
  "reciprocal": true,
  "oldDomainReference": false,
  "canonicalUrl": "https://example.com/de/preise",
  "issueCodes": [],
  "error": null,
  "checkedAt": "2026-08-07T09:15:00+00:00"
}
```

`reciprocal` is `true`/`false` only when the target is itself an audited page; otherwise it is `null` (the Actor does not crawl beyond the audited set). `validLocale` is `false` for both malformed tags and tags with codes outside the maintained sets; the raw attribute value is kept in `locale` when it cannot be normalized.

Issue codes: `invalid-locale`, `duplicate-locale`, `invalid-target-url`, `missing-x-default`, `missing-self-reference`, `non-reciprocal`, `old-domain-reference`, `redirected-target`, `redirect-loop`, `redirect-chain-too-long`, `redirect-without-location`, `http-4xx`, `http-5xx`, `canonical-mismatch`, `not-html`, `fetch-error`, `target-not-checked`, `target-limit-truncated`.

### Pricing

The Actor uses Apify pay-per-event pricing with the `page-validated` charge event. When monetization is enabled, users are charged **$0.002 per input page validated ($2 per 1,000 pages)**. One `page-validated` event corresponds to one input page audit, including its bounded hreflang target checks.

Apify platform usage (compute units and other resources consumed by the run) may still be shown to users according to their plan and Apify's pricing rules, as described in the Actor's listing.

The Actor respects the run's maximum total charge: if the remaining budget cannot cover every input page, it validates only the chargeable prefix; if no page can be charged, it stops before page checks.

Final pricing is configured in the Apify Store listing and may change subject to Apify's pricing-change notice rules.

### Security and privacy

- Only public HTTP(S) targets are accepted.
- URL credentials, localhost, and non-public, loopback, link-local, multicast, unspecified, or reserved addresses are rejected.
- Every redirect target is resolved and validated before it is followed; a redirect to a private address fails that page or target with an error record instead of being fetched.
- Page counts, target counts, concurrency, redirects, response bytes, sitemap sizes, and timeouts are all capped before or during network work.
- The Actor does not use a browser, proxy, LLM, external database, or third-party analytics service; locale validation uses an embedded deterministic data set.
- Each run is stateless; results live only in the run's default dataset, subject to the retention and access settings of the Apify account running the Actor.

Do not place secrets, private URLs, or personal data in any input field.

### Limitations

- JavaScript-rendered pages are not rendered; checks use HTTP responses only.
- Only `<link rel="alternate" hreflang="...">` annotations in the HTML head are validated; HTTP-header and XML-sitemap hreflang annotations are out of scope.
- Reciprocal checks apply between audited input pages only; targets outside the audited set are fetched for status and canonical but their own annotations are not parsed.
- The locale validator is deliberately narrow: `lang`, `lang-REGION`, and `x-default`. Script subtags (for example `zh-Hant`) are reported as invalid rather than parsed.
- Sitemap inputs must be valid XML sitemaps; nested indexes are followed up to 20 files.
- Network failures and rate limits are reported as per-page or per-target errors; they are not automatically retried indefinitely.
- The Actor does not send notifications itself. Use Apify schedules, webhooks, or an automation platform.

### Support

For reproducible issues, open an issue from the Actor page and include the Apify run ID, sanitized input, expected result, and affected public URL. Do not include API tokens or private data.

This Actor validates hreflang annotations; it does not provide legal, security-audit, or uptime guarantees.

# Actor input Schema

## `sitemapUrl` (type: `string`):

Public HTTP(S) XML sitemap listing the destination pages to audit. Provide this or urls, not both. Nested sitemap indexes and gzip payloads are supported.

## `urls` (type: `array`):

Explicit list of destination page URLs to audit. Use instead of sitemapUrl. Each page is fetched once and its hreflang declarations are validated.

## `oldDomains` (type: `array`):

Optional pre-migration domains (for example old-example.com). Any hreflang target on these domains or their subdomains is flagged as an old-domain reference.

## `maxPages` (type: `integer`):

Hard cap on audited input pages. URL lists longer than this are rejected before any network work; sitemap-derived sets are truncated.

## `maxTargetsPerPage` (type: `integer`):

Hard cap on hreflang declarations validated per page. Extra declarations are skipped and the page is flagged as truncated.

## `maxTotalTargets` (type: `integer`):

Hard cap on unique hreflang target URLs fetched per run. Targets beyond the cap are reported as not checked.

## `concurrency` (type: `integer`):

Maximum number of pages or targets fetched concurrently.

## `timeoutSecs` (type: `integer`):

Per-request timeout applied to sitemap, page, and target fetches.

## `maxRedirects` (type: `integer`):

Maximum redirect hops followed per page or target. Longer chains are reported as redirect-chain-too-long.

## `requireXDefault` (type: `boolean`):

Flag pages whose hreflang set has no x-default declaration.

## `requireSelfReference` (type: `boolean`):

Flag pages whose hreflang set does not include the page's own final URL.

## `checkReciprocal` (type: `boolean`):

For hreflang targets that are themselves audited pages, flag missing return links back to the referencing page.

## Actor input object example

```json
{
  "maxPages": 100,
  "maxTargetsPerPage": 20,
  "maxTotalTargets": 5000,
  "concurrency": 10,
  "timeoutSecs": 20,
  "maxRedirects": 5,
  "requireXDefault": true,
  "requireSelfReference": true,
  "checkReciprocal": true
}
```

# Actor output Schema

## `dataset` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/hreflang-migration-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/hreflang-migration-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 '{}' |
apify call kingii98/hreflang-migration-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/hreflang-migration-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/afpRu70XCEss80SkX/builds/fPWhBc0Se7hMxj1oY/openapi.json
