WordPress他でページ本文部分をAMP化するための一歩

HTMLで書かれたウェブサイトをAMP化して表示を行うにあたり、HTMLタグをAMP-HTMLタグに書き換えたりAMPでは書いてはいけない属性を削除するなどの処理が必要になる。 WordPressでは既にAMP表示用のプラグインが提供されていたりAMP化処理を実装したテーマなどが出ているが、ここぞというところだけ変換したいということはある。WordPressオフィシャル(Automattic)のAMPプラグインのClassicモードでは特にそう。
もしかしたらAMPプラグインの変換用の関数が簡単に利用できるかもしれないが、AMPプラグインのソースを見るのが面倒だったのと変換の必要な細かい部分全てを網羅しようと思わなければ自分で関数を作ってしまっても良いかもしれない。と「がとらぼ」の中の人は思って自作のWordPress用テーマBonyo/凡庸用に書いてみた。その内容。

ページのbody部分をAMP用に書き換える際のテキトールール

  • HTMLタグにstyle属性をつけたらダメ
  • HTMLタグにonclickイベントハンドラ属性をつけたらダメ
  • aタグにtarget属性は敢えて書かない(_blankは可だけど要らないでしょ?)
  • scriptタグは禁止 (JSON-LD可だけど元本文にはそんなの無いでしょ?)
  • styleタグは禁止 (body内には書けない)
  • imgタグではなくamp-imgを使う
  • audioタグではなくamp-audioを使う
  • videoタグではなくamp-videoを使う
  • iframeタグではなくamp-iframeを使う
  • base, frame, frameset, object, param, applet, embedタグは使用禁止

イベントハンドラ属性はたくさんあるが、この記事では特に使うことの多いonclickだけ。必要なら他のも足していただければと。
amp-adとかyoutube-adなど幾つかの専用タグは今回は対象外とする。

処理用の関数

処理としては文字列の置換を使うのもありだろうが、前回の画像遅延表示で本文を書き換えたのと同じくDOMで行う。むしろ画像遅延表示の置換よりも今回のAMPの方がDOM向きかと個人的には思う。

1
2
3
4
5
6
function 関数名($content) {
    //1. $contentをDOMDocumentに読み込む処理
    //2. 変換・削除などの処理
    //3. DOMDocumentを出力する
    return 出力内容;
}

$contentをDOMDocumentにする

1
2
3
4
$buf  = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>';
$buf .= $content;
$dom = new DOMDocument();
@$dom->loadHTML($buf, LIBXML_HTML_NODEFDTD | LIBXML_NOERROR);

文字化けさせない為に<html>タグで包みHTML文書として完成させる。そのときにHTMLヘッダで文字エンコーディングとしてUTF-8を指定する。新しいDOMDocumentを作ってloadHTML()で$contentを読み込む。このときloadHTML()にオプションを指定してLIBXML_HTML_NODEFDTDで<!DOCTYPE hoge>を付けさせないのと$contentがXMLとして正しくない場合にエラーにさせない?ようにする。

不要な属性を削除する

1
2
3
4
5
foreach ($dom->getElementsByTagName('*') as $node) {
    $node->removeAttribute('style');
    $node->removeAttribute('target');
    $node->removeAttribute('onclick');
}

DOMDocment内の全てのタグをgetElementsByTagName()でノードとして得て、順に評価し、不要な属性をremoveAttribute()で取り除く。

タグの付け替え

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$arrtags = array('img', 'iframe', 'video', 'audio');
foreach ($arrtags as $convtag) {
    foreach ($dom->getElementsByTagName($convtag) as $node) {
        $newtag = 'amp-' . $convtag;
        $newEle = $dom->createElement($newtag , '');
        $node->parentNode->appendChild($newEle);
        foreach ($node->attributes as $nodeattr) {
            $newEle->setAttribute($nodeattr->nodeName, $nodeattr->nodeValue);
        }
        $node->parentNode->removeChild($node);
    }
    unset($convtags);
}

タグの置換というのは例えばimgをamp-imgというのに付け替えるやつだけど、DOMDocumentでは単に名前を変えるというようなのはできないようなので、新しい要素としてamp-imgを作成して、元のimgノードの親に対して新しく作ったamp-imgをappendChild()する。imgノードの親の子供なので元のimgノードと同列の兄弟になる。元のimgノードに付いていた属性と同じ属性を順にamp-imgノードに付ける。
最後に親の子供である元のimgノードを削除する。これでimgノードがamp-imgに変わったのと同じになる。

Formにtarget属性を付ける

1
2
3
foreach ($dom->getElementsByTagName('form') as $node) {
    $node->setAttribute('target', '_top');
}

これは自分の中の元々のAMP化の知識には無かった部分。AMPでFormが使えるようになってからFormにはTargetを付けないとAMPエラーになるみたい。結果の出力先であるtargetは_blankである必要はなく何でも良いっぽい。
なのでFormタグのノードにtarget属性をセットして属性値として_topを指定した。
それだけ。

出力

1
2
3
$buf = preg_replace('/^\<html\>.*?\<body\>/', '', $dom->saveHTML());
$buf = preg_replace('/\<\/body\>.*?\<\/html\>\n/', '', $buf);
return $buf;

DOMDocumentをsaveHTML()で出力して前後の不要なタグを削除する。
最後に関数の返り値にする。

まとめた

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function 関数名($content) {

    $buf  = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>';
    $buf .= $content;
    $dom = new DOMDocument();
    @$dom->loadHTML($buf, LIBXML_HTML_NODEFDTD | LIBXML_NOERROR);

    //delete attributes
    foreach ($dom->getElementsByTagName('*') as $node) {
        $node->removeAttribute('style');
        $node->removeAttribute('target');
        $node->removeAttribute('onclick');
    }

    //converting tags    <img>, <iframe>, <video>, <audio>
    $arrtags = array('img', 'iframe', 'video', 'audio');
    foreach ($arrtags as $convtag) {
        foreach ($dom->getElementsByTagName($convtag) as $node) {
            $newtag = 'amp-' . $convtag;
            $newEle = $dom->createElement($newtag , '');
            $node->parentNode->appendChild($newEle);
            foreach ($node->attributes as $nodeattr) {
                $newEle->setAttribute($nodeattr->nodeName, $nodeattr->nodeValue);
            }
            $node->parentNode->removeChild($node);
        }
    }

    //removing some tags.
    $convtags = array('script', 'style', 'base', 'frame', 'frameset', 'object', 'param', 'applet', 'embed');
    foreach ($convtags as $convtag) {
        foreach ($dom->getElementsByTagName($convtag) as $node) {
            $node->parentNode->removeChild($node);
        }
    }

    //adding target to form
    foreach ($dom->getElementsByTagName('form') as $node) {
        $node->setAttribute('target', '_top');
    }

    //output
    $buf = preg_replace('/^\<html\>.*?\<body\>/', '', $dom->saveHTML());
    $buf = preg_replace('/\<\/body\>.*?\<\/html\>\n/', '', $buf);
    return $buf;
}

こんな感じ。

ただし、このままやるとエライことになるようなので、foreach()で該当するノードを配列に入れて、その配列に対してもう一度foreach()で回して処理する方が安全かもしれない。

関連記事:

IPv6専用サイトでLet's Encryptの証明書更新がトラブった

所有しているウェブサイトの1つでIPv6専用のものがある。
Let's EncryptのTLS証明書を使用していて、certbotで管理していた。
で、同じホストで稼働している他の通常のホストと一緒にcertbotで証明書が自動更新される筈が、このIPv6専用サイトだけが証明書の更新に失敗していた。

何で失敗するのか解らなかったので手動で更新してみた。

# certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /usr/local/etc/letsencrypt/renewal/hoge.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert is due for renewal, auto-renewing...
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for hoge.example.com
Cleaning up challenges
Attempting to renew cert (hoge.example.com) from 
/usr/local/etc/letsencrypt/renewal/hoge.example.com.conf produced an unexpected error: 
Problem binding to port 80: Could not bind to IPv4 or IPv6.. Skipping.                      
All renewal attempts failed. The following certs could not be renewed:
  /usr/local/etc/letsencrypt/live/hoge.example.com/fullchain.pem (failure)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
All renewal attempts failed. The following certs could not be renewed:
  /usr/local/etc/letsencrypt/live/hoge.example.com/fullchain.pem (failure)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 renew failure(s), 0 parse failure(s)

Problem binding to port 80: Could not bind to IPv4 or IPv6と表示されている。「IPv4とIPv6のポート80のどちらかが繋がらないよ」ということか。そりゃそうだIPv6専用サイトなんだから。どうも証明書の対象ウェブサイトが稼働している限りはIPv4への接続は必須っぽい。(IPv6は必須じゃない?)
certbotで特定のホストだけIPv4無しで更新させるという指定は無いっぽい。
と、思ったら、ウェブサーバが停まってたら(インターネットから繋がらなければ)上手くいくとの情報が。

以下のservice nginx hogeというのはFreeBSDでNginxのサービスをhogeしろというコマンド

/usr/local/bin/certbot renew --quiet && service nginx reload

上はこれまでcrontabに書いていたコマンド部分。
certbotで証明書の更新をして、成功したら、Nginxの設定を再読込みするという内容。
これだとウェブサーバのダウンタイムは限りなくゼロに近いけど、Nginxが動いたままなので証明書の更新NGになった。(IPv6専用サイト以外の証明書は更新される。)


service nginx stop; /usr/local/bin/certbot renew --quiet; service nginx start

Nginxを停止して、certbotで証明書を更新して、Nginxを起動、という内容に書き換えた。
こちらは数秒から1分程度とはいえウェブサーバのダウンタイムがしっかり発生する。crontabで頻繁に起動させないように、さらに実行する時間も注意。月1,2回深夜に実行する程度が無難?

手動でやってみた。

# service nginx stop
Stopping nginx.
Waiting for PIDS: 63339.
# certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /usr/local/etc/letsencrypt/renewal/hoge.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert is due for renewal, auto-renewing...
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for hoge.example.com
Waiting for verification...
Cleaning up challenges

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/usr/local/etc/letsencrypt/live/hoge.example.com/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
中略

# service nginx start
Performing sanity check on nginx configuration:
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
Starting nginx.

うん、上手くいく。(黄字部分)
でも、謎仕様だなぁ。

Up