管理しているサイトに、長い記事を下書きで入れていたときのことです。記事の本文は、コードの枠や表を含む HTML で、20KB ほどありました。それを、管理画面にログインしたブラウザーの開発ツール(コンソール)から、REST API で下書きとして入れる手順にしました。
そのとき、本文をコンソールに貼り付けるのはやめて、手元のファイルから読んで送る形にしています。この記事では、その手順のコードと、入れたあとに確かめる方法、作業環境で確かめた結果をまとめます。
なぜファイルから読むのか
短い本文なら、コンソールに文字列として貼り付けて送れます。ただ、20KB ほどの本文になると貼り付けるのが大変で、貼り間違いや、途中の文字が欠けていないかも心配になります。
さらに、本文を JavaScript の文字列に入れると、本文の中の文字によっては、中身が変わったり、エラーになったりします。
| 本文の中の文字 | テンプレート文字列(`…`)に貼ったとき |
|---|---|
`(バッククォート) |
そこで文字列が終わり、エラーになる |
${…} |
JavaScript の式として計算され、値に置きかわる |
\n・\t など |
改行やタブになる。\ 自体は消える |
\x のあとに 16 進数でない文字 |
エラーになる(Windows のパスの C:\xdata など) |
コードを説明する記事ほど、こうした文字が本文に多く入ります。一方で、ファイルから読んで JSON.parse() した文字列なら、ファイルに書いたとおりの中身になります。そこで、本文とタイトルなどを 1 つの JSON のファイルにまとめ、ブラウザーで選んで読む形にしました。
なお、REST API の nonce の取り方は、前の記事(canvas で作った画像をアイキャッチに登録する記事)で書いたものと同じです。
流れ
| 手順 | すること | 使うもの |
|---|---|---|
| 0 | 本文・タイトル・抜粋などを、1 つの JSON のファイルにまとめる(UTF-8) | テキストエディターや、ファイルを作るスクリプト |
| 1 | 管理画面のページに、ファイルを選ぶ欄(<input type="file">)を足す |
document.createElement() |
| 2 | JSON のファイルを選ぶ | input.click() |
| 3 | ファイルを読んで、下書きとして送る | File.text()・JSON.parse()・POST /wp-json/wp/v2/posts |
| 4 | 読み直して、送ったものと同じかを比べる | GET /wp-json/wp/v2/posts/{id}?context=edit |
手順 1〜4 は、管理画面にログインした同じタブのコンソールで、順に流します。前の手順で作った input・data・nonce・post を、あとの手順で使うためです。開発ツールのコンソールでは、await をそのまま書けます。
JSON のファイルの形
JSON のファイルには、REST API の /wp/v2/posts に送る項目の名前で、そのまま書いておきます。こうしておけば、読んだ中身を変えずにそのまま送れます。
{
"title": "記事のタイトル",
"excerpt": "一覧や検索結果に出す、記事の短い説明です。",
"slug": "my-long-article",
"categories": [
6,
8
],
"featured_media": 123,
"content": "<p>本文の 1 つ目の段落です。</p>\n<h2>見出し</h2>\n<p>本文の続きです。</p>"
}
| 書き方 | 役目 |
|---|---|
title |
記事のタイトル |
excerpt |
抜粋 |
slug |
スラッグ(URL に使われる名前)。同じスラッグの記事がすでにあるときは、後ろに -2 などが付く |
categories |
カテゴリーの ID の配列。名前ではなく ID で書く |
featured_media |
アイキャッチにする画像の ID。先にメディアに上げておいた画像の ID を書く |
content |
本文の HTML。改行は \n、" は \" と書く |
とはいえ、JSON の中の改行や " を手で書くと、間違えやすくなります。本文の HTML のファイルを別に作り、スクリプトで読んで JSON.stringify() で JSON のファイルにすれば、書き間違いは起きません。ファイルは UTF-8 で保存しておきます。
手順 1:ファイルを選ぶ欄を足す
// ページに、画面には出さないファイルの欄を足す
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.style.display = 'none';
document.body.appendChild(input);
// ファイルが選ばれたら知らせる
input.addEventListener('change', () => {
console.log('選んだファイル', input.files[0]?.name, input.files[0]?.size);
});
| 書き方 | 役目 |
|---|---|
document.createElement('input') と type = 'file' |
ファイルを選ぶ欄を作る |
accept = '.json,application/json' |
選ぶ画面で、JSON のファイルを出しやすくする。ほかのファイルを選べなくするものではない |
style.display = 'none' |
画面には出さない。管理画面の見た目は変わらない |
document.body.appendChild(input) |
ページに足す。ページを読み直すと消える |
addEventListener('change', …) |
ファイルが選ばれたら、名前と大きさ(バイト)をコンソールに出す |
欄はこのタブのページの中にだけ足されるので、サイトのファイルや設定は変わりません。
手順 2:ファイルを選ぶ
// ファイルを選ぶ画面を開く input.click();
ファイルを選ぶ画面が開いたら、JSON のファイルを選びます。コンソールに、選んだファイルの名前と大きさが出れば、次へ進めます。
ただし、ブラウザーによっては、クリックなどの操作のすぐあとでないと、ファイルを選ぶ画面を開かない決まりがあります。開かないときは、input.style.display = 'block' にして欄を画面に出し、その欄を直接押して選んでください。
手順 3:読んで、下書きとして送る
// 選んだファイルを UTF-8 の文字として読み、JSON にする
const data = JSON.parse(await input.files[0].text());
// REST API の nonce をもらう(ログインしているブラウザーのクッキーで動く)
const nonce = await fetch('/wp-admin/admin-ajax.php?action=rest-nonce', { credentials: 'same-origin' }).then((r) => r.text());
// 下書きとして入れる
const res = await fetch('/wp-json/wp/v2/posts', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-WP-Nonce': nonce, 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, status: 'draft' }),
});
const post = await res.json();
if (!res.ok) {
throw new Error(post.message);
}
console.log(post.id, post.status, post.link);
| 書き方 | 役目 |
|---|---|
input.files[0].text() |
選んだファイルを、UTF-8 の文字として読む。読み終わるのを await で待つ |
JSON.parse() |
読んだ文字を、オブジェクトにする。JSON の書き方が間違っているときは、ここでエラーになって止まる |
admin-ajax.php?action=rest-nonce |
ログインしているユーザーの、REST API 用の nonce をもらう |
credentials: 'same-origin' |
同じサイトへのリクエストに、ログインのクッキーを付ける |
'X-WP-Nonce': nonce |
もらった nonce を送る。ないと、ログインしていない扱いになり、401 で断られる |
'Content-Type': 'application/json' |
送るものが JSON だと伝える |
{ ...data, status: 'draft' } |
ファイルの中身に、下書きの指定を足して送る。ファイルの中に status を書いてしまっていても、下書きになる |
res.ok |
うまくいくと 201 が返る。それ以外は message にわけが入っているので、エラーにして止める |
post.id |
できた下書きの ID。次の手順で使う |
パーマリンクの設定が「基本」のサイトでは、/wp-json/ の URL が使えないので、/?rest_route=/wp/v2/posts のように書きます。
また、同じ JSON のファイルで手順 3 をもう一度流すと、下書きがもう 1 つできてしまいます。このとき、2 回目のスラッグには後ろに -2 が付きます。
手順 4:読み直して、送ったものと同じかを比べる
送ったあとの記事を読み直し、送ったものと 1 文字も違わないかを、=== で比べます。
// 入れた記事を、編集用の形(raw)で読み直す
const saved = await fetch(`/wp-json/wp/v2/posts/${post.id}?context=edit`, {
credentials: 'same-origin',
headers: { 'X-WP-Nonce': nonce },
}).then((r) => r.json());
// 送ったものと同じか、文字化けで出る置き換えの文字(U+FFFD)がないか
const all = saved.title.raw + saved.excerpt.raw + saved.content.raw;
console.table({
title: saved.title.raw === data.title,
excerpt: saved.excerpt.raw === data.excerpt,
content: saved.content.raw === data.content,
noFFFD: !all.includes('\uFFFD'),
length: saved.content.raw.length,
});
| 書き方 | 役目 |
|---|---|
?context=edit |
タイトル・抜粋・本文を、表示用に変える前の形(raw)で受け取る。ない場合は、段落のタグが足されるなどした表示用の形しか返らない |
saved.content.raw === data.content |
保存された本文が、ファイルの本文と同じなら true |
'\uFFFD' |
文字化けしたときに入る置き換えの文字(黒い菱形の中に ? の形)。ないことを見る |
console.table() |
結果を表の形で出す |
管理しているサイトで入れたときは、タイトル・抜粋・本文の 3 つとも true で、置き換えの文字もありませんでした。
=== が false になるとき
作業環境で試したところ、本文に絵文字を入れたときだけ、本文の比べた結果が false になりました。送った箱の絵文字が、保存されたあとは 📦 という文字参照になっていたのです。
今回検証したサイトでは、データベースの本文の列の文字コードが utf8mb3 でした。utf8mb3 は絵文字のような 4 バイトの文字をしまえないので、WordPress はこのようなときに、絵文字を HTML の文字参照に変えて保存します。表示は同じ絵文字になるのですが、中身の文字は変わるので、=== では違うと出ます。
false が出たときは、どこから違うかを見ます。下のように先頭から同じところを数えていくと、違いの場所がわかります。
let i = 0;
while (i < data.content.length && data.content[i] === saved.content.raw[i]) {
i++;
}
console.log(data.content.slice(i, i + 40));
console.log(saved.content.raw.slice(i, i + 40));
また、unfiltered_html の権限がないユーザー(投稿者・寄稿者や、マルチサイトの特権管理者でないユーザーなど)が送ると、本文の HTML の一部が保存のときに取り除かれることがあります。この場合の結果は、試していません。
メディアの画面で気づいたこと
記事に使う画像は、先にメディアの新規追加の画面(media-new.php)からアップロードしました。この画面では、ファイルの欄にファイルを入れると、そのまますぐアップロードが始まります。
そのため、このページにもともとある file の欄は、JSON のファイルを読むのには使えません。JSON のファイルを選んでも、メディアとしてアップロードされてしまうからです。手順 1 で、自分で別の input を足しているのはこのためで、手順 1〜4 も、メディアの画面ではなく、ダッシュボードなどほかの管理画面のページで流すほうが安心です。
縦長の写真が大きく出すぎたとき
本文に入れた縦長の写真が、記事の幅いっぱいに大きく出すぎていました。そこで、REST API で本文を読み直し、その写真の img にだけ、幅の style を足して保存しました。
// 本文の中の、ファイル名に fileName を含む img に、幅の style を足す
const fileName = 'photo-tall.jpg';
const style = 'width:450px;max-width:100%;height:auto';
const addWidth = (html) => html.replace(/<img\b[^>]*>/g, (tag) => {
if (!tag.includes(fileName) || tag.includes(' style=')) {
return tag;
}
return tag.replace(/^<img\b/, `<img style="${style}"`);
});
// いまの本文を raw で読み、書き換えて保存する
const cur = await fetch(`/wp-json/wp/v2/posts/${post.id}?context=edit`, {
credentials: 'same-origin',
headers: { 'X-WP-Nonce': nonce },
}).then((r) => r.json());
const next = addWidth(cur.content.raw);
console.log('変わった img の数', (next.match(/style="width:450px/g) || []).length);
const res2 = await fetch(`/wp-json/wp/v2/posts/${post.id}`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-WP-Nonce': nonce, 'Content-Type': 'application/json' },
body: JSON.stringify({ content: next }),
});
const updated = await res2.json();
if (!res2.ok) {
throw new Error(updated.message);
}
console.log(updated.id, updated.modified);
| 書き方 | 役目 |
|---|---|
fileName |
幅を変えたい画像のファイル名。src にこの名前を含む img だけを書き換える |
width:450px;max-width:100%;height:auto |
幅を 450px にする。画面が狭いときは画面の幅に合わせ、縦横の比は保つ |
/<img\b[^>]*>/g |
本文の中の img のタグを、1 つずつ見る |
tag.includes(' style=') |
すでに style があるタグは変えない。2 回流しても、同じ style が 2 つ付かない |
?context=edit の content.raw |
表示用の形ではなく、保存されている形の本文に足す |
{ content: next } |
本文だけを送る。タイトルや抜粋は変わらない |
管理しているサイトでは、更新したあとにプレビューを開き、写真の幅が 450px になったことを確かめました。
なお、記事の編集の画面は、更新が終わってから開き直してください。開いたままの編集の画面で「下書きとして保存」を押すと、その画面が開いたときの本文で保存され、足した style が戻ってしまうおそれがあります。
作業環境で確かめた結果
作業環境(本番から複製した WordPress 7.1.3 のサイト)で確かめました。ただし、作業環境ではブラウザーの操作は使っていません。ブラウザーでの手順(手順 1〜4 と、img に幅を足すコード)は、本番の管理画面で使ったものです。
作業環境では、PHP のコマンドラインで WordPress の wp-load.php を読み込み、wp_set_current_user() で管理者にしてから、rest_do_request() で REST API に送りました。rest_do_request() は WordPress の中から REST API を呼ぶ関数なので、nonce は使いません。
JSON のファイルは file_get_contents() で読み、json_decode() したものに status: 'draft' を足して、JSON のまま POST /wp/v2/posts に送りました。そのあと GET /wp/v2/posts/{id}?context=edit で読み直し、=== で比べています。
試しの本文
| 項目 | 中身 |
|---|---|
| 本文の大きさ | 20,170 バイト(UTF-8) |
| JSON のファイルの大きさ | 21,265 バイト |
| 中身 | 日本語の段落・見出し 25 個・コードの枠・表・縦長の画像の img |
| コードの枠の中 | `・${count}・\n・C:\xdata・< などの文字 |
| そのほか | カテゴリー 1 つ・アイキャッチに作業環境のメディアの画像の ID |
下書きを入れて、読み直して比べる
| 見たこと | A:試しの本文 | B:A の終わりに絵文字を 1 つ足した本文 |
|---|---|---|
POST /wp/v2/posts |
201 | 201 |
GET …?context=edit |
200 | 200 |
タイトルが同じか(===) |
true | true |
抜粋が同じか(===) |
true | true |
本文が同じか(===) |
true | false(絵文字が 📦 になった) |
| 置き換えの文字(U+FFFD)がないか | true | true |
| 本文の大きさ(送った → 保存された) | 20,170 → 20,170 バイト | 20,201 → 20,206 バイト |
| 状態 | 下書き | 下書き |
| カテゴリー・アイキャッチ | ファイルのとおり | ファイルのとおり |
A は、20KB ほどの本文が、` や \ を含めて、1 文字も変わらずに保存されました。B のほうは、上の「=== が false になるとき」のとおり、絵文字のところだけが変わっています。
img に幅を足す
「img に幅を足す」のコードの addWidth() をそのまま取り出して Node で A の本文に使い、できた本文を POST /wp/v2/posts/{id} で送りました。
| 見たこと | 結果 |
|---|---|
| 書き換わった img | 1 個のうち 1 個 |
| もう一度流したとき | 同じ本文のまま(style が 2 つにならない) |
| style のほかの文字 | 元の本文と同じ |
更新(POST /wp/v2/posts/{id}) |
200・読み直した本文が送った本文と同じ(true) |
| 表示のときの img | 足した style が残り、loading="lazy" と decoding="async" が足された |
今回検証したサイトの管理者は unfiltered_html の権限があり、style は取り除かれませんでした。
nonce を付けずに送ったとき
作業環境のサイトに、ログインのクッキーも nonce も付けずに POST /wp-json/wp/v2/posts を送ったところ、401 が返り、code は rest_cannot_create でした(何も保存されません)。
本文をテンプレート文字列に貼ったとき(Node)
「なぜファイルから読むのか」の表を、A の本文で確かめました。本文を ` で囲んで JavaScript として動かし、元の本文と比べています。
| 貼った本文 | 結果 |
|---|---|
| A の本文そのまま | SyntaxError(コードの枠の中の ` で文字列が終わる) |
` を ' に変えた本文 |
SyntaxError(C:\xdata の \x) |
さらに \x をなくした本文 |
ReferenceError(${count} の count がない) |
さらに、先に count を作ったとき |
エラーにならないが、元の本文と違う(14,514 文字 → 14,089 文字・\ が 75 個 → 0 個・${count} が 50 個 → 0 個) |
ファイルから読んで JSON.parse() |
元の本文と同じ(true) |
後片付け
確かめる前に、投稿の表など、関係するデータベースの表のバックアップを取りました。試しで入れた下書きは、すべて wp_delete_post() に true を付けて、ゴミ箱に入れずに消しています。消したあと、どの ID も get_post() で見つからないこと、投稿・投稿のメタ・カテゴリーとのつながりの数と、カテゴリーごとの記事の数が、試す前と同じになったことを見ました。
試していないこと
- 作業環境のブラウザーで、手順 1〜4 を流すことは、試していません(ブラウザーでの手順は、本番の管理画面で使ったものです)
- そのため、コンソールから
input.click()で選ぶ画面が開くかを、ブラウザーごとには確かめていません unfiltered_htmlの権限がないユーザーで送ったときの結果は、試していません- ブロックエディターで書いた記事の本文を送ったときの見え方は、試していません
まとめ
- 長い本文は、コンソールに貼らず、JSON のファイルにまとめて、ブラウザーで選んで読む
- 管理画面のページに見えない
<input type="file">を足し、input.files[0].text()→JSON.parse()→POST /wp-json/wp/v2/postsの順で、そのまま下書きにできる - 本文をテンプレート文字列に貼ると、
`・${…}・\で、エラーになったり中身が変わったりする。ファイルから読めば変わらない - 送ったあとは
?context=editで読み直し、===で比べ、置き換えの文字(U+FFFD)がないことを見る - データベースが
utf8mb3のサイトでは、絵文字が文字参照に変わるので、===がfalseになる - メディアの新規追加の画面の file の欄は、入れるとすぐアップロードされるので、JSON を読むのには自分で足した input を使う


