おことわり

  • ここで記載していることは特記のない限り 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 の導入がセキュリティ上の理由[1]で躊躇われることからアイネットディー(www.inetd.co.jp) に移転したうえで独自ドメインを取得。月額270円の格安サーバーにしては Cron の制限が緩いのがありがたかった。

2017年1月

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

サーバー構成

サーバー
Apache HTTP Server(httpd.apache.org)
アプリケーション
Node.js(nodejs.org)
フレームワーク
Express(expressjs.com)
プロセス・マネージャー
PM2(pm2.keymetrics.io)
データベース (RDBMS)
SQLite(sqlite.org) , MySQL(www.mysql.com)
ドメイン一覧
ドメイン 用途 備考 ソースコード
w0s.jp 個人サイト GitHub
blog.w0s.jp ブログ GitHub
media.w0s.jp 画像、動画、音声 画像は URL パラメーターによりサイズや画質を指定して動的生成可能 GitHub
report.w0s.jp エラーレポート収集 GitHub
analytics.w0s.jp アクセス解析 Matomo Analytics(matomo.org) を利用
labs.w0s.jp Web 技術の遊び場 HTTP でもアクセス可 GitHub
  • 他に slide.w0s.jp もあるが、ほとんど活用していないのであえてリンクは張らない。

HTML

HTML ファイルの構成図
└packages
	├ html
	│└ *.html
	│└ *.json
	└ .markuplintrc
	└ packages
		└frontend
			└ public /* Static directory */
				├ *.html
				└ *.html.br
  • 静的コンテンツは <main> 要素内を HTML 断片ファイル、タイトルや更新日時などのメタ情報を同名の JSON ファイルとして管理している。例えば当ページ(「当サイトの Web 技術」ページ)のコンテンツは以下の2ファイルとなる。
  • その HTML 断片ファイルと JSON ファイルのデータを自作の Node.js プログラム(GitHub) でビルドし、以下の2ファイルを静的ディレクトリに配置する。
    • Prettier(prettier.io) でフォーマットした HTML ファイル(e.g. index.html
    • vajahath/brotlin(GitHub) で Brotli 圧縮したファイル(e.g. index.html.br
  • ビルドに際しては主に以下の変換を行っている。
  • ビルドの際、Markuplint(markuplint.dev) を使用したチェックを実施している。Markuplint の設定には公式のプリセット(markuplint.dev) が用意されているが、以下に挙げる理由からあえてそれらを使うことはせず、本サイト向けに改めて定義した設定ファイル(GitHub) を使用している。
    • すべてのルールについてその効果を把握したい(公式のプリセットに頼るとその確認が疎かになってしまう)。
    • markuplint:recommended より厳しいルールを適用させたい箇所が複数存在する。
    • 公式のプリセットは HTML Standard や WAI-ARIA などへの仕様適合だけではなく、パフォーマンスやセキュリティを含めたベストプラクティス的な考えも取り入れられている。純粋な構文チェックに留まらないところは Markuplint の良い点であるが、そのすべてに賛同できるわけではない。たとえば markuplint:performance では <img> 要素に decoding=async の指定が必須とされているが、これで得られる効果は限定的であり、必要な箇所に限って指定するべき属性だと考えている(少なくとも当サイトでは明示的な async 指定が必要な画像は皆無である)。
  • HTML ページの MIME タイプは text/html としている。コンテンツのデータをスクレイピングで活用したいユーザーにとっては XML として処理できた方が便利だろうとの考えで以前は application/xhtml+xml で配信していたが、Google 翻訳(URL 指定)が対応していないなどの外部要因によりデメリットの方が大きくなってきたため、やむなく変更した次第。
  • ウェブページの URL に拡張子が含まれるのは好ましくないと考えているので[2]、拡張子なしの URL でアクセスできるようにしている。

3xx リダイレクトページ

URL が変更になった場合は 301 Moved Permanently、フォームの POST 送信後には 303 See Other などいくつかのケースでは HTTP レスポンス 3xx でリダイレクトを設定している。

RFC 7231 の 6.4. Redirection 3xx(datatracker.ietf.org) では、ステータスコード 3xx で Location ヘッダーフィールドが設定されている場合のユーザーエージェントの挙動としてthe user agent MAY automatically redirect its request to the URIと書かれており(あくまで MAY であることに注目)、実際ユーザーの環境によっては自動リダイレクトが行われず、レスポンスボディの内容が画面に表示されることがある[3]

当サイトで使用している Node.js フレームワークの Express では、res.redirect()(expressjs.com) メソッドでリダイレクト設定を行うことができるが、この場合レスポンスボディは <p>Moved Permanently. Redirecting to <a href="${path}">${path}</a></p> のように、DOCTYPE や <title> 要素のない不正な HTML となってしまう。これについては Issue を上げており(GitHub) 、それに対する Pull Request も提出されている(GitHub) が、2023年6月現在マージはされていない。そのためこの機能は使わず、res.send()(expressjs.com) メソッドにて独自の HTML を返すようにカスタマイズしている。

4xx クライアントエラーページ

403 Forbidden および 404 Not Found のクライアントエラーページには JavaScript で以下の機能を組み込んでいる。

  • 同一ドメインのリファラーがあった場合(= サイト内から無効なリンクが張られている)は管理者へ通知する。検知プログラムは GitHub で公開(GitHub) している。
  • 直近の有効な祖先ディレクトリへのリンクを提示する。(e.g. /foo/bar/baz へのリクエストに対し、/foo/bar/baz のレスポンスコードが 404 で /foo/bar/ が 403、/foo/ が 200 の場合、/foo/ へのリンクアンカーを提示)
  • <portal> 要素(wicg.github.io) に対応した環境では祖先ディレクトリへの誘導をテキストリンクだけでなく、<portal> 要素を利用した埋め込みも行うことで、遷移前にそのページのイメージが掴めるようにしている。

スタイルシート(CSS)

CSS ファイルの構成図
└packages
	└frontend
		└ style
		│└ *.css
		└ .postcssrc
		└ .stylelintrc.json
		└ public /* Static directory */
			└ assets
				└ style
					├ *.css
					├ *.css.br
					└ *.css.map
  • packages/frontend/style ディレクトリ内の CSS ファイルを PostCSS(postcss.org) でビルドし、packages/frontend/public/assets/style に配置。ビルドの際、Brotli 圧縮ファイルも同時に出力している。
  • PostCSS の導入はブラウザが対応していない機能の先行使用、あるいはファイルをまとめることによるパフォーマンス向上が目的であり、標準化の見込みのないプラグインは使っていない。将来的には変換前のコードをそのままブラウザに読み込ませても問題ないようにすることが目標である。
    使用しているプラグイン
    プラグイン CSS 仕様 W3C プロセス(2023年6月現在)
    postcss-media-minmax(GitHub) Range Context (Media Queries Level 4)(W3C) Candidate Recommendation Draft
    postcss-custom-media(GitHub) Custom Media Queries (Media Queries Level 5)(W3C) Working Draft
    postcss-nesting(GitHub) CSS Nesting Module(W3C) Working Draft
    postcss-import(GitHub)
    CSSNANO(cssnano.co)
  • ブラウザから読まれる CSS ファイルは上記のとおり CSSNANO(cssnano.co) を適用しているが、minify 処理が目的ではなくコメントの除去など最小限の最適化のみを行っている。以前は minify 処理も行っていたが、これを止めた理由はブログ記事「CSS ファイルの最小化を止めた」(2022年6月)にまとめている。

印刷用スタイル

  • ディスプレイ表示の眩しさを低減するため、ページの背景色は白色ではなく、若干黄味がかった色を設定している。印刷時にはそのような配慮は不要なので @print を使って完全な白(#ffffff)に設定している。

リーダーモード

  • 昨今のブラウザはリーダーモードを備えたものも多いが、制作者の意図どおりに表示されるとは限らないため、それとは別個に制作者スタイルシートにてリーダーモードを実装している。
  • ヘッダーやフッターを非表示にする簡易なスタイルシートを用意し、代替スタイルシート(WHATWG) で任意に適用可能な状態としている。PC 版 Firefox ではメニューバーの「表示」→「スタイルシート」から切り替え可能である。Chrome など他ブラウザでも拡張機能が公開されている。

スクリプト(JavaScript)

JavaScript ファイルの構成図
└packages
	└frontend
		└ script
		│└ *.ts
		└ .eslintrc.json
		└ rollup.config.js
		└ public /* Static directory */
			└ assets
				└ script
					├ *.mjs /* 一部は拡張子が `.js` */
					├ *.mjs.br
					└ *.mjs.map
  • 基本的な考え方として JavaScript は補助的な使用とし、スクリプトが動かない環境でもコンテンツの閲覧、操作に支障がないようにしている。昨今ではモバイルブラウザも含めてほとんどの閲覧環境が JavaScript に対応しているが、一方でユーザーの環境によって(有効設定であっても)スクリプトが動かないケースは往々にして存在する[4]。当サイトのようなテキストと画像による表現が中心の Web サイトは現代においてもスクリプト無効で閲覧できるようにするのはメリットが大きいと考えている。
  • packages/frontend/script ディレクトリ内の TypeScript ファイルを Rollup(www.rollupjs.org) でビルドし、packages/frontend/public/assets/script に配置。ビルドの際、ソースマップ、Brotli 圧縮ファイルも同時に出力している。
  • 外部サービスに関係した機能など一部を除き ES modules で作成しており、本来であれば機能毎に別れたファイルを import / export を使ってそのままブラウザに読ませることができるが、一時期そのようにしてみたところ HTTP/2 通信下であっても画面描画への影響が体感できるほど大きかったため、現在ではビルド時にファイル結合を行うようにしている。

エラー検知

  • 制作したスクリプト機能は普段使いのブラウザでの軽い動作確認はしているが、きちんとしたテストは行っていない。というより、サーバーサイドプログラムとは異なりブラウザには様々な設定項目があり、Bot 等も含めたあらゆる環境を想定したテストを行うことなど不可能だと考えている。もとより前述のとおりスクリプトが動かなくてもコンテンツの閲覧には支障がないため、テストはそこそこで良いと割り切り、せめて発生してしまったエラーは把握できるよう、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]),
    	});
    });
    実際の検知プログラムは GitHub で公開(GitHub) している。
  • これにより、事前確認が難しい以下のようなケースで発生したエラーも検知することができる。
    • 古いブラウザ
    • ブラウザの設定やアドオンに起因するもの
    • 検索エンジンのロボットなどブラウザ以外の環境

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

  • 昨今、高解像度ディスプレイの普及や WebP, AVIF など新フォーマットの登場により、環境に合わせて画像表示を最適化しようとすれば多くの出し分けが必要になってきている。当サイトでは基本的に以下6種類の画像を <a> 要素と <picture> 要素を使って提示している。
    • 大画像(JPEG ないし PNG)
    • サムネイル: AVIF
    • サムネイル: AVIF, @2x
    • サムネイル: WebP
    • サムネイル: WebP, @2x
    • サムネイル: JPEG (※ WebP 未対応環境は減少しているため、JPEG の @2x 画像は用意しない)
    マークアップのイメージを下記に記す。なお、画像の説明は極力本文か <figcaption> 要素で行う方針のため、<img> 要素の alt 属性値に長々とした文章を書くことはあまりなく、大抵は一律の短い文言である。
    <a href="image.jpg" type="image/jpeg">
    	<picture>
    		<source type="image/avif" srcset="thumb.avif, thumb@2x.avif 2x"/>
    		<source type="image/webp" srcset="thumb.webp, thumb@2x.webp 2x"/>
    		<img src="thumb.jpeg" alt="画像"/>
    	</picture>
    </a>
  • 大画像(上記コード例でいえば image.jpg)は Photoshop などの画像編集ソフトで生成する。大画像といっても、デジタルカメラの画像あるいはフィルム写真をスキャンした元素材そのままではなく Web 用に縮小をしているが、一般的な PC 用ディスプレイで全画面表示しても問題ない大きさとしている。なお、JPEG ファイルの拡張子は Photoshop のデフォルト動作に合わせて .jpg としている(.jpeg ではない)。

サムネイル生成

サムネイル画像は大画像ファイルをソースとして自動生成しているが、以下に示す理由から事前に静的ファイルを生成する方式は運用が面倒になることが予想された。

  • 使用するページによって表示する大きさが異なる
  • ページのリニューアルや CSS の調整で後から大きさが変わることもあり得る

そのためパフォーマンス的に不利な面はあるが、/thumbimage/path/to/image.jpg?type=webp;w=360;h=240;quality=30 のように URL パラメーターでサイズや画質を指定して動的生成する方式とした。詳細は画像を配置している media.w0s.jp にドキュメントを置いているが、ドキュメントで触れていないポイントについて下記に述べる。

  • URL パラメーターの区切り文字は一般的な & だけでなく ; にも対応しており、後者を利用する。これは HTML4 時代に B.2.2 Ampersands in URI attribute values(W3C) で推奨されていたテクニックである。設定は Express の app.set('query parser', value)(expressjs.com) にカスタムクエリ文字列解析関数を指定することで実現している。
  • 画像生成は sharp(sharp.pixelplumbing.com) を利用しているが、AVIF への変換は JPEG や WebP と比較して遅く、多数の画像を埋め込んだページでは画像がすべて表示されるまでに多大な時間が掛かったり、タイムアウトしたりするケースが多発してしまった。 そのため AVIF の初回リクエスト(サムネイルファイルが生成されていない状態でのリクエスト)に限っては代替として WebP を生成して返し、別途バッチ処理で AVIF を生成するようにしている。この場合、ブラウザ視点では「<source type="image/avif"> の画像をリクエストしたらレスポンスで image/webp が返ってきた」という状態になる。一見違和感があるものの HTML 仕様における <source> 要素の定義(WHATWG) では The type attribute gives the type of the images in the source set, to allow the user agent to skip to the next source element if it does not support the given type. とされているため問題なく、どのブラウザも正常に動作する。
  • 一般論として 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 リクエストでないと実現できない
    といった理由から GET リクエストでファイル生成を行うようにしている。

ベクター画像(SVG)

SVG ファイルの構成図
└packages
	└frontend
		├ image
		│└ *.svg
		├ svgo.config.cjs
		└ public /* Static directory */
			├ *.svg
			└ *.svg.br
  • 簡単なものはエディターで手打ちするが、複雑な画像は Illustrator などの画像編集ソフトで作成し、 packages/frontend/image ディレクトリ(非公開領域)に配置する。
  • packages/frontend/image ディレクトリ内の SVG ファイルを SVGO(GitHub) で最適化し、静的ディレクトリに配置。最適化の際、Brotli 圧縮ファイルも同時に出力している。

ファビコン(favicon.ico)

  • ファビコンは SVG 形式で提供している。
  • Internet Explorer 5 の実装を発端とする歴史的な経緯(web.archive.org) により、/favicon.ico のファイルパスにすることでブラウザが自動的に読み取るようになっている。あくまで URL が /favicon.ico であれば良く、ファイルの実体が ICO 形式である必要はないため、SVG 形式のファビコンファイルを /favicon.ico で提供することで <link rel="icon"> の記述を省略できる。このため以下の特別な処理を行っている。
    • ファビコン用 SVG ファイルの生成は他の SVG 画像と同様に、SVGO(GitHub) による最適化や Brotli 圧縮ファイルの同時生成を行っているが、ビルド処理の際に拡張子を favicon.svgfavicon.ico に変更する。
    • 通常、Content-Type レスポンスヘッダー(MIME タイプ)はファイル拡張子によって決定されるが、/favicon.ico に限ってはファイルパスで判断し、image/svg+xml を返す。
    詳細はブログ記事「SVG ファビコンのファイル名を favicon.ico にして <link rel="icon"> を省略する」(2021年10月)にまとめている。

圧縮

  • 前述のとおり、事前に静的ファイルをビルドする HTML, CSS, JavaScript, SVG はビルド時に Brotli ファイル(*.br)も生成している。これらは圧縮品質を最高にし、極力ファイルサイズが小さくなるようにしている。
  • それ以外の静的リソース、および動的ページ(リクエストがある毎にサーバーサイドで HTML を生成するページ)は Express のミドルウェア expressjs/compression(GitHub) により、レスポンス返却時に gzip の圧縮を行っている。ただし、しきい値の設定を行っており、robots.txt などファイルサイズが僅かなリソースは圧縮されない。

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) を利用。
  • 当サイトでは主に以下の外部サービスを使っているため、これらの指定が多くを占める。
    • Google AdSense
    • YouTube 動画埋め込み
    • Amazon アソシエイトリンク

脚注

  • 1.

    高木浩光@自宅の日記 - 共用SSLサーバの危険性が理解されていない(takagi-hiromitsu.jp) で解説されているように、フレームを利用して他サイトから読み出すことが可能なため。 ↩ 戻る

  • 2.

    Tim Berners-Lee による Cool URIs don't change(W3C) でも URL にファイル名拡張子を含めることは問題を引き起こすと言われている。 ↩ 戻る

  • 3.

    Android Firefox ではアプリ連携された URL に 3xx でリダイレクトすると、当該アプリが自動で起動するが、ブラウザでは 3xx のレスポンスボディが表示された状態になる。(2022年2月現在、Android Firefox 96 にて確認) ↩ 戻る

  • 4.

    具体的な事例をすべて挙げるとキリがないが、一例として JavaScript の URL に `ads` が含まれていると広告ブロッカーを適用した環境ではそのスクリプトがロードされず、結果的にスクリプト無効設定と同じ状況になるかもしれない。ここで考えるべきは「そのようなツールを使って不利益を被るのは自己責任」か、それとも「広告ブロッカーを含めブラウザをカスタマイズするのはユーザーの権利であり配慮が必要」かということだが、私は後者の考え方を持っている。一方で「URL に `ads` を含める」ことは JavaScript や HTTP はもとより Web の仕組み上なんら問題なく、特定のツールに対する配慮で URL を変更するのは避けたい。そもそもが小手先の対策をせずとも、スクリプト無効でコンテンツが閲覧できる状態さえ保っていれば、ユーザーのブラウザ上でスクリプトが想定どおり動くかどうかなど気にする必要すらないのであり、制作上のコストパフォーマンスの観点からもこれがもっとも簡単なのである。 ↩ 戻る