タオルケット体操

サツバツいんたーねっと

はてなはSEOに弱いときいたのでとりあえずパン屑リストを実現するスクリプトを作った

今回のソースコードなどは以下に貼る、僕のGitHubからも閲覧出来ます。

hachibeeDI/hatena-modules-io · GitHub

無知なので、今までSEOはオカルトと対処療法の世界だと思い込んでいました。しかし最近詳しい人に質問する機会があったのですが、実際はすべてが感じそういうことでもないみたいです。

なので今後の参考のためにも、Web標準やセマンティクスなんかの方面はキャッチアップしても良いかなと思いました。

で、タイトルなんですが、どうやらはてなブログがSEOに強かったのは過去の話で、今ではだいぶ微妙みたいです(伝聞)。
特にカスタマイズ性の低さがクソみたいです。

パン屑リストとは

色々なWebサイトの上に表示されている、現在地をしるすアレのことです。ヘンゼルとグレーテルですね?

パン屑リストは見た目だけの存在ではなく、HTML5のmicrodataに従った形で情報を与えてあげることでGoogleのサーチエンジンがそのサイトの構造を把握してインデックスくれたりとか、なんかそういう良いことがあるみたいです。
詳しいところはググってください。

itemscopeを使って、どういう風にパン屑リストを構造化すれば良いのかはGoogleのドキュメントを読むのが一番間違いないでしょう。 Breadcrumbs - Structured Data — Google Developers こんな感じみたいです。

他の人による実装

SEOなどに全く興味がなかったので知らなかったのですが、なんか割と盛り上がっていたようでいくつか既存の実装がありました。

いつだって既存のものを使うのが一番楽です。
が、機能的に欲しい要件に合わなかったのと、いじるのもしんどそうな感じなんで僕も作りました。

シンプル + 多機能 パン屑リストのブログツール決定版だ(自画自賛)

一応、興味のある人のためにソースコードを掲載しておきます。

CoffeeScriptで書いてあるのでこれをそのまま貼付けても動きません。ソースコードに興味はないけど使いたい方は下の黒い部分が切れるまでスクロールしまくってください。

###
The MIT License (MIT)

Copyright (c) <2014> <OGURA_Daiki>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
###

do () ->
  # print = console.log.bind console
  $$CC = document.createElement.bind document
  $$Q = document.querySelector.bind document
  $$QA = document.querySelectorAll.bind document

  isEntryPage = $$Q('html').getAttribute('data-page') == 'entry'
  return unless isEntryPage

  ###
  example:
    var _CATEGORY_DEFINITIONS = {
      'programming': {
        'JavaScript': ['jQuery', 'React', 'Vue'],
        'Python': ['Django', 'Flask', 'numpy'],
        'Haskell': []
      },
      'foods': {
        'cooking': ['bread', 'meet'],
        'alcohol': ['beer', 'wine']
      }
    }
  ###
  _CATEGORY_DEFINITIONS = window._CATEGORY_DEFINITIONS
  return unless _CATEGORY_DEFINITIONS


  _setupMetadataAsPlaceholder = (placeholder) ->
    placeholder.setAttribute 'itemscope', ''
    placeholder.setAttribute 'itemtype', 'http://www.data-vocabulary.org/Breadcrumb/'
    placeholder


  _makeUrlProp = (title, uri) ->
    # print 'make url prop', uri, title
    anchor = $$CC 'a'
    anchor.className = 'breadcrumb--urlprop'
    anchor.setAttribute 'itemprop', 'url'
    anchor.setAttribute 'href', uri
    titleElem = $$CC 'span'
    titleElem.setAttribute 'itemprop', 'title'
    titleElem.innerText = title
    anchor.appendChild titleElem
    anchor

  _makeChildProp = (title, uri) ->
    holder = _setupMetadataAsPlaceholder $$CC 'div'
    holder.className = 'breadcrumb--child inline-block'
    holder.setAttribute 'itemprop', 'child'
    holder.appendChild _makeUrlProp title, uri
    holder

  _makeThisPageProp = () ->
    uri = window.location.href
    title = $$Q('.entry-title a').text
    _makeChildProp title, uri


  ### 無制限にも実装出来るけど三つあれば充分っしょ ###
  _buildBreadcrumbFromHierarchy = (top) ->
    # print 'build from top', top
    _genHolder = () ->
      holder = _setupMetadataAsPlaceholder $$CC 'div'
      holder.appendChild _makeUrlProp top.title, top.uri
      holder.className = 'breadcrumb--row'
      holder

    if _.isEmpty top.children
      # print 'create only top breadcrumb'
      holder = _genHolder()
      holder.appendChild _makeThisPageProp()
      return [holder]

    breadcrumbes = []
    for second in top.children
      # print 'create Breadcrumb second ', second
      if _.isEmpty second.children
        # print 'second has no children', second
        holder = _genHolder()
        secondElem = _makeChildProp second.title, second.uri
        secondElem.appendChild _makeThisPageProp()
        holder.appendChild secondElem
        breadcrumbes.push holder
      else
        for third in second.children
          holder = _genHolder()
          secondElem = _makeChildProp second.title, second.uri
          thirdElem = _makeChildProp third.title, third.uri
          thirdElem.appendChild _makeThisPageProp()
          secondElem.appendChild thirdElem
          holder.appendChild secondElem
          breadcrumbes.push holder
    breadcrumbes


  ### parse anchores as uri and title ###
  _parseHatenaCategoryElements = (parent) ->
    return unless parent
    anchors = parent.querySelectorAll 'a'
    _.map anchors, (elem) -> [elem.text, elem.href]


  ### category: {title: uri} ###
  _parseHierarchy = (category) ->
    category_key = _.keys category
    hierarchies = []
    containedKeys = []
    for parent, _child of _CATEGORY_DEFINITIONS when parent in category_key
      parentObj = 'title': parent, 'uri': category[parent], 'children': []
      containedKeys.push parent
      for child, g_children of _child when child in category_key
        containedKeys.push child
        childObj = 'title': child, 'uri': category[child], 'children': []
        parentObj.children.push childObj
        for g_child in g_children when g_child in category_key
          containedKeys.push g_child
          childObj.children.push 'title': g_child, 'uri': category[g_child]
      hierarchies.push parentObj

    # undefined keys are all in parent hierarchy
    # print 'containedKeys', containedKeys
    definedParents = _.keys _CATEGORY_DEFINITIONS
    for key in category_key when not(key in containedKeys or key in definedParents)
      hierarchies.push 'title': key, 'uri': category[key]
    return hierarchies


  document.addEventListener 'DOMContentLoaded', () ->
    # BASE_URI = $('html').attr('data-blogs-uri-base')

    PLACE_HOLDER = $$Q '.categories'

    categories = _parseHatenaCategoryElements PLACE_HOLDER
    # print categories
    categories = categories.reduce(
      (x, y) ->
        x[y[0]] = y[1]
        return x
      ,
      {}
    )
    return if _.isEmpty categories
    # このタイミングならjQueryがロードされている
    $PLACE_HOLDER = $ PLACE_HOLDER
    $PLACE_HOLDER.hide()
    root = document.createElement('div')
    root.className = 'breadcrumb--root'
    hierarchies = _parseHierarchy categories
    # print 'hierarchies----', hierarchies
    # 2つあればいいよね
    for h in _.first hierarchies, 2
      _buildBreadcrumbFromHierarchy(h).forEach (e) -> root.appendChild e
    $PLACE_HOLDER.after(root)

手癖の悪そうなコードだなオイ!!
なおテストコードはまだ書いていないもよう。まぁ対したコードじゃないんでいらないっすよね(妥協)。

使い方

書き立てホヤホヤなのでテストが不十分なこと、筆者はSEOド素人であることなどを考慮しつつ自己責任で導入してください。
なおバグ報告や要望や文句なんかにはちゃんと対応するモチベーションがあります。

まず、カテゴリーの親子を定義する必要があります。
色々めんどかったので、この設定はグローバル変数としてスクリプトに渡します。

以下は例です。

<script>
    window._CATEGORY_DEFINITIONS = {
      'プログラミング': {
        'JavaScript': ['jQuery', 'React', 'Vue'],
        'Python': ['Django', 'Flask', 'numpy'],
        'Haskell': []
      },
      'foods': {
        'cooking': ['bread', 'meet'],
        'alcohol': ['beer', 'wine']
      }
    };
</script>

はい。JSON形式で階層ごとにカテゴリ名を記入することで親、子、孫まで設定出来ます。
どうなっているのかは、例を見れば一目瞭然だと思うので説明しません。

既存のタグを設定しなおすことなく、かつキメ細やかな親子設定が可能になっている、というのが当スクリプトの利点でしょうか。

あとはカテゴリー欄の中身をパン屑に置き換えるスクリプトを読み込むだけです。

どこに置いてもいいんですが、フッターに配置することをお勧めします。というかscriptタグはDOMの描画を止めないためにも、可能な限り下の方に置きましょう。

親子の定義と合わせて、以下のようになりました。

<script>
    window._CATEGORY_DEFINITIONS = {
      'プログラミング': {
        'JavaScript': ['jQuery', 'React', 'Vue'],
      };
</script>
<script src="//hachibeedi.github.io/hatena-modules-io/dist/breadcrumb.js"></script>

はい。
どうでしょう、記事ページにいけば変わるとおもいます。変わりますよね?

結果や見た目に関しては、この記事ページの上部、タイトル下を見ましょう。そういう感じになります。

不安な人はGoogleWebマスターツールとかで確認しましょう。
なんかおかしかったら、コメントとかメールとかTwitterとかなんかそういう適当なアレで連絡くれれば対応します(不安感)。

見た目を整える

エ? 見た目がおかしい?
各エレメントに適当なclassを付与してあるので、それに対して適当なスタイルをやっつけてあげてください。

以下は例です。スクリプトの実装で疲れたので適当です。

<style>
.breadcrumb--root{
border-top:#fee9d7 solid 2px;
border-right:#d6b8a7 solid 5px;
border-bottom:#ecd6b8 solid 4px;
padding:4px 0;
background-color:#f9f6ed;
border-radius:3px;
margin-top:-10px;
box-sizing:border-box
}
.breadcrumb--row{margin:2px auto}
.breadcrumb--urlprop{border:none !important;background-color:inherit !important}
.breadcrumb--child:before{content:'>';margin:0 6px}
</style>

以上のクラスやCSSを参考に適当にカッコ良くしましょう。

なお、マークアップの中に直接>などを書くのは嫌なので疑似要素を使ってます。
これによって、別の文字やアイコンフォントを使うなどしてユーザのカスタマイザビリティが向上するわけで、これは実際マーケティング的にも正解ですよ!

余談ですが、 Cinnamon - テーマ ストア - はてなブログ を使うと筆者の気が向いた時に勝手にカッコいい(本当か?)を付与してくれるみたいです。囲い込みですね。

注意点

現状、足りていない機能として

  • 複数あるパン屑の優先度(Googleは一番上の物しかみない)

  • 未定義タグの挙動を詰めきれていない

などがあります。

また、

などを考慮に入れて、ある程度SEOやJavaScriptの知識がある人、野良スクリプトを利用する『覚悟』が出来ている人のみが導入することをお勧めします…ッ!

まぁレコメンドにしろこのパン屑にしろ、公式が対応するか出力フォーマットのカスタマイズ性をあげるかしてくれるのが一番楽なんでしょうが、当座の繋ぎにはなりますかね。

おしまい!