# WordPal theme status (/v1/wordpal)

WordPal converts a WordPress block theme to a Drupal theme. Its gallery at
[wordpal.tresbien.tech](https://wordpal.tresbien.tech/) converts every block
theme on wordpress.org each night. These routes answer what the gallery shows
for each theme.

## Request

### GET /v1/wordpal/theme

Every theme the gallery shows, largest install base first, then by slug.

| Name | In | Type | What it does |
|---|---|---|---|
| status | query | string | `exact`, `diff` or `failed`. Keeps the themes whose `run.status` has that value. |
| tag | query | string | A wordpress.org tag slug, such as `blog`. Keeps the themes that have it. |
| q | query | string | Part of a slug or of a name, in any case. |
| limit | query | int | Themes per page, default 100, capped at 1000. |
| offset | query | int | Themes to skip, in the same order. |
| all | query | bool | `?all=1` returns every match in one page. |

The three filters combine: a theme must pass each one that is set.

Example: `/v1/wordpal/theme?status=exact&tag=blog&limit=10`

### GET /v1/wordpal/theme/{slug}

One theme, with the result of each compared route.

| Name | In | Type | What it does |
|---|---|---|---|
| slug | path | string | The theme's wordpress.org slug, such as `twentytwentyfive`. |

Example: `/v1/wordpal/theme/twentytwentyfive`

`GET /v1/wordpal/` answers this page.

## Status

`run.status` is a verdict on the home page only. The gallery compares up to
eight routes per theme. The route counts and the `run.routes` array cover the
other seven.

| Value | Meaning |
|---|---|
| `exact` | The markup of the home page has no difference, and its screenshot matches the WordPress screenshot. |
| `diff` | The markup differs, the screenshots differ, or the two home pages could not be compared. |
| `failed` | There is no pair of home page screenshots, as when the conversion fails. |

A markup difference is an element, a class, an attribute or an inline style
that differs between the WordPress page and the Drupal page. Text and
stylesheets are not compared. The screenshots are compared on the home page only.

## Response

The list answers the pagination envelope `{total, limit, offset,
first_offset, prev_offset, next_offset, last_offset, data}`, with `by_status`
next to `data`.

```json
{
  "total": 2343,
  "limit": 1,
  "offset": 0,
  "first_offset": 0,
  "prev_offset": null,
  "next_offset": 1,
  "last_offset": 2342,
  "by_status": {"exact": 897, "diff": 1421, "failed": 25},
  "data": [
    {
      "slug": "twentytwentyfive",
      "name": "Twenty Twenty-Five",
      "author": "wordpressdotorg",
      "installs": 1000000,
      "parent": null,
      "tags": ["blog", "full-site-editing"],
      "version": "1.6",
      "page_url": "https://wordpal.tresbien.tech/themes/twentytwentyfive.html",
      "run": {
        "started_at": "2026-10-07T20:53:34Z",
        "theme_version": "1.5",
        "wordpal_release": "1.0.0-alpha5",
        "wordpress_version": "7.1.2",
        "status": "exact",
        "reason": null,
        "plugins": [],
        "routes_measured": 8,
        "routes_matching": 8,
        "markup_diffs_total": 0,
        "card_url": "https://wordpal.tresbien.tech/img/twentytwentyfive/wordpal-twentytwentyfive-card.avif?v=1.0.0-alpha5",
        "wp_capture_url": "https://wordpal.tresbien.tech/img/twentytwentyfive/wordpal-twentytwentyfive-wordpress.avif?v=1.0.0-alpha5",
        "drupal_capture_url": "https://wordpal.tresbien.tech/img/twentytwentyfive/wordpal-twentytwentyfive-drupal.avif?v=1.0.0-alpha5",
        "convert_command": "drush wordpal:convert twentytwentyfive --mapping=mapping.yml"
      }
    }
  ]
}
```

`total` counts the themes that pass every filter. `by_status` counts the
themes that pass `tag` and `q`, so it stays whole under a `status` filter.

`?all=1` puts every match into that one page.

A row holds the theme's own fields, and `run`, the run the gallery shows for
it.

| Theme field | What it holds |
|---|---|
| `slug` | The wordpress.org slug of the theme. |
| `name` | The display name on wordpress.org. |
| `author` | The wordpress.org user name of the author. |
| `installs` | Active installs as wordpress.org reports them, a lower bound: 1000 means 1000 or more. |
| `parent` | The slug of the parent theme, or null. |
| `tags` | The wordpress.org tag slugs, sorted. |
| `version` | The theme version wordpress.org lists now. |
| `page_url` | The theme's page on the gallery, with the full comparison. |
| `run` | The run the gallery shows, with the fields below. |

| Run field | What it holds |
|---|---|
| `started_at` | When the conversion started, RFC 3339, UTC. |
| `theme_version` | The theme version the conversion ran on. It is older than `version` until the gallery converts the new one. |
| `wordpal_release` | The WordPal release that converted: a `1.*` tag, or a short commit hash. |
| `wordpress_version` | The WordPress version that rendered the theme, or null when the log lacks it. |
| `status` | `exact`, `diff` or `failed`, as listed under Status. |
| `reason` | One line on why the conversion failed. Null unless `status` is `failed`. |
| `plugins` | The WordPress plugins the conversion installed for the theme's blocks, in install order. |
| `routes_measured` | The routes whose markup was compared on both sites, out of eight. |
| `routes_matching` | The measured routes whose markup has no difference. |
| `markup_diffs_total` | The markup differences, summed over the measured routes. Null when none was measured. |
| `card_url` | A 1200x630 AVIF image: the WordPress home page on the left, Drupal on the right. Null when it could not be composed. |
| `wp_capture_url` | A screenshot of the WordPress home page, AVIF, or null. |
| `drupal_capture_url` | A screenshot of the converted Drupal home page, AVIF. Null for a failed conversion. |
| `convert_command` | The drush command that repeats the conversion. |

Converted Drupal sites read five fields of the single-theme route once a
day: `version`, `page_url`, `run.theme_version`, `run.status` and
`run.wordpal_release`. Released WordPal versions read these exact keys, and
renaming one breaks them.

The single-theme route answers one object with the same fields. Its `run`
adds `routes`, ordered by route. Two routes of another theme:

```json
"routes": [
  {
    "route": "search_empty",
    "markup_outcome": "skipped",
    "markup_diffs": null,
    "visual_outcome": null,
    "visual_bands": null,
    "visual_changed": null,
    "wp_height": null,
    "drupal_height": null,
    "wp_only_rows": null,
    "drupal_only_rows": null
  },
  {
    "route": "single",
    "markup_outcome": "diff",
    "markup_diffs": 14,
    "visual_outcome": null,
    "visual_bands": null,
    "visual_changed": null,
    "wp_height": null,
    "drupal_height": null,
    "wp_only_rows": null,
    "drupal_only_rows": null
  }
]
```

| Field | What it holds |
|---|---|
| `route` | The page compared: `home`, `single`, `page`, `archive`, `search`, `not_found`, `home_page_2` or `search_empty`. |
| `markup_outcome` | The markup comparison: `match`, `diff`, `skipped` or `error`. |
| `markup_diffs` | The markup differences: 0 for `match`, the count for `diff`, null otherwise. |
| `visual_outcome` | The screenshot comparison: `match`, `diff`, `unstable` or `error`. Null when the screenshots were not compared. |
| `visual_bands` | The changed bands among the rows both screenshots have, or null. |
| `visual_changed` | The share of the pixels in those rows that changed, from 0 to 1, or null. |
| `wp_height` | The height of the WordPress screenshot in pixels, or null. |
| `drupal_height` | The height of the Drupal screenshot in pixels, or null. |
| `wp_only_rows` | The pixel rows that only the WordPress screenshot has, or null. |
| `drupal_only_rows` | The pixel rows that only the Drupal screenshot has, or null. |

| Markup outcome | Meaning |
|---|---|
| `match` | The markup has no difference. |
| `diff` | The markup has at least one difference. |
| `skipped` | WordPress rendered no page for the route. |
| `error` | The comparison did not complete. |

| Visual outcome | Meaning |
|---|---|
| `match` | No band of the page changed. |
| `diff` | The two heights differ, or a band changed. |
| `unstable` | Two screenshots of the WordPress page differ from each other. |
| `error` | The comparison did not complete. |

The gallery compares the screenshots of the home route only. The five visual
fields are null on the other routes.

A route the gallery never compared for a theme has no entry.

The single-theme route also answers `history`: every run the gallery has
shown for the theme since 2026-10-07, newest first. It covers the home page
only. Two runs of one theme:

```json
"history": [
  {
    "started_at": "2026-10-12T12:16:11Z",
    "wordpal_release": "1.0.0-alpha7",
    "wordpress_version": "7.1.2",
    "theme_version": "1.7",
    "status": "diff",
    "markup_outcome": "match",
    "markup_diffs": 0,
    "visual_outcome": "diff",
    "direction": "regressed"
  },
  {
    "started_at": "2026-10-07T21:40:02Z",
    "wordpal_release": "1.0.0-alpha5",
    "wordpress_version": "7.1.2",
    "theme_version": "1.7",
    "status": "exact",
    "markup_outcome": "match",
    "markup_diffs": 0,
    "visual_outcome": null,
    "direction": null
  }
]
```

| Field | What it holds |
|---|---|
| `started_at` | When the conversion started, RFC 3339, UTC. |
| `wordpal_release` | The WordPal release that converted. |
| `wordpress_version` | The WordPress version that rendered the theme, or null. |
| `theme_version` | The theme version the conversion ran on. |
| `status` | `exact`, `diff` or `failed`, as the gallery last judged this run. |
| `markup_outcome` | The markup comparison of the home page, or null for a failed run. |
| `markup_diffs` | The markup differences of the home page: 0 for `match`, the count for `diff`, null otherwise. |
| `visual_outcome` | The screenshot comparison of the home page. Null for a run logged before 2026-10-13, when the gallery had no visual check. |
| `direction` | How the result moved from the run before. Null for the first run. |

| Direction | Meaning |
|---|---|
| `improved` | The result is better than the run before: fewer markup differences, a match, or a conversion where the run before failed. |
| `regressed` | The result is worse than the run before. |
| `same` | The result reads the same as the run before. |
| `changed` | The result differs, and the two cannot be ranked, as with an error. |

## Limits

The api-wide rate limit per IP applies. These routes have no budget of their
own.

A page holds at most 1000 themes. `?all=1` has no cap.

The data changes once a day, after the gallery's nightly run. A theme answer
and a list are cached for 1 hour. This page is cached for 24 hours.

## Errors

| Status | When |
|---|---|
| 400 | `status` is not `exact`, `diff` or `failed`. |
| 400 | The slug is not lowercase letters and digits joined by single hyphens. |
| 404 | The gallery shows no theme with that slug. |
| 429 | The rate limit is spent. `Retry-After` gives the wait. |
| 503 | The daily data is not published yet. `Retry-After` gives the wait. |
