Use Case

Build schema validation into WordPress, Shopify, and Webflow plugins

Schema errors created at publish time don't surface until Google drops your rich results — sometimes weeks later. SchemaCheck gives you an API to catch them the moment content is published, not after.

Why validate at publish time?

Most schema errors are introduced by CMS updates, theme changes, or content editors removing structured data blocks. By the time you notice a drop in rich results, the damage has already been done. Validating on publish catches issues within seconds — before they reach Google.

Hours

to notice a schema regression from Google Search Console

Seconds

to catch the same error with a publish-time webhook

100%

of schema errors are preventable with CI or publish hooks

Supported platforms

WordPress + WooCommerce

Hook into publish_post, publish_page, and woocommerce_update_product. Display schema health scores in the admin editor.

Shopify Apps

Verify HMAC-signed webhooks on products/create and products/update. Write schema scores back as metafields.

Webflow CMS

Receive cms_item_published webhooks and validate every newly published collection item.

Any CMS with webhooks

The API is platform-agnostic. If your CMS can fire an HTTP request on publish, SchemaCheck can validate it.

WordPress plugin integration

Hook into WordPress publish actions to validate schema on every post, page, or WooCommerce product update. Shows schema score and errors directly in the admin editor.

php
<?php
/**
 * Plugin Name: SchemaCheck Validator
 * Description: Validate Schema.org structured data on post publish/update
 */

define('SCHEMACHECK_API_KEY', get_option('schemacheck_api_key'));
define('SCHEMACHECK_ENDPOINT', 'https://schemacheck.dev/api/v1/validate');

// Hook into post publish and update
add_action('publish_post', 'schemacheck_validate_on_publish', 10, 2);
add_action('publish_page', 'schemacheck_validate_on_publish', 10, 2);
add_action('woocommerce_update_product', 'schemacheck_validate_product');

function schemacheck_validate_on_publish($post_id, $post) {
    $permalink = get_permalink($post_id);
    $result    = schemacheck_validate_url($permalink);

    if (!$result || !$result['success']) return;

    $errors = array_merge(
        ...array_map(fn($s) => $s['errors'], $result['schemas'])
    );

    if (count($errors) > 0) {
        // Store validation result as post meta for admin notices
        update_post_meta($post_id, '_schemacheck_errors', $errors);
        update_post_meta($post_id, '_schemacheck_score', $result['summary']['score']);

        // Optionally notify via email
        $admin_email = get_option('admin_email');
        wp_mail(
            $admin_email,
            "Schema errors on: {$post->post_title}",
            "Found " . count($errors) . " schema error(s) on {$permalink}\n\n" .
            implode("\n", array_map(fn($e) => "• {$e['property']}: {$e['message']}", $errors))
        );
    }
}

function schemacheck_validate_url($url) {
    $response = wp_remote_get(
        SCHEMACHECK_ENDPOINT . '?url=' . urlencode($url) . '&access_key=' . SCHEMACHECK_API_KEY,
        ['timeout' => 30]
    );

    if (is_wp_error($response)) return null;
    return json_decode(wp_remote_retrieve_body($response), true);
}

// Admin notice on post edit screen
add_action('admin_notices', function() {
    $screen = get_current_screen();
    if (!in_array($screen->base, ['post', 'page'])) return;

    $post_id = get_the_ID();
    $errors  = get_post_meta($post_id, '_schemacheck_errors', true);
    $score   = get_post_meta($post_id, '_schemacheck_score', true);

    if (!$errors || count($errors) === 0) return;

    echo '<div class="notice notice-warning">
        <p><strong>Schema issues detected</strong> (score: ' . esc_html($score) . '/100)</p>
        <ul>' .
        implode('', array_map(
            fn($e) => '<li>' . esc_html($e['property']) . ': ' . esc_html($e['message']) . '</li>',
            $errors
        )) .
        '</ul></div>';
});

Shopify app extension

Build a Shopify app that validates product schema on every create or update webhook. Writes the schema health score back as a product metafield so it's visible in the Shopify admin.

typescript
// Shopify app extension — validate schema on product create/update
// Uses Shopify webhooks + SchemaCheck API

import { json } from "@remix-run/node";
import crypto from "crypto";

const SCHEMACHECK_KEY = process.env.SCHEMACHECK_API_KEY!;
const SHOPIFY_SECRET  = process.env.SHOPIFY_WEBHOOK_SECRET!;

// Verify Shopify webhook signature
function verifyWebhook(body: string, hmacHeader: string): boolean {
  const digest = crypto
    .createHmac("sha256", SHOPIFY_SECRET)
    .update(body, "utf8")
    .digest("base64");
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(hmacHeader));
}

export async function action({ request }: { request: Request }) {
  const body     = await request.text();
  const hmac     = request.headers.get("x-shopify-hmac-sha256") ?? "";
  const topic    = request.headers.get("x-shopify-topic") ?? "";
  const shopDomain = request.headers.get("x-shopify-shop-domain") ?? "";

  if (!verifyWebhook(body, hmac)) {
    return json({ error: "Unauthorized" }, { status: 401 });
  }

  // Handle products/create and products/update
  if (!["products/create", "products/update"].includes(topic)) {
    return json({ skipped: true });
  }

  const product = JSON.parse(body);
  const productUrl = `https://${shopDomain}/products/${product.handle}`;

  // Give Shopify time to publish before fetching
  await new Promise((r) => setTimeout(r, 2000));

  const result = await fetch(
    `https://schemacheck.dev/api/v1/validate?url=${encodeURIComponent(productUrl)}&access_key=${SCHEMACHECK_KEY}`
  ).then((r) => r.json());

  if (!result.success) return json({ validated: false });

  const productSchema = result.schemas.find(
    (s: { type: string }) => s.type === "Product"
  );

  if (productSchema?.errors.length > 0) {
    // Create a metafield on the product with the schema health score
    // so it's visible in the Shopify admin
    await setProductMetafield(shopDomain, product.id, {
      namespace: "schemacheck",
      key: "score",
      value: String(result.summary.score),
      type: "number_integer",
    });

    console.log(`Schema issues on ${productUrl}:`, productSchema.errors);
  }

  return json({ score: result.summary.score, errors: productSchema?.errors ?? [] });
}

async function setProductMetafield(
  shop: string,
  productId: number,
  metafield: { namespace: string; key: string; value: string; type: string }
) {
  // Use Shopify Admin API to write metafield
  const endpoint = `https://${shop}/admin/api/2024-01/products/${productId}/metafields.json`;
  await fetch(endpoint, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Shopify-Access-Token": process.env.SHOPIFY_ACCESS_TOKEN!,
    },
    body: JSON.stringify({ metafield }),
  });
}

Webflow CMS webhook handler

Receive Webflow's cms_item_published webhook, validate each page, and post a Slack alert if required properties are missing. Deploy as a Vercel function or Cloudflare Worker.

typescript
// Webflow CMS integration — validate on publish via Webflow webhook
// Deploy as a serverless function (Vercel, Netlify, or Cloudflare Workers)

export async function POST(request: Request) {
  const payload = await request.json();

  // Webflow sends { site, items } on cms_item_published
  const { site, items } = payload;

  if (!items || items.length === 0) {
    return Response.json({ skipped: true });
  }

  const results = await Promise.all(
    items.map(async (item: { slug: string; url: string }) => {
      const pageUrl = item.url ?? `https://${site.shortName}.webflow.io/${item.slug}`;

      const res = await fetch(
        `https://schemacheck.dev/api/v1/validate?url=${encodeURIComponent(pageUrl)}&access_key=${process.env.SCHEMACHECK_API_KEY}`
      );
      const data = await res.json();

      return {
        url: pageUrl,
        score: data.summary?.score ?? 0,
        errors: data.schemas?.flatMap((s: { errors: unknown[] }) => s.errors) ?? [],
        richResultEligible: data.summary?.rich_result_eligible ?? false,
      };
    })
  );

  // Post summary to Slack if any items have errors
  const itemsWithErrors = results.filter((r) => r.errors.length > 0);

  if (itemsWithErrors.length > 0 && process.env.SLACK_WEBHOOK_URL) {
    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        text: `⚠️ Schema issues detected on ${itemsWithErrors.length} newly published page(s)`,
        attachments: itemsWithErrors.map((item) => ({
          color: "warning",
          title: item.url,
          text: `Score: ${item.score}/100 — ${item.errors.length} error(s)\n` +
            item.errors
              .slice(0, 3)
              .map((e: { property: string; message: string }) => `• ${e.property}: ${e.message}`)
              .join("\n"),
        })),
      }),
    });
  }

  return Response.json({ validated: results.length, errors: itemsWithErrors.length });
}

Common integration patterns

Publish webhook → validate → admin notice

Trigger validation immediately after publish. Show schema errors inline in the CMS editor so authors can fix them before the page is live for long.

Validate → write score back as metadata

Store the schema health score as a custom field or metafield. Query pages with scores below 80 for targeted remediation.

Validate → Slack or email alert

Route schema errors to the team who can fix them — SEO leads, developers, or content managers — without requiring anyone to manually check.

Nightly cron across full content archive

Re-validate your entire content library weekly. Catch regressions introduced by theme updates, plugin upgrades, or third-party schema injectors.

“A Yoast plugin update removed the datePublished field from our Article schema across 4,000 posts. Our publish hook caught it on the first post — we fixed the config before the rest of the site was affected.”

— Senior developer at a digital media publisher (name withheld)

Add publish-time validation in an afternoon

Free plan — 100 validations/month. No credit card. The webhook handler above takes about 30 minutes to deploy.

Related