読者です 読者をやめる 読者になる 読者になる

アトラシエの開発ブログ

株式会社アトラシエのブログです

シンプルなデコレータで記事内のページングを実装する

シンプルなデコレータで記事内のページングを実装する

ブログのような記事の内容をページングしたいとき、どうやって実装するか?というお話です。 ページングといえば検索結果のページングと、東洋経済のようなサイト(こういうやつ http://toyokeizai.net/articles/-/80257) で記事の中でページングするものがあると思います(他にもいろんな種類があると思います)。

検索結果ページングの場合はモデルの集合に対してoffsetやlimitを使って別の集合にして取り出す実装になるはずです。デファクトスタンダードとも言えるKaminariもだいたいそういうロジックです。

ただ、記事の中でページを作るとなるとどう実装するかは選択肢があると思います。

1. ページごとのモデルを作る

例えば@article.bodiesのような関連を作って、記事の本文はArticleBodyクラスが担うという実装です。

idx = params[:page] ? (params[:page].to_i - 1) : 0
@body = @article.bodies[idx]

まぁこれでもいいのですが関連モデルのソートを書かないといけないしなんだかそこまでやるのはめんどくさいなという印象です。

2. 特殊な区切り文字で区切る

特殊な区切り文字を使った実装です。

例えば

hogehoge
fugafuga
[(paging)]
piyopiyo

という文字列は、[(paging)]ごとに改ページになるような実装。今作っているサービスではもともとmarkdownとか特殊タグで記述するものが多く、こちらのほうが今の実装に馴染んでいたので今回はこの方法を試してみました。

この方法で実装するなら、例えばページング用のオブジェクトに表示コンテンツに関する責務を担わせるのがスジが良さそうですが、たまたまArticleDecoratorという表示用のデコレータがあったのでこれに合わせました。

まぁ現実問題これでいいと思うのですが、もう少しオブジェクトの責務について厳格に考えるのであればやはりArticlePagerというようなインスタンスを対応させて、out_of_range?などのメソッドはそちらにdelegateしたほうがいい気もします。

このデコレータはViewの為に簡単なDecoratorをつくるにアイデアをもらっています。DraperやActiveDecoratorでもいいんですが標準ライブラリで実装できてますしモデルにArticle#decorateみたいなメソッドを生やせばいいだけなので私はこっちのほうが好きです。

class ArticleDecorator < SimpleDelegator
  PAGING_NOTATION = "[(paging)]"

  attr_reader :current_page

  def initialize(*args)
    @current_page = 1
    super(*args)
  end

  def current_page=(page)
    @current_page = page.to_i
  end

  def out_of_range?
    @current_page > total_pages
  end

  def text_html
    processor.call(text)[:output].to_s
  end

  def text_plain
    processor.call(text)[:output].text
  end

  def text_html_on_page
    idx = @current_page - 1
    text_on_page = text.split(PAGING_NOTATION)[idx]
    processor.call(text_on_page)[:output].to_s
  end

  def total_pages
    text.split(PAGING_NOTATION).length
  end

  def last_page?
    total_pages == current_page
  end

  def meta_title
    "#{title}"
  end

  def meta_title_on_page
    if @current_page == 1
      meta_title
    else
      "#{meta_title} #{@current_page}ページ"
    end
  end

  def meta_description
    original_meta_description || "#{text_plain.truncate(60)}"
  end

  def short_description(length: 60)
    if original_meta_description
      original_meta_description.truncate(length)
    else
      text_plain.truncate(length)
    end
  end

  def og_title
    meta_title
  end

  def og_description
    meta_description
  end

  def next_page
    @current_page + 1
  end

  def prev_page
    @current_page - 1
  end

  def has_next_page?
    total_pages > @current_page
  end

  def has_prev_page?
    @current_page > 1
  end

  private

  def processor
    @processor ||= Mitsumo::Processors::ArticleTextProcessor.new(
      photos: self.photos,
      embed_contents: self.embed_contents,
    )
  end
end

これで使うときは

@article.current_page = params[:page] || 1
@body_html = @article.text_html_on_page

metaタグ関係

SEO上1記事を表示の上で分割する際にどのようにmetaタグを指定するかはいくつかの説があるようです。

全ページをまとめたスーパーページを用意

全ページをまとめたページを用意して各ページからはcanonicalでスーパーページに正規化する方法です。 これはgoogleの推奨ですが、ランディングページがスーパーページになるのか1ページ目になるのかよくわからなかったので止めました。

prev,nextタグ

以下のようなlinkタグでページ関係を明示する方法です。

<link href="/page/1" rel="prev" />
<link href="/page/3" rel="next" />

この場合ページは別個のものとして扱い、URL(canonical)はページごとに正規化します。 これは2ページ目以降をnoindexにするか否かはページの作り方によるみたいですが、noindexの指定はしてないほうが多い気がします。

1ページ目にcanonical

2ページ目以降をcanonicalで1ページ目に正規化する方法です。これはgoogleでは非推奨のようですが実際はこういう実装も多いみたいです。 この場合は2ページ目以降はfollow,noindexにするほうが多いみたいです。