# PWA Install Readiness and Manifest Asset Gate (`kingii98/pwa-install-readiness-and-manifest-asset-gate`) Actor

Reads the manifest link from the HTML head of each origin, reads the web app manifest, then verifies every declared icon, screenshot and shortcut icon with one HEAD and one ranged GET. It reports the true pixel size read from the file header against the d

- **URL**: https://apify.com/kingii98/pwa-install-readiness-and-manifest-asset-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.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?

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

## PWA Install Readiness and Manifest Asset Gate

Test the **install contract** of your web app after each deploy. The Actor reads the manifest link
from the HTML head, reads the web app manifest, and then verifies every icon, every screenshot and
every shortcut icon that the manifest declares. The report gives one verdict of **PASS**, **WARN**
or **FAIL** for each origin, and a reason code for each rule that did not pass.

A build change or an asset rename removes a screenshot or an icon. The install dialog, the splash
screen and the long-press shortcuts then break, and nobody sees the loss until a user reports it.
This Actor finds that loss in one run.

The store holds many Actors that *extract* icons. This one *validates an install contract*. The
output is a verdict, not an asset list.

HTTP only. No browser, no proxy, no external database. The Actor never downloads a whole image: it
reads the true pixel size from the first bytes of the file.

### What the Actor sends

For each origin:

1. `GET <origin>` — read the HTML head, find `<link rel="manifest">` and the inline
   `navigator.serviceWorker.register(...)` hints. The body read stops at `maxDocumentBytes`.
2. `GET <manifest URL>` — read the manifest and parse it as JSON.
3. `HEAD <service worker script>` — try each inline hint first, then each path of
   `serviceWorkerPaths`, and stop at the first script that answers.

For each declared icon, screenshot and shortcut icon:

4. `HEAD <asset URL>` — read the status, the content type and the byte size.
5. `GET <asset URL>` with `Range: bytes=0-<assetHeaderBytes - 1>` — read the file header, and get
   the true pixel size from it.

The Actor follows each redirect by hand, one hop at a time. Every hop takes the public-address test
again. The Actor never sends a state-changing request.

#### Byte count that each image format needs

The true size can be read only when the header is complete inside the bytes that were read. The
default of 64 bytes is sufficient for every format in this table. An incomplete header gives a
size of `unknown`, which is a skipped comparison, never a failure.

| Format | Bytes needed |
| --- | --- |
| PNG | 24 |
| WebP | 30 |
| BMP | 26 |
| GIF | 10 |
| ICO | 8 |
| JPEG | The frame header can sit far into the file. Raise `assetHeaderBytes` for JPEG screenshots. |
| SVG | The `width`, the `height` or the `viewBox` attribute must be inside the read bytes. |

### Input

Run the Actor with **empty input** to see the default fixture against a stable public PWA. Every
field has a default, so an empty run always starts.

| Field | Default | Meaning |
| --- | --- | --- |
| `origins` | `["https://squoosh.app/"]` | 1 to 100 public site origins or page URLs. Private, loopback and reserved addresses are refused, and so are URLs that hold credentials. |
| `requiredIconSizes` | `["192", "512"]` | Square icon edges that a reachable general-purpose icon must cover. A larger declared square covers a smaller requirement. |
| `requireMaskableIcon` | `true` | The manifest must declare at least one reachable icon with purpose `maskable`. |
| `requiredScreenshotFormFactors` | `["narrow", "wide"]` | Form factors that the screenshots must cover for the rich install dialog. An empty list turns the rule off. |
| `requiredShortcutCount` | `0` | How many shortcuts the manifest must declare. `0` turns the rule off. |
| `allowedDisplayModes` | all four installable modes | The display modes that count as installable. `display`, or one entry of `display_override`, must hold one of them. |
| `rules` | all 19 rules | The rules that decide the verdict. An empty list runs every rule. |
| `serviceWorkerPaths` | `["/sw.js", "/service-worker.js", "/serviceworker.js"]` | Paths to try for the service worker script. 10 entries or fewer. |
| `maxAssets` | `200` | Hard bound on the assets that one run verifies. Maximum 500. |
| `assetHeaderBytes` | `64` | Bytes of each asset to read with the ranged GET. 32 to 8192. |
| `maxRedirects` | `5` | Maximum redirect hops for one request. 0 to 10. |
| `timeoutSeconds` | `20` | Timeout of one request. 1 to 60. |
| `maxDocumentBytes` | `2097152` | Maximum bytes to read from one HTML page or one manifest. |
| `concurrency` | `4` | Asset probes at the same time. 1 to 8. |
| `userAgent` | `PwaInstallGate/0.1 (+https://apify.com)` | The `User-Agent` header. ASCII only. |

The profile is the group of the six fields that carry the mark `(profile)` in the schema:
`requiredIconSizes`, `requireMaskableIcon`, `requiredScreenshotFormFactors`,
`requiredShortcutCount`, `allowedDisplayModes` and `rules`.

### Output

The dataset holds three record types.

**`origin`** — one record for each origin. It gives `originStatus` (`OK`, `TARGET_REFUSED` or
`UNREACHABLE`), `pageUrl`, `pageStatus`, `manifestUrl`, `manifestStatus`, `manifestContentType`,
`manifestParsed`, the manifest members `name`, `shortName`, `startUrl`, `scope`, `display`,
`backgroundColor` and `themeColor`, the counts `iconsDeclared`, `screenshotsDeclared`,
`shortcutsDeclared`, `shortcutIconsDeclared`, `assetsVerified`, `assetsBroken` and
`assetsSizeMismatch`, the coverage maps `iconSizeCoverage` and `screenshotFormFactors`,
`maskableIcons`, `serviceWorkerUrl`, `serviceWorkerStatus`, and the verdict fields `verdict`,
`failedRules`, `warnRules`, `reasonCodes`, `ruleStatuses`, `ruleDetails`, `unusableManifestEntries`
and `note`.

**`asset`** — one record for each declared icon, screenshot and shortcut icon. It gives
`assetKind`, `assetUrl`, `declaredSrc`, `declaredSizes`, `declaredType`, `purposes`, `formFactor`,
`shortcutName`, `httpStatus`, `headStatus`, `rangeStatus`, `finalUrl`, `redirectDepth`,
`contentType`, `byteSize`, `headerBytesRead`, the true size read from the file header
(`trueWidth`, `trueHeight`, `trueSize`, `trueFormat`), `sizeMatch`, `reachable`, `error` and
`billed`.

**`summary`** — one record for the run: `originsRequested`, `originsAudited`, `originsBlocked`,
`originsPass`, `originsWarn`, `originsFail`, `assetsDeclared`, `assetsVerified`, `assetsBroken`,
`assetsSizeMismatch`, `reasonCodeCounts`, `rulesEnabled`, `profile`, `installGatePass` and
`status`.

A business verdict is never a failed run. A failed gate, a refused target, an unreachable origin
and a missing manifest all end with a **SUCCEEDED** run, a dataset record and a status message.
Only a malfunction, for example an input that cannot be parsed, gives a failed run.

#### Verdict

Each rule gives `pass`, `warn`, `fail` or `skip`. A rule that cannot be decided is `skip`, never a
failure. The verdict of one origin is **FAIL** when one rule failed, **WARN** when one rule warned,
and **PASS** when no rule failed and no rule warned. The gate of the run passes when no origin
failed and no origin was blocked.

A rule that breaks the install dialog itself has the severity `fail`. A rule that only degrades the
installed experience has the severity `warn`.

#### Reason codes

| Reason code | Severity | Meaning |
| --- | --- | --- |
| `INSECURE_ORIGIN` | fail | The page finally served over HTTP. A browser offers the install dialog on a secure origin only. |
| `MANIFEST_LINK_MISSING` | fail | The HTML head declares no `<link rel="manifest">` element. |
| `MANIFEST_UNREACHABLE` | fail | The manifest gave an error status or no answer. |
| `MANIFEST_NOT_JSON` | fail | The manifest bytes are not a JSON object. |
| `MANIFEST_CONTENT_TYPE_MISSING` | warn | The manifest was served without a `Content-Type` header. |
| `MANIFEST_CONTENT_TYPE_GENERIC` | warn | The manifest was served as `application/json`, not as `application/manifest+json`. |
| `MANIFEST_CONTENT_TYPE_WRONG` | warn | The manifest was served with an unrelated media type. |
| `NAME_MISSING` | fail | The manifest declares neither `name` nor `short_name`, so the install dialog has no title. |
| `SHORT_NAME_MISSING` | warn | The manifest declares no `short_name`, so the home-screen label uses `name`. |
| `START_URL_CROSS_ORIGIN` | fail | `start_url` points at another origin. |
| `SCOPE_EXCLUDES_START_URL` | warn | `scope` does not hold `start_url`. |
| `DISPLAY_MISSING` | fail | The manifest declares no `display` member. |
| `DISPLAY_NOT_INSTALLABLE` | fail | `display` and `display_override` hold no allowed mode. |
| `BACKGROUND_COLOR_MISSING` | warn | No `background_color`, so the splash screen uses a browser default. |
| `THEME_COLOR_MISSING` | warn | No `theme_color`, so the title bar uses a browser default. |
| `ICON_SIZE_MISSING` | fail | No reachable general-purpose icon covers a required edge. |
| `ICON_ASSET_UNREACHABLE` | fail | One declared icon did not load. |
| `ICON_SIZE_MISMATCH` | fail | One icon file does not hold the size that the manifest declares. |
| `MASKABLE_ICON_MISSING` | fail | No reachable icon declares purpose `maskable`, so Android crops the icon into the adaptive shape. |
| `SCREENSHOT_FORM_FACTOR_MISSING` | warn | No reachable screenshot serves a required form factor. The rich install dialog falls back to the plain one. |
| `SHORTCUT_COUNT_LOW` | warn | The manifest declares fewer shortcuts than the profile requires. |
| `SHORTCUT_ICON_UNREACHABLE` | warn | One declared shortcut icon did not load. |
| `SHORTCUT_ICON_SIZE_MISMATCH` | warn | One shortcut icon file does not hold the declared size. |
| `SERVICE_WORKER_UNREACHABLE` | warn | No service worker script answered, and the page holds no inline registration. A worker that is registered inside a bundled script file is not seen by an HTTP-only check. |

### Pay per event

| Event | Unit | Price |
| --- | --- | --- |
| `run-started` | one run | USD 0.02 |
| `manifest-audited` | one origin manifest audited | USD 0.03 |
| `asset-verified` | one declared icon, screenshot or shortcut icon verified | USD 0.001 |

The default maximum charge for one run is **USD 5.00**. That covers about 90 origins with 20
assets each.

**When each event fires:**

- `run-started` fires one time for each run, after the input was read and accepted. An input that
  cannot be parsed is not charged.
- `manifest-audited` fires **one time for each origin whose page was reached** and whose rules were
  evaluated. This includes an origin whose HTML head declares **no** manifest link: the work — the
  page request, the service worker probe and the rule evaluation — was done, and the buyer gets the
  `MANIFEST_LINK_MISSING` verdict. **You pay for a FAIL verdict of this kind.** An origin that the
  target guard refused, and an origin whose page gave an error status or no answer, produced no
  audit. Those are reported with `originStatus` of `TARGET_REFUSED` or `UNREACHABLE`, and they are
  **not** charged.
- `asset-verified` fires **one time for each declared asset that got a HEAD and a ranged GET**. An
  asset URL that the target guard refused got no request. It is reported with `billed: false`, and
  it is **not** charged. An asset that the `maxAssets` bound cut from the run got no request
  either, and it is not charged. The `note` field of the origin record counts the assets that were
  cut.

The `billed` field of each asset record says exactly what was charged, so you can compare the
dataset with your invoice.

### Repeat use

Run the Actor after each deploy of the web app or of the CDN, from your build pipeline, and once a
week as a regression run. The install contract is a group of static files that a build step
renames or removes without a warning. The `reasonCodes` field names the loss.

### Limits

- 100 origins for one run.
- 500 assets for one run, and `maxAssets` bounds it further.
- Loopback, private, link-local and reserved targets are refused, at the origin, at each redirect
  hop, and at each asset URL that the manifest supplies.
- A URL that holds credentials is refused.
- A service worker that is registered inside a bundled script file is not seen. This is an
  HTTP-only check, and it runs no JavaScript.

### Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `origins` (type: `array`):

1 to 100 public site origins or page URLs. The Actor reads the manifest link from the HTML head of each one. Private, loopback and reserved addresses are refused, and so are URLs that hold credentials.

## `requiredIconSizes` (type: `array`):

Square icon edges that the manifest must cover with a reachable general-purpose icon. An icon that declares a larger square covers a smaller requirement.

## `requireMaskableIcon` (type: `boolean`):

The manifest must declare at least one reachable icon with purpose "maskable". Without one, Android crops the icon into the adaptive shape.

## `requiredScreenshotFormFactors` (type: `array`):

Form factors that the screenshots must cover for the rich install dialog. A screenshot with no form\_factor member counts as narrow. An empty list turns the rule off.

## `requiredShortcutCount` (type: `integer`):

How many shortcuts the manifest must declare for the long-press menu. 0 turns the rule off.

## `allowedDisplayModes` (type: `array`):

The display modes that count as installable. The manifest passes when display, or an entry of display\_override, holds one of them.

## `rules` (type: `array`):

The rules that decide the verdict. An empty list runs every rule.

## `serviceWorkerPaths` (type: `array`):

Candidate paths for the service worker script, tried in order. A registration found inside an inline script of the page is tried first. Each path is resolved against the page URL.

## `maxAssets` (type: `integer`):

Hard cap on the number of verified assets in one run. Assets above the cap are counted in the origin record and are not verified or charged.

## `assetHeaderBytes` (type: `integer`):

How many bytes of each asset the ranged GET reads. 64 bytes is enough for PNG, GIF, WebP, BMP and ICO. Raise it to about 4096 when your icons are JPEG or SVG, because their size sits deeper in the file.

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

Hop limit for one request. A chain that does not end inside this limit is reported as unreachable.

## `timeoutSeconds` (type: `integer`):

Timeout for one HTTP request.

## `maxDocumentBytes` (type: `integer`):

Hard cap on the bytes read from one HTML page or one manifest. A larger document is cut.

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

How many asset probes run at the same time. Keep this low, so your CDN is not put under load.

## `userAgent` (type: `string`):

User-Agent header sent with every request, so your host can identify this Actor in its logs. ASCII characters only.

## Actor input object example

```json
{
  "origins": [
    "https://squoosh.app/"
  ],
  "requiredIconSizes": [
    "192",
    "512"
  ],
  "requireMaskableIcon": true,
  "requiredScreenshotFormFactors": [
    "narrow",
    "wide"
  ],
  "requiredShortcutCount": 0,
  "allowedDisplayModes": [
    "standalone",
    "fullscreen",
    "minimal-ui",
    "window-controls-overlay"
  ],
  "rules": [
    "manifest_link",
    "manifest_reachable",
    "manifest_parsable",
    "manifest_content_type",
    "name",
    "short_name",
    "start_url",
    "scope",
    "display",
    "background_color",
    "theme_color",
    "icon_sizes",
    "icon_assets",
    "maskable_icon",
    "screenshots",
    "shortcuts",
    "shortcut_icon_assets",
    "service_worker",
    "secure_origin"
  ],
  "serviceWorkerPaths": [
    "/sw.js",
    "/service-worker.js",
    "/serviceworker.js"
  ],
  "maxAssets": 200,
  "assetHeaderBytes": 64,
  "maxRedirects": 5,
  "timeoutSeconds": 20,
  "maxDocumentBytes": 2097152,
  "concurrency": 4,
  "userAgent": "PwaInstallGate/0.1 (+https://apify.com)"
}
```

# 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 = {
    "origins": [
        "https://squoosh.app/"
    ],
    "requiredIconSizes": [
        "192",
        "512"
    ],
    "requireMaskableIcon": true,
    "requiredScreenshotFormFactors": [
        "narrow",
        "wide"
    ],
    "requiredShortcutCount": 0,
    "allowedDisplayModes": [
        "standalone",
        "fullscreen",
        "minimal-ui",
        "window-controls-overlay"
    ],
    "rules": [
        "manifest_link",
        "manifest_reachable",
        "manifest_parsable",
        "manifest_content_type",
        "name",
        "short_name",
        "start_url",
        "scope",
        "display",
        "background_color",
        "theme_color",
        "icon_sizes",
        "icon_assets",
        "maskable_icon",
        "screenshots",
        "shortcuts",
        "shortcut_icon_assets",
        "service_worker",
        "secure_origin"
    ],
    "serviceWorkerPaths": [
        "/sw.js",
        "/service-worker.js",
        "/serviceworker.js"
    ],
    "maxAssets": 200,
    "assetHeaderBytes": 64,
    "maxRedirects": 5,
    "timeoutSeconds": 20,
    "maxDocumentBytes": 2097152,
    "concurrency": 4,
    "userAgent": "PwaInstallGate/0.1 (+https://apify.com)"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/pwa-install-readiness-and-manifest-asset-gate").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 = {
    "origins": ["https://squoosh.app/"],
    "requiredIconSizes": [
        "192",
        "512",
    ],
    "requireMaskableIcon": True,
    "requiredScreenshotFormFactors": [
        "narrow",
        "wide",
    ],
    "requiredShortcutCount": 0,
    "allowedDisplayModes": [
        "standalone",
        "fullscreen",
        "minimal-ui",
        "window-controls-overlay",
    ],
    "rules": [
        "manifest_link",
        "manifest_reachable",
        "manifest_parsable",
        "manifest_content_type",
        "name",
        "short_name",
        "start_url",
        "scope",
        "display",
        "background_color",
        "theme_color",
        "icon_sizes",
        "icon_assets",
        "maskable_icon",
        "screenshots",
        "shortcuts",
        "shortcut_icon_assets",
        "service_worker",
        "secure_origin",
    ],
    "serviceWorkerPaths": [
        "/sw.js",
        "/service-worker.js",
        "/serviceworker.js",
    ],
    "maxAssets": 200,
    "assetHeaderBytes": 64,
    "maxRedirects": 5,
    "timeoutSeconds": 20,
    "maxDocumentBytes": 2097152,
    "concurrency": 4,
    "userAgent": "PwaInstallGate/0.1 (+https://apify.com)",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/pwa-install-readiness-and-manifest-asset-gate").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 '{
  "origins": [
    "https://squoosh.app/"
  ],
  "requiredIconSizes": [
    "192",
    "512"
  ],
  "requireMaskableIcon": true,
  "requiredScreenshotFormFactors": [
    "narrow",
    "wide"
  ],
  "requiredShortcutCount": 0,
  "allowedDisplayModes": [
    "standalone",
    "fullscreen",
    "minimal-ui",
    "window-controls-overlay"
  ],
  "rules": [
    "manifest_link",
    "manifest_reachable",
    "manifest_parsable",
    "manifest_content_type",
    "name",
    "short_name",
    "start_url",
    "scope",
    "display",
    "background_color",
    "theme_color",
    "icon_sizes",
    "icon_assets",
    "maskable_icon",
    "screenshots",
    "shortcuts",
    "shortcut_icon_assets",
    "service_worker",
    "secure_origin"
  ],
  "serviceWorkerPaths": [
    "/sw.js",
    "/service-worker.js",
    "/serviceworker.js"
  ],
  "maxAssets": 200,
  "assetHeaderBytes": 64,
  "maxRedirects": 5,
  "timeoutSeconds": 20,
  "maxDocumentBytes": 2097152,
  "concurrency": 4,
  "userAgent": "PwaInstallGate/0.1 (+https://apify.com)"
}' |
apify call kingii98/pwa-install-readiness-and-manifest-asset-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/pwa-install-readiness-and-manifest-asset-gate"
        }
    }
}

```

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/gCcp0INDFjnKYbkIz/builds/zw6gtvogFXiQ6TLh7/openapi.json
