TryGhost/Ghost · error · ThemeValidationError

Theme is not compatible or contains errors.

Error message

Theme is not compatible or contains errors.

What it means

ThemeValidationError, thrown by handleResponse when errors[0].type === 'ThemeValidationError'. It signals that a theme being uploaded or activated failed Ghost's theme validation (GScan): missing required templates, invalid package.json, deprecated helpers, or structural defects. The detailed GScan report is in error.data.

Source

Thrown at apps/admin-x-framework/src/utils/api/handle-response.ts:33

        }
        throw new UnauthorizedError(response, await response.text());
    } else if (!response.ok) {
        if (!response.headers.get('content-type')?.includes('json')) {
            throw new APIError(response, await response.text());
        }

        const data = await response.json() as ErrorResponse;

        if (response.status === 403 && data.errors?.[0]?.message === 'Authorization failed') {
            throw new UnauthorizedError(response, data);
        } else if (data.errors?.[0]?.type === 'VersionMismatchError') {
            throw new VersionMismatchError(response, data);
        } else if (data.errors?.[0]?.type === 'ValidationError') {
            throw new ValidationError(response, data);
        } else if (data.errors?.[0]?.type === 'NoPermissionError') {
            throw new ValidationError(response, data);
        } else if (data.errors?.[0]?.type === 'ThemeValidationError') {
            throw new ThemeValidationError(response, data);
        } else if (data.errors?.[0]?.type === 'HostLimitError') {
            throw new HostLimitError(response, data);
        } else if (data.errors?.[0]?.type === 'EmailError') {
            throw new EmailError(response, data);
        } else {
            throw new JSONError(response, data);
        }
    } else if (response.status === 204) {
        return;
    } else if (response.headers.get('content-type')?.includes('text/csv')) {
        return await response.text();
    } else {
        return await response.json();
    }
};

export default handleResponse;

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Inspect error.data.errors for the GScan report — it lists each failed check with file/line context.
  2. Run GScan locally (npx gscan <theme>) and fix all errors before re-uploading.
  3. Correct package.json (version, engines.ghost, api.compatibility) and ensure required templates exist.
Defensive patterns

Strategy: type-guard

Validate before calling

// Validate the theme zip locally before upload (run GScan).
// npx gscan ./my-theme  → exits non-zero on errors.

Type guard

import {ThemeValidationError} from '@tryghost/admin-x-framework/utils/errors';
function isThemeValidationError(e: unknown): e is ThemeValidationError {
    return e instanceof ThemeValidationError;
}

Try / catch

import {ThemeValidationError} from '@tryghost/admin-x-framework/utils/errors';
try {
    await api.themes.upload(file);
} catch (e) {
    if (e instanceof ThemeValidationError) {
        // data.errors carries the GScan report
        showThemeErrors(e.data?.errors ?? []);
    } else throw e;
}

Prevention

When it happens

Trigger: Uploading a .zip theme whose contents fail GScan, or activating a theme whose package.json/templates are invalid. Ghost returns the validation report as errors data.

Common situations: Custom theme missing required files (e.g., post.hbs); package.json with invalid/capabilities/engines fields; theme built for a newer Ghost API version; deprecated template usage flagged as errors.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/256496a6f9f8f206. Report an issue: GitHub.