というわけで、このブログのつくりについて

前回予告していた、どういう風に実装したとか、その意図とかとか。
投稿日時:
タグ: 雑記

今回は、前回で予告した通り、このブログをどういう風に作ったとか、その作り方の意図とかを書いていこうと思う。

ミドルウェアにはEleventy

とりあえず、採用したエンジン?ミドルウェア?から。これは前回のところでもちょっと言及したとおり、静的サイトジェネレーターであるEleventyを使っている。ちなみに前回の冒頭で「着想から長い時を経て」と書いていたとおり、構築には(実働ではなく足かけで)非常に長い時間がかかっており、書き始めた当時(2021年11月)は1.0にも満たないバージョンだった。現在は3.0に載っかっている。

Twitterで、ただのペラをEleventyに載せ替えた話をしていたが、これは当時Eleventyでブログを組み始めていたので、そこで学んだものを他のサイトにも生かして、テンプレートエンジンを噛ませることでhttps://mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw.xyz/を多言語対応させてみるという、いわば副産物、あるいは習作だったのである。その後順次、https://about.i-otsuki.com/https://i-otsuki.com/も載せ替えを行っている。

「静的」へのこだわり

話が脱線してしまった。Eleventyを使い始めたのがそれくらいの時期だったことは分かった。じゃあなんで当時正式版すら出ていないEleventyを採用したのか、という話に進めよう。

大前提として静的サイトジェネレーターを採用したのは、前回言及したように静的アセットとしてアーティファクトを得るだめだが、その中でもEleventyを採用したのは、生成されるサイト自体を完全に静的にできるからだ。たとえば当時だとGatsbyが台頭していたのだが、Gatsbyの出力は基本的にReactアプリ形式で、閲覧者はJavaScriptを有効にすることが求められた。

大槻さんは、スクリプトやスタイルに依存するWebページを作ることをかなり嫌っている。とくにサードパーティーのスクリプトを読み込むものはもってのほかだ。それはアクセシビリティの点からでもある(このサイトを、JavaScript無効で読み込み直してみて欲しい、何も変わらず使えるはずだ。なんならCSSをオフにしても充分読めるだろう)し、そもそもただ記事を読みたいだけなのに無関係なスクリプトに計算機資源を喰われることを嫌っているからでもあるし、単に追跡が気持ち悪いからでもある。

追跡やだよね

いや、個人の興味に合わせた提案をしてくれること自体は歓迎だけど、あんまり精度良くないし、追跡ってやっぱ印象悪いし。

ってなわけで、このブログにはGoogle AnalyticsやGoogle Tag Managerなんかも一切組み込んでいない。とはいえ収益化だけはしたいのでGoogleの広告タグは入れてある。 ただし、不正検出用も含め一切のCookie類を許可しない、完全コンテキストベースの制限付き広告に限定する設定になっている。実際にはたぶんそんな狭い設定で出稿されている広告は存在しないと思うので、皆さんが広告を目にすることはないだろう。万が一広告が出現したら超レアなので写真撮るなりして報告してほしい。

で、実装どうなってんの?

話が逸れた。Eleventyを採用しているのは分かった、その上はどうなってんの?という話をしよう。といっても、かなり素のEleventyをそのまま使っていて、ほぼそのまま各ページをそのままのパスに*.mdとして配置しているだけだが。

わざわざ追加した処理としては、プリプロセッサで記事のタグを正規化している。具体的には、URLやファイルシステムのパス名に出現すると都合が悪い文字を、全角文字に置き換えている。

あとは、11tyのコレクションとページネーションをそのまま組み合わせると、ネストされたページングができないので、タグ一覧ページやタグのついた記事の一覧なんかは、あらかじめページ分けをした配列を、カスタムコレクションとして定義して、テンプレート1枚で展開している。

配列のイメージはこんな感じで、ページ内で列挙する分を1ページ分ずつに小分けしたものを詰め込んでいる。[Object]となっているものは11tyのページだ。

[
  {
    title: 'タグ「○○」がついた記事',
    permalink: '/tags/○○/',
    totalItemCount: 10,
    currentPage: 1,
    iterator: 'postiterator.njk',
    itemsToIterate: [ [Object], [Object], [Object], [Object], [Object] ],
    next: { url: '/tags/○○/page/2/', label: 'もっと古いの' }
  },
  {
    title: 'タグ「○○」がついた記事',
    permalink: '/tags/○○/page/2/',
    totalItemCount: 10,
    currentPage: 2,
    previous: { url: '/tags/○○/', label: 'もっと新しいの' },
    iterator: 'postiterator.njk',
    itemsToIterate: [ [Object], [Object], [Object], [Object], [Object] ]
  },
  {
    title: 'すべてのタグ',
    permalink: '/tags/',
    totalItemCount: 10,
    currentPage: 1,
    iterator: 'tagiterator.njk',
    itemsToIterate: [
      [Object], [Object], [Object],
      [Object], [Object]
    ],
    next: { url: '/tags/page/2/', label: '次のページ' },
    totalPages: 2
  },
  {
    title: 'すべてのタグ',
    permalink: '/tags/page/2/',
    totalItemCount: 10,
    currentPage: 2,
    previous: { url: '/tags/', label: '前のページ' },
    iterator: 'tagiterator.njk',
    itemsToIterate: [
      [Object], [Object], [Object],
      [Object], [Object]
    ],
    totalPages: 2
  }
]

1つのコレクションで違う種類のコンテンツも全部混載しているので、テンプレートの方はこんな感じで、各ページ用に指定されたテンプレートを使って、ページ内のコンテンツを列挙するようになっている。

---
permalink: /{{ generatedPage.permalink }}
eleventyComputed:
    title: "{{ generatedPage.title }}"
pagination:
    addAllPagesToCollections: true
    data: collections.__internal_nestedPaging
    size: 1
    alias: generatedPage
---
{%- extends "base.njk" %}
{%- block mainBlockTopNavigation %}
    {%- if generatedPage.previous -%}<nav class="Pager"><a href="{{ generatedPage.previous.url }}">{{ generatedPage.previous.label }}</a></nav>{%- endif -%}
{%- endblock %}
{%- block mainBlockContentArea %}
{%- set itemsToIterate = generatedPage.itemsToIterate %}
{%- include generatedPage.iterator %}
{%- endblock %}
{%- block mainBlockBottomNavigation %}
    {%- if generatedPage.next -%}<nav class="Pager"><a href="{{ generatedPage.next.url }}">{{ generatedPage.next.label }}</a></nav>{%- endif -%}
{%- endblock %}

んでご覧の通り、コードブロックのシンタックスハイライトすら入れてないという簡素っぷり……。

あとはなんかプレビュー内の#hogeみたいなリンクをちゃんとページ内の該当箇所へのリンクになるようにするフィルタとか、Atomフィード用に全部のリンクをhttps://~に書き換えるフィルタは用意したけど、まぁ説明するほどのことはしていない。

最後にポストプロセッサを通して、外部サイトへのリンクにtarget="_blank"を付けたり、ページ内で参照されている画像をサムネイルに差し替えたり、minifyをかけたりしている。

フィルタやポストプロセッサではDOMを弄くるのでJSDOMを使っているが、フルスペックのDOMを作ると重たいのでフラグメントだけ作って処理している。

デプロイ先はCloudflare Pages

前回言及していた各種のサイトは、もともとNetlifyでホストしていた。以前に一度Cloudflare Pagesへ移行しようとしたこともあったが、一部の設定がリポジトリ内で完結できないなどでめんどくさがってやめていた。 しかし、今回のブログを構築するにあたっては「そろそろ機能充実しているのでは?」ということで最初からCloudflare Pagesでの構築を試みた。結果として、リダイレクトはバルクリダイレクト側に寄せる必要があったものの、基本的には容易に構築ができて、プレビュー用ブランチのデプロイへのアクセスを自宅からのみに制限することもできた。

このブログの公開に先立って、既存のサイトを順次Cloudflareへ移行させたのは、そこで得たノウハウを展開したものだ。ただ、リダイレクト回りはやはり少しばかり手がかかり、https://mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw.xyz/に実装しているAccept-Languageベースのリダイレクトは、Netlifyではとても簡便に書けた一方、Cloudflare Pages内では処理できずRulesのほうでガチャガチャしたリダイレクトルールを記述する必要があった。まぁそのルールの書き方の件はまたいつか別の記事で扱おう。

見た目の話

見た目はもう、あんま作り込むのも面倒なので、比較的シンプルなDOMになっているはず。CSSも大した量書いてない。一応var()prefers-color-schemeを使ってダークモード対応くらいはしてあるけど。 大槻さんは「グレースケール+赤アクセント」が好きなので、何も考えないと作ったサイト全部その配色になりがちなんだが、今回は青基調にしている。この配色とレイアウトは、「大槻さんの備忘録」がまだはてなダイアリーでホストされていたころに設定していたhatena2を少しばかり意識している。ヘッダー部分以外の彩度は0にしちゃったし、ほぼ文字しか置いてないのであんまり雰囲気似てないけど。

コンテンツ移行の話

はてなブログのエクスポート機能で出したMovableType形式データと、はてなフォトライフのフィードと、noteのエクスポート機能で出したWordPress形式データと、を力業で適当に処理した。 うん、ここまでだいぶ長く書いてきたので、疲れてきてだんだん説明が雑になってきたな。というか作成したスクリプト自体は捨ててしまったので、過去のコミット調べないとどういう実装してたか思い出せない。

MT形式は行ごとに読んで区切りを認識してデータ詰めて、って感じだったはず。フォトライフとnoteはJSDOMのXMLパーサで中身取り出して処理した。フォトライフにあった画像はフォトライフで設定してあったコメントをちゃんとEXIFに埋めたりもしている。

はてなもnoteも余計なタグが多かったので割とそれをきれいに消すのに手間がかかった。excerpt部分の切り出しは、最初自動化を試みたけど結局全部人力で切り直した。

ちなみに、はてなブログははてなダイアリーから移行したときに消えたのか、下書き記事が残っていなかったのだが、noteの方には下書き記事があったので、それらも一応下書き状態で移行してある(下書きのステータスになってると公開はされないのであくまでこっちの話ではあるが)。 Twitterもアーカイブの取得はしてあるけど、ちょっと分量多すぎて手をつける気力がない。果たしてそれらのコンテンツに手をつけられる日は来るのだろうか。

移行元からの誘導

移行元の各サイトでは、万が一にも僕の新しい投稿を待ち続けている人がいるかもしれないので、移転の告知記事を掲載した。

また、はてなブログの方には、この新しいブログへの誘導を挿入した。<head>内に任意のコードを置ける機能を使ってスクリプトを埋め込んである。 スクリプトでは、前述のコンテンツ移行スクリプトで移行処理をしながら新旧のURL対応マップを出力させたものを使って、アクセスされた記事に対応する移行先記事のURLにつないでくれるようになっている。 8月からは、一定秒数での強制ジャンプ処理も追加した。なるべく負担なく新しい方へ行って欲しいからだ。

noteにはそういう高度なことができる機能はないので、移行した全投稿に、移行先のURLを掲載して回った。

下書きの管理

Eleventyなので、基本的に記事を書くときには出したいURLに対応するところに*.mdを置けばそれで終わりなのだが、やはりブログとして使うとなると、下書き中の記事というものができてくる。 下書き中の記事は、公開される時点まで公開日時が確定しないので、はてダに寄せて/posts/公開年/公開月/ほにゃららというURLを採用したこのブログでは地味に扱いがめんどくさい。

そんなわけで、新規作成時には適宜テンプレを埋めて一旦仮のパスに下書きとして保存してくれるスクリプトを用意し、編集時にはファイルをステータス別でブラウズしてエディタに読み込ませてくれるスクリプトを用意し、公開時には下書きから公開状態に切り替え公開日時を設定してそれに基づくパスへ移動させてくれるスクリプトを用意し、更新時には更新日時だけを追加設定してくれるスクリプトを用意し、という具合に、結局記事管理用のスクリプト一式を書いた。

そりゃまぁ、公開日時とちぐはぐなURLで公開はしたくないし、Atomフィード上で個々のエントリを安定して一意に特定するためのIDを確実に振りたいし、などでなんか結局重鈍になってしまった感がある。移転の告知で妙なことを言っていたのはこの辺の話である。うーん、コンテンツとCMSモドキが一つのリポジトリになってるの、歴史の管理からするとだいぶ気持ち悪いが……。

Atomフィードを置いたわけ

「今時フィードなんていらんでしょう」と思う向きがあるのはわかるのですが、いかんせん新規投稿の通知をTwitterに投げ込む機能もないので(高いAPI買ってまでやるほどのことではないし人力で投げ込めばいいだけではあるが……)、過去からの繋がりで僕のことを「フォロー」したいと思ってくれる方がいるかもな、という理由で付けてみました。

しかしFirefoxにはもうライブブックマーク機能はないし、テストが全然できていない。一応W3Cのバリデータは通っているし、Windows版OutlookでRSSフィードとして追加もできて受信できてはいる。いるが、更新された記事はID変わってないのにダブったりする……。AtomじゃなくてRSSにするのが正解だったんだろうか……。

というわけで

だいたいそんくらいのものを作ってできているのがこのブログです。今後ともどうぞよろしく。

ちなみに前回の記事を公開したのが5月末、この記事の下書きを始めたのが6月初旬、公開が8月上旬なので、次回の記事が出るのも、早くて2ヶ月はかかるんじゃないだろうか。気長に待っていてほしい。待つほどの価値があればの話だが。