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()で回して処理する方が安全かもしれない。

関連記事: