WordPressの記事に埋め込んだJavaScriptで、&&が表示のときだけ書き換わって動かない。引き金は手前の不等号だった
手元では動くJavaScriptが、WordPressの公開ページでだけ止まりました。
JavaScriptは、ページの中で動くプログラムのことです。
ボタンを押しても、結果の欄は空のままでした。
公開ページの中身を見ると、&& が && に変わっています。&& は「かつ」を表す記号です。& は & を番号で書き表した形で、JavaScriptとしては読めません。
引き金は、その手前にあった比較の < でした。
WordPressは表示のときに、< から次の > までをタグとみなします。
その間にある & は、script の中でも & に置き換えられます。
比較をすべて > の向きに書き直すと、書き換えは0か所になりました。
7月22日の朝、8本のうち3本が止まっていた
置いていたのは、別のサイトの記事に埋め込んだ無料の診断ツールです。
2026年7月21日の夜から22日の朝にかけて、部下役のAIが新しく7本を公開しました。
最後の1本を公開した直後に試すと、入力して診断のボタンを押しても、何も出てきません。
ブラウザの中でJavaScriptを読み込ませると、構文エラーが返ってきました。
構文エラーは、書き方の決まりに合わず、読み込みの段階で止まることです。
SyntaxError: Invalid or unexpected token
手元のファイルと公開ページを1行ずつ比べました。&& が && に置き換わった行が、3行あります。
その時点で公開していた8本を調べると、同じ書き換えは3本にありました。& の数は、2つ、2つ、12です。
残りの5本は0でした。
壊れていたのは、こういう行です。
変数の名前は置き換えましたが、記号の並びは元のままにしています。
// 手元のファイル
const over = d.max!=null && x>d.max && x-d.max<=2;
const under = d.min!=null && x<d.min && d.min-x<=2;
// 公開ページ
const over = d.max!=null && x>d.max && x-d.max<=2;
const under = d.min!=null && x<d.min && d.min-x<=2;
この下に続く2行でも、同じように && が書き換わっていました。
書き換えていたのは、表示のときの wptexturize
正体は、WordPressの wptexturize という処理でした。
記事を表示するたびに、引用符やダッシュなどの記号を、見た目の整った文字に置き換える処理です。
WordPressのコードでは、記事の本文を表示するときの処理の一覧に入っています。
公式のコードリファレンスには、一部のHTMLの中のコードは飛ばす、とあります。
飛ばす対象は pre code kbd style script tt の6つです。
script の中は、本来は手を付けない場所のはずでした。
ところが、< で始まる部分だけは扱いが別です。
wptexturize は本文を、タグらしい部分とそれ以外に切り分けてから処理します。
タグらしい部分は、< から次の > まで、と正規表現で決まっていました。
正規表現は、文字の並びを探すための書き方です。
// formatting.php(抜粋)
'<' // Find start of element.
'[^>]*>?' // Find end of element. If not found, match all input.
// タグらしい部分では、& を & に置き換える
$curl = preg_replace( '/&(?!#(?:\d+|x[a-f0-9]+);|[a-z1-4]{1,8};)/i', '&', $curl );
タグらしい部分の & は、script の中かどうかに関係なく置き換えられます。
さっきの行なら、x-d.max<=2; の < から、次に > が出てくるまでが1つのタグに見えます。
その間の && が、&& になっていたのです。> が最後まで出てこなければ、本文の終わりまでがタグの扱いになります。
データベースに保存された本文は無傷でした。
表示のたびに書き換わるので、保存し直しても直りません。
1回目は、不等号と英字の組み合わせしか直していなかった
最初に部下役のAIが疑ったのは、x<d.min のように < のすぐ後ろに英字が来る形でした。
HTMLのタグも、<div のように < と英字で始まるからです。
この形を d.min>x と逆向きに書き直し、&& の前後に空白も入れました。
公開ページで数え直すと、& は2つ、2つ、8つ残っています。
残っていたのは、<=2 や <3 のように、< のあとに数字や = が続く比較でした。
さっきのコードのとおり、WordPressは < の次の文字を見ていません。
2回目に、script の中の比較の < を、すべて > の向きに直しました。
3本とも & は0になりました。
公開ページで実際に入力して、結果が出ることも確かめています。
8月8日の夜には、同じサイトに置いた管理者用のツールが、同じ形で止まりました。
出す前の点検で、前後が空白の < しか数えておらず、g.position <= 20 を見落としていたのです。
公開ページのこの1行は、手元の73字より10字長い83字でした。
1字の & が6字の & になり、それが2つで10字ふえた計算です。20 >= g.position と向きを変えると、長さは手元と同じ12,056字に戻りました。
10月には、タグを組み立てる文字列の途中で起きた
10月13日には、比較ではなく、HTMLを組み立てる文字列で起きました。
ボタンの部品を文字列でつなぎ、その途中に && を書いていた行です。
名前を置き換え、関係のない部分を省いて載せます。
return '<button class="' + cls + '"' + ((learn && done) ? ' disabled' : '') + '>';
'<button の < から、最後の >' までが1つのタグに見えます。
その間の && が書き換わり、構文エラーになりました。
このときも、保存されていた本文は無傷で、壊れていたのは表示だけです。
'<div class="x">' のようなタグの文字列そのものは、何度も通っています。
間に & が無ければ、置き換える文字が無いからです。
比較は > に向け、タグの文字列の中に & を置かない
直し方は2つでした。
1つ目は、比較の < を > の向きに書き直すことです。
意味は変わりません。
// 前
for (var i = 0; i < list.length; i++) { }
if (a <= 20 && b >= 3) { }
// 後
for (var i = 0; list.length > i; i++) { }
if (20 >= a && b >= 3) { }
2つ目は、タグを組み立てる文字列の途中で && を使わないことです。
条件は先に変数へ出してから、文字列につなぎます。
var dis = (learn && done) ? ' disabled' : '';
return '<button class="' + cls + '"' + dis + '>';
もっと固くするなら、script の中に & を1つも書かない、という決め方もあります。
JavaScriptを本文の外のファイルにして読み込めば、本文にかかるこの処理は通らないはずです。
ただ、僕の置き場所はメディアに .txt や .json すら上げられず、データまで本文に埋め込んでいます。
出す前に、WordPressと同じ区切り方で確かめる
目で探すと、また見落とします。
そこで部下役のAIに、WordPressの区切り方をそのまま写した検査を書いてもらいました。
ツールのHTMLファイルを渡すと、表示のときに & が書き換わる行を出します。
目次プラグインが割り込む見出しタグも、あわせて探します。
# 使い方: python wp_script_check.py tool.html
import re
import sys
text = open(sys.argv[1], encoding="utf-8").read().replace("\r\n", "\n")
# WordPress の wptexturize と同じ区切り方。< から次の > までをタグとみなす
TAG = re.compile(r"(<(?:(?=!--)!(?:-(?!->)[^-]*)*(?:-->)?|(?!!--)[^>]*>?))")
AMP = re.compile(r"&(?!#(?:\d+|x[a-f0-9]+);|[a-z1-4]{1,8};)", re.I)
bad = 0
for js in re.findall(r"<script[^>]*>(.*?)</script>", text, re.S):
parts = TAG.split(js)
after = "".join(AMP.sub("&", p) if p.startswith("<") and not p.startswith("<!--") else p
for p in parts)
for a, b in zip(js.split("\n"), after.split("\n")):
if a != b:
print("& が書き換わる行: " + a.strip())
bad += 1
for m in re.finditer(r"<h[2-4]\b", js):
print("JSの中の見出しタグ: " + js[m.start():m.start() + 40])
bad += 1
print("問題 %d か所" % bad)
sys.exit(1 if bad else 0)
11月11日、記録に残っていた行に、この検査と同じ区切り方を当ててみました。
7月22日の3行は、公開ページに残っていた形と1文字も違わずに書き換わりました。& は12個で、記録の数と同じです。
8月8日の行と10月13日の行も、公開ページと同じく10字ふえました。
書き換わった行を手元のNode.js(パソコンでJavaScriptを動かす道具)に読ませると、同じ構文エラーが出ます。
これはWordPress本体を動かした確かめ方ではありません。
テーマやほかのプラグインが表示に足す処理も、ここでは見ていません。
WordPressに入れたあとは、公開ページで動かして確かめます。
公開ページに出た形で、原因をしぼる
埋め込んだJavaScriptでは、ほかの止まり方にも当たっています。
公開ページの形を見ると、原因の見当がつきます。
&&が&&になっている:手前の<と、この記事の話です- script の中に
<p>がある:<!-- wp:html -->の包み忘れです - 見出しタグに
id="rtoc-…"がふえている:目次プラグインの書き換えです - 保存そのものが403で止まる:サーバーのWAFが止めていました
<p> のほうは、8月11日にデータを差し替えたときに起きました。
コンソールに出たのは SyntaxError: Unexpected token '<' です。
script の中には、<p> と </p> が31個ずつ入っていました。
本文を、エディタの「カスタムHTML」ブロックの印である <!-- wp:html --> で包まずに保存していたのです。<p> を入れたのは、空行を段落の <p> に変える wpautop という処理でした。
WordPressのコードには、ブロックのある本文では、あとで wpautop を通さない、という注記があります。
包み直して保存すると、<p> は0になり、構文エラーも消えました。
参考
- 運営記録 2026-07-22:公開直後に3本が構文エラー・
&の数・1回目と2回目の手直し - 運営記録 2026-08-08:管理者用ツールの
<=・公開ページで10字ふえた1行 - 運営記録 2026-08-11:
<!-- wp:html -->の包み忘れ・<p>の混入 - 運営記録 2026-10-13:タグの文字列の途中の
&&・表示だけが壊れた - 運営記録 2026-11-11:この記事のための再現(WordPressのコードの正規表現を写したもの)
- wptexturize()(2026年11月11日確認):https://developer.wordpress.org/reference/functions/wptexturize/
- formatting.php の _get_wptexturize_split_regex()(同):https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/formatting.php
- default-filters.php の the_content(同):https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/default-filters.php
- do_blocks()(同):https://developer.wordpress.org/reference/functions/do_blocks/
- wpautop()(同):https://developer.wordpress.org/reference/functions/wpautop/
保存の段階で止まるときは、原因が別のところにあります。
WordPressの保存が「閲覧できません (Forbidden access)」403で止まる。原因はサーバーのWAFだった