| @ブログ

嘉穂アルプス 屏山

最近結構走ってるから iOS のフィットネスアプリとか HealthFit でめっちゃログや統計を確認してる。

前月との比較 直近 12 ヶ月間の月ごとの走行距離 日毎の積み上げの前年比
前月との比較、直近 12 ヶ月間の月ごとの走行距離、日毎の積み上げの前年比

いま自分は月間 100km 以上走ることを目標にしている。月間 100km はガチラン勢からしたら大した距離ではないのだが、油断して 3, 4 日走るのをサボったり雨が降ったりすると達成が難しくなるので、前月の同日と比べて進捗はどうかを確認してる。

同じような感じで進捗や達成状況を確認できる機能がブログにも必要なのではないかと思って、自分のブログのアーカイブページで年を選ぶとその年の月ごとの投稿数をグラフで表示するようにした(これまでは年で絞っても年ごとのグラフしか表示されてなかった)。

これで去年の同月に比べてどうかとか、その時期にどんな内容(カテゴリー)の記事をよく書いていたのかがわかるようになった。ランニングと同じように自分の頑張り状況が可視化されるようになった。


そもそもブログを書くことの意義とは何だろうか。大まかに二つあって、以下のように整理されるのではないかと思う。

  1. バズる記事を書いてアクセスを集める
    • 集めたアクセスで自分の認知度を高める
    • 集めたアクセスをマネタイズする(広告)
    • 世の中を変える
  2. 自分の内省のために書く
    • 読書や学習で得た知識を貯めておく
    • 考えを整理する

1 の観点でいうとアクセス数が重要なのだが、いまどきブログで小銭を稼ごうとか世界を変えようとかしてる人はいないはずで、ほとんどのブログは著者自身のために書かれてる(↑の 2 の方)と思う。自分のために文章を書きたい人にとって、書いた内容と密度や頻度、また過去に書いた記事を簡単に振り返ることができることの方が重要なはずだ。

なのに、たいていのブログサービスはアクセス解析機能はあるが、過去の自分の執筆状況を統計的に確認できる機能はない(少なくとも ameblo とはてなブログと livedoor Blog にはなかった)。アクセス数は Twitter で影響力のある人にシェアされたとか外部要因で増えることもあるから、それだけで自分のブログアクティビティを正確に測るのは難しい。

同じような話を以前にもブログに書いている。

振り返りの観点でも既存のブログはあまり良くない。たいていのブログはそもそも自分で書いた過去記事を読むのも大変だったりする。時系列に全文表示されてページネーションして辿らないといけない。

検索機能もいまいちだ。自分の考えたことを貯めておく場としてはちょっと貧相すぎる。

世の中に一般的に出回ってるブログは著者自身のためというより、いかにアクセスを集めて広告でマネタイズするかに主眼が置かれているから、内省や情報の取り出しやすさはあまり考慮されていないのだろう。いかに PV を上げるか、いかに滞在時間を長くするか、いかに回遊させるかが大事になってくる。

しかし先ほども書いた通り、芸能人や著名人を除き、ブログは自分のために書いている人が多いはずで、アクセス数を集めることよりも書き手の体験にフォーカスしたブログがあってもよいのではないだろうか。

ランニングはアプリを使ってログを取ることでいろんなデータが可視化されめっちゃモチベーションが上がる。「今月 100km 走っちゃった〜」とか、「 1km のペースが 6 分切れるようになってきた〜」とか。前年同月との進捗比較も面白い。

ブログでも「今月は 10 記事書けた〜」とか「 1000 文字以上の記事を 5 記事書いた〜」みたいな達成感はあるんじゃないかと思う。つまり、 GitHub の Contribution グラフのブログ版みたいなやつが必要だと思うのだ。

GitHub Contributions Graph

ランニングだと時計だったり Strava などのサービスが走るモチベーションを喚起してくる。一方でブログは誰も書くのを促してくれない。

ブログを書く人が少なくなったのは SNS のせいという面は間違いなくあるけど、ブログ自体の進化が足りないのだと思う。もっと書き手の脳みそをハックして、著者に書きたいと思わせるような仕組みがないとブログは衰退していくばかりなんじゃないだろうか。

突き詰めると、ブログの主なマネタイズ手法が広告しかないのが根っこの問題な気がする。読み手に広告を見せることで収益化するのではなく、書き手の使い勝手や満足度を高めることで収益化できる可能性(書き手に対して機能課金する)はないのだろうか。ランニング系のサービスやアプリは実際そうやって収益化してる(うまく行っているかはわからない)。

| @WWW

以前、以下の記事でこのウェブサイトへのアクセス元 User Agent について書いた。

そのとき Hatena::Russia::Crawler というのが謎だということを書いた。最近のアクセスログを見ても相変わらずこの User Agent からのアクセスが多い。またアクセス頻度も高く、同一の URL に対して何度もアクセスしている。

これはやはりはてなの名を騙った怪しいクローラーなのではないかと思い調べてみた。

まず Hatena::Russia::Crawler という User Agent からのアクセスの IP アドレスを調べてみたところ以下だった。

cat log/access.log | grep 'useragent:Hatena::Russia::Crawler/0.01' | cut -f2 | sort | uniq -c | sort -nr
    434 remote_addr:52.68.0.227
    419 remote_addr:54.249.85.140
    417 remote_addr:54.92.97.59
    379 remote_addr:54.250.227.185

whois してみると AWS で運用されているものであることがわかるが、はてなのものかは断定できない。

もしこの IP からはてなブックマークやはてなアンテナなどの User Agent でのアクセスもあれば Hatena::Russia::Crawler ははてなのクローラーであると断定できるだろう。ということで調べてみたところこんな感じだった。

zcat -f log/access.log* | grep -E 'remote_addr:(52\.68\.0\.227|54\.249\.85\.140|54\.92\.97\.59|54\.250\.227\.185)' | cut -f13,2 | sort | uniq -c | sort -nr
  16687 remote_addr:54.250.227.185      useragent:Hatena::Russia::Crawler/0.01
  16448 remote_addr:54.92.97.59 useragent:Hatena::Russia::Crawler/0.01
  16370 remote_addr:54.249.85.140       useragent:Hatena::Russia::Crawler/0.01
  16272 remote_addr:52.68.0.227 useragent:Hatena::Russia::Crawler/0.01
     73 remote_addr:54.249.85.140       useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     60 remote_addr:54.250.227.185      useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     56 remote_addr:52.68.0.227 useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     50 remote_addr:54.92.97.59 useragent:HatenaBookmark/4.0 (Hatena::Bookmark; Scissors)
     31 remote_addr:54.92.97.59 useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     31 remote_addr:54.250.227.185      useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     31 remote_addr:54.249.85.140       useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     26 remote_addr:52.68.0.227 useragent:Hatena::Fetcher/0.01 (master) Furl/3.13
     19 remote_addr:54.92.97.59 useragent:Hatena::Scissors/0.01
     19 remote_addr:54.250.227.185      useragent:Hatena::Scissors/0.01
     16 remote_addr:52.68.0.227 useragent:Hatena::Scissors/0.01
      9 remote_addr:54.249.85.140       useragent:Hatena::Scissors/0.01

なんと、 IP アドレスで検索してはてなのその他のクローラーもヒットしてしまった。つまり Hatena::Russia::Crawler ははてなのクローラーということだ。

ただしググっても一切情報が出てこない。 Hatena::Russia::Crawler で検索してトップヒットするのは自分のブログだ。

改めて Hatena::Russia::Crawler による直近 30 日間のアクセス状況を調べてみるとこんな感じだ。

zcat -f log/access.log* | grep 'useragent:Hatena::Russia::Crawler/0.01' | cut -f5 | sort | uniq -c | sort -nr
  15697 request_uri:/index.atom
   9913 request_uri:/2022/04/20/integrate-charts-category-with-select-boxs
   9373 request_uri:/2022/05/04/reputation-and-interpretation
   8422 request_uri:/2022/05/11/fly-to-kamikochi-from-fukuoka
   7716 request_uri:/2022/05/16/using-tantivy-over-tantiny
   5906 request_uri:/2022/04/17/quit-using-hey
   5139 request_uri:/2021/12/29/thoughts-on-manga-subscription
   1308 request_uri:/2021/12/13/keep-a-stack-books-whether-reading-them-or-not
    787 request_uri:/2022/06/23/each-entry-title-should-be-marked-up-with-h1
    741 request_uri:/2021/12/13/keep-stack-books-whether-reading-them-or-not
    456 request_uri:/2022/06/24/if-you-feel-apple-musics-recommendation-is-awful
    100 request_uri:/2022/06/14/thoughts-on-hatena-bookmark
     46 request_uri:/2015/12/07/thoughts-on-rural-life
     46 request_uri:/2015/12/02/thoughts-on-t-on-t
     46 request_uri:/2015/12/02/thoughts-on-christmas-song
     45 request_uri:/2019/12/02/stop-drinking-outside-frequently
     16 request_uri:/2020/11/08/where-i-went-in-2019
     11 request_uri:/2015/12/05/omm-writer-music-is-nice-to-listen-to-while-writing
      5 request_uri:/2022/05/04/the-golden-maintenance-week
      2 request_uri:/2022/06/24/
      2 request_uri:/2022/06/24

index.atom はフィードの URL なので除外するとして、特定の記事に対して数千回もアクセスがある。 30 日間で 9000 回ということは一日あたり 300 回だ。 1 時間あたり 12.5 回である。何のためにこんなに高頻度でクローリングしているのだろうか。

とここまで調べたところほかの Bot 系アクセスはどうなのかと改めて User Agent 毎のアクセス数を調べてみたらこんな感じだった。

zcat -f log/access.log* | cut -f13 | sort | uniq -c | sort -nr | head -10
 124894 useragent:Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)
  65794 useragent:Hatena::Russia::Crawler/0.01
  58274 useragent:Ruby
  31493 useragent:Mozilla/5.0 (iPhone; CPU iPhone OS 15_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1
  29454 useragent:Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)
  21492 useragent:Tiny Tiny RSS/21.11-7cfc30a (https://tt-rss.org/)
  20351 useragent:Slackbot 1.0 (+https://api.slack.com/robots)
  18765 useragent:Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)
  18395 useragent:Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)
  17942 useragent:Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)

bingbot からのアクセスの方が Hatena::Russia::Crawler からのアクセスの 2 倍近くあった。ただし bingbot は検索エンジンのクローラーらしく、サイト全体をまんべんなくクローリングするような挙動で、特定の URL に一ヶ月間で数千回アクセスするような感じではない。

zcat -f log/access.log* | grep 'useragent:Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' | cut -f5 | sort | uniq -c | sort -nr | head -25
    571 request_uri:/robots.txt
    352 request_uri:/
    205 request_uri:/category/misc
    160 request_uri:/2007/01/13/732
    156 request_uri:/2005/10/28/129
    150 request_uri:/2009/03/23/1010
    148 request_uri:/category/music
    146 request_uri:/archives
    144 request_uri:/category/www
    143 request_uri:/2016/07/
    143 request_uri:/2009/08/31/1074
    139 request_uri:/2010/07/05/1140
    138 request_uri:/category/photo
    138 request_uri:/2009/02/
    137 request_uri:/2006/09/09/658
    136 request_uri:/tags/netatmo
    136 request_uri:/2010/07/17/1145
    136 request_uri:/2006/07/23/611
    135 request_uri:/2014/03/
    134 request_uri:/?page=32
    134 request_uri:/2007/02/09/747
    133 request_uri:/category/shopping
    133 request_uri:/2021/07/26/how-to-get-to-kamikochi-from-fukuoka
    133 request_uri:/2011/11/03/finally-got-hhkpro2
    132 request_uri:/2006/01/

Hatena::Russia::Crawler は同一 URL に数千回もアクセスして何をしているのだろう? 謎は深まるばかりだ。

| @読書

買った本は全部読まなければならない心構えでいたけどそれをやめた。気になった本が Kindle のセールで安くなっていたらとりあえず買っておくようになった。すべて買っただけで満足して読まずに終わるのではないかという不安はあったが、買ってから半年後に読み始めて読了し学びもあったというケースもあるので、「読んでみたい」というその時のモチベーションを信じることにした。人生は短く時間は有限で、読書はいかに有益な情報を書籍から学びとることが重要だと思う。本は一冊 1000 円から 2000 円くらいだし、書籍代は年間でせいぜい数万円くらいのものだ。それをけちるよりはガンガン積読して、読んだ本の中身をしっかり学んで、仕事などを通して払った書籍代以上のリターンを得た方が効率的だ。なので買っただけで読まない本が出てくることを受け入れた上で、本を積極的に積読していくことにした。「読みたい」という気持ちをキープし続けることが大事だと思う。

| @ブログ

インデックスページをいじって各カテゴリーの最新記事 4 件を配置するようにしてみた。最近の個人サイト復興ブームでみなさんインデックスページを工夫されているのを見ていて真似したくなってやった。

カテゴリーごとに最新記事 4 件を表示

昔ながらのブログだとインデックスページというのは最新の記事 10 件くらいが表示されていて、「次へ」を押すと古い記事が出てくるという構成になっている。以前のこのブログもそうだった。

しかし自分自身が他人のブログで「次へ」を押して次々に記事を読んでいくということをやった記憶がほとんどない。自分のブログでだって何か目的があって特定のキーワードで検索したあとに引っかかった記事を読むという感じなので、時系列に本文とセットで記事が 10 件ずつ表示される UI というのは意味をなしていないと思った。そもそもインデックスという名称なのに最近の記事数件しか表示していないのはおかしい。インデックスというからにはすべての記事の目次になるべきだ。

このブログはカテゴリーがあるので、サイトマップを作るとするとこんな感じになると思う。

+----------+        +------------+        +-----------+
|          |        |            |        |           |
|   Blog   +---+--->+  Category  +------->+   Entry   |
|          |   |    |            |        |           |
+----------+   |    +------------+        +-----------+
               |
               |
               |    +------------+        +-----------+
               |    |            |        |           |
               +--->+  Category  +---+--->+   Entry   |
                    |            |   |    |           |
                    +------------+   |    +-----------+
                                     |
                                     |
                                     |    +-----------+
                                     |    |           |
                                     +--->+   Entry   |
                                     |    |           |
                                     |    +-----------+
                                     |
                                     |
                                     |    +-----------+
                                     |    |           |
                                     +--->+   Entry   |
                                          |           |
                                          +-----------+

第一階層がインデックスページで、第二階層がカテゴリートップ、そして各記事がある。なのでインデックスページは二階層目の一覧ページになっているのが望ましいはずだ。しかし伝統的なブログはカテゴリーという記事をまとめる概念がありつつも、インデックスページから各記事ページへ直接遷移するのが主な導線だった。常に最新の記事が時系列順に並んでいるだけでは味気ないし、常連の読者ではないコンテキストを知らない訪問者には不親切だろう。

しかも SNS の隆盛で個人のブログのインデックスページが参照される機会というのはとんとなくなってしまった。個人が書いたブログ記事は SNS 経由で読まれ、個別記事だけが読まれる。インデックスページやトップページが読まれることはほとんどない。 SNS でシェアされている URL をクリックして個別記事を読んで、それ以上そのブログのほかの記事を読むことなく離脱してしまう。前後のコンテキストは無視して、一つのコンテンツだけがつまみ食いされてしまう。そんな流れにあらがいたいと思った。

これまで関連記事を記事下に表示するなどやってきたが、気に入っていくつか記事を読んで「ホーム」( = インデックスページ)を訪れたユーザーがもう少しブログを深掘りしてみたくなるようにインデックスページを各カテゴリーの最新記事一覧とするようにしてみた。このブログは現在カテゴリーが 13 個あるので、それぞれから 4 件ずつ記事を取得すると 52 記事になる。全カテゴリーからまんべんなく 4 記事ずつ取得して表示するのは簡単なようで結構難しい。普通の SQL ではできない。 OR マッパーではまず無理だろう。

いろいろ調べてみた結果、 MySQL では GROUP_CONCAT というのが使えそうだった。以下のような SQL を書いた。

select entries.id
from entries
inner join (
  select
    category_id,
    GROUP_CONCAT(id order by created_at desc) as entry_ids,
    max(created_at) as last_created_at
  from entries
  where entries.draft = false
  group by category_id
) as grouped_entries
on grouped_entries.category_id = entries.category_id
and FIND_IN_SET(id, entry_ids) between 1 and 4
order by last_created_at desc, entries.id desc;

GROUP_CONCATFIND_IN_SET という関数を組み合わせることで、各カテゴリーから作成日の降順に記事を 4 件ずつ取得できた。このクエリでは記事 id のみ取得して、もう一回 DB に記事を取得するクエリを ActiveRecord で投げる。 ActiveRecord でクエリを組み立てるときは N+1 が起こらないように関連テーブルを JOIN する。

query = <<~SQL
  select entries.id
  from entries
  inner join (
    select
      category_id,
      group_concat(id order by created_at desc) as entry_ids,
      max(created_at) as last_created_at
    from entries
    where entries.draft = false
    group by category_id
  ) as grouped_entries
    on grouped_entries.category_id = entries.category_id
    and find_in_set(id, entry_ids) between 1 and 4
  inner join categories on categories.id = entries.category_id
  order by last_created_at desc, entries.id desc;
SQL
entry_ids = ActiveRecord::Base.connection.select_all(query).rows.flatten
entries = Entry.includes(:category, :user, :tags, :comments).where(id: entry_ids)

PostgreSQL のときに最初に取得した entry_ids の並び順通りに結果が受け取れるかは怪しいが、 MySQL の場合は一回目のクエリで取得した id 順に各レコードが ActiveRecord のクエリ結果として取得できた。あとはこれをカテゴリーごとにグルーピングして View でよろしくやれば良い。

なお各カテゴリーはカテゴリー内の最新の記事の作成日で降順ソートするようにしている。例えば現在のインデックスページの最下部には音楽カテゴリーがあるが、これは音楽についての記事を最後に書いたのが一年以上前だからだ。もしいま音楽の記事を書けば音楽カテゴリーがトップに浮上するようになっている。

見た目に関しては各カテゴリーの記事最新一件は大きなサイズで表示している。最も最近書かれた記事なのでより多く人の目に付いた方がよいだろうという考えだ。またすべての記事にサムネイルというか、アイキャッチ画像を表示するようにした。画像がない記事に関してはデフォルトのサイトアイコン画像を表示するようにしている。やっぱり視覚的に情報を捉えられるのは重要だ。画像がごちゃごちゃ表示されるのを嫌う人もいるかもしれないが、テキストだけでは人間の認知というのはどうしても追いつかない。

あわせて今回、インデックスページの冒頭部にこのサイトについての説明文を載せることにした。ながしまきょうさんr7kamura さんがやっているののパクりだ。伝統的なブログのインデックスページは初めて訪れた人のことを無視しすぎていたと思う。そのブログ自体について説明するページがあるブログは少ない。最初の記事でブログを始めた経緯みたいなことが書かれたきり、そのブログは何なのか、誰が書いているのかが書かれることは希だ。きちんとブログについての説明ページと著者についての説明ページがあっても、左右のサイドバーやトップのナビゲーションの端っこに押し込まれて見られることはない。これではいけないだろう。というわけでインデックスページの一番目立つ位置にブログと自分自身の簡単な紹介を入れた。

インデックスページトップに紹介文を表示

ブログはなぜ衰退したかを考えてみると、 Facebook や Twitter の隆盛はあるにせよ、ブログ自体に初めて訪れた人に読まれるための工夫が欠けていたのだと思う。誰も自分のブログの継続率を計測したりコホート分析したりはしない。読者が前後の記事を読んでいることを前提に書かれた記事やサイト構成では初めて訪れた人はどうやっても離脱してしまう。特に書き手が芸能人でもない一般人の場合はなおさらだ。誰も RSS フィードを購読していないし、ほとんどの読者は初めて訪れる人なのだから、そういう人たちが読んでブログのテーマや著者の人となりが分かる構成にしていかなければならないのだと思う。でないと SNS でたまにバズったときだけ読んでもらえる、ソーシャルメディアの肥やしにしかならない。

この新しいインデックスページが正解なのかどうかは分からないが、ブログ衰退の流れにあらがっていきたい。

| @ブログ

blog.8-p.info の過去記事ページの真似をして、 Archive ページにタグを表示するようにしてみた。

Archive ページにタグを表示

タグはあまり使っていなかったのだけど、一覧で記事タイトルだけ並んだときその記事にどんな内容が書いてあるのかを把握するためにはタグが便利だなと思い直し、タグを表示させてみることにした。いくつか過去のタグが付いていない記事にタグを振ってもみた。

このブログは技術情報からポエム、日々の日記まで何でもありのごった煮ブログなので、カテゴリーによる情報分類には限界がある。現在 13 個のカテゴリーがあるが、記事数にバラツキがあり、情報分類としてあまり機能していない。カテゴリーの粒度をもっと荒くして緩い分類に変更し、そこから先はタグによって超細かくラベリングすると情報の分類としてはまともになるのではないかと思った。

いま、カテゴリーの内訳がこんな感じ。

- "雑談":303
- "技術/プログラミング":272
- "映画/ドラマ/テレビ":150
- "Mac/iPhone":134
- "WWW":113
- "散財":95
- "旅行/ハイキング":70
- "ブログ":69
- "音楽":63
- "読書":34
- "写真":32
- "料理/食事":31
- "労働":27

もっと緩い分類にして以下みたいな感じにするとよさそう。

- 雑記
- パソコン・インターネット
- 見た・読んだ・聞いた
- 出かけた・撮った・食べた

カテゴリーとタグの使い分けは 10 年以上前から悩んでいる気がする。

情報分類の手法でありつつコンテンツの内容そのものを指し示すものでもあるからだろう。インスタグラムで #ラーメン #からの #うどん とかやってる投稿を見るととても嫌な気持ちになるのだけど、そういうことがされるくらいにタグというものは不安定なもので、正しく使おうとか気負わず、もっと緩く使えばいいのかもしれない。

もう廃れてしまったが、フォークソノミーが勢いを取り戻して、情報の発信者ではなく受け取り側がコンテンツにタグ付けできるような世の中になるとおもしろいのかもしれない。

| @ブログ

75 件ほどあった tech.portalshit.net の記事を取り込んだ。実家に住んでいた 10 年前に始めた技術ブログで、最初は Rails 製の Mephisto 、その次に Jekyll で構築した。まだ GitHub Pages の仕組みが存在する前で、自前で用意したさくら VPS に git push すると自動でビルドして記事が公開されるような仕組みを作ったりしてた。

職業プログラマーになろうとしてもがいてた頃にやってたブログで、いま読み返すと「頑張ってたんだな」感があっていなたい記事が多い。

だいぶ放置していて、いまは S3 で静的サイトとして公開していたのでそのまま放置でもよかったが、 10 年前と違って何でも一カ所にまとめて書いておきたいという気持ちが強くなって取り込むことにした。ブログはトピックを混ぜずに一つのトピックにフォーカスした方がよいと 10 年前は考えていたのだけど、最近の世の中のブログ記事の読まれ方は変わってきていて、一人の人のブログをフィードリーダーに登録して読むというより、 SNS をだら見していて流れてきた記事を適当に消費するというスタイルに変わってきているので、一つのブログに一つのテーマという書き分けは不要になったと感じる。

tech.portalshit.net を取り込んだおかげで Archive ページのグラフに占める技術記事の割合が増えた。

Tech category Bar extension

ちなみに取り込みは以下のようなコードを書いて SQL の INSERT 文に変換した。

require 'yaml'
require 'pathname'

files = Dir.glob(File.join(__dir__, '_posts', '*.markdown'))
files.each do |file|
  content = File.read(file)
  _, header, body = content.split('---')
  header_yml = YAML.load(header)
  title = header_yml['title']
  tags = header_yml['category']
  tags = tags.is_a?(Array) ? tags.map(&:downcase) : [tags&.downcase]
  slug = Pathname.new(file).basename.to_s.sub(/\d{4}\-\d{2}\-\d{2}\-(.+?)\.markdown/, '\1').gsub('_', '-')
  body = body.strip.gsub(/\n/, "\\n").gsub('\'', '\'\'')
  created_at = File.birthtime(file).to_s.sub(' +0900', '')
  updated_at = File.mtime(file).to_s.sub(' +0900', '')
  puts <<~EOS
    INSERT INTO entries(title, user_id, category_id, slug, markup, type, draft, body, frozen_tag_list, created_at, updated_at) VALUES('#{title}', 1, 6, '#{slug}', 'redcarpet', 'Post', 0, '#{body}', '#{tags.join(',')}', '#{created_at}', '#{updated_at}');
  EOS
end

| @技術/プログラミング

Chart

Rechars という React のチャートライブラリを利用して、 Archive ページにカテゴリーごとに記事を集計してグラフ化する機能を作った。

グラフの Bar にカーソルを載せると Tooltip が表示されて、具体的な件数がわかる。

Chart Tooltip

カテゴリごとに表示・非表示を切り替えることも可能。グラフ下のカテゴリー名( Legend )をクリックして切り替えられる。

Chart Show-Hide Toggle

ただし残念なことに Bar を非表示にしたときに Legend の表示を変化させるのが難しくてできていない。

仕事で使ってる Looker とか Redash であれば Legend をクリックして表示・非表示を切り替えることができ、それに連動して Legend の色をトーンダウンさせたりする機能が付属しているが、利用した Recharts にはその機能がなかった。 Bar の表示・非表示切り替えも標準サポートされていなかったので、 GitHub の Issue の情報を頼りに無理矢理実装した。

コードはこんな感じ。結構汚い Hack で、 Bar の表示・非表示を、表示用のキー文字列に空白を追加するかしないかで切り替えている。

  // クリックされたアイテムが `this.state.disabled` 配列の中にすでに存在していれば除外し、
  // 存在してなければ追加する
  selectBar(event) {
    let dataKey = event.dataKey.trim()
    if (this.state.disabled.includes(dataKey)) {
      this.setState({ disabled: this.state.disabled.filter(item => item !== dataKey) })
    } else {
      this.setState({ disabled: this.state.disabled.concat([dataKey]) })
    }
  }

  render() {
    return (
      <ResponsiveContainer height={500}>
        <BarChart
          data={this.state.data}
          margin={{
            top: 20, right: 20, left: 0, bottom: 20,
          }}
          style={{ fontSize: '14px' }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="year" />
          <YAxis />
          <Tooltip labelStyle={{ color: '#000', fontWeight: 'bold' }} itemStyle={{ margin: '0 2px 0 4px', padding: '0' }} />
          {// Legend クリック時のコールバックに `this.selectBar` を指定する }
          <Legend onClick={this.selectBar} />
          {/*
            `this.state.categories` 配列と `this.state.disabled` 配列の内容を比較し、
            `this.state.disabled` に追加済のカテゴリーは dataKey に空白を追加することで非表示に
          */}
          {this.state.categories.map((category, index) => {
            let dataKey = this.state.disabled.includes(category) ? category + " " : category
            let color = this.colors[index % this.colors.length]
            return(<Bar key={index} dataKey={dataKey} stackId="a" fill={color} />)
          })}
        </BarChart>
      </ResponsiveContainer>
    );
  }