当サイトの Web 技術

更新

  1. おことわり
  2. 歴史
  3. 全体
  4. HTML
  5. スタイルシート(CSS)
  6. スクリプト(JavaScript)
  7. 画像
  8. 圧縮(Brotli)
  9. CSP

おことわり

  • ここで記載していることはすべて w0s.jp ドメインで公開しているコンテンツに関することである。サブドメインのコンテンツは状況が異なる部分も多い。

  • 当サイトは Web 技術の実験場としての役割も兼ねており、一時的なものも含めて細かい変更は度々実施しているが、本ドキュメントはそれらに追従できるとは限らない。実際の状況と異なる部分もあるかもしれないが、細かい点はご容赦いただきたい。

歴史

2001年2月

前身のサイトを開設。 COOL ONLINE(web.archive.org) の無料会員枠を利用(容量20MB)。「地域コミュニティ」を謳っており、登録の際にまず都市を選択、それによってサブドメインが振り分けられた(e.g. tokyo.cool.ne.jp)。

2001年6月

掲載写真の増大により容量面で厳しくなり、 goo フリーホームページ(web.archive.org) に移転(容量50MB)。 URL は http://users.goo.ne.jp/{userID} だが、 IP アドレスのドメインにリダイレクトされる(ブラウザのアドレスバーには IP アドレスの数字が表示される)というすごい仕様だった。

2002年3月

ネットワーク利用技術研究会(NURS)(www.nurs.or.jp) に移転。レンタルサーバーではなく研究・実験目的のサーバーであり、 telnet が制限なしに使えたので、 root 権限が必要なこと以外は割となんでもできた。

2012年2月

NURS はドメインが全ユーザー共通であり(サブドメインで分ける方式ではなかった)、 Cookie の導入がセキュリティ上の理由で躊躇われることから アイネットディー(inetd)(www.inetd.co.jp) に移転したうえで独自ドメインを取得。¥270/月の格安サーバーにしては cron の制限が緩いのがありがたかった。

2017年1月

常時 TLS の波に乗り、 さくらのVPS(vps.sakura.ad.jp) に移転。 Let's Encrypt(letsencrypt.org) を利用して TLS 対応を行う。

全体

  • HTML, CSS, JavaScript はエディターで手書き & FTP ソフトでアップという昔ながらのやり方。

  • ファイル管理は Git を使っているが、外部サービスのシークレットキーなどが書かれたファイルも含まれているので、今のところ非公開リポジトリにしている。いずれ切り分けて、公開できるものは公開したい。

HTML

HTML ファイルの構成図
├─ html
│  └─ *.html
└─ w0s.jp /* DocumentRoot */
    ├─ *.html
    ├─ *.min.html
    └─ *.min.html.br
  • html ディレクトリ(非公開領域)内の HTML ファイルを自作の Node.js プログラムでビルドし、 DocumentRoot 以下に配置。ビルドの際はページヘッダー、サイドバーなど共通部分を結合した後、以下の3ファイルを出力している。

    • Prettier(prettier.io) でフォーマットしたファイル(e.g. index.html
    • html-minifier(github.com) で最小化したファイル(e.g. index.min.html
    • 最小化ファイルを Brotli 圧縮したファイル(e.g. index.min.html.br
  • ウェブページの URL に拡張子が含まれるのは好ましくないと考えているので、 Apcahe の設定で拡張子なしの URL でアクセスできるようにしており、その場合は *.min.html のファイルが読まれる。

    • Brotli に対応したブラウザ(Accept-Encoding リクエストヘッダーに br が含まれる場合)は、 *.min.html.br が読まれる。圧縮の詳細については後述する。

    • 上記のとおり、最小化されていないファイルも公開領域にアップロードしているので、手動で URL 末尾に .html を付けると、人間が目で見やすいソースコードを見ることができる(隠し機能扱い)。

    • ただし、ブログページなどサーバーサイドで動的にレスポンスを出力しているコンテンツは HTML ファイルが存在しないため、 URL に拡張子を付けても意味がなく、「404 Not Found」になる。

  • 以前は application/xhtml+xml で配信していたが、 Twitter の埋め込みウィジェット(widgets.js)が動かないなどの外部要因により、やむなく text/html に変更している。それに伴い、最小化設定も属性値の引用符を削除するなど XML 構文としては well-formed でないものにしている。

  • 編集用ファイルは引き続き XML 構文で作成している(e.g. <input readonly=""/>)。周辺状況が改善されれば配信ファイルも再び application/xhtml+xml に戻すかもしれない(と思い続けて幾年月)。

ビルド

  • ビルドの際、整形や最小化以外にも下記3つの処理を行っている。

    • (1) ページヘッダー、サイドバーなど共通部分を結合
    • (2) リンクアンカー直後にドメイン情報を付与
    • (3) <time> 要素の datetime 属性を生成
  • 当サイトでは外部サイトへのハイパーリンクのアンカーに対してドメイン名(FQDN)を明記している。 URL から手動でドメイン部分を抜き出すのはミスが起こりうるため、クラス名 htmlbuild-domain を記した <a> 要素の href 属性値を解析し、アンカー直後に自動挿入する。

    • 変換前: <a href="https://example.com/foo" class="htmlbuild-domain c-anchor">Link</a>
    • 変換後: <a href="https://example.com/foo" class="c-anchor">Link</a><b class="c-domain">(example.com)</b></p>
  • <time> 要素の datetime 属性を手動でマークアップするのはミスが起こりうるため、クラス名 htmlbuild-datetime を記した <span> 要素の中身(textContent)を解析し、 <time> 要素に自動変換する。

    • 変換前: <span class="htmlbuild-datetime c-date">2000年1月1日</span>
    • 変換後: <time datetime="2000-01-01" class="c-date">2000年1月1日</time>

    なお、 datetime 属性が不要なケースや、 <time datetime="2019-05-01">令和最初の日</time> のように機械的な解析が困難なケースは <time> 要素を直接マークアップする。

エラーページ

  • Apache がデフォルトで用意しているエラーページは味気ないので、下記に示すレスポンスコードの場合は独自のエラーページを用意している。(※下記リンク先は実際の応答までは再現していないので、いずれも 200 になる。)

  • 403, 404, 410 のエラーページには JavaScript で以下の機能を組み込んでいる。

    • 同一ドメインのリファラーがあった場合(= サイト内から無効なリンクが貼られている)は管理者へ通知する。なお、上記リンクアンカーには referrerpolicy="no-referrer" を設定しており、例外的に通知は行われない。

    • 直近の有効な祖先ディレクトリへのリンクを提示する。(e.g. /foo/bar/baz へのリクエストに対し、 /foo/bar/baz のレスポンスコードが 404 で /foo/bar/ が 403、 /foo/ が 200 の場合、 /foo/ へのリンクアンカーを提示)

    • <portal> 要素(wicg.github.io)に対応した環境では祖先ディレクトリへの誘導をテキストリンクだけでなく、 <portal> を利用した埋め込みも行うことで、遷移前にそのページのイメージが掴めるようにしている。(@see ブログ記事「404ページに <portal> 要素を導入」)

スタイルシート(CSS)

CSS ファイルの構成図
└─ w0s.jp /* DocumentRoot */
    └─ assets
        ├─ _css
        │  └─ *.css
        └─ style
            ├─ *.css
            ├─ *.css.br
            └─ *.css.map
  • w0s.jp/assets/_css ディレクトリ内の CSS ファイルを PostCSS(postcss.org)でビルドし、 w0s.jp/assets/style に配置。ビルドの際、ソースマップ、 Brotli 圧縮ファイルも同時に出力している。

  • PostCSS の導入はブラウザが対応していない機能の先行使用、あるいはファイルをまとめることによるブラウザからの読み込みパフォーマンスの向上が目的であり、標準化の見込みのないプラグインは極力使わず、将来的には変換前のコードをそのままブラウザに読み込ませても問題ないようにすることが目標である。

    現在使っているプラグインは以下の5つ。このうち postcss-apply は仕様策定が破棄されたのでいずれ廃止したい。

    プラグインCSS 仕様W3C プロセス
    postcss-applyCSS @apply RuleUnofficial Proposal Draft
    postcss-custom-mediaCustom Media Queries (Media Queries Level 5)Editor’s Draft
    postcss-import
    postcss-nestingCSS Nesting ModuleEditor’s Draft
    CSSNANO
  • ブラウザから読まれる CSS ファイルは上記のとおり CSSNANO(cssnano.co)を利用した minify 処理を行っているが、 SourceMap ヘッダーでソースマップを出力しているため、ブラウザの開発者ツールを使えば元ファイルにアクセスできる。

印刷用スタイル

  • 用紙やインクを節約するため、背景色を #ffffff にしたり、ヘッダーやフッターを非表示にしたりといった簡易な印刷用スタイルを作成している。

  • 以前は @print を使っていたが、半強制的に画面表示時と異なる状態で印刷されることが混乱をもたらす可能性を考え、代替スタイルシート(html.spec.whatwg.org)機構を使うようにした。 Firefox ではメニューバーの「表示」-「スタイルシート」から切り替え可能である。 Chrome や Vivaldi でも拡張機能が公開されている。

  • 切替機構を持たないブラウザに対しては JavaScript を使って強制適用させるようにしている。(@see ブログ記事「印刷用スタイルを @media print から代替スタイルシートへ変更」)

スクリプト(JavaScript)

JavaScript ファイルの構成図
└─ w0s.jp /* DocumentRoot */
    └─ assets
        ├─ _js
        │  ├─ *.js
        │  ├─ *.ts
        │  ├─ *.mjs.js
        │  └─ *.mjs.ts
        └─ script
            ├─ *.js
            ├─ *.js.br
            ├─ *.js.map
            ├─ *.mjs
            ├─ *.mjs.br
            └─ *.mjs.map
  • ほとんどのスクリプトは module script であり、拡張子を *.mjs.ts で作っている。これは最終的に *.mjs に変換され、 *.js ファイルと区別することができる。

  • w0s.jp/assets/_js ディレクトリ内の TypeScript ファイルを tsc コマンドで変換して同一ディレクトリ内に生成。

    • *.ts*.js
    • *.mjs.ts*.mjs.js

    その生成を検知して terser(terser.org)で最小化したファイルを w0s.jp/assets/script に配置。その際、ソースマップ、 Brotli 圧縮ファイルも同時に出力している。

  • Apache では拡張子と MIME types の紐付けは mime.types ファイルで定義されているが、 JavaScript 関係は .js のみで .mjs は含まれていない。どちらにせよ文字エンコーディングの指定を行う必要があるため、 .conf ファイルで以下の指定をしている。

    AddType "application/javascript; charset=utf-8" .js .mjs

  • CSS と異なり、ファイルの結合は行っていない。

  • ブラウザから読まれる JavaScript ファイルは上記のとおり minify 処理を行っているが、 SourceMap ヘッダーでソースマップを出力しているため、ブラウザの開発者ツールを使えば元ファイルにアクセスできる。

エラー検知

  • 制作したスクリプト機能は Firefox, Chrome 最新版での軽い確認はしているが、きちんとしたテストは行っていない。その代わり、 error イベントを検知して管理者へ通知する機能を組み込んでいる。簡略化したコード例を下記に示す。

    window.addEventListener('error', (ev) => {
      const formData = new FormData();
      formData.append('location', location.toString());
      formData.append('message', ev.message);
      formData.append('filename', ev.filename);
      formData.append('lineno', String(ev.lineno));
      formData.append('colno', String(ev.colno));
    
      fetch(ENDPOINT, {
        method: 'POST',
        body: new URLSearchParams([...formData]),
      });
    });
    
  • これにより、事前のテストでは把握が難しい以下のようなケースも検知することができる。

    • 古いブラウザや検索エンジンのロボットで発生するエラー
    • ブラウザのアドオンが原因のエラー

画像

ビットマップ画像とベクター画像で大きく異なるので分けて説明する。

ビットマップ画像(WebP, JPEG, PNG)

ビットマップ画像ファイルの構成図
├─ thumbimage
│  ├─ *.jpeg
│  └─ *.webp
└─ w0s.jp /* DocumentRoot */
    ├─ *.jpg
    ├─ *.png
    └─ thumbimage.php
  • 画像ファイルは Photoshop などの画像編集ソフト(GUI)を使って DocumentRoot 以下に配置。

  • DocumentRoot 以下に配置する JPEG ファイルの拡張子は Photoshop のデフォルト動作に合わせて .jpg にしている(.jpeg ではない)。

  • 一部のページを除き、サムネイル画像は自作の thumbimage.php による自動生成を行っている。

    • /thumbimage/${path};${type};w${width};mh${max-height};q${quality} 形式の URL を <img src> 等で埋め込むことで、指定したパラメーターに則りサムネイル画像を生成し、 thumbimage ディレクトリ配下に保存する。その際、ファイル名に width, quality の情報を含めることで(e.g. foo@w=360_q=30.webp)、さまざまな横幅、画質のファイルを生成することができる。

    • 既にサムネイル画像が保存されている場合は、改めての生成処理は行わず、保存済みのファイルを読み取る。

    • ユーザーがブラウザのアドレスバーや wget コマンド等で各種パラメーターを変更した URL を直接指定した際、本来不要なファイルが無駄に生成されてしまうことを避けるため、画像生成時に限り Sec-Fetch-Site リクエストヘッダを参照することで、自サイトからの埋め込み / リンクであるかどうかを判定している。

    • 現在、自動生成の画像タイプは WebP と JPEG に対応している。そのため、 <picture> 要素を使って下記のようなマークアップをすれば、手動で用意する画像ファイルは1つだけで WebP や高画素密度ディスプレイに対応した表示を行うことができる。将来的に AVIF など新しい画像フォーマットを追加する際も、 thumbimage.php を改良すれば、マークアップは機械的な置換で済むだろう。

      <a href="/foo.jpg" type="image/jpeg"> <!-- この foo.jpg は Photoshop 等の画像編集ソフトで作成 -->
        <picture>
          <source type="image/webp"
            srcset="/thumbimage/foo.jpg;webp;w360;q60, /thumbimage/foo.jpg;webp;w720;q30 2x"/>
          <img src="/thumbimage/foo.jpg;jpeg;w360;q60"
            srcset="/thumbimage/foo.jpg;jpeg;w720;q30 2x" alt=""/>
        </picture>
      </a>
      
    • 一般論として GET リクエストでファイルを生成するという副作用を及ぼすのは好ましいことではなく、可能なら POST や PUT を使うべきだろう。これについては RFC 7231 の 4.2.1. Safe Methods(tools.ietf.org) では This definition of safe methods does not prevent an implementation from including behavior that is potentially harmful, that is not entirely read-only, or that causes side effects while invoking a safe method.と書かれており、副作用を起こす実装が禁止されているものではないこと、アクセスログのように GET でサーバー内にファイルを追加・更新する機能は一般的に存在すること、そもそも本機能の要件は GET でないと実現できないといった理由からこのようにしている。

ベクター画像(SVG)

SVG ファイルの構成図
├─ svg
│  ├─ _dataurl
│  │  ├─ *.dataurl.txt
│  │  └─ *.svg
│  └─ *.svg
└─ w0s.jp /* DocumentRoot */
    ├─ *.svg
    └─ *.svg.br
  • 簡単なものはエディターで手打ちするが、複雑な画像は Illustrator などの画像編集ソフト(GUI)で作成し、 svg ディレクトリ以下に配置。

  • svg ディレクトリ(非公開領域)内の SVG ファイルを SVGO(github.com) で最適化し、 DocumentRoot 以下に配置。最適化の際、 Brotli 圧縮ファイルも同時に出力している。

  • CSS ファイルなどに data: URLs で埋め込む用途の SVG 画像は svg/_dataurl ディレクトリ内に配置する。ここに置いたファイルは SVGO(github.com) で最適化された後に Base64 でエンコードされ、同一ディレクトリに *.dataurl.txt のテキストファイルが生成される。これらのファイルはページ制作の過程で素材として必要なものに過ぎないので、 DocumentRoot 以下には配置されない。

圧縮(Brotli)

  • 以下のレスポンスは Apache の機能により、リクエスト発生時に Brotli の圧縮を行う。圧縮品質の個別の調整は行わず、 mod_brotli(httpd.apache.org) のデフォルト設定に従う。

    • text/css
    • text/html
    • application/atom+xml
    • application/javascript
    • application/json
    • application/opensearchdescription+xml
    • application/xhtml+xml
    • image/svg+xml
  • 前述のとおり、 HTML, CSS, JavaScript, SVG は事前に静的な Brotli ファイル(*.br)を生成する。これらは圧縮品質を最高にし、極力ファイルサイズが小さくなるようにしている。

  • 他に text/plain (e.g. ads.txt, robots.txt)や application/manifest+json (e.g. manifest.webmanifest)で提供されるリソースも存在するが、いずれもファイルサイズが小さく、また用途も限定されているため、圧縮は行わない。

CSP

  • Fetch ディレクティブ(*-src)は Content-Security-Policy-Report-Only で設定。

    ユーザースタイルシート、ユーザースクリプトによるカスタマイズを妨げないため、あくまで実態調査目的として Report-Only にしている。

  • Trusted Types 関係も現状は Content-Security-Policy-Report-Only で設定。

  • それ以外のディレクティブは Content-Security-Policy で設定。

  • Reporting は Report URI(report-uri.com) を利用。

  • 当サイトでは主に以下の外部サービスを使っているため、これらの指定が多くを占める。

    • Amazon アソシエイトリンク
    • Google AdSense
    • Google アナリティクス
    • ツイート埋め込み
    • YouTube 動画埋め込み

Content-Security-Policy の設定値

ディレクティブ備考
base-uri'none'
form-action'self'
https://www.google.comページヘッダーのサイト内検索
https://api.twitter.comTwitter アカウントによるログイン機能(OAuth)
frame-ancestors'self'この設定により X-Frame-Options を廃止
block-all-mixed-content
report-urihttps://w0sjp.report-uri.com/r/d/csp/enforce
実際の設定値

Content-Security-Policy-Report-Only の設定値

ディレクティブ備考
default-src'self'
child-src'self'
https://www.google.comGoogle マップ埋め込み
https://googleads.g.doubleclick.netGoogle AdSense
https://tpc.googlesyndication.com
https://platform.twitter.comツイート埋め込み
https://www.youtube-nocookie.comYouTube 動画埋め込み
connect-src'self'
https://pagead2.googlesyndication.comGoogle AdSense
https://www.google-analytics.comGoogle アナリティクス
https://fcmregistrations.googleapis.comGoogle Firebase
https://firebaseinstallations.googleapis.com
https://validator.w3.org検証用
img-src'self'
data:ブラウザアドオンやユーザースタイルシート
https://m.media-amazon.comAmazon アソシエイトリンク
https://pagead2.googlesyndication.comGoogle AdSense
https://www.google-analytics.comGoogle アナリティクス
https://pbs.twimg.comツイート埋め込み
https://syndication.twitter.com
https://i1.ytimg.comYouTube 動画のサムネイル画像
script-src'self'
'unsafe-inline'ブラウザアドオン、ユーザースクリプト
https://adservice.google.co.jpGoogle AdSense
https://adservice.google.com
https://partner.googleadservices.com
https://pagead2.googlesyndication.com
https://tpc.googlesyndication.com
https://www.googletagservices.com
https://www.google-analytics.comGoogle アナリティクス
https://www.googletagmanager.com
https://www.gstatic.comGoogle Firebase
https://platform.twitter.comツイート埋め込み
style-src'self'
'unsafe-inline'ブラウザアドオン、ユーザースタイルシート
trusted-typesdefault
goog#htmlGoogle AdSense
'allow-duplicates'
require-trusted-types-for'script'
report-urihttps://w0sjp.report-uri.com/r/d/csp/reportOnly
実際の設定値