長い記事の本文をファイルから読んで、REST API で下書きにする:コンソールに貼らない方法

広告WordPress・PHP を学べる本

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

管理しているサイトに、長い記事を下書きで入れていたときのことです。記事の本文は、コードの枠や表を含む 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 に送る項目の名前で、そのまま書いておきます。こうしておけば、読んだ中身を変えずにそのまま送れます。

my-long-article.json(形の例)
{
    "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:ファイルを選ぶ欄を足す

開発ツールのコンソール(手順 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:ファイルを選ぶ

開発ツールのコンソール(手順 2)
// ファイルを選ぶ画面を開く
input.click();

ファイルを選ぶ画面が開いたら、JSON のファイルを選びます。コンソールに、選んだファイルの名前と大きさが出れば、次へ進めます。

ただし、ブラウザーによっては、クリックなどの操作のすぐあとでないと、ファイルを選ぶ画面を開かない決まりがあります。開かないときは、input.style.display = 'block' にして欄を画面に出し、その欄を直接押して選んでください。

手順 3:読んで、下書きとして送る

開発ツールのコンソール(手順 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 文字も違わないかを、=== で比べます。

開発ツールのコンソール(手順 4)
// 入れた記事を、編集用の形(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 になりました。送った箱の絵文字が、保存されたあとは &#x1f4e6; という文字参照になっていたのです。

今回検証したサイトでは、データベースの本文の列の文字コードが 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 を足して保存しました。

開発ツールのコンソール(img に幅を足す)
// 本文の中の、ファイル名に 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・&lt; などの文字
そのほか カテゴリー 1 つ・アイキャッチに作業環境のメディアの画像の ID

下書きを入れて、読み直して比べる

見たこと A:試しの本文 B:A の終わりに絵文字を 1 つ足した本文
POST /wp/v2/posts 201 201
GET …?context=edit 200 200
タイトルが同じか(===) true true
抜粋が同じか(===) true true
本文が同じか(===) true false(絵文字が &#x1f4e6; になった)
置き換えの文字(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 を使う

参考

関連機能

  1. 日本語を含むファイル名のファイルをアップロードした時に自動的にリネームさせる

  2. WordPress カテゴリーの並び順を変更できるプラグイン Custom Taxonomy Order

  3. WordPress に画像の大きさを足す add_image_size:切り抜く・切り抜かないの違いと使い方

サイドバー

よく使うカスタマイズ

最近の記事

Profile

PAGE TOP