噂の WebP 導入時に後方互換性を持たせる方法いろいろ

# TL;DR

コンフィグの前にフロントエンドの工夫でどうにかできないか少し考えた方が好さそうです。

## 想定読者

Web 界隈に興味のある人。

マークアップ(HTML5)から基盤(Apache,Nginx...)レベルまでのどこかを触ったことある方むけです。

## あらすじ

次世代 Web 画像フォーマットとして台頭した JPEG2000 と WebP。
どちらも従来の JPEG や PNG よりも圧縮効率が良く、 Web 界隈でにわかに注目されていました。

すでに次世代画像形式に対応した端末は結構な普及率に達しています。
そこで、Web 界隈では「古い端末には従来の画像、新しい端末には次世代の画像をそれぞれ見せる」ことで**互換性と次世代UXの両立**を図ろうという動きが少なからずありました。

今回は、そのアプローチを2つの視点で集めてみました。

2020年現在 WebP が優勢のようなのでこれ以降は WebP のみ採用する前提で話を進めますが、少し工夫すれば JPEG2000 や両方に対応することもできるはずです。

## 前提

どこかに置いてある従来形式の画像はそのままに、**形式と拡張子を webp に変えただけの画像を機械的に生成**する場合を想定します。

-   foo/
    -   images/
        -   foo.*png* ← 元の画像
        -   foo.*webp* ← 次世代形式
        -   foo.*png.webp* ← もしくはこうなる

# 既知の対策

## HTML5 の `<picture>` タグで分岐

参考: [pictureタグを使ってWebPをレスポンシブ対応させる](https://qiita.com/a_Iwahashi/items/2f8809eaec0a9545dda7)
いきなりですが、**多分これが2021年春現在の最適解です。**

というのも、これから紹介するアプローチはいずれも結構センシティブな箇所を触ります。
ちょっとタイポしたら大惨事になるので、その点では `<picture>` の方がいくらか安全・安心です。
さらに言うと、**閲覧者が従来形式の画像を手軽に落としたい**という場合もあるようなので、一方的なリダイレクトより親切かもしれません。~~(コピーガードしたければ話は別です。)~~

もっとも、この方法は何らかのフレームワークか静的サイトジェネレーターの類を使ってないとすこぶる面倒くさくなります。
あるいは、 `<picture>` タグはその性質上 `<img>` 要素にしか効きません。どういうことかというと、**背景など Web パーツの出し分けには不向き[^101]**ってことですね。
[^101]: CSS は CSS で何かフォールバックする方法があるような気がしたのですが、すぐには見つかりませんでした。

そのような時は下記のコンフィグによるアプローチが有効でしょう。

## 基盤をコンフィグして適宜リダイレクト

Apache なり Nginx なりのコンフィグを工夫し、次世代画像に対応しているユーザーからのリクエストには次世代画像で応答するという方法です。

これの利点は、フロントエンドの工数がほぼないことです。欠点は先ほど述べた通りで、そもそも**着手する箇所が結構センシティブで影響範囲が広い**ことです。
具体的には、パスが .png, .jpg, .jpe, .jpeg のいずれかで終わるような URL でのリクエスト全部が影響します。

それでもこの方法を採るのであれば、以下に基盤別の参考記事とサンプルを紹介しておきます。

以下は URL の拡張子部分を .webp に変更するスクリプトの例です。
本来の拡張子にさらに .webp を追加した方がベターだとする流派[^102]もあるのでこちらも参照: https://qiita.com/miyanaga/items/6570c3c9ae8e15dbb57c

[^102]: だったらいっそ拡張子変更派と追記派の両方に対応すればいいじゃないかという方はサンプル設定を適宜アレンジしてください。

### 共通仕様

- クエリ文字列で `raw=on` が指定されているときはこの機能を無効にする
- 有効時は `Vary` ヘッダーに `Accept` を追加し、 CDN キャッシュを利かせる
- WebP 画像への書き換え時には、元のファイルの拡張子を変更したパスで検索する

### Apache

```conf:httpd.conf
<IfModule rewrite_module>
    # まあエンジンを有効化するよね
    RewriteEngine on
    # .htaccess で使うときのセキュリティ施策
    Options FollowSymLinks

    <IfModule mime_module>
        # WebP のMIMEタイプを明示
        AddType image/webp *.webp
    </IfModule>

    # PNG, JPEG 画像へのリクエストに対して WebP への書き換えを有効化
    RewriteRule \.(?:png|jp(?:g|eg?))$ - [E=_webp_available,C,NC]
    # ただしクエリ文字列で固定が要求されてたら無視 (以降の Rewrite もぜんぶ無効)
    RewriteCond %{QUERY_STRING} (?:^|&)raw=on(?:$|&)
    RewriteRule ^ - [E=!_webp_available,L]
    <IfModule headers_module>
        # CDN 対応: Accept ヘッダーの値ごとにキャッシュ内容を分けさせる
        Header append Vary Accept env=_webp_available
    </IfModule>

    # ここから WebP リダイレクトの設定
    # on VirtualHost context
    # 相手が WebP に対応しているか確認
    RewriteCond %{HTTP_ACCEPT} (?:^|,)\s*image/webp\s*(?:,|$)
    # 代替の WebP が置いてあるか確認 (これは拡張子を変更するパターン)
    RewriteCond %{DOCUMENT_ROOT}$1.webp -f
    # 書き換え
    RewriteRule ^(/.*)\.(?:png|jp(?:g|eg?))$ $1.webp [NC]

    # on Directory context (.htaccess)
    # 相手が WebP に対応しているか確認
    RewriteCond %{HTTP_ACCEPT} (?:^|,)\s*image/webp\s*(?:,|$)
    # 代替の WebP が置いてあるか確認 (これは拡張子を変更するパターン)
    RewriteCond %{REQUEST_FILENAME} ^(.+)\.\w+$
    RewriteCond %1.webp -f
    # 書き換え
    RewriteRule ^(?!/)(.*)\.(?:png|jp(?:g|eg?))$ $1.webp [NC]
</IfModule>
```

コンフィグの VirtualHost 内に入れる場合と Directory 内 (または .htaccess) に綴る場合で必要な RewriteCond が変わってきます。
最後の段落をよく読んで、必要な方を残してください。

### Nginx

参考: https://qiita.com/miyanaga/items/94447efae0bf767b9f2b

```nginx:nginx.conf
# このファイルは http コンテキストで包含されます #

# 相手が WebP に対応しているか確認
map $http_accept $webp_uri_when_available {
    default "";
    "~(?:^|,)\s*image/webp\s*(?:,|$)" $uri;
}
# ただしクエリ文字列で固定が要求されてたら無視
map $query_string $webp_uri_when_priored {
    default $webp_uri_when_available;
    "~(?:^|&)raw=on(?:$|&)" "";
}
# 上記を踏まえて WebP 優先リクエストなら代替 URI を使う
# (これは拡張子を変更するパターン)
map $webp_uri_when_priored $webp_uri {
    # 優先時は変数に置換前 URI が入るが、
    # そうでないときは空文字列が入るため必ず default が選択される
    default $uri;
    "~^(.*)\.(?:[^/.]+)$" $1.webp;
}
# 後述の Vary の値も同様に決定
map $webp_uri_when_priored $webp_vary {
    default Accept;
    "" "";
}

# 以下 location 以外はご定義のサーバーに合わせてください
server {
    listen 8080;
    charset utf-8;
    root   /usr/share/nginx/html;
    index  index.html index.htm;

    # 画像リクエストなら必要に応じて WebP を探す
    location ~* \.(?:png|jp(?:g|eg?))$ {
        # CDN 対応: Accept ヘッダーの値ごとにキャッシュ内容を分けさせる
        add_header Vary $webp_vary;
        # 上記ロジックで選んだファイルを探し、なければ本来の URI に基づいて応答
        try_files $webp_uri $uri =404;
    }
}
```

※必要に応じて調節してください。

map の分岐を連鎖させることで if の使用を回避しました。
いやなに、 [`location` コンテキスト内における `if` は邪悪](https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/)と言われているらしいので…。

連鎖させる際に空文字列を偽値のように扱っているのがポイントです。
いや読みづらすぎて運用できねえよって思うかもしれませんが、つまりそういうことです。

### IIS

参考:
-   Rewrite の使い方: https://www.atmarkit.co.jp/ait/articles/1407/23/news103.html

IIS民がいくらいるか知りませんが一応(

これをうまく設定すると、以下のような内容の web.config ファイルが生まれます。

~~あるいはこれを images/ フォルダーの直下にでも置いてください。~~

```xml:web.config
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
            <!-- WebP のMIMEタイプを明示 -->
            <mimeMap fileExtension=".webp" mimeType="image/webp" />
        </staticContent>
        <rewrite>
            <rules>
                <!-- PNG, JPEG 画像へのリクエストに対して WebP への書き換えを有効化 -->
                <rule name="JPEG, PNG to WebP" stopProcessing="true">
                    <match url="^(.*)\.(?:png|jp(?:g|eg?))$" />
                    <conditions trackAllCaptures="true">
                        <!-- 相手が WebP に対応しているか確認 -->
                        <add input="{HTTP_ACCEPT}" pattern="(?:(?:^|,)\s*)image/webp(?=\s*(?:,|$))" />
                        <!-- ただしクエリ文字列で固定が要求されてたら無視 -->
                        <add input="{QUERY_STRING}" pattern="(?:^|&amp;)raw=on(?:$|&amp;)" ignoreCase="false" negate="true" />
                        <!-- 代替の WebP が置いてあるか確認 (これは拡張子を変更するパターン) -->
                        <add input="{PATH_TRANSLATED}" pattern="^(.*)\.\w+$" />
                        <add input="{C:1}.webp" matchType="IsFile" />
                    </conditions>
                    <!-- 書き換え -->
                    <action type="Rewrite" url="{R:1}.webp" />
                </rule>
            </rules>
            <!-- CDN 対応: Accept ヘッダーの値ごとにキャッシュ内容を分けさせる -->
            <outboundRules>
                <rule name="Append Vary value for probably WebP" preCondition="The response may be a WebP image">
                    <match serverVariable="RESPONSE_VARY" pattern="^(?!\*$).+$" />
                    <action type="Rewrite" value="{R:0}, Accept" />
                </rule>
                <rule name="Add Vary header for probably WebP" preCondition="The response may be a WebP image">
                    <match serverVariable="RESPONSE_VARY" pattern="." negate="true" />
                    <action type="Rewrite" value="Accept" />
                </rule>
                <preConditions>
                    <preCondition name="The response may be a WebP image">
                        <add input="{UNENCODED_URL}" pattern="\.(?:png|jp(?:g|eg?))(?=\?|$)" />
                        <add input="{QUERY_STRING}" pattern="(?:^|&amp;)raw=on(?:$|&amp;)" ignoreCase="false" negate="true" />
                    </preCondition>
                </preConditions>
            </outboundRules>
        </rewrite>
    </system.webServer>
</configuration>
```

# ライセンス

The preceding codes are licensed under CC0.

このページに記載されているサンプル設定は CC0 (ぱぶりっくどめいん) とするので自由に持ってって構いませんが、参考記事に載ってる設定はこの限りではありません。

あと、このページの URL をコードコメントに残しておくと親切です。