# SEO Release Diff – Staging vs Production (`resolute_client/seo-release-diff`) Actor

Catch SEO changes before a site release: compare titles, canonicals, noindex and H1 with exact before/after evidence. See the real Next.js 14 vs 15 example: 4 changes in 1 page pair ($0.006 event ceiling). Try your production and staging URLs.

- **URL**: https://apify.com/resolute_client/seo-release-diff.md
- **Developed by:** [Kais](https://apify.com/resolute_client) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 page pair compareds

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

## SEO Release Diff — Staging vs Production

Compare old and new public pages before a redesign, theme change or migration. Get field-level before/after evidence and a machine-readable CI flag in one run, without exporting and joining two audits.

### Live Output Example — Next.js 14 vs 15

A framework upgrade can change the canonical path, page title and heading before your release team notices. Compare the current and candidate pages in one run to see exactly what needs review.

**Real public-page capture, 13 October 2026 (UTC).** This example runs the Actor's actual HTTP fetching and comparison code against the official Next.js 14 and 15 upgrade guides, with robots.txt checks enabled. Both pages returned HTTP 200. It is a comparison of two documentation pages, not a claim about a customer migration or every Next.js website.

- Reference: [Next.js 14 upgrade guide](https://nextjs.org/docs/14/app/building-your-application/upgrading/version-14)
- Candidate: [Next.js 15 upgrade guide](https://nextjs.org/docs/15/app/guides/upgrading/version-15)

| Measured field | Next.js 14 reference | Next.js 15 candidate |
| --- | --- | --- |
| title | Upgrading: Version 14 | Next.js | Upgrading: Version 15 | Next.js |
| description | Upgrade your Next.js Application from Version 13 to 14. | Upgrade your Next.js Application from Version 14 to 15. |
| canonical | //{origin}/docs/app/building-your-application/upgrading/version-14 | //{origin}/docs/app/guides/upgrading/version-15 |
| h1 | Version 14 | How to upgrade to version 15 |

**Actual result: `CHANGED`, `changeCount: 4`, `ciPassed: true`.** The Actor reported `CANONICAL_CHANGED` as a warning. Neither page added noindex, lost a title/description/H1/canonical, or increased malformed JSON-LD. The canonical values in the table use the Actor's default `{origin}` normalization; the path change remains visible. With `failOnAnyChange:true`, these same changes make `ciPassed:false`.

For a concrete cost example, this is **1 page pair / 2 pages**, with a maximum event charge of **$0.006** at 256 MB. Ten completed pairs have an event-price ceiling of **$0.051**. Those figures are the listed run charges, not estimated SEO revenue or guaranteed savings.

[Try this comparison in SEO Release Diff](https://apify.com/resolute_client/seo-release-diff): open the Actor input, paste the example below, and click Start. Use your own production/staging URLs for your release check.

```json
{"pairs":[{"label":"Next.js 14 vs 15 upgrade guides","referenceUrl":"https://nextjs.org/docs/14/app/building-your-application/upgrading/version-14","candidateUrl":"https://nextjs.org/docs/15/app/guides/upgrading/version-15"}],"maxPairs":1,"normalizeOrigins":true,"failOnAnyChange":false,"respectRobotsTxt":true}
```

This checks server-returned HTML metadata. Review whether the reported changes are intentional before releasing your own site.

### Quick start

```json
{"pairs":[{"label":"Demo: identical page","referenceUrl":"https://example.com","candidateUrl":"https://example.com"}],"maxPairs":1,"normalizeOrigins":true,"failOnAnyChange":false,"respectRobotsTxt":true}
```

1. Open this Actor on Apify Store or Console and sign in to your Apify account. Paste the JSON above into the input editor.
2. Keep the default **256 MB** memory. Set the run's **maximum charge** to **$0.01** for this one-pair demo. Its event-price ceiling is **$0.006**, not $0.01; available Apify credits depend on your account.
3. Start the run. Open the run's dataset to see the comparison, and the default key-value store's **SUMMARY** record to check `allPairsCompleted` and `ciPassed`.

The demo compares the same public page with itself. It should return PASS if the page and its robots.txt can be inspected; an access or fetch failure returns ERROR. For a real release check, use your current public page as `referenceUrl` and the corresponding public staging page as `candidateUrl`. Repeat that object for each page pair and raise `maxPairs` to the number you want inspected. No target-site login, target API key, proxy subscription or AI service is required.

### What changes are checked?

- Title, meta descriptions and H1 text.
- Canonical URLs, meta robots/googlebot and the X-Robots-Tag header, including newly added noindex/none.
- HTML language and hreflang language/target inventory.
- Detected JSON-LD types and the number of malformed JSON-LD blocks.

Each result contains `reference` and `candidate` snapshots, `changes` with exact before/after values, and `findings` with reproducible codes. Whitespace in title/H1 text is normalized. Canonical and hreflang origins from the compared pages are treated as equivalent by default; paths and query strings remain significant. Set `normalizeOrigins:false` for literal origin comparison. Final redirect URLs remain in the snapshots but changing host alone does not fail the comparison.

### Example: catching an added noindex

**Synthetic example, generated offline from this Actor's current comparison code. These are invented HTML fixtures, not a customer run or a public website result.** The reference HTML contains `index,follow`; the candidate HTML contains `noindex,follow`. Both pages keep the same title, H1, description and canonical path.

Dataset excerpt (snapshots, URLs and timestamps omitted):

```json
{
  "status": "REGRESSION",
  "ciPassed": false,
  "changeCount": 2,
  "changes": [
    {
      "field": "robots",
      "before": [
        "follow",
        "index"
      ],
      "after": [
        "follow",
        "noindex"
      ]
    },
    {
      "field": "hasNoindex",
      "before": false,
      "after": true
    }
  ],
  "findings": [
    {
      "code": "NOINDEX_ADDED",
      "severity": "error",
      "field": "robots",
      "message": "Candidate adds noindex/none. This may be intentional on staging; remove before production if indexing is intended."
    }
  ]
}
```

The corresponding `SUMMARY` excerpt is:

```json
{
  "counts": {
    "PASS": 0,
    "CHANGED": 0,
    "REGRESSION": 1,
    "ERROR": 0
  },
  "requestedPairs": 1,
  "completedThisSession": 1,
  "previouslyCompleted": 0,
  "allPairsCompleted": true,
  "ciPassed": false
}
```

`allPairsCompleted:true` means the requested comparisons finished; it does not mean they passed. Here, `ciPassed:false` reports the added noindex. Staging noindex may be intentional: confirm the intended production setting before release. This checks the HTML directives, not Google's actual indexing.

### Results and CI

`PASS`: the measured fields match. `CHANGED`: at least one field changed, with no detected high-priority regression. `REGRESSION`: candidate added noindex, removed a reference title/description/H1/canonical, or increased malformed JSON-LD blocks. `ERROR`: a target could not be inspected. Intentional staging noindex is still reported: decide whether it will be removed before deployment.

`ciPassed` is false for REGRESSION and ERROR. With `failOnAnyChange:true`, CHANGED also returns false. Your CI should also require `SUMMARY.allPairsCompleted:true`. The SUMMARY record is in the default key-value store; results export as JSON, CSV or Excel in Apify Console.

For CI, also confirm the dataset contains a result for each unique page pair you requested, within `maxPairs`. Block the release check if the run hits its cost limit, is interrupted, or is missing any expected pair; do not rely on the SUMMARY record or on the platform run status alone.

A PASS is limited to these checks. It is not approval to deploy, an SEO score or a ranking prediction. A changed title is not automatically a regression.

### Pricing

Price: **$5 per 1,000 completed page-pair comparisons** ($0.005 per pair), plus **$0.001 for the Actor start at the default 256 MB memory**. Apify charges one start event up to 1 GB, then scales the number of start events with memory. Platform usage is included in this Actor's event pricing; there is no additional default dataset-item event. A pair means two inspected pages; an identical URL is fetched once. A completed comparison is charged even if it detects a regression. ERROR rows have no custom comparison fee. Duplicate URL pairs within one run are processed once; a later run is new work.

At the default 256 MB memory, one completed pair costs at most **$0.006**, and ten completed pairs cost at most **$0.051** in event charges. The current default run cap of **$0.05** is below the ten-pair ceiling. For ten pairs, set a maximum charge of **at least $0.051** (for example, $0.06); this cap limits spending and is not a flat fee. A lower cap may stop the run before all requested pairs finish. Always require `SUMMARY.allPairsCompleted:true` together with `SUMMARY.ciPassed:true` before treating the measured checks as passed. Platform-displayed pricing is authoritative. No external paid API or proxy is used. Developer margins depend on actual platform resource costs; they are not guaranteed by this price.

### Bounds and limitations

Up to 50 unique pairs; default maxPairs is 10. Each response is limited to 2 MiB, each request has a 12-second timeout, redirect depth is capped and processing stops near nine minutes. Incomplete runs must not pass your release check; verify completion in SUMMARY and the dataset. Requests are sequential with a short delay. There is no automatic proxy escalation.

Reads server-returned HTML only. JavaScript-injected tags may be absent. Private networks, localhost, login pages and credential-bearing URLs are excluded; every network connection uses a validated public IP, including redirects. Robots.txt is checked by default; an unavailable robots file (other than 404) or a disallowed path produces ERROR. Use only pages you are authorized to inspect. Turning robots checks off is for sites you control.

JSON-LD inspection checks parsing and type inventory, not full schema vocabulary, required rich-result fields or Google's rich-result eligibility. Hreflang targets and reciprocal links are not fetched. This version does not measure browser appearance, Core Web Vitals, full content parity, broken links or actual search indexing.

### Practical uses

- Compare a Shopify theme preview that is publicly accessible with production.
- Check a headless website rebuild for lost canonical, H1 or JSON-LD types.
- Add a small URL-pair release check to GitHub Actions, n8n or Make using the Apify API.
- Compare a previous static deployment with a candidate deployment.

For support, include the run ID and public reproduction URLs; never include credentials or customer personal data.

# Actor input Schema

## `pairs` (type: `array`):

Each pair has referenceUrl (current/old) and candidateUrl (new). Public HTTP(S) only. Up to 50 unique pairs.

## `maxPairs` (type: `integer`):

Stop after this many unique pairs.

## `normalizeOrigins` (type: `boolean`):

Treat the reference and candidate origins as equivalent for canonical and hreflang URLs. Path/query differences remain visible.

## `failOnAnyChange` (type: `boolean`):

If true, any measured difference fails the CI flag. Otherwise only deterministic high-priority regressions fail it.

## `respectRobotsTxt` (type: `boolean`):

Check robots.txt for KaisSEOReleaseDiff before reading pages. If unavailable (except 404), report ERROR rather than assume access.

## Actor input object example

```json
{
  "pairs": [
    {
      "label": "Demo: identical page",
      "referenceUrl": "https://example.com",
      "candidateUrl": "https://example.com"
    }
  ],
  "maxPairs": 10,
  "normalizeOrigins": true,
  "failOnAnyChange": false,
  "respectRobotsTxt": true
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "pairs": [
        {
            "label": "Demo: identical page",
            "referenceUrl": "https://example.com",
            "candidateUrl": "https://example.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("resolute_client/seo-release-diff").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 = { "pairs": [{
            "label": "Demo: identical page",
            "referenceUrl": "https://example.com",
            "candidateUrl": "https://example.com",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("resolute_client/seo-release-diff").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 '{
  "pairs": [
    {
      "label": "Demo: identical page",
      "referenceUrl": "https://example.com",
      "candidateUrl": "https://example.com"
    }
  ]
}' |
apify call resolute_client/seo-release-diff --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,resolute_client/seo-release-diff"
        }
    }
}
```

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/YVuoUX2HV4gJgeAbi/builds/H7ZLR5gJCrmDF1pcC/openapi.json
