# Hreflang & Sitemap Auditor (`ect200/hreflang-sitemap-auditor`) Actor

Audit multilingual sites for missing hreflang return links, invalid locale codes, canonical conflicts and noindex. Export page evidence as JSON, CSV and HTML.

- **URL**: https://apify.com/ect200/hreflang-sitemap-auditor.md
- **Developed by:** [Emilio](https://apify.com/ect200) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 audited pages

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Hreflang & Sitemap Auditor

Find missing hreflang return links, invalid locale codes, canonical conflicts and noindex warnings across multilingual pages. Get page-level evidence in an Apify dataset, a spreadsheet-safe CSV and an HTML report.

Use it to review a translated site before launch, check language variants after a migration, or run recurring QA for clients in Spain, Portugal, Italy, France and Germany. It works with other supported languages too. No AI, browser, proxy, CAPTCHA solver or paid data API is used.

### Why use it

A page can list its French version while the French page omits the return link. This Actor reports that exact relationship when both pages are in the audited sample. If a target is outside the sample, it clearly reports **unchecked**, so a limited crawl is not presented as a complete audit.

The output is designed for agency QA and integration workflows: one row per requested page, stable issue codes, the affected URL and explicit coverage limits. No SEO rank or revenue improvement is promised.

### Input

Audit only public sites you own or have permission to audit. Add every exact hostname that may be fetched, including `www` and separate language domains. The permission checkbox must be true. Add direct page URLs, direct sitemap URLs, or both.

The Store form starts with a one-page `example.com` demo, limited to 3 requests and 10 seconds. Its prefilled permission confirmation covers only that demo. Before replacing the host or URLs, confirm that you have permission to audit every target site, then choose your sample limits.

```json
{
  "permissionConfirmed": true,
  "allowedHosts": ["your-authorized-site.com"],
  "pageUrls": [
    "https://your-authorized-site.com/es/product",
    "https://your-authorized-site.com/fr/product"
  ],
  "maxPages": 100,
  "maxResults": 100,
  "maxDurationSecs": 120,
  "requestDelayMs": 500
}
```

For sitemap input, use `"sitemapUrls": ["https://your-authorized-site.com/sitemap.xml"]`. Standard XML indexes and bounded `.xml.gz` files are supported. There is no automatic site-wide link crawl or sitemap discovery.

The runtime default, when no page limit is supplied, is a sample of 100 pages. Each execution allows at most 1,000 pages/results, 20 sitemaps, 3,000 source HTTP requests, 5 concurrent requests and 300 seconds of audit time. The process has a separate 330-second safety deadline. The source may permit fewer requests. Slow sites can finish with partial results.

### Output

This shortened example is **synthetic**; it illustrates a missing reciprocal link, not a customer result.

```json
{
  "schemaVersion": "1.0",
  "url": "https://shop.example.com/es",
  "finalUrl": "https://shop.example.com/es",
  "statusCode": 200,
  "outcome": "ok",
  "errorCode": null,
  "title": "Zapatos · España",
  "htmlLang": "es",
  "canonical": "https://shop.example.com/es",
  "noindex": false,
  "checkedAlternates": 2,
  "uncheckedAlternates": 1,
  "findings": [{
    "code": "HREFLANG_MISSING_RETURN",
    "severity": "error",
    "evidence": "html: https://shop.example.com/fr"
  }]
}
```

The full row also includes a stable ID, description, alternate annotations and truncation status. Outputs:

- Default dataset: page audit rows; export JSON or other formats from Console/API.
- `RESULTS.csv`: flattened, formula-safe spreadsheet export.
- `REPORT.html`: readable page findings.
- `SUMMARY`: requests, response bytes, retries, errors, stop reason, sample coverage and next page offset when known.

Rows record the requested URL. Distinct URLs that redirect to the same final URL remain distinct diagnostic checks. Exact normalized duplicate inputs do not create extra rows.

### Pricing

**$2 per 1,000 page rows, plus Apify platform usage.** Verify the effective price displayed in Console before running.

One `audited-page` event is associated with each saved page row, including an error diagnostic such as HTTP404, robots denial or a timeout. Sitemap errors appear in the summary and are not separate billable events. If no page row is saved, there is no page event charge; platform usage can still accrue. There is no separate start fee.

100 rows cost $0.20 in event fees; 1,000 cost $2; 10,000 across multiple runs cost $20. Platform compute, traffic, storage operations, retention and downloads are additional. Your event spending limit is not a guarantee of the total cost when usage is charged separately. Start with a small batch and review the usage breakdown.

Free-plan users receive the same features within their platform credits and the same technical limits. No paid plan is required by this Actor itself. The developer does not earn revenue from Free-plan usage under the documented Apify model.

### Rules and limits

- Checks server-returned UTF-8 HTML and XML sitemap annotations. JavaScript-rendered tags and HTTP `Link` hreflang annotations are not evaluated; headers containing hreflang are flagged for manual review.
- HTML and sitemap methods are checked separately. A site using only sitemaps is not required to duplicate its annotations in HTML.
- Reciprocity is evaluated only when both pages were successfully fetched within the sample. A target outside the sample or an ambiguous redirect alias remains unchecked.
- Locale validation supports ISO639-1 languages, ISO3166-1 alpha2 regions, `x-default` and common scripts. Full BCP47 and every search-engine-specific locale are not supported. `en-UK` is invalid; `uk` is Ukrainian.
- Up to 20 combined annotations per page and 16KiB per exported row. Truncation is flagged; absence of a finding in a truncated row is not proof of correctness.
- Noindex warnings include meta robots/googlebot and X-Robots-Tag. Inspect user-agent scope manually before treating them as a Google indexing decision. Only HTML canonical tags are evaluated.
- Public HTTP(S), exact declared hosts and standard ports only. Private networks, credentials, local IPs and unsafe redirects are blocked. Robots rules are respected. No bypass of login, antibot or rate limits.
- A timeout, transfer/request cap or source error can produce a partial report. No guarantee of crawling an entire site or of 1,000 pages finishing in one run.
- No persistent baseline, automatic change alerts, rich-result certification or search ranking prediction.

### FAQ

**Why is an alternate unchecked?** Its page was not in the successful audited sample, or a final-URL alias was ambiguous. Include it explicitly, allow its hostname and choose a sufficient sample. The Actor does not automatically expand the scope to alternate domains.

**Can I audit multiple language domains?** Yes. Add each exact hostname to `allowedHosts` and include its pages or sitemaps. Cross-domain redirects also require the destination host to be declared.

**How do I continue a large sitemap?** `SUMMARY.nextPageOffset` indicates the next offset when more discovered pages exist. Set `pageOffset` on a new run. Sources must remain stable; this is offset pagination, not a persisted snapshot. `discoveryComplete: false` means discovery was limited or failed; a null next offset then does not prove there are no more URLs. Split sitemap roots or provide explicit URL batches.

**Why was robots.txt unavailable?** The Actor fails closed on denied, malformed or failed robots responses. 404/410 means no file. Increase delay for a larger crawl-delay requirement; never use this Actor to bypass restrictions.

**Do errors cost money?** Saved page diagnostics cost the same as successful rows because they are completed checks. Even with zero saved rows, the platform can charge usage. No synthetic start or default dataset item event should be added on top of `audited-page`.

**Can I resume or resurrect a billed run?** Start a new audit. A cloud run with saved results, existing charges, restarts or resurrection is rejected to avoid accidental duplicate billing. A fresh execution is a new audit and can be billed again.

**Where are data stored?** In the run's Dataset and key-value store. No HTML bodies, cookies or contact databases are retained by this product. URLs may contain sensitive query parameters: do not supply them. Manage access and delete stored outputs through Apify.

### Support

Report a reproducible issue through this Actor's Issues tab, including an example input with secrets removed, the run ID, expected result and issue code. Do not include private URLs, access tokens or credentials.

Version0.1.0: first local review candidate. No testimonials, sales or customer outcomes are claimed.

# Changelog

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

# Actor input Schema

## `permissionConfirmed` (type: `boolean`):

Required. Must be true at runtime. The prefilled confirmation covers only the example.com demo. Before changing hosts, confirm you have permission to audit every site. Only public pages and explicitly allowed hosts are fetched.

## `allowedHosts` (type: `array`):

Exact hostnames, including each www/subdomain used. No wildcards, schemes or ports.

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

Explicit public page URLs. Combined with sitemap URLs; at least one source is required.

## `sitemapUrls` (type: `array`):

Direct XML or XML.gz sitemap URLs. Indexes are followed within maxSitemaps.

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

Max Pages. Allowed range: 1 to 1000. Default: 100.

## `maxResults` (type: `integer`):

Max Results. Allowed range: 1 to 1000. Default: 100.

## `maxSitemaps` (type: `integer`):

Max Sitemaps. Allowed range: 1 to 20. Default: 10.

## `pageOffset` (type: `integer`):

Skip this many unique discovered page URLs; stable sources required across runs. SUMMARY provides the next offset when known.

## `maxRequests` (type: `integer`):

Max Requests. Allowed range: 1 to 3000. Default: 500.

## `maxDurationSecs` (type: `integer`):

Max Duration Secs. Allowed range: 1 to 300. Default: 120.

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

Max Concurrency. Allowed range: 1 to 5. Default: 2.

## `maxRetries` (type: `integer`):

Max Retries. Allowed range: 0 to 2. Default: 1.

## `requestTimeoutMs` (type: `integer`):

Request Timeout Ms. Allowed range: 100 to 15000. Default: 5000.

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

Request Delay Ms. Allowed range: 200 to 5000. Default: 500.

## `maxResponseBytes` (type: `integer`):

Max Response Bytes. Allowed range: 1024 to 1048576. Default: 524288.

## `maxTotalBytes` (type: `integer`):

Max Total Bytes. Allowed range: 1024 to 67108864. Default: 16777216.

## Actor input object example

```json
{
  "permissionConfirmed": true,
  "allowedHosts": [
    "example.com"
  ],
  "pageUrls": [
    "https://example.com/"
  ],
  "sitemapUrls": [],
  "maxPages": 1,
  "maxResults": 1,
  "maxSitemaps": 1,
  "pageOffset": 0,
  "maxRequests": 3,
  "maxDurationSecs": 10,
  "maxConcurrency": 1,
  "maxRetries": 1,
  "requestTimeoutMs": 3000,
  "requestDelayMs": 500,
  "maxResponseBytes": 65536,
  "maxTotalBytes": 131072
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `report` (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 = {
    "permissionConfirmed": true,
    "allowedHosts": [
        "example.com"
    ],
    "pageUrls": [
        "https://example.com/"
    ],
    "sitemapUrls": [],
    "maxPages": 1,
    "maxResults": 1,
    "maxSitemaps": 1,
    "pageOffset": 0,
    "maxRequests": 3,
    "maxDurationSecs": 10,
    "maxConcurrency": 1,
    "maxRetries": 0,
    "requestTimeoutMs": 3000,
    "requestDelayMs": 500,
    "maxResponseBytes": 65536,
    "maxTotalBytes": 131072
};

// Run the Actor and wait for it to finish
const run = await client.actor("ect200/hreflang-sitemap-auditor").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 = {
    "permissionConfirmed": True,
    "allowedHosts": ["example.com"],
    "pageUrls": ["https://example.com/"],
    "sitemapUrls": [],
    "maxPages": 1,
    "maxResults": 1,
    "maxSitemaps": 1,
    "pageOffset": 0,
    "maxRequests": 3,
    "maxDurationSecs": 10,
    "maxConcurrency": 1,
    "maxRetries": 0,
    "requestTimeoutMs": 3000,
    "requestDelayMs": 500,
    "maxResponseBytes": 65536,
    "maxTotalBytes": 131072,
}

# Run the Actor and wait for it to finish
run = client.actor("ect200/hreflang-sitemap-auditor").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 '{
  "permissionConfirmed": true,
  "allowedHosts": [
    "example.com"
  ],
  "pageUrls": [
    "https://example.com/"
  ],
  "sitemapUrls": [],
  "maxPages": 1,
  "maxResults": 1,
  "maxSitemaps": 1,
  "pageOffset": 0,
  "maxRequests": 3,
  "maxDurationSecs": 10,
  "maxConcurrency": 1,
  "maxRetries": 0,
  "requestTimeoutMs": 3000,
  "requestDelayMs": 500,
  "maxResponseBytes": 65536,
  "maxTotalBytes": 131072
}' |
apify call ect200/hreflang-sitemap-auditor --silent --output-dataset

```

## MCP server setup

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

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/NZzR218Pf3afyooXj/builds/da5bqrpdfHG4tP048/openapi.json
