| @ブログ

天山七曲峠登山道

表示していた Google Adsense の広告を外した。自動広告をオンにすると本文の途中など意図しないところに広告が掲載されまくってしまって大分読みづらい。かといって記事下に入れる程度だとまったくクリックされなくて貼ってる意味がなかった。加えて、ここ一年間で数度行われた Google のコアアルゴリズムアップデートで検索流入が 1/3 くらいになってしまって PV が激減してしまった。 Google としてはどこの馬の骨とも付かない個人がやってる泡沫ブログにトラフィックを流してインターネットの果実を分け与えるつもりはないのだろう。 Adsense 広告の読み込みでサイトの読み込み速度も少し遅くなっていたし大分すっきりした。

消した理由で一番大きいのはアルゴリズムやリターゲティングによる広告のむなしさだ。 Amazon アフィリエイトなど自分の意思でレビューしたり紹介したアイテムの広告ではない広告が自分のブログに表示される必然性はない。アルゴリズムやリターゲティングによる広告は、掲載される場所はどこでもよくて、たまたま空いてたから表示されました、というような間に合わせ感がある。自分が楽天で見た商品の広告が自分のブログに延々表示される様を見て、こういう広告を表示し続けることで自分のブログの価値が毀損されているような気がしてきた。一つ前の翻訳記事がバズって結構アクセスがあったが、翻訳元のブログには広告が表示されていないのに翻訳した自分のブログには広告が表示されていて、自分が広告を入れていることで元の記事の価値も下げているような罪悪感もあった。

元からブログで儲けようとは思っていなかったが、ブログを書くことで得られる一番の利益というか目的は、広告によって小銭を稼ぐことではなく、ブログを書くことで自分自身の価値を高めることだと思う。ブログによってインターネット上でのプレゼンスを高めて知名度を獲得したり、ブログを書いたり運用したりすることで技術的な知見を得てスキルアップにつなげられる。それらを自分の給料に上乗せできるような仕組みを作っていくことが大切なのかなと思った

| @ブログ

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>
    );
  }

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

react-select-before-after.jpg

一個前の記事のキャプチャにあるように、従来テキストリンクだった年とカテゴリーの選択をセレクトボックスにした。 git log -S したところ去年( 2019 年)の 11 月頃に変更を行ったみたいだ。もうそろそろ 2020 年になろうとしていて、 2005 年からやっていて年の数が 16 個になろうとしていてさすがに多すぎると思ったので整理のためにセレクトボックス化してコンパクトにした。

利用したのは React Select というパッケージで、色々カスタマイズできるみたいだけど面倒だったので素のまま使ってる。

Archives ページのリファクタリング のときのように、子コンポーネントのイベントをトリガーに親コンポーネントの setState() を呼び出すような作りになっている。作るときは結構難儀したけどおかげでスマートフォンで見たときも年やカテゴリーだけでファーストビューが埋まるということがなくなった。

react-select-smartphone-view.jpg

現在のコードをまるっと貼り付けるとこんな感じ↓。

子コンポーネント(年のセレクトボックス)

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import Select from 'react-select'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'

import history from './history'

class YearSelect extends Component {
  constructor(props) {
    super(props)
    this.state = {
      data: [],
      selectedOption: null
    }
    this.handleChange = this.handleChange.bind(this)
  }

  static propTypes = {
    match: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
  }

  async loadYearSelectFromServer() {
    const request = await fetch('/archives/years.json')
    const response = await request.json()
    this.setState({ data: response })
  }

  componentDidMount() {
    this.loadYearSelectFromServer()
  }

  handleChange(selectedOption) {
    const year = selectedOption ? selectedOption.value : null
    if (year) {
      this.props.history.push(`/archives/${year}`)
    } else {
      this.props.history.push("/archives")
    }
    this.setState(
      { selectedOption },
      () => { this.props.update(year) }
    )
  }

  render() {
    const options = this.state.data.map(year => {
      return { value: year, label: year }
    })
    return (
      <div className="year-list">
        <Select
          value={this.state.selectedOption}
          onChange={this.handleChange}
          options={options}
          placeholder="Year"
          isClearable
        />
      </div>
    )
  }
}

const YearList = withRouter(YearSelect)

export default YearList

親コンポーネント( App.js )

import React, { Component } from 'react'
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'

import YearList from './YearList'
import CategoryList from './CategoryList'
import Archives from './Archives'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      category: null,
      year: null,
      length: 0
    }
    this.updateCategory = this.updateCategory.bind(this)
    this.updateYear = this.updateYear.bind(this)
    this.setLength = this.setLength.bind(this)
  }

  updateCategory(category) {
    this.setState({ category })
  }

  updateYear(year) {
    this.setState({ year })
  }

  setLength(length) {
    this.setState({ length })
  }

  render() {
    return(
      <Router history={history}>
        <div className="archive-filter">
          <YearList update={this.updateYear} />
          <CategoryList update={this.updateCategory} activeCategory={this.state.category} />
          <div className="entry-length"><p>{this.state.length} entries</p></div>
        </div>
        <Switch>
          <Route
            exact path="/archives"
            render={(props) =>
              <Archives
                category={this.state.category}
                setLength={this.setLength}
                {...props}
              />
            }
          />
          <Route
            path="/archives/:year(\d{4})"
            render={(props) =>
              <Archives
                category={this.state.category}
                setLength={this.setLength}
                year={this.state.year}
                {...props}
              />
            }
          />
        </Switch>
      </Router>
    )
  }
}

export default App

| @写真

箱石峠から眺める阿蘇高岳

2018 年末に NIKON Z6 を買った。その前は NIKON D90 を 10 年近く使ってた。 NIKON D90 で撮れる写真にあまり不満はなかったが、フルサイズへの憧れと動画性能のしょぼさ(静止画は D90 の方がよいものが撮れるが、動画は明らかに iPhone の方がよいものが撮れた)に困っていて買い換えを決意した。人生で初めて新発売のカメラを発売前から予約して購入した。 48 回分割払い。

NIKON Z6 の良さは色々ある。通し F4 で撮れる 24-70m レンズ(レンズキットを買った)、 ISO 感度の高さ、チルト液晶、奥行きにも対応したデジタル水準器、 SnapBridge 対応(スマートフォンの GPS を利用した位置情報埋め込み、撮った写真をすぐに Bluetooth でスマートフォンに送れる)などなど。スマートフォン時代に対応するための正統進化という趣がある。 NIKON は真っ当ないい製品を作ったなと思う。

というわけでまずは NIKON Z6 で撮った写真からご覧下さい。そのあとに iPhone 7 で撮った写真で今年を振り返ります。

Continue reading...

| @WWW

Temple wall at Hakata

ヒトデさんのブログを読んでGoogleの広告設定を共有してメンバー間でつながるプロジェクトに参加した

Google の広告設定のページにアクセスすると Google からどういう属性として認識されているかがわかり、それを共有して遊ぼうというもの。 Scrapbox はタグ付けが簡単なので人と人の関連性が表現しやすい。

おもしろかったので、普段使ってる三つの Google アカウントでそれぞれでどういう結果になるかやってみた。

個人アカウント

iPhone の Safari でも自宅の Mac でもログインしててよく使うアカウント。ただ最近は Google 検索をあまり使わないようになって、検索には DuckDuck Go を使うようにしている。 #企業向けテクノロジー #業種:_ヘルスケア業界 #業種:_テクノロジー業界 あたりが入っているのがおもしろい。前職が B2B の SaaS / クラウドソーシング企業だったこと、現職がアウトドア関連の企業であることを反映してそう。

#35~44_歳 #男性 #Anova_Culinary #TechAcademy_[テックアカデミー] #Mynavi #Amazon #American_Express #Aha! #Wantedly #Fujitsu #Bic_Camera #Yodobashi_Camera #Kakaku.com #SoftBank_Telecom #Apple_iOS #Mac_OS #SF_映画、ファンタジー映画 #SF_番組、ファンタジー番組 #TV_ゲーム、PC_ゲーム #アウトドア #アクション映画、アドベンチャー映画 #アニメ、漫画 #アメリカン_フットボール #イベント情報 #インディーズ音楽、オルタナティブ_ミュージック #ウェブデザイン、開発 #エクストリーム_スポーツ #オーディオ機器 #オフィス、ビジネスソフトウェア #お祝い、ギフト、祝祭日用グッズ #カメラ #カメラ_レンズ #カメラ、写真機材 #クーポン、割引サービス #クラウドストレージ #クラシック音楽 #グルメ食品、特別食 #クレジット_カード #ゲーム機 #コーヒー_メーカー、エスプレッソ_マシン #コーヒー、紅茶 #コミック、アニメーション #コメディ映画 #コンピュータ_コンポーネント #コンピュータ_ドライブ、ストレージ #コンピュータ_ハードウェア #コンピュータ_モニター、ディスプレイ #コンピュータ、電化製品 #コンピュータ周辺機器 #コンピュータ用メモリ #サイクリング #サッカー #ジャズ #ショッピング #スポーツ #スポーツ衣料 #スマートフォン #ソーシャル_ネットワーク #タブレット_PC #ダンス、電子音楽 #ツアー旅行 #デジタル一眼レフ_カメラ #テレビ、ビデオ、動画 #テレビドラマ #ドキュメンタリー番組、ノンフィクション番組 #トラック、バン、SUV #ニュース #バー、クラブ、ナイトライフ #ハイキング、キャンプ #ハッチバック #ビーチ、島 #ビジネス_サービス #ビジネス_ニュース #ビジュアル_アート、デザイン #ファースト_フード #ファッション、スタイル #フィットネス用品 #フォーク、伝統音楽 #ブランド品、高級品 #ブルース #プログラミング #ペット #ボート #ホームの自動化 #ポップ_ミュージック #マセラティ #ラグビー #ラップトップ、ノートパソコン #ランニング、ウォーキング #リフォーム #レストランのレビュー、予約 #レンタカー、タクシー #ロック_ミュージック #ワールド_ミュージック #飲食店 #映画 #音楽、オーディオ #価格比較 #家庭 #家電 #会計、財務ソフトウェア #確定申告、税務 #学歴:_学士号 #環境に優しい生活、環境問題 #企業向けテクノロジー #業種:_ヘルスケア業界 #業種:_テクノロジー業界 #銀行 #携帯電話 #芸術写真、デジタル_アート #芸能ニュース #個人ブログ、サイト #佐賀 #財務プランニング、マネジメント #山岳、スキー_リゾート #仕事 #子育て、育児 #子供の有無:_子供なし #子供服 #自動車 #自動車販売 #室内装飾、内装 #写真、画像の共有 #写真の印刷サービス #写真編集ソフト #社員数:_中規模雇用者(従業員数:_250~999_人) #授乳用品、離乳食用品 #住宅所有状況:_住宅所有 #書籍、文学 #商品レビュー、価格比較 #食器洗い機 #世界のニュース #世帯収入:_高 #政治 #送金・決済システム、サービス #大学 #都市交通 #投資 #東京 #動画編集ソフトウェア #日本 #配偶者の有無:_既婚 #美容、フィットネス #表計算ソフトウェア #舞台芸術 #福岡 #分散コンピューティング、クラウド_コンピューティング #宝石、アクセサリー #旅行 #料理、レシピ #量販店、デパート #腕時計

趣味アカウント

昔はよく使っていたが最近はあんまり使ってない。主に Mac で使ってた。いまは YouTube でのみこのアカウントを使ってる。

#35~44_歳 #男性 #SF_映画、ファンタジー映画 #SF_番組、ファンタジー番組 #アメリカン_フットボール #イベント情報 #ギター #クイズ番組 #クラシック音楽 #グルメ食品、特別食 #コーヒー、紅茶 #コミック、アニメーション #コンピュータ_ハードウェア #コンピュータ、電化製品 #ショッピング #スポーツ #ソーシャル_ネットワーク #テレビ、ビデオ、動画 #テレビドラマ #トーク番組 #ニュース #バスケットボール #ビジネス_サービス #ビジュアル_アート、デザイン #フィットネス #ヘヴィメタル #ペット #ホームの自動化 #ポップ_ミュージック #ラグビー #リアリティ番組 #リフォーム #ロック_ミュージック #ワールド_ミュージック #映画 #音楽、オーディオ #家族向けテレビ番組 #学校、教室関連用品 #環境に優しい生活、環境問題 #業種:_テクノロジー業界 #芸能ニュース #子供の有無:_3_要素 #自動車 #自動車販売 #室内装飾、内装 #社員数:_小規模雇用者(従業員数:_1~249_人) #住宅所有状況:_住宅所有 #書籍、文学 #商品レビュー、価格比較 #食料品小売業 #世帯収入:_平均以上 #政治 #都市交通 #東アジアの音楽 #配偶者の有無:_既婚 #美容、フィットネス #舞台芸術 #服飾 #分散コンピューティング、クラウド_コンピューティング #野球 #料理、レシピ #量販店、デパート

仕事用アカウント

会社の Gsuite のアカウント。 Chrome の Canary Channel で利用していて、個人アカウントとは明確に使い分けてる。仕事関係の検索やサイト閲覧はほぼほぼこのブラウザーで行っている。仕事用アカウントなので買い物とか趣味の検索はほとんどしない。どうも消費に結びつく検索をしないと世帯収入が低く出る模様。逆に個人アカウントでは頻繁に消費に関係する検索を行っているので世帯収入が高いと判定されてるっぽい。また個人的な用途に使わないので年代を特定できず、 25〜54_歳 という扱いになってるのも面白い。

#25~54_歳 #男性 #Apple_iOS #TV_ゲーム、PC_ゲーム #アウトドア #アクション映画、アドベンチャー映画 #アメリカン_フットボール #エクストリーム_スポーツ #オーディオ機器 #オフィス、ビジネスソフトウェア #カメラ、写真機材 #クーポン、割引サービス #クラウドストレージ #グルメ食品、特別食 #クレジット_カード #コミック、アニメーション #コンピュータ_ハードウェア #コンピュータ、電化製品 #ショッピング #スポーツ #スマートフォン #ソーシャル_ネットワーク #テレビ、ビデオ、動画 #テレビドラマ #ニュース #ハイキング、キャンプ #ビジネス_サービス #ビジュアル_アート、デザイン #ファッション、スタイル #フィットネス #ブランド品、高級品 #ペット #ホームの自動化 #ラグビー #ランニング、ウォーキング #飲食店 #映画 #音楽、オーディオ #価格比較 #家庭 #家電 #学校、教室関連用品 #学歴:_学士号 #企業向けテクノロジー #起業準備 #業種:_テクノロジー業界 #携帯電話 #芸術写真、デジタル_アート #芸能ニュース #犬 #山岳、スキー_リゾート #子供の有無:_子供なし #自動車 #自動車販売 #室内装飾、内装 #社員数:_中規模雇用者(従業員数:_250~999_人) #住宅所有状況:_住宅所有 #書籍、文学 #商品レビュー、価格比較 #世帯収入:_平均以下 #送金・決済システム、サービス #都市交通 #配偶者の有無:_既婚 #美容、フィットネス #舞台芸術 #服飾 #分散コンピューティング、クラウド_コンピューティング #旅行 #料理、レシピ

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

Archive ページ をリファクタリングした。

これまで gulp をビルドに利用していた(Archive ページを React Router 化)が、 webpack を使うように変えた。

React のコードも見直して、 DOM の状態に依存して表示・非表示を切り替えるコードがあったりして( 🐙 Archive ページにカテゴリごとの記事件数を表示する機能を追加 )ごちゃごちゃしてたので DOM を直接ごちゃごちゃ操作するのをやめて React で管理するように変えた。親コンポーネント、子コンポーネント、孫コンポーネント、子コンポーネントの兄弟コンポーネント間で状態を共有する必要があって、結構難儀した。

Archives React Component 1.png

実際の画面を見るとこんな感じ。

Archives React Component 2.png

App というコンポーネントがルートにあって、子に CategoryListCategoryList の子コンポーネント( App からすると孫)に Category コンポーネントがある。記事一覧自体は CategoryList と兄弟コンポーネントである Archive コンポーネントが担当している。

Archives React Component 3.png

こんな感じで特定の Category が選ばれたことを Category のクリックイベントをトリガーに CategoryList に伝達し、 CategoryList はさらにそれを App コンポーネントに伝える。その結果が App から Archive コンポーネントに伝えられ、表示内容が変更される。

この辺を参考にして実装した。コールバック関数を props として引き回し、状態を回収する感じ。

ただこういう込み入った状態の管理を React で行う場合は Redux などを利用するのが良いようだった。

前職のとき、 Redux とか Flux が出てきた頃に F/E のエンジニアの人たちが熱狂してたけど自分はいまいち理解できなくて、傍観するだけだったが、いまさらにして何となく Flux アーキテクチャの概要的なものを把握することができた気がする。ただ自分の場合は深みに入り込まず極力シンプルに作りたいと思っていたので Redux などには手を出さず、 Callback で愚直に状態を親コンポーネントに伝達していく方法をとった。

React 、やっぱり大分良いものだとという感じがした。 jQuery でクラスや CSS で show - hide を Toggle していた頃とは隔世の感がある。