# GitHub Pages Custom Domain Go-Live and Dangling-Domain Watch (`kingii98/github-pages-custom-domain-go-live-and-dangling-domain-watch`) Actor

Checks GitHub Pages sites on custom domains: DNS records for the apex and www, HTTPS status of each required path, the HTTP-to-HTTPS redirect, the certificate host name, dangling-domain risk, and the Pages API state when a token is given. Gives PASS, WARN

- **URL**: https://apify.com/kingii98/github-pages-custom-domain-go-live-and-dangling-domain-watch.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (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 run starteds

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

## GitHub Pages Custom Domain Go-Live and Dangling-Domain Watch

This Actor checks GitHub Pages sites on custom domains. For each domain it tells you:

- when DNS **stops pointing** at GitHub Pages,
- when **HTTPS** or the **HTTP-to-HTTPS redirect** breaks,
- when the **certificate** does not match the domain name,
- when the domain still points at GitHub Pages but **no site serves it** (dangling-domain risk). Another GitHub user can then claim the domain for their own site.

It gives each domain a verdict: `PASS`, `WARN` or `FAIL`. It keeps the last verdict of each domain, and it sends **one alert for each verdict change**. A site that stays broken does not send one alert for each run.

Use it when you run several GitHub Pages sites on custom domains, for example as an agency, an open-source maintainer or a documentation team.

The Actor is HTTP only. It uses DNS-over-HTTPS queries to free public resolvers and plain HTTP and HTTPS requests. It uses no browser, no proxy and no database.

### When to run it

- **On an Apify schedule**, for example once each day. The Actor sends an alert only when a verdict changes.
- **From a Pages deploy workflow**, after each publish. Read `gate_pass` from the summary record.

### How it works

For each domain, the Actor does these steps:

1. It asks Google Public DNS (and Cloudflare when Google does not answer) for the A and AAAA records of the domain and of its www or apex partner. Each answer includes the CNAME chain. It marks each record that is not a documented GitHub Pages target.
2. It sends `GET http://<domain>/` and follows redirects until the first HTTPS URL. `http_to_https` is true when HTTP redirects to HTTPS on the domain or its partner.
3. It sends `GET https://<domain><path>` for the root path and each required path, and follows at most 3 redirects. A path passes on HTTP 2xx.
4. It reads the certificate result of the HTTPS request for the root path.
5. It looks for the GitHub Pages "site not found" page in the HTTP and HTTPS answers.
6. When you give `github_token` and the domain has a `repo`, it reads the GitHub Pages API state of the repository.
7. It gives the verdict, compares it with the stored verdict, and sends one alert when the verdict changed.

At the end it writes one summary record with the gate result, and it writes the new verdicts to the state record.

#### GitHub Pages targets

These targets were confirmed with a probe on 2026-09-15:

| Record type | GitHub Pages target |
| --- | --- |
| A | `185.199.108.153`, `185.199.109.153`, `185.199.110.153`, `185.199.111.153` |
| AAAA | `2606:50c0:8000::153`, `2606:50c0:8001::153`, `2606:50c0:8002::153`, `2606:50c0:8003::153` |
| CNAME | a host name that ends in `.github.io` |

The "site not found" page is an HTTP 404 with the text `There isn't a GitHub Pages site here.` or the title `Site not found · GitHub Pages`.

#### The www or apex partner

- For `www.example.com`, the partner is `example.com`.
- For `example.com` and `example.co.uk`, the partner is `www.example.com` and `www.example.co.uk`.
- A deeper subdomain, for example `docs.example.com`, has no partner.

A partner that does not exist is correct. A partner that exists and has a record that is not a GitHub Pages target gives a warning.

### Input

| Field | Required | Default | Description |
| --- | --- | --- | --- |
| `domains` | Yes | `choosealicense.com` and `semver.org` | 1 to 100 items. |
| `domains[].domain` | Yes | none | A custom domain of a GitHub Pages site, for example `docs.example.com`. |
| `domains[].repo` | No | none | The repository in the form `owner/name`. The Actor uses it only for the Pages API state. |
| `domains[].required_paths` | No | `["/"]` | 0 to 5 paths that start with `/`. The root path `/` is always checked. |
| `github_token` | No | none | A token with Pages read access. The Actor sends it only to `api.github.com`. It is a secret input field. |
| `alert_webhook_url` | No | empty | One public HTTPS URL. The Actor sends one JSON POST for each verdict change. |

Example input:

```json
{
  "domains": [
    {"domain": "docs.example.org", "repo": "example/docs", "required_paths": ["/", "/getting-started/"]},
    {"domain": "www.example.net"}
  ],
  "alert_webhook_url": "https://hooks.example.com/pages-watch"
}
```

The default input checks `choosealicense.com` with the paths `/` and `/licenses/`, and `semver.org` with the path `/`. Both are stable GitHub Pages sites with custom domains, so the default run gives two `PASS` verdicts.

### Output

#### One record for each domain

| Field | Description |
| --- | --- |
| `domain`, `repo` | The domain and the repository from the input. |
| `verdict` | `PASS`, `WARN` or `FAIL`. |
| `previous_verdict` | The stored verdict from the previous complete check, or null. |
| `changed` | True when `previous_verdict` exists and is different from `verdict`. |
| `reasons` | The reason codes. See the tables below. |
| `check_incomplete` | True when the check was not complete. See [Incomplete checks](#incomplete-checks). |
| `dns` | For the domain and its partner: the name, the role, the DNS status, and each A, AAAA and CNAME record with `pages_target` (true or false). |
| `dns_points_at_pages` | True when the domain has one or more GitHub Pages target records. |
| `non_pages_records` | Each record that is not a GitHub Pages target, as `name TYPE value`. |
| `https_checks` | For each path: the URL, the HTTP status, the final URL, the number of redirects, `ok` and the error. |
| `http_to_https` | True or false. Null when the HTTP request got no response. |
| `http_redirect_location` | The first redirect target of the HTTP request. |
| `cert_hostname_match` | True when the certificate is valid for the domain name. False on a host name mismatch. |
| `cert_valid` | True when the certificate is trusted. False when it is expired, self-signed or not trusted for another reason. |
| `tls_error` | The TLS error text, when there is one. |
| `dangling_domain_risk` | True when DNS points at GitHub Pages and the answer is the GitHub Pages "site not found" 404. |
| `pages_api` | When a token and a repo are given: `status`, `cname`, `public`, `https_enforced`, `certificate_state`, `certificate_expires_at`, `html_url`, `http_status` and `error`. Otherwise null. |
| `alert_status`, `alert_error` | `sent` or `failed`, when an alert was sent for this domain. |
| `checked_at` | The time of the check. |

##### FAIL reasons

| Reason | Meaning |
| --- | --- |
| `dns-not-found` | The domain has no A, AAAA or CNAME record (NXDOMAIN or an empty answer). |
| `dns-lookup-failed` | The resolvers gave SERVFAIL or another DNS error for the domain. |
| `dns-not-pointing-at-pages` | The domain has records, but none is a GitHub Pages target. |
| `dangling-domain-risk` | DNS points at GitHub Pages and GitHub Pages answers "site not found" for the domain. |
| `certificate-hostname-mismatch` | The HTTPS certificate is not valid for the domain name. |
| `certificate-invalid` | The HTTPS certificate is not trusted. |
| `https-path-failed` | A required path did not answer HTTP 2xx over HTTPS. |

##### WARN reasons

| Reason | Meaning |
| --- | --- |
| `dns-non-pages-record` | The domain points at GitHub Pages and also has a record that is not a GitHub Pages target. |
| `partner-non-pages-record` | The www or apex partner has a record that is not a GitHub Pages target. |
| `http-not-redirected-to-https` | HTTP does not redirect to HTTPS on the domain or its partner. |
| `http-check-failed` | The HTTP request for the redirect check got no response. |
| `pages-api-error` | The GitHub Pages API did not return the Pages state. |
| `pages-cname-mismatch` | The custom domain in the Pages API is not the domain or its partner. |
| `pages-https-not-enforced` | The Pages API reports that HTTPS is not enforced. |
| `pages-certificate-not-approved` | The certificate state in the Pages API is not `approved`. |
| `pages-build-errored` | The Pages API reports the build status `errored`. |
| `dns-check-incomplete` | No resolver answered, so the DNS checks were not done. |
| `check-timed-out` | The check of the domain took longer than 150 s. |
| `check-error` | The check of the domain stopped on an unexpected error. |

A domain with one or more FAIL reasons gets `FAIL`. A domain with only WARN reasons gets `WARN`. A domain with no reason gets `PASS`.

#### One summary record

| Field | Description |
| --- | --- |
| `gate_pass` | True when each domain had a complete check and no domain has `FAIL`. |
| `domains_requested`, `domains_checked` | The domains in the input, and the domains with a complete check. |
| `verdict_counts` | The count of `PASS`, `WARN` and `FAIL`. |
| `changed_count`, `changed_domains` | The domains whose verdict changed. |
| `failing_domains` | The domains with `FAIL`. |
| `incomplete_domains` | The domains with an incomplete check. |
| `domains_not_checked` | The domains that were not started, because the maximum total charge was reached. |
| `alerts_sent`, `alert_failures` | The webhook results. |
| `state_error` | The error, when the state record was not read or not written. |

The run ends SUCCEEDED for every result. A `FAIL` verdict, a failed gate and an unreachable domain are dataset results and a status message, not a failed run.

### The verdict state

The Actor keeps the last verdict of each domain in the record `VERDICTS` of the named key-value store `github-pages-domain-watch-state`. A named store stays after the run ends, so the state continues from one scheduled run to the next.

- The first check of a domain writes its verdict. It has no previous verdict, so `changed` is false and no alert is sent.
- A run changes only the entries of its own domains. The entries of other domains stay.
- When the Actor cannot read the state, it shows no change, sends no alert and does not write the state.
- Two runs that end at the same time can write the state in the wrong order. Do not start two runs with the same domains at the same time.

#### Incomplete checks

A check is incomplete when no DNS resolver answered, when the check took longer than 150 s, or when it stopped on an unexpected error. An incomplete check does not change the stored verdict, sends no alert, is not charged as `domain-checked`, and sets `gate_pass` to false.

### Webhook payload

The Actor sends one POST for each domain whose verdict changed:

```json
{
  "event": "verdict-changed",
  "actor": "github-pages-custom-domain-go-live-and-dangling-domain-watch",
  "domain": "docs.example.org",
  "repo": "example/docs",
  "verdict": "FAIL",
  "previous_verdict": "PASS",
  "reasons": ["dangling-domain-risk", "certificate-hostname-mismatch", "https-path-failed"],
  "dns_points_at_pages": true,
  "dangling_domain_risk": true,
  "http_to_https": false,
  "cert_hostname_match": false,
  "https_checks": [{"path": "/", "status": null, "ok": false}],
  "checked_at": "2026-09-15T06:00:04Z"
}
```

The payload never holds the token.

### Pricing

This Actor uses pay-per-event pricing.

| Event | Unit | Price (USD) | When it is charged |
| --- | --- | --- | --- |
| `run-started` | one actor run | 0.005 | Once for each run, after the input is valid and before the first request. |
| `domain-checked` | one custom domain checked for DNS, HTTPS, redirect, and Pages state in one run | 0.004 | For each domain with a complete check, for each verdict, also in the first run. |
| `verdict-changed` | one domain whose verdict differs from the previous run | 0.01 | For each domain whose verdict changed. It is charged also when no webhook URL is given. |

An incomplete check is not charged as `domain-checked`. The first check of a domain is not charged as `verdict-changed`. A domain that keeps the same verdict is charged `domain-checked` only.

The default maximum total charge for one run is USD 1.50. When a run reaches the maximum total charge, the Actor starts no more domains and lists them in `domains_not_checked`.

#### Examples

| Case | Runs | Domains per run | Verdict changes per run | Cost per run (USD) | Total (USD) |
| --- | --- | --- | --- | --- | --- |
| 10 domains once each day for 30 days, no change | 30 | 10 | 0 | 0.045 | 1.35 |
| One call after each of 20 deploys, 1 domain, no change | 20 | 1 | 0 | 0.009 | 0.18 |
| One daily run of 10 domains with 2 verdict changes | 1 | 10 | 2 | 0.065 | 0.065 |
| One run of 100 domains where every verdict changes | 1 | 100 | 100 | 1.405 | 1.405 |

The run of 100 domains where every verdict changes costs USD 1.405, which is less than the default maximum total charge of USD 1.50.

### Safety and limits

- Each HTTP and HTTPS request target, and each redirect hop, is checked. A loopback, private or reserved address is refused.
- At most 3 redirects for each request. The webhook POST and the Pages API call follow no redirect.
- Each body read stops at 16 KB. Each request has a 10 s timeout and a 20 s deadline. The check of one domain has a 150 s deadline. At most 5 domains are checked at the same time.
- Certificate verification is always on.
- The token goes only to `api.github.com`. It is not logged and not written to the dataset or the webhook payload.

### Scope

The Actor checks GitHub Pages hosting state. It does not compare certificates across endpoints after a renewal, it does not wait for DNS propagation after a cutover, and it does not audit `robots.txt`, sitemaps or `llms.txt` content.

# Actor input Schema

## `domains` (type: `array`):

1 to 100 items. Each item has domain (a custom domain of a GitHub Pages site, for example docs.example.com), optional repo (owner/name, used only for the Pages API state) and optional required\_paths (0 to 5 paths that start with '/', default \["/"]). The root path '/' is always checked.

## `github_token` (type: `string`):

Optional. A token with Pages read access. The Actor sends it only to api.github.com, to read the Pages API state (custom domain, HTTPS enforced, certificate state) of each domain that has a repo. All other checks work without a token.

## `alert_webhook_url` (type: `string`):

Optional. One public HTTPS URL. The Actor sends one JSON POST for each domain whose verdict changed since the previous run. Redirects are not followed. Leave empty to send no alert.

## Actor input object example

```json
{
  "domains": [
    {
      "domain": "choosealicense.com",
      "repo": "github/choosealicense.com",
      "required_paths": [
        "/",
        "/licenses/"
      ]
    },
    {
      "domain": "semver.org",
      "repo": "semver/semver.org",
      "required_paths": [
        "/"
      ]
    }
  ],
  "alert_webhook_url": ""
}
```

# Actor output Schema

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

No description

## `domains` (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 = {
    "domains": [
        {
            "domain": "choosealicense.com",
            "repo": "github/choosealicense.com",
            "required_paths": [
                "/",
                "/licenses/"
            ]
        },
        {
            "domain": "semver.org",
            "repo": "semver/semver.org",
            "required_paths": [
                "/"
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/github-pages-custom-domain-go-live-and-dangling-domain-watch").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 = { "domains": [
        {
            "domain": "choosealicense.com",
            "repo": "github/choosealicense.com",
            "required_paths": [
                "/",
                "/licenses/",
            ],
        },
        {
            "domain": "semver.org",
            "repo": "semver/semver.org",
            "required_paths": ["/"],
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("kingii98/github-pages-custom-domain-go-live-and-dangling-domain-watch").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 '{
  "domains": [
    {
      "domain": "choosealicense.com",
      "repo": "github/choosealicense.com",
      "required_paths": [
        "/",
        "/licenses/"
      ]
    },
    {
      "domain": "semver.org",
      "repo": "semver/semver.org",
      "required_paths": [
        "/"
      ]
    }
  ]
}' |
apify call kingii98/github-pages-custom-domain-go-live-and-dangling-domain-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/github-pages-custom-domain-go-live-and-dangling-domain-watch"
        }
    }
}
```

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/ueCEdLfgVU3qnneSS/builds/OqwKA58MCRUuiYScD/openapi.json
