没有不值得去解决的问题,也没有不值得去学习的技术!

Before Batch-Filling WordPress Excerpt History With AI, I Built a Read-Only Format Audit Pipeline

图3:100 篇历史文章的编辑器格式、代码格式和风险等级统计

My WordPress blog has accumulated over two thousand Chinese articles, a considerable number of which lack excerpts.

On the surface, this requirement seems straightforward: query all articles without excerpts, pass the title and content to an AI, and write the generated excerpt back to WordPress.

But once I actually started planning, I quickly realized the problem was not just “which model to choose for generating excerpts.”

These articles span over a decade and have used different editors, code highlighting plugins, and content structures. Some articles were later subjected to Gutenberg conversion, theme migration, or code block replacement. If I directly read the content in bulk, called the AI, and wrote back to the database without first inventorying the formats, it would be difficult to accurately determine which articles could be processed automatically and which required manual review.

Therefore, instead of immediately starting bulk excerpt generation, I first built a read-only historical article format audit pipeline.

The current task of this pipeline is not to generate excerpts, nor will it write back to WordPress. It is solely responsible for:

  • Controlled export of articles from the production environment;
  • Identifying the editor and code formats of historical articles;
  • Detecting potentially broken or uncertain structures;
  • Performing deterministic risk classification on articles;
  • Establishing a safe candidate pool for future AI excerpt backfill.
Figure 1: Chinese and English README and directory structure of the wordpress-ai-excerpt-backfill project
Figure 1: Chinese and English README and directory structure of the wordpress-ai-excerpt-backfill project

1. Why not directly generate excerpts in bulk

My historical articles do not consist of a single uniform format.

They include both plain HTML generated by the early Classic Editor and articles later edited or converted using Gutenberg. The code presentation methods have also gone through multiple changes, including:

  • Plain <pre> and <code>;
  • SyntaxHighlighter shortcodes;
  • SyntaxHighlighter Gutenberg blocks;
  • Code Block Pro;
  • Gutenberg built-in code blocks;
  • Articles with mixed editor formats;
  • Unknown or deprecated shortcodes.

Some articles, although published in 2014, were re-edited in 2026 and their content contains both Gutenberg blocks and unconverted classic paragraphs.

Obviously, determining the format based solely on the article publication date is unreliable.

Additionally, the code content frequently contains square brackets, HTML tags, regular expressions, and template syntax. For example:

Plaintext
[if]
[endif]
[name]
[unknown-widget id="123"]

These could be code examples, or they could be actual WordPress shortcodes. If the detector handles them improperly, it will misidentify normal code as unknown shortcodes or broken structures.

Therefore, before backfilling excerpts, at least a few questions need to be answered:

  1. Does the article actually belong to Gutenberg, Classic Editor, or mixed?
  2. Does it contain SyntaxHighlighter, Code Block Pro, or classic code structures?
  3. Are the Gutenberg blocks properly paired?
  4. Are there unclosed shortcodes, orphaned closing tags, or misaligned nesting?
  5. Will the bracketed content inside code regions be misjudged?
  6. Which articles can be processed automatically, and which must be excluded or manually reviewed?

If these questions remain unanswered, writing directly back to production data is not safe.

2. First limit the project scope to “read-only audit”

I set up a dedicated directory for this project:

Plaintext
/home/wangqiang/code/wordpress-ai-excerpt-backfill

The production environment tool is deployed outside the web root directory:

Plaintext
/root/tools/wordpress-ai-excerpt-backfill

WordPress is still located at:

Plaintext
/data/wwwroot/www.shuijingwanwq.com

The project currently explicitly excludes the following capabilities:

  • AI excerpt generation;
  • WordPress excerpt write-back;
  • Bulk article modification;
  • Modifying categories, tags, or other article fields;
  • Database write operations;
  • Cache clearing;
  • Translation generation;
  • AI API calls.

In other words, even if the detector makes a misjudgment, currently it will at most affect the local analysis results and will not directly modify production articles.

This is the first layer of security boundary I set for subsequent automation.

3. The production exporter is solely responsible for reading

The production side uses a PHP exporter to read WordPress data.

The exporter only processes articles that meet the following conditions:

  • post_type=post;
  • post_status=publish;
  • Polylang language is Chinese;
  • Read in ascending order by article ID.

The export results use JSONL, with each line corresponding to one article. The fields contain information needed for subsequent validation, for example:

Plaintext
post_id
post_type
post_status
language
published_at
modified_at
title
excerpt
content
permalink
content_sha256

Among them, content_sha256 is used to confirm that the content has not changed during the export, download, and analysis process.

The exporter has no WordPress write functions, nor does it have SQL operations like $wpdb->insert(), $wpdb->update() or UPDATE, DELETE.

The official runtime entry requires explicitly providing the export quantity:

Bash
bin/run-readonly-export.sh \
  --limit 100 \
  --after-id 95 \
  --batch-size 100

Currently, the single export quantity is limited to:

Plaintext
1~100

Initially, this upper limit only existed in the shell execution script. During the pre-commit audit, I realized that if someone bypassed the shell and called the PHP exporter directly, a larger quantity could be passed in.

Therefore, an independent hard upper limit was later added inside the PHP exporter:

PHP
const SWQ_MAX_EXPORT_LIMIT = 100;

This way, regardless of which entry point it is called from, requests exceeding 100 items will be rejected before touching Polylang and database queries.

This is a typical defense in depth: you cannot rely solely on the correctness of the outermost entry point.

4. Deployment and export must be two separate things

The production deployment script and the export script are explicitly separated.

By default, the deployment script does not perform any network operations:

Bash
bin/deploy-to-production.sh --dry-run

Only by explicitly specifying:

Bash
bin/deploy-to-production.sh --deploy

will it connect to the production server.

The deployment process includes:

  1. Uploading the PHP file as a temporary file;
  2. Running PHP lint on the server;
  3. Verifying the file hash;
  4. Setting strict permissions;
  5. Replacing the official file using an atomic rename.

Deployment does not automatically run the export.

Similarly, the export script only runs the already deployed fixed PHP file and will not incidentally upload or replace production code.

Although this adds an extra step, it avoids the coupling risk of “accidentally processing production data immediately after deployment is completed.”

5. Export results are validated before becoming official files

Remote exports are not directly written to the final filename.

The execution script first generates a temporary JSONL, and then uses a fixed Python interpreter to check:

  • Whether the file exists;
  • Whether each line is valid JSON;
  • Whether the record count matches the expectation;
  • Whether required fields are complete;
  • Whether there are duplicate article IDs;
  • Whether the language and publication status meet the requirements.

After all checks pass, the temporary file is atomically renamed to the official JSONL.

If the export or validation fails, the temporary file is cleaned up, preventing an official result that looks normal but actually contains only partial records.

The first test exported 3 articles. This was then expanded to 20 articles, and finally a controlled export of 100 articles was completed.

Figure 2: Production environment completes read-only export of 100 published Chinese articles
Figure 2: Production environment completes read-only export of 100 published Chinese articles

The results of the 100-article export file are:

Plaintext
Record count: 100
File size: 558106 bytes

The SHA-256 of the server file and the locally downloaded file are exactly the same, indicating that no changes occurred during transmission.

6. The local analyzer continues to keep inputs read-only

After the production JSONL is downloaded locally, it is then processed by:

Plaintext
bin/analyze-export.py

for analysis.

The analyzer requires explicitly providing the expected record count:

Bash
bin/analyze-export.py \
  --expected-count 100 \
  data/raw/example.jsonl \
  data/analysis/example.analysis.jsonl

The allowed range for --expected-count is also:

Plaintext
1~100

Before the analysis begins, it validates:

  • JSONL schema;
  • Exact record count;
  • Article type;
  • Publication status;
  • Polylang language;
  • Duplicate article IDs;
  • Content SHA-256.

The official analysis results are also not directly overwritten; instead, a temporary file is written first, and after executing flush and fsync, it is atomically replaced.

During the pre-commit audit, another easily overlooked issue was discovered: if a user specifies the same path for the input file and the output file, the original export file might be replaced by the analysis results upon completion.

For example:

Bash
bin/analyze-export.py \
  --expected-count 100 \
  data/raw/example.jsonl \
  data/raw/example.jsonl

Later, I added same-file protection to the analyzer, covering the following scenarios:

  • The input and output path strings are exactly the same;
  • Equivalent paths formed by ./;
  • Equivalent paths formed by ..;
  • The output is a symbolic link to the input;
  • The output and input are hard links to the same inode.

As long as the input and output ultimately point to the same file, the analyzer will refuse to execute before reading the input and creating the temporary file.

7. Analysis results do not contain sensitive fields like content

The original JSONL contains the content, title, excerpt, and URL, and is therefore treated as local sensitive data.

The analysis results only retain the sanitized fields needed for format classification and risk assessment, for example:

Plaintext
post_id
published_at
editor_format
code_format
primary_format
matched_rule_ids
risk_level
risk_reasons
manual_review
phase1_status
phase1_exclusion_reasons

The project’s .gitignore explicitly excludes:

Plaintext
data/raw/
data/analysis/
__pycache__/
*.pyc

Therefore, the public GitHub repository will not contain production article content, raw export files, or analysis results.

8. Historical format differences exposed by 100 real samples

After completing the analysis of 100 samples, the resulting editor format distribution is:

Plaintext
classic:98
mixed:2

The code format distribution is:

Plaintext
none:95
classic-pre-code:4
syntaxhighlighter:1

The risk levels are:

Plaintext
low:91
medium:7
high:2
manual-review:0

The publication dates of this batch of articles roughly cover the period from 2013 to 2016.

The results show that the main body of early articles is still in Classic Editor format, but the following have already appeared:

  • Classic <pre>/<code> code structures;
  • SyntaxHighlighter;
  • Mixing of Gutenberg and classic content formed by later edits;
  • Multi-image articles;
  • Bracketed content in code that is easily misjudged as shortcodes.
Figure 3: Statistics on editor formats, code formats, and risk levels for 100 historical articles
Figure 3: Statistics on editor formats, code formats, and risk levels for 100 historical articles

9. Real samples helped uncover multiple detection misjudgments

In this work, what really took time was not exporting 100 articles, but iteratively refining the detection rules.

1. Plain bracketed words misjudged as shortcodes

Historical articles and code examples might contain:

Plaintext
[length]
[method]

Initially, the detector might treat such unknown bracketed words as unclosed shortcodes.

The rules were later tightened: unknown bare bracketed words must have stronger evidence, such as a paired structure, attribute assignment, or explicit self-closing, to be treated as shortcodes.

2. caption was not recognized as a known paired shortcode

WordPress historical image captions often use:

Plaintext
[caption]...[/caption]

Only after adding caption to the known and paired shortcodes could the historical image structures be correctly classified.

3. Brackets inside code regions were misjudged

The initial shortcode detector would scan the entire content.

This meant that bracketed text in the following regions could also be treated as WordPress shortcodes:

  • SyntaxHighlighter Gutenberg blocks;
  • Code Block Pro;
  • <pre>;
  • <code>;
  • Inside
    ...
    .

Code region protection was later added:

  1. Calculate protected code intervals;
  2. Merge overlapping intervals;
  3. Ignore normal shortcode matches if they fall within code regions;
  4. The outer start and end markers of still participate in strict pairing detection.

This avoided code content misjudgments while retaining the detection of unclosed, orphaned closing, and misaligned nesting.

4. Empty structures outside Gutenberg blocks misjudged as mixed

Initially, as long as any HTML tags remained outside Gutenberg blocks, it would trigger:

Plaintext
CLASSIC_SUBSTANTIAL_OUTSIDE_BLOCKS

This would judge the following non-semantic residues as mixed:

HTML
<p></p>
<p> </p>
<p><br></p>
<div><span></span></div>

Later, structural judgment based on HTMLParser was added.

The following content can be ignored:

  • Plain whitespace and line breaks;
  • &nbsp;;
  • &#160;;
  • &#xA0;;
  • Specified zero-width characters;
  • Paired empty p, div, span, section;
  • <br> and <br/>.

However, as long as a single real Chinese character, letter, number, or punctuation mark exists, it is still judged as mixed.

Media, links, functional tags, unknown tags, and broken HTML also continue to be conservatively judged as substantial content.

I did not use fixed thresholds like “ignore if less than 50 characters” because short text could also be an important hint, warning, or command instruction.

5. Self-closing syntax for non-void tags

The audit also found:

HTML
<p/>
<div/>
<span/>
<section/>

Initially, these would be treated as safe empty structures.

However, these are not true HTML void elements, and a self-closing slash does not necessarily mean they are properly closed in HTML semantics.

The final rules were tightened:

  • <br/> can be ignored;
  • <p/>, <div/>, <span/>, <section/> are conservatively judged as substantial or abnormal structures.

10. Why two mixed articles were ultimately retained

Two articles in the real samples were judged as mixed.

Initially, I suspected the detector might have only deleted the Gutenberg comments without deleting the entire block content, causing the HTML inside the block to be mistakenly counted as classic content.

After diagnosis, I confirmed there was no issue with the Gutenberg span calculation. The detector deletes the complete top-level block from the opening comment to the closing comment.

One article still had a large amount of classic content outside the Gutenberg blocks, so mixed was obviously correct.

The other article had only 56 visible characters outside the blocks, but they were not line breaks, empty paragraphs, or invisible characters; rather, they were two complete classic <p> paragraphs containing:

  • 43 Chinese characters;
  • 3 ASCII letters;
  • 4 digits;
  • 6 punctuation marks.

These paragraphs are located between two SyntaxHighlighter blocks and constitute real visible content.

Therefore, even if they account for a very small proportion of the full text, they should not be ignored because there are “too few characters.”

This diagnosis ultimately led me to conclude: the mixed judgment should be based primarily on structural semantics, not on character count or full-text proportion.

11. The first phase eligibility scope remains strict

The current first phase eligibility criteria are intentionally set very narrowly.

Only articles that simultaneously meet the following conditions may enter the future automatic excerpt candidate pool:

  • Published WordPress articles;
  • Polylang language explicitly set to Chinese;
  • Complete Gutenberg structure;
  • Contains complete Code Block Pro structure;
  • Does not contain SyntaxHighlighter;
  • Does not belong to mixed;
  • Does not contain unknown formats;
  • Does not require manual-review.

gutenberg/plain, Classic Editor, and other historical formats currently remain only as inventory or migration candidates and do not directly enter the automatic excerpt phase.

This does not mean they can never generate excerpts; rather, it indicates they require different content protection, migration, or review strategies.

12. 143 automated tests as the first stable baseline

After iteratively adding edge case tests, the project ultimately formed 143 automated tests.

The test scope includes:

  • JSONL input contract;
  • Exact record count check;
  • Atomic writes;
  • Input file remains unchanged;
  • Input-output same-file protection;
  • Gutenberg block pairing;
  • Classic Editor and mixed classification;
  • Code Block Pro;
  • SyntaxHighlighter;
  • <pre>/<code>;
  • Known and unknown shortcodes;
  • Broken structures;
  • Empty structure semantic judgment;
  • Production script read-only constraints;
  • Deployment dry-run;
  • Consistent export limits between PHP and Shell;
  • First phase eligibility judgment.

The full test command is:

Bash
PYTHONDONTWRITEBYTECODE=1 \
python3 -m unittest discover -s tests -v

Before the first commit, all 143 tests passed.

Figure 4: All 143 automated tests passed
Figure 4: All 143 automated tests passed

13. Adding Chinese and English READMEs to the project

This project ultimately adopted a Chinese-first bilingual documentation structure:

Plaintext
README.md
README.en.md

Where:

  • README.md is the main Chinese version;
  • README.en.md is the English version;
  • The top of both files allows switching between the two;
  • Both versions use the same status, security boundaries, and directory descriptions.

The README clearly distinguishes:

Completed

  • Read-only export;
  • Local format analysis;
  • Risk classification;
  • Eligibility judgment;
  • Automated testing;
  • Validation with 3, 20, and 100 production samples.

In Progress

  • Expanding historical samples;
  • Validating format boundaries;
  • Identifying low-risk candidates.

Not Yet Implemented

  • AI excerpt generation;
  • WordPress excerpt write-back;
  • Bulk article modification;
  • Automated deployment write tools.

This prevents repository visitors from mistakenly assuming the project can already directly modify WordPress.

14. Creating a public GitHub repository

After completing the sensitive information audit, I confirmed the repository does not contain:

  • API Keys;
  • Tokens;
  • Private keys;
  • Passwords;
  • Server IPs;
  • User emails;
  • Database configurations;
  • Real article content;
  • Raw JSONL;
  • Local analysis results.

Therefore, instead of continuing to use a private repository this time, I created a public GitHub repository:

shuijingwan/wordpress-ai-excerpt-backfill

The first commit is:

Plaintext
fa2d766 feat: Add read-only WordPress historical article format audit pipeline

The repository currently contains:

Plaintext
.gitignore
README.md
README.en.md
bin/
config/
docs/
src/
tests/

It does not contain:

Plaintext
data/raw/
data/analysis/
__pycache__/
*.pyc

15. Why this step is worth documenting separately

Looking at the final results, I have not yet generated excerpts for any historical articles, nor have I written back a single piece of WordPress data.

If we look only at the ultimate goal of “excerpt backfill,” this phase seems to have no direct output.

But in reality, this step solved the most fundamental problems in subsequent automation:

  • I know what the main formats of the historical articles are;
  • I know which formats can be deterministically identified;
  • I know how to avoid shortcode misjudgments in code regions;
  • I know how mixed should be judged based on structure;
  • I know how to read production data in a controlled manner;
  • I know that export and analysis failures will not leave behind pseudo-official results;
  • I know which sensitive files cannot enter Git;
  • I already have a testing framework that allows for continuously adding fixtures and rules.

Without these foundations, even if the AI-generated excerpts are of high quality, the entire production process remains untrustworthy.

16. Next steps

Next, we still will not immediately enable bulk write-back.

A more reasonable sequence is:

  1. Continue expanding read-only samples in batches of no more than 100;
  2. Tabulate the distribution of editor and code formats across different eras;
  3. Identify unknown, mixed, SyntaxHighlighter, and damaged samples;
  4. Confirm the low-risk excerpt candidate pool;
  5. Design the AI excerpt generation process separately;
  6. Compare model quality, processing speed, and API costs;
  7. Finally, design the WordPress write-back tool.

Before truly entering the write phase, the following must also be added:

  • Article-level backups;
  • dry-run;
  • Idempotency;
  • Conflict detection;
  • Original excerpt protection;
  • Failure retries;
  • Audit logs;
  • Rollback capability.

Excerpt generation and excerpt write-back should continue to be treated as two independent phases, rather than executed in a single run.

17. Summary

The most important decision in this project was not which AI model to choose, but rather not rushing to let AI modify WordPress.

Faced with over two thousand historical articles spanning more than a decade, I first built a read-only, quantity-limited, verifiable, and testable format audit pipeline.

The first batches of 3, 20, and 100 production samples have been validated, all 143 automated tests have passed, and the project has been made public on GitHub with both Chinese and English documentation.

The current phase still has not generated excerpts, nor has it written back to production data.

But from the perspective of a long-term production process, this is not a detour; rather, it is about clarifying format boundaries, data security, and failure protection before truly starting automation.

Only by first knowing which articles can be safely processed can the subsequent AI excerpt backfill gradually evolve from a one-off script into a long-term, sustainable production process.

系列导航

需要长期技术维护或远程问题排查?

我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。

如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:

  • ✅ PHP / Laravel / Yii2 老项目无人维护
  • ✅ Go / Gin 后端接口需要排查或优化
  • ✅ WordPress 网站访问慢、报错或插件冲突
  • ✅ Nginx / MySQL / Redis / Linux 服务器异常
  • ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
  • ✅ 需要长期远程技术支持或兼职维护

更多介绍请查看:关于我 & 合作

微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理