# Hreflang Tag Checker (`maximedupre/hreflang-checker`) Actor

Check public pages for hreflang declarations and SEO findings. Get language or region codes, target URLs, coverage details, HTTP status, and a compliance grade in a dataset.

- **URL**: https://apify.com/maximedupre/hreflang-checker.md
- **Developed by:** [Maxime Dupré](https://apify.com/maximedupre) (community)
- **Categories:** SEO tools, Developer tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.90 / 1,000 hreflang audits

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 Checker for International SEO Audits

SEO specialists, developers, and site owners can use Hreflang Checker to audit public pages. It returns requested and final URLs, HTTP status, page details, hreflang declarations, coverage, findings, and a compliance grade in a dataset, so you can review international SEO setup in one place.

- Review page declarations with [**Hreflang Checker**](https://apify.com/maximedupre/hreflang-checker/examples/hreflang-checker).
- Inspect language and region codes with [**Hreflang Tag Checker**](https://apify.com/maximedupre/hreflang-checker/examples/hreflang-tag-checker).
- Run a hosted page check with [**Hreflang Checker Online**](https://apify.com/maximedupre/hreflang-checker/examples/hreflang-checker-online).
- Check self-reference, x-default, and findings before a release with [**Hreflang Testing Tool**](https://apify.com/maximedupre/hreflang-checker/examples/hreflang-testing-tool).
- Compare missing codes and target URLs across pages with [**Hreflang Audit Tool**](https://apify.com/maximedupre/hreflang-checker/examples/hreflang-audit-tool).
- Review code validity and the page grade with [**Hreflang Validator**](https://apify.com/maximedupre/hreflang-checker/examples/hreflang-validator).

#### 📦 Page Audit Data

Each saved row covers one distinct publicly reachable page, including a page where no hreflang declarations are found. The row includes the requested and final URLs, response status, available page metadata, declarations from HTML or HTTP Link headers, coverage, findings, and a compliance grade.

#### 🔎 Running Hreflang Page Audits

Add public page URLs, choose the checks for the run, and open the dataset after the run finishes. All submitted pages use the same options. By default, the Actor audits the URLs you submit; you can also follow same-site links and optionally check whether alternate targets link back.

#### ⚙️ Input

Add one or more public HTTP or HTTPS page URLs. The same options apply to every page in the run.

| Field | Type | What it does |
|---|---|---|
| `pageUrls` | array<object> | Required. The public pages to audit. Add one or more items. |
| `pageUrls[].url` | string | Required. The public page URL to audit. |
| `verifyReciprocalLinks` | boolean | Optional, default `false`. When `true`, checks whether each alternate target links back to the source page. |
| `expandSameSiteAudit` | boolean | Optional, default `false`. When `true`, follows same-site links to audit pages beyond the URLs you submit. |
| `maxPages` | integer | Optional positive limit for pages audited through same-site links. It is used only when `expandSameSiteAudit` is on. Leave it empty to return all available results until the source is exhausted. |
| `crossDomainPolicy` | string (`flag` or `accept`) | Optional, default `flag`. Choose whether an alternate URL on another domain is accepted or flagged in the findings. |

The example below is the public input from a successful current beta run:

```json
{
  "pageUrls": [
    {
      "url": "https://www.bbc.com/"
    }
  ],
  "verifyReciprocalLinks": false,
  "expandSameSiteAudit": false,
  "maxPages": 50,
  "crossDomainPolicy": "flag"
}
```

#### 🧾 Output

The run output exposes `datasetUrl`, which links to the page audit rows. The dataset uses one row shape for normal audits, optional reciprocal checks, and cross-page comparisons. Optional fields are omitted when the related data is not available or the option was not used.

| Field | Type | What it does |
|---|---|---|
| `datasetUrl` | string | Run output link to the page audit dataset. |
| `requestedUrl` | string | URL submitted for the page audit. |
| `finalUrl` | string | Final page URL after redirects. |
| `httpStatus` | integer | HTTP response status for the final page. |
| `pageTitle` | string (optional) | Page title when it is available. |
| `domain` | string | Domain of the final page URL. |
| `canonicalUrl` | string (optional) | Canonical URL declared by the page when it is available. |
| `declarations` | array<object> | Hreflang declarations found on the page. |
| `declarations[].hreflang` | string | Language or region code in the declaration. |
| `declarations[].targetUrl` | string | Absolute URL targeted by the declaration. |
| `declarations[].codeValidity` | string (`valid`, `invalid`, or `unknown`) | Validity assessment for the language or region code. |
| `declarations[].declarationSource` | string (`html`, `httpHeader`, or `both`) | Where the declaration was found. |
| `declarations[].reciprocalCheck` | object (optional) | Result of checking whether the target links back to the source page. |
| `declarations[].reciprocalCheck.status` | string | Reciprocal check result: `linksBack`, `doesNotLinkBack`, or `unreachable`. |
| `declarations[].reciprocalCheck.finalUrl` | string (optional) | Final URL reached when the target was checked. |
| `declarations[].reciprocalCheck.httpStatus` | integer (optional) | HTTP response status from the target check. |
| `coverage` | object | Summary of the page's hreflang coverage. |
| `coverage.declarationCount` | integer | Number of hreflang declarations found. |
| `coverage.hasXDefault` | boolean | Whether the page has an `x-default` declaration. |
| `coverage.hasSelfReference` | boolean | Whether a declaration points back to the page. |
| `findings` | array<object> | Warnings and errors found during the page audit. |
| `findings[].severity` | string (`warning` or `error`) | Severity of the finding. |
| `findings[].code` | string | Stable code for the finding. |
| `findings[].message` | string | Plain-language explanation of the finding. |
| `findings[].hreflang` | string (optional) | Hreflang code linked to the finding, when applicable. |
| `complianceGrade` | string (`A`, `B`, `C`, `D`, or `F`) | Page-level grade based on the audit findings. |
| `crossPageComparison` | object (optional) | Differences found when the page is compared with peer pages in the run. |
| `crossPageComparison.missingHreflang` | string\[] | Codes found on peer pages but missing here. |
| `crossPageComparison.missingTargetUrls` | string\[] | Alternate target URLs found on peer pages but missing here. |

**A no-declaration page audit row from the current beta:**

```json
{
  "requestedUrl": "https://www.w3.org/International/questions/qa-html-language-declarations",
  "finalUrl": "https://www.w3.org/International/questions/qa-html-language-declarations",
  "httpStatus": 200,
  "domain": "www.w3.org",
  "pageTitle": "Declaring language in HTML",
  "declarations": [],
  "coverage": {
    "declarationCount": 0,
    "hasXDefault": false,
    "hasSelfReference": false
  },
  "findings": [
    {
      "severity": "warning",
      "code": "missing-declarations",
      "message": "No hreflang declarations were found in the HTML or HTTP Link headers."
    }
  ],
  "complianceGrade": "B",
  "crossPageComparison": {
    "missingHreflang": [],
    "missingTargetUrls": []
  }
}
```

**A header-based page audit row from the current beta:**

```json
{
  "requestedUrl": "https://httpbin.org/response-headers?Link=%3Chttps%3A%2F%2Fexample.com%2Fen%3E%3B%20rel%3D%22alternate%22%3B%20hreflang%3D%22en%22",
  "finalUrl": "https://httpbin.org/response-headers?Link=%3Chttps%3A%2F%2Fexample.com%2Fen%3E%3B%20rel%3D%22alternate%22%3B%20hreflang%3D%22en%22",
  "httpStatus": 200,
  "domain": "httpbin.org",
  "declarations": [
    {
      "hreflang": "en",
      "targetUrl": "https://example.com/en",
      "codeValidity": "valid",
      "declarationSource": "httpHeader"
    }
  ],
  "coverage": {
    "declarationCount": 1,
    "hasXDefault": false,
    "hasSelfReference": false
  },
  "findings": [
    {
      "severity": "warning",
      "code": "cross-domain-alternate",
      "message": "The en alternate target is on another hostname.",
      "hreflang": "en"
    },
    {
      "severity": "warning",
      "code": "missing-x-default",
      "message": "No x-default declaration was found.",
      "hreflang": "x-default"
    },
    {
      "severity": "warning",
      "code": "missing-self-reference",
      "message": "No hreflang declaration points back to the fetched page."
    }
  ],
  "complianceGrade": "C",
  "crossPageComparison": {
    "missingHreflang": [],
    "missingTargetUrls": []
  }
}
```

**A reciprocal-check row from the current beta:**

```json
{
  "requestedUrl": "https://www.bbc.com/",
  "finalUrl": "https://www.bbc.com/",
  "httpStatus": 200,
  "domain": "www.bbc.com",
  "pageTitle": "BBC Home - Breaking News, World News, US News, Sports, Business, Innovation, Climate, Culture, Travel, Video & Audio",
  "canonicalUrl": "https://www.bbc.com/",
  "declarations": [
    {
      "hreflang": "en",
      "targetUrl": "https://www.bbc.com/",
      "codeValidity": "valid",
      "declarationSource": "html",
      "reciprocalCheck": {
        "status": "linksBack",
        "finalUrl": "https://www.bbc.com/",
        "httpStatus": 200
      }
    },
    {
      "hreflang": "en-gb",
      "targetUrl": "https://www.bbc.co.uk/",
      "codeValidity": "valid",
      "declarationSource": "html",
      "reciprocalCheck": {
        "status": "linksBack",
        "finalUrl": "https://www.bbc.co.uk/",
        "httpStatus": 200
      }
    }
  ],
  "coverage": {
    "declarationCount": 2,
    "hasXDefault": false,
    "hasSelfReference": true
  },
  "findings": [
    {
      "severity": "warning",
      "code": "cross-domain-alternate",
      "message": "The en-gb alternate target is on another hostname.",
      "hreflang": "en-gb"
    },
    {
      "severity": "warning",
      "code": "missing-x-default",
      "message": "No x-default declaration was found.",
      "hreflang": "x-default"
    }
  ],
  "complianceGrade": "C",
  "crossPageComparison": {
    "missingHreflang": [],
    "missingTargetUrls": []
  }
}
```

**A cross-page comparison row from the current beta:**

```json
{
  "requestedUrl": "https://www.bbc.com/watch-live-news",
  "finalUrl": "https://www.bbc.com/watch-live-news",
  "httpStatus": 200,
  "domain": "www.bbc.com",
  "pageTitle": "BBC News Channel | Watch Live Breaking News",
  "canonicalUrl": "https://www.bbc.com/watch-live-news",
  "declarations": [
    {
      "hreflang": "en",
      "targetUrl": "https://www.bbc.com/watch-live-news",
      "codeValidity": "valid",
      "declarationSource": "html"
    }
  ],
  "coverage": {
    "declarationCount": 1,
    "hasXDefault": false,
    "hasSelfReference": true
  },
  "findings": [
    {
      "severity": "warning",
      "code": "missing-x-default",
      "message": "No x-default declaration was found.",
      "hreflang": "x-default"
    }
  ],
  "complianceGrade": "B",
  "crossPageComparison": {
    "missingHreflang": [
      "en-gb"
    ],
    "missingTargetUrls": [
      "https://www.bbc.co.uk/",
      "https://www.bbc.com/"
    ]
  }
}
```

#### 💳 Pricing

The Actor uses pay-per-event pricing. The `Hreflang audit` event covers one publicly reachable page audit with its hreflang findings.

| Event | Price | What it covers |
|---|---:|---|
| Hreflang audit | $0.0009 | One publicly reachable page audit with hreflang findings saved. |

#### 🔌 Integrations

Use the dataset URL to export audit rows or read them through the Apify API. This walkthrough shows an Apify integration:

https://www.youtube.com/watch?v=bNACk1\_S\_6w\&list=PLObrtcm1Kw6MUrlLNDbK9QRg8VDJg0gOW\&index=4

#### ❓ FAQ

##### Can it audit a page with no hreflang declarations?

Yes. The Actor keeps an outcome for a publicly reachable page even when no hreflang declarations are found, and the findings explain that result.

##### Does it inspect HTML and HTTP Link headers?

Yes. Each declaration records whether it came from HTML, an HTTP Link header, or both.

##### Does it check x-default and self-reference?

Yes. The `coverage` object reports whether the page has an `x-default` declaration and whether it points back to itself. Findings can also flag missing coverage.

##### What does reciprocal checking do?

Set `verifyReciprocalLinks` to `true` to check whether each alternate target links back to the source page. The output can include the check status, final target URL, and target HTTP status.

##### Can it compare several pages in one run?

Yes. Add multiple `pageUrls` items. The output can compare peer pages and list hreflang codes or target URLs missing from the page.

##### What happens when same-site audit expansion is on?

When `expandSameSiteAudit` is `true`, the Actor follows same-site links beyond the submitted URLs. `maxPages` limits those pages; leave it empty to return all available results until the source is exhausted.

##### Does it crawl my whole website?

No. By default it audits the submitted pages. Same-site expansion can extend the run, but the Actor does not discover a complete URL inventory from XML sitemaps or crawl unrelated external sites.

##### Does it check alternate pages for broken links or redirects?

No. It reports the absolute target URLs it finds. Reciprocal checking can inspect whether a target links back, but this Actor does not validate target-page content, broken links, or redirect chains as a separate audit.

##### Can it find tags that are missing from the fetched response?

No. It reports declarations present in the fetched public response. A declaration that is not present there may not be found.

##### Do I need source-site login credentials or an external API key?

No. The Actor audits publicly reachable pages without source-site login credentials or external API keys.

### 📝 Changelog

**0.0: Initial release**

### 🆘 Support

For issues, questions, or feature requests, [file a ticket](https://console.apify.com/actors/maximedupre~hreflang-checker/issues) and I'll fix or implement it in less than 24h 🫡

### 🔗 Related Actors

- [Website URL Crawler & Link Extractor](https://apify.com/maximedupre/website-url-crawler) — Build a public URL inventory from links and sitemaps before choosing hreflang pages to audit.
- [Sitemap Sniffer](https://apify.com/maximedupre/sitemap-sniffer) — Find public sitemap files and export listed URLs before a multilingual page audit.
- [XML Sitemap Health Validator](https://apify.com/maximedupre/sitemap-validator) — Check sitemap-listed URLs for HTTP status, redirects, and issue details alongside hreflang findings.
- [Broken Link Checker & Site Auditor](https://apify.com/andok/broken-links-checker) — Check page links and resources when an hreflang target needs a separate link audit.
- [URL Redirect Chain & Loop Analyzer](https://apify.com/andok/redirect-chain-analyzer) — Trace alternate target redirects when a page audit shows a URL that may change.

**Made with ❤️ by Maxime Dupré**

# Actor input Schema

## `pageUrls` (type: `array`):

Add one or more public page URLs to audit. The same options apply to every page.

## `verifyReciprocalLinks` (type: `boolean`):

Check whether each alternate target links back to the source page.

## `expandSameSiteAudit` (type: `boolean`):

Follow same-site links to audit pages beyond the URLs you submit.

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

Optional positive integer limit for pages audited through same-site links. It is used only when Follow same-site links is on. Leave it empty to return all available results until the source is exhausted.

## `crossDomainPolicy` (type: `string`):

Choose whether an alternate URL on another domain is accepted or flagged in the findings.

## Actor input object example

```json
{
  "pageUrls": [
    {
      "url": "https://www.bbc.com/"
    }
  ],
  "verifyReciprocalLinks": false,
  "expandSameSiteAudit": false,
  "maxPages": 50,
  "crossDomainPolicy": "flag"
}
```

# Actor output Schema

## `datasetUrl` (type: `string`):

Link to the dataset with page audit rows.

# 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 = {
    "pageUrls": [
        {
            "url": "https://www.bbc.com/"
        }
    ],
    "maxPages": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("maximedupre/hreflang-checker").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 = {
    "pageUrls": [{ "url": "https://www.bbc.com/" }],
    "maxPages": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("maximedupre/hreflang-checker").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 '{
  "pageUrls": [
    {
      "url": "https://www.bbc.com/"
    }
  ],
  "maxPages": 50
}' |
apify call maximedupre/hreflang-checker --silent --output-dataset

```

## MCP server setup

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

```

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/WH7Lg8iF4whCqHwbm/builds/NdBN5AiteuE2koOcG/openapi.json
