WordPress in 2026: Build a Pre-Publish Content QA Pipeline with Custom WP-CLI Commands

If your WordPress workflow still depends on manual preview checks, you are probably shipping avoidable mistakes. Broken links, missing alt text, oversized images, weak meta descriptions, and inconsistent heading structures can quietly hurt SEO and user trust. The good news is that you can automate most of this before a post goes live.

In this practical guide, we will build a pre-publish content QA pipeline using a custom WP-CLI command. The command will scan draft or pending posts, report issues, and optionally fail your CI job if quality thresholds are not met. This is especially useful when multiple authors contribute and editorial standards need to stay consistent.

What we are building

  • A custom WP-CLI command: wp content:qa
  • Checks for: broken links, missing image alt text, heading hierarchy, thin content, and meta quality
  • Structured JSON output for CI pipelines
  • An optional “strict” mode that exits non-zero on critical failures

This approach keeps quality checks close to your WordPress codebase, version-controlled, and reproducible.

Project structure

Create a small plugin so the command is portable between environments.

wp-content/plugins/content-qa-cli/
├── content-qa-cli.php
└── src/
    └── Command.php

Plugin bootstrap

<?php
/**
 * Plugin Name: Content QA CLI
 * Description: Pre-publish QA checks for WordPress content.
 * Version: 1.0.0
 */

if (!defined('ABSPATH')) {
    exit;
}

require_once __DIR__ . '/src/Command.php';

if (defined('WP_CLI') && WP_CLI) {
    WP_CLI::add_command('content:qa', \SevenTech\ContentQA\Command::class);
}

Command implementation

<?php

namespace SevenTech\ContentQA;

use WP_CLI;
use WP_Query;

class Command {
    /**
     * Run content QA checks.
     *
     * ## OPTIONS
     *
     * [--status=<status>]
     * : Post status to scan. Default: draft,pending
     *
     * [--limit=<number>]
     * : Max posts to scan. Default: 20
     *
     * [--strict]
     * : Exit non-zero if critical issues found.
     *
     * [--format=<format>]
     * : table|json. Default: table
     */
    public function __invoke($args, $assoc_args) {
        $statuses = isset($assoc_args['status']) ? explode(',', $assoc_args['status']) : ['draft', 'pending'];
        $limit    = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : 20;
        $strict   = isset($assoc_args['strict']);
        $format   = $assoc_args['format'] ?? 'table';

        $query = new WP_Query([
            'post_type'      => 'post',
            'post_status'    => $statuses,
            'posts_per_page' => $limit,
            'orderby'        => 'modified',
            'order'          => 'DESC',
        ]);

        $rows = [];
        $criticalCount = 0;

        foreach ($query->posts as $post) {
            $issues = $this->inspect_post($post->ID);
            if (!empty($issues['critical'])) {
                $criticalCount += count($issues['critical']);
            }

            $rows[] = [
                'id'         => $post->ID,
                'title'      => get_the_title($post),
                'critical'   => implode('; ', $issues['critical']),
                'warnings'   => implode('; ', $issues['warnings']),
                'score'      => $issues['score'],
            ];
        }

        if ($format === 'json') {
            WP_CLI::line(wp_json_encode($rows));
        } else {
            WP_CLI\Utils\format_items('table', $rows, ['id', 'title', 'critical', 'warnings', 'score']);
        }

        if ($strict && $criticalCount > 0) {
            WP_CLI::error("Critical issues found: {$criticalCount}");
        }

        WP_CLI::success('QA scan complete.');
    }

    private function inspect_post(int $post_id): array {
        $content = get_post_field('post_content', $post_id);
        $title   = get_the_title($post_id);
        $critical = [];
        $warnings = [];
        $score = 100;

        // Thin content
        $wordCount = str_word_count(wp_strip_all_tags($content));
        if ($wordCount < 500) {
            $critical[] = 'Content under 500 words';
            $score -= 25;
        }

        // Missing meta description
        $metaDesc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true);
        if (empty($metaDesc) || strlen($metaDesc) < 120) {
            $warnings[] = 'Meta description missing or too short';
            $score -= 10;
        }

        // Heading hierarchy quick check
        if (preg_match('/<h1[^>]*>/i', $content)) {
            $warnings[] = 'H1 found inside content (theme should own H1)';
            $score -= 8;
        }

        // Image alt text check
        preg_match_all('/<img[^>]*>/i', $content, $imgs);
        foreach ($imgs[0] as $img) {
            if (!preg_match('/alt="[^"]+"/i', $img)) {
                $warnings[] = 'Image without alt text';
                $score -= 5;
            }
        }

        // External links check placeholder (can be expanded with HTTP calls + caching)
        preg_match_all('/href="(https?:\/\/[^\"]+)"/i', $content, $links);
        if (count($links[1]) > 20) {
            $warnings[] = 'High external link count, validate relevance';
            $score -= 5;
        }

        return [
            'critical' => array_values(array_unique($critical)),
            'warnings' => array_values(array_unique($warnings)),
            'score'    => max(0, $score),
        ];
    }
}

Run the command locally

wp content:qa --status=draft,pending --limit=30
wp content:qa --status=pending --format=json
wp content:qa --strict --format=json

The JSON output is useful when you want to gate publishing in CI. For example, if your editorial team uses pull requests for content changes, your pipeline can fail when critical issues are detected.

CI integration example (GitHub Actions)

name: Content QA

on:
  workflow_dispatch:
  pull_request:
    paths:
      - "wp-content/**"

jobs:
  qa:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - name: Install WP-CLI
        run: |
          curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
          chmod +x wp-cli.phar
          sudo mv wp-cli.phar /usr/local/bin/wp
      - name: Run content QA
        run: wp content:qa --strict --format=json

Practical enhancements for production teams

  1. Broken-link verifier with caching: Resolve outbound URLs asynchronously and cache status codes in transients to avoid slow repeated checks.
  2. Readability thresholds: Add sentence-length and paragraph-size heuristics for better scanability.
  3. Policy packs: Enforce internal standards, such as at least one code block for technical posts or mandatory FAQ sections for tutorials.
  4. Author feedback loop: Push QA results into admin notices so writers can fix issues before submitting for review.

Why this workflow helps in 2026

AI-assisted drafting has increased publishing speed, but it also raises consistency risk. Automated QA is the missing layer between writing fast and publishing responsibly. By moving checks into WP-CLI, you get:

  • Consistent standards independent of individual editors
  • Fast feedback before publish
  • Clear auditability in logs and CI history
  • Lower SEO regressions over time

Start simple, then evolve your rules based on real editorial mistakes you see repeatedly. The most effective QA pipeline is not the strictest one, it is the one your team actually runs every day.

Final checklist

  • Package your command as a plugin and keep it versioned
  • Run QA on draft and pending statuses
  • Use --strict only for critical errors at first
  • Export JSON for CI and reporting dashboards
  • Review and tune scoring every month

Once this is in place, your WordPress publishing workflow becomes safer, faster, and easier to scale, especially when your content volume grows.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials