公開前の記事をまとめてチェック:REST API で下書きと予約投稿から直したい言葉を探す

広告WordPress・PHP を学べる本

Amazonのアソシエイトとして、WordPress functions.phpは適格販売により収入を得ています。

管理しているサイトで、予約投稿と下書きがたまっていたときのことです。

公開の前に、表記ゆれや、出してはいけない名前が残っていないかを確かめたいと考えました。

たとえば、「版」と「バージョン」がまざっている、作業環境のホスト名(example.local など)がリンクに残っている、といったものです。

記事を 1 本ずつ編集画面で開いて探すと、時間がかかり、見落としも出ます。

そこで、管理画面にログインしたブラウザーの開発ツールから REST API を使い、公開前の記事をまとめて調べました。

この記事では、使ったコードと、作業環境で確かめた結果をまとめます。

探したもの

今回は、次の 4 つを探しました。

名前はすべて例の名前にしています。

種類 例 探す理由
表記ゆれ 「版」(「バージョン」にそろえたい) 同じものを指す言葉が記事によって違うと、読みにくくなる
作業環境のホスト名 example.local 作業環境で書いたリンクや画像の URL が、そのまま残ることがある
自分のハンドル名 myhandle メモのつもりで書いた名前が、本文に残ることがある
関数の頭の名前 my_ で始まる名前 コードの枠の外の文に、作業中の関数の名前が残ることがある

ただし、コードの枠(<pre>)の中の言葉は直しません。

コードの中の関数の名前や URL は、例として正しく書いているためです。

また、「無料版」のように、決まった言い方として使う言葉は外します。

やり方の流れ

手順 すること
1 REST API で、予約投稿・下書き・非公開の記事を、編集用の形(context=edit)ですべて読む
2 探す言葉(正規表現)と、外す言い方のリストを決める
3 コードの枠を外した本文とタイトルを調べ、当たった所の前後の文字を表に出す
4 直す記事だけ置き換えて保存し、保存された本文を読み直して、送ったものと同じかを確かめる

コードは、管理画面(/wp-admin/ の中のどのページでもよい)を開いた状態で、開発ツールのコンソールに貼って動かします。

同じサイトのページから動かすので、ログインのクッキーがそのまま使われます。

Chrome では、はじめてコンソールに貼るときに、貼ってよいかを確かめる文が出ることがあります。

手順 1〜3 は読むだけなので、記事は変わりません。

1. 公開前の記事をすべて読む

開発ツールのコンソール(手順 1)
// 1. REST API の nonce を取り、公開前の記事をすべて読む
const nonce = await (await fetch('/wp-admin/admin-ajax.php?action=rest-nonce')).text();
const headers = { 'X-WP-Nonce': nonce };

const loadAll = async (type) => {
    const list = [];
    for (let page = 1; ; page++) {
        const url = `/wp-json/wp/v2/${type}?status=future,draft,private&context=edit&per_page=100&page=${page}&_fields=id,type,status,date,title,content`;
        const res = await fetch(url, { headers });
        if (!res.ok) throw new Error(`${type} ${res.status}`);
        list.push(...(await res.json()));
        if (page >= Number(res.headers.get('X-WP-TotalPages'))) break;
    }
    return list;
};

const posts = [...(await loadAll('posts')), ...(await loadAll('pages'))];
console.log('読んだ記事', posts.length);
書き方 役目
admin-ajax.php?action=rest-nonce REST API 用の nonce(使い捨ての確かめ用の文字列)を取る。WordPress に入っている処理で、ログインしているときに返る
'X-WP-Nonce': nonce クッキーでログインしている人の REST API の呼び出しには、この nonce が要る。ないと、ログインしていない扱いになる
status=future,draft,private 予約投稿・下書き・非公開の記事だけを読む。公開済みの記事は読まない
context=edit 編集用の形で返す。content.raw(保存されているままの本文)と title.raw が入る
per_page=100 1 回で読む数。100 がいちばん大きい値
X-WP-TotalPages 全部で何ページあるかを返すヘッダー。この数まで page を増やして読む
_fields=… 使う項目だけを返してもらう。返す量が減る
loadAll('pages') 固定ページも同じように読む。カスタム投稿タイプ(例:my_article)を調べるときは、その rest_base を足す

content.raw を使う理由

ふつうの読み方(context=view)で返る content.rendered は、表示用に手を加えた HTML です。

段落のタグが足されたり、プラグインがリンクに属性を足したりしています。

直すのは保存されている本文なので、content.raw を調べます。

context=edit は、記事を編集できる人だけが使えます。

2. 探す言葉と、外す言い方を決める

開発ツールのコンソール(手順 2)
// 2. 調べる言葉と、外す言い方
const rules = [
    { name: '版 → バージョン', re: /版/g, allow: ['無料版', '有料版', '出版'] },
    { name: '作業環境のホスト名', re: /[a-z0-9-]+\.local\b/gi, allow: [] },
    { name: 'ハンドル名', re: /myhandle/gi, allow: [] },
    { name: '関数の頭の名前', re: /\bmy_[a-z0-9_]*/gi, allow: [] },
];
書き方 役目
name 結果の表に出す名前
re 探す言葉の正規表現。手順 3 の matchAll() で使うので、g を必ず付ける
i 大文字と小文字を区別しない(Example.Local も当たる)
[a-z0-9-]+\.local\b .local で終わるホスト名。\. は「.」そのもの、\b は語の切れ目
\bmy_[a-z0-9_]* 語の頭が my_ の名前。dummy_ の中の my_ には当たらない
allow 外す言い方のリスト。当たった所がこの言葉の中にあれば、結果に出さない

外す言い方は、はじめは空にして流し、結果を見ながら足していくと決めやすくなります。

3. コードの枠を外して調べる

開発ツールのコンソール(手順 3)
// 3. コードの枠を外して調べ、当たった所の前後を出す
const stripPre = (html) => html.replace(/<pre\b[\s\S]*?<\/pre>/gi, '');

const isAllowed = (text, index, allow) => allow.some((word) => {
    for (let p = text.indexOf(word); p >= 0; p = text.indexOf(word, p + 1)) {
        if (p <= index && index < p + word.length) return true;
    }
    return false;
});

const rows = [];
for (const post of posts) {
    const text = post.title.raw + '\n' + stripPre(post.content.raw);
    for (const rule of rules) {
        for (const m of text.matchAll(rule.re)) {
            if (isAllowed(text, m.index, rule.allow)) continue;
            rows.push({
                id: post.id,
                type: post.type,
                status: post.status,
                date: post.date,
                rule: rule.name,
                hit: m[0],
                around: text.slice(Math.max(0, m.index - 20), m.index + m[0].length + 20).replace(/\s+/g, ' '),
            });
        }
    }
}
console.table(rows);
書き方 役目
stripPre() <pre> 〜 </pre> を消してから調べる。[\s\S]*? は、改行も含めて、いちばん短く当てる書き方
isAllowed() 当たった位置が、外す言い方(例:「無料版」)の中にあるかを見る
post.title.raw + '\n' + … タイトルと本文をまとめて調べる
text.matchAll(rule.re) 当たった所をすべて取り出す。m.index が位置
text.slice(… - 20, … + 20) 当たった所の前後 20 文字を出す。前後を見れば、直すかどうかをその場で決められる
replace(/\s+/g, ' ') 改行を空白にして、表の 1 行に収める
console.table(rows) 結果を表で出す

本文の HTML のタグは消していません。

リンクの href や画像の src に残ったホスト名も、見つけたいためです。

文の中の <code> は消していないので、短いコードの中の言葉は当たります。

当たったものを直すかどうかは、前後の文字を見て決めます。

4. 置き換えて、読み直して確かめる

直す記事が決まったら、1 本ずつ置き換えます。

ここからは記事が変わるので、必要ならデータベースのバックアップを取ってから進めます。

開発ツールのコンソール(手順 4・手順 1 のあとに)
// 4. 1 本だけ置き換えて保存し、読み直して確かめる
const id = 123; // 直す記事の id(固定ページなら下の posts を pages に)
const pairs = [['最新版', '最新バージョン']];
const base = `/wp-json/wp/v2/posts/${id}`;

const before = await (await fetch(`${base}?context=edit`, { headers })).json();
const parts = before.content.raw.split(/(<pre\b[\s\S]*?<\/pre>)/i);
const fixed = parts
    .map((part, i) => (i % 2 === 1 ? part : pairs.reduce((s, [from, to]) => s.split(from).join(to), part)))
    .join('');
if (fixed === before.content.raw) throw new Error('置き換える所がありません');

const res = await fetch(base, {
    method: 'POST',
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify({ content: fixed }),
});
if (!res.ok) throw new Error(`保存できません ${res.status}`);

const after = await (await fetch(`${base}?context=edit`, { headers })).json();
console.log({
    same: after.content.raw === fixed,
    replacementChar: after.content.raw.includes('\uFFFD'),
    status: [before.status, after.status],
    date: [before.date, after.date],
});
書き方 役目
pairs 置き換える前と後の組み合わせ。「版」だけを置き換えると「無料版」も変わるので、「最新版」のように前後を含めて書く
split(/(<pre\b[\s\S]*?<\/pre>)/i) 本文を、コードの枠とそれ以外に分ける。かっこで囲むと、枠も配列に残り、奇数番目に入る
i % 2 === 1 ? part : … コードの枠(奇数番目)はそのまま、それ以外だけを置き換える
method: 'POST' と { content: fixed } 本文だけを送って保存する。タイトルや状態は送らない
after.content.raw === fixed 保存された本文を読み直し、送ったものと 1 文字も違わないかを見る
includes('\uFFFD') 文字化けのときに入る置き換え文字(U+FFFD)がないかを見る
status・date 保存の前と後の状態と日付を並べる。予約投稿が予約のままかを見る

same が true、replacementChar が false なら、送ったとおりに保存されています。

最後に手順 1 と 3 をもう一度流して、直した所が結果から消えたことを見ます。

日付を決めていない下書きは、日付が変わる

作業環境で試したとき、日付を決めずに作った下書きは、保存した時刻に日付が変わりました。

予約投稿は、日付が変わりませんでした。

下書きの日付を残しておきたいときは、保存の前の date をメモしておくと、あとで比べられます。

作業環境で確かめた結果

本番から複製した作業環境に、試しの記事を 5 本作り、上の手順 1〜4 のコードを流しました。

ブラウザーのコンソールではなく、Node.js から同じコードを作業環境に HTTP で送る形で動かしました。

ログインは、パスワードを使わずに、PHP で管理者のログインのクッキーを作って使いました。

確かめたあと、試しの記事と、作ったログインの情報は消して、元に戻しました。

試しの記事と、チェックの結果

試しの記事 入れた言葉 結果に出たもの 出なかったもの
A:下書き(投稿) 「最新版」「無料版」、リンクの example.local、コードの枠の中の my_setup・example.local・「版」 「最新版」の「版」、リンクの example.local 「無料版」(外す言い方)、コードの枠の中の 3 つ
B:予約投稿 タイトルの「版」、my_custom_title()、myhandle 3 つとも (なし)
C:非公開 「出版」「日本語版」 「日本語版」の「版」 「出版」(外す言い方)
D:公開済み 「最新版」、example.local (なし) 読む対象に入れていないため、2 つとも出ない
E:下書き(固定ページ) 「旧版」 「旧版」の「版」 (なし)

試しの記事では、結果に出たのは 7 か所で、出てほしいものと同じでした。

作業環境に元からあった下書きでも、「版」がいくつか見つかりました。

置き換えのあと

記事 置き換え same replacementChar status date
A:下書き 「最新版」→「最新バージョン」 true false draft のまま 保存した時刻に変わった
B:予約投稿 myhandle → example true false future のまま 変わらない

A の本文を PHP でデータベースから読み、コードの枠の中の「版」と「無料版」が、置き換わらずに残っていることも確かめました。

もう一度チェックを流すと、A の「版」と B の myhandle が結果から消え、ほかの 5 か所は残りました。

今回検証したサイトでは、保存すると A のリビジョンが 1 つ増えました。

リビジョンが残るので、置き換えが合わなかったときは、編集画面のリビジョンから戻せます。

試していないこと

  • ブラウザーの開発ツールのコンソールに貼って動かすことは、試していません。動かしたのは同じコードを Node.js から送る形です
  • 記事が 100 本を超えて、2 ページ目以降を読む動きは、試していません
  • カスタム投稿タイプの記事を読むことは、試していません
  • ブロックエディターで書いた記事は、試していません。試しの記事は HTML だけの本文です
  • 本番のサイトでは、試していません

気をつけること

  • 手順 4 は、記事を保存します。先にデータベースのバックアップを取ってください
  • 置き換えは、1 本ずつ、前後を含めた言葉で行ってください。短い言葉だけで置き換えると、外したい言い方まで変わります
  • 文の中の <code> の中も置き換えの対象です。コードの名前が入るときは、pairs に入れないでください
  • ほかの人が同じ記事を編集画面で開いているときは、上書きされることがあります。編集中の記事は避けてください
  • コンソールに貼るコードは、中身を読んでから動かしてください。ログインしている人の権限で動きます

まとめ

  • REST API で status=future,draft,private と context=edit を付けて読むと、公開前の記事の保存されているままの本文(content.raw)をまとめて調べられる
  • ブラウザーから呼ぶときは、admin-ajax.php?action=rest-nonce で取った nonce を X-WP-Nonce に付ける
  • コードの枠(<pre>)は外して調べ、「無料版」のような決まった言い方は除外のリストで外す
  • 当たった所の前後の文字を表に出すと、記事を開かずに、直すかどうかを決められる
  • 置き換えたあとは、保存された本文を読み直し、送ったものと同じか、置き換え文字(U+FFFD)がないかを確かめる
  • 日付を決めていない下書きは、保存で日付が変わる。予約投稿は変わらない

参考

関連機能

  1. WordPress でプラグインなしにカスタム投稿を足す方法:register_post_type と single・archive のテンプレート(WordPress 7.1 で確かめた)

  2. WordPress ナビゲーションメニューに標準では登録項目の無いカスタム投稿の記事、カテゴリーを登録できるようにする

  3. functions.php処理内のデータをdebug.logに出力する

サイドバー

よく使うカスタマイズ

最近の記事

Profile

PAGE TOP