nisshiee.org

ブログを移管しました

2018-12-01

ただの告知エントリっぽいタイトルだけど、この記事は、「Speee Advent Calendar 2018」の1日目です。
2日目は、秒速@284kmさんより、「Apache Arrow 0.11.0 Released - English translation of selfishly」です。

今日は、個人ブログを移管した話をします。


僕はこれまで、いくつかのブログサービスを利用してきた。

今回、次の選択肢として、自分で管理しているWebサイト「nisshiee.org」下にブログを移管することにした。

なぜ移管したのか

「ブログを移管しました」というタイトルのアドベントカレンダーをわざわざ開いていただいた方は、おそらくなぜ移管したのかが気になると思う。というところで大変申し訳無いが、そんなに大した理由はない
でもそれを書かないと記事にならないので書く。「それはこのサービスでもよくない?」みたいな理由ばかりなので、ツッコミはナシで頼む。いいじゃんやってみたかったんだから!

理由1: ブラウザのタブがくるくる回ってる時間が長い

僕が書くブログはだいたいソフトウェアエンジニアリングに関する内容なので、何かを調べたい人がググって記事にたどり着くというケースが多いと考えている。また、自分の備忘録としての意味合いも兼ねて書いているので、自分で「そういえばブログに書いたなこれ」と思い立って過去記事を漁ったりしている。

こういうユースケースの場合、課題解決が最優先事項であり、「そのために必要な情報がこの記事に載っているのか」が、閲覧者にとってまず最初に重要な情報である。なので、SERPsのリンクをクリックしてから記事表示後にスムーズにスクロールできるようになるまでの時間が最も大切であると考える。

理由2: 手元のエディタで書いてgit pushしたい

え?したいよね?w
僕はブラウザのtextareaより普段使い慣れたキーボードショートカットで操れるエディタの方が良い。

理由3: 自分のドメインの価値上げたほうが気分良い

気分いいよね(自分のドメインでブログ作れるサービスもあるけど)

理由4: テスト勉強してると部屋の掃除したくなる的なやつ

アドベントカレンダーのネタ考えてたらブログ移管したくなっちゃったんだから仕方ない。
ついでにブログ移管した話をアドベントカレンダーに書けて一石二鳥。

構築したシステム

Webpackでサイトを構築する全てのHTML、JS、CSSを生成するようにした。生成したファイルを自動でAWS S3にアップロードし、CloudFront経由で配信するようにした。

と言いつつ、実はもともと「nisshiee.org」はそうなってたんだけど、以下の点を修正した。

  • nodeを10.13.0に更新
  • CircleCI1を2に更新
  • webpack1を4に更新
  • その他、npmパッケージを全体的に最新化
  • TypeScriptを導入(ただのノリ。JSほぼ書いてないw)
  • markdown-loaderの導入

nodeを更新した事による大変さはほとんどなかった。
ただし、ビルド環境をCircleCI1で動かしており、これをnode10で動かすのに一手間必要だった。で、そもそもCircleCI1は終了してしまうので2にしなきゃいけないという別の課題もあったので、CircleCI1+node10の手間をスキップしてCicleCI2+node10にした。
.circleci/config.yml は記事の最後に貼った

webpack1から4への更新は無理だったので諦めたw
2年前に書いたwebpack.config.jsで何やってるかは多少覚えてたし読めばわかったので、webpackのドキュメントを読み直して、同じことをできるようにnpmパッケージを選定し直してwebpack.config.jsを書き直した。
難しかったかと言われるとそんなでもなかったけど、他人に「簡単だよ」とは言えないかな。

あとは、ブログの記事自体はMarkdownで書きたかったのでmarkdown-loaderを導入した。
rendererをいじってビルド時highlightさせたり、headingのHTMLタグが<h3>から始まるようにしたりしている。

画像の自動圧縮は、セルマーテナーサックスさんiioptを開発する際に社内のデザイナーさんと協議して決めた圧縮率を採用させていただいた。
webpack.config.js は記事の最後に貼った

ブログ移管してみた結果

速度

「移管した理由」に速度だけはすごい真面目に理由として挙げたので、ちゃんと計測しよう。
というわけで、Google PageSpeed InsightsのPCページ計測結果がこちら。

「インタラクティブになるまでの時間」で自サイトが他を圧倒できたので、ひとまず満足。
ま、これはすごい技術を使っているわけではなく、不要なもの(特にブラウザ上で動くJS)をごっそり削ぎ落としているので、当たり前と言えば当たり前なのだが。

書き心地

もちろんエディタで書きながらプレビューをブラウザでリアルタイムで確認できる仕組みは構築済み。

この記事を書いているときの様子

ブログ記事もMarkdownで書けるし、コードもハイライトされていい感じ。

デメリット

がんばればできるだろうけど、今のところindexページの自動生成は実装しておらず、手で書いている。他にも大抵のブログサービスだとやってくれそうなカテゴリ別ページや年月別ページは作ってない。

indexページもうちょっと楽に作りたいなぁという気持ちは少しある。GoogleBotにクロールしてもらわないといけないからindexページは必要だ。でも、「記事の内容のコピーが並べられたindexページ」って要らなくない?って思ったりはしていて、模索中。

先に述べたように、このブログはググってたどり着く場所なので、カテゴリページとか年月ページは要らないかなと思っている。

まとめ

というわけで、テスト勉強中の部屋掃除的にブログを移管した話を書いた。
とは言ってもまだ1記事しか書いてないので、しばらくこれで続けてみようと思う。

良ければSpeee Advent Calendar 2018を購読してね☆

appendix

.circleci/config.yml

version: 2
jobs:

  build:
    docker:
      - image: 'circleci/node:10.13.0'
    steps:
      - checkout
      - restore_cache:
          key: node-modules-{{ arch }}-
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: node-modules-{{ arch }}-{{ checksum "package-lock.json" }}
      - run: npm run lint
      - run: npm run build
      - persist_to_workspace:
          root: ./
          paths:
            - '*'

  deploy-dev:
    environment:
      NODE_ENV: development
    docker:
      - image: 'circleci/node:10.13.0'
    steps:
      - attach_workspace:
          at: ./
      - run: npm run upload

  deploy-prd:
    environment:
      NODE_ENV: production
    docker:
      - image: 'circleci/node:10.13.0'
    steps:
      - attach_workspace:
          at: ./
      - run: npm run upload

workflows:
  version: 2

  git-push:
    jobs:
      - build
      - deploy-prd:
          requires:
            - build
          filters:
            branches:
              only: master
      - deploy-dev:
          requires:
            - build
          filters:
            branches:
              ignore: master

  nightly:
    triggers:
      - schedule:
          cron: '38 19 * * *'
          filters:
            branches:
              only: master
    jobs:
      - build
      - deploy-prd:
          requires:
            - build

webpack.config.js

const fs = require('fs')
const path = require('path')

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const ImageminPlugin = require('imagemin-webpack-plugin').default
const HtmlWebpackPlugin = require('html-webpack-plugin')

const hljs = require('highlight.js')
const marked = require('marked')

module.exports = (env, argv) => {
  const isPrd = argv.mode === 'production'

  const pageFiles = (() => {
    const retrieve = (filepath) => {
      const stat = fs.statSync(filepath)
      if (stat.isFile() && filepath.endsWith('.pug')) {
        return [filepath]
      } else if (stat.isDirectory()) {
        return fs.readdirSync(filepath)
          .map(child => retrieve(`${filepath}/${child}`))
          .reduce((a, e) => a.concat(e), [])
      } else {
        return []
      }
    }
    return retrieve('src/page')
  })()
  const htmlPlugins = pageFiles.map(template => {
    const filename = template.replace(/^src\/page\//, '').replace(/\.pug$/, '.html')
    return new HtmlWebpackPlugin({ filename, template, inject: false })
  })

  const markedRenderer = new marked.Renderer()
  markedRenderer.code = function(code, lang) {
    if (hljs.getLanguage(lang)) {
      return `<pre><code class="hljs">${hljs.highlight(lang, code, true).value}</code></pre>`
    } else {
      return `<pre><code class="hljs">${hljs.highlightAuto(code).value}</code></pre>`
    }
  }
  markedRenderer.link = function(href, title, text) {
    if (href.startsWith('http')) {
      return `<a target="_blank" href="${href}">${text}</a>`
    } else {
      return `<a href="${href}">${text}</a>`
    }
  }
  markedRenderer.heading = function(text, level) {
    level = level + 2
    return `<h${level}>${text}</h${level}>`
  }

  const config = {
    entry: './src/index.ts',
    output: {
      filename: isPrd ? '[name]-[chunkhash].js' : '[name].js',
      path: path.resolve(__dirname, 'dist'),
    },

    plugins: [
      new MiniCssExtractPlugin({
        filename: isPrd ? '[name]-[chunkhash].css' : '[name].css',
      }),
    ].concat(htmlPlugins),

    module: {
      rules: [
        {
          test: /\.(ts|tsx)$/,
          use: [
            'babel-loader',
            'ts-loader',
          ],
        },
        {
          test: /\.css$/,
          use: [
            MiniCssExtractPlugin.loader,
            'css-loader',
          ],
        },
        {
          test: /\.(sass|scss)$/,
          use: [
            MiniCssExtractPlugin.loader,
            {
              loader: 'css-loader',
              options: { sourceMap: !isPrd },
            },
            {
              loader: 'sass-loader',
              options: { sourceMap: !isPrd },
            },
          ],
        },
        {
          test: /\.(jpg|png|gif|svg|ttf)$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                context: './src',
                name: isPrd ? '[path][name]-[hash].[ext]' : '[path][name].[ext]',
                outputPath: './',
                publicPath: '/',
              },
            },
          ],
        },
        {
          test: /\.pug$/,
          use: [
            {
              loader: 'pug-loader',
              options: {
                globals: 'process',
                pretty: !isPrd,
              },
            },
          ],
        },
        {
          test: /\.md$/,
          use: [
            {
              loader: 'html-loader',
              options: {
                minimize: isPrd,
              },
            },
            {
              loader: 'markdown-loader',
              options: {
                breaks: true,
                gfm: true,
                headerIds: false,
                renderer: markedRenderer,
              },
            },
          ],
        },
      ],
    },
  }

  if (isPrd) {
    config.optimization = {
      minimizer: [
        new OptimizeCSSAssetsPlugin({}),
        new UglifyJsPlugin({
          uglifyOptions: {
            compress: {
              drop_console: true,
            },
          },
        }),
        new ImageminPlugin({
          pngquant: {
            quality: '85',
          },
        }),
      ],
    }
  } else {
    config.devtool = 'inline-source-map'
  }

  return config
}