シンプルなデコレータで記事内のページングを実装する
シンプルなデコレータで記事内のページングを実装する
ブログのような記事の内容をページングしたいとき、どうやって実装するか?というお話です。 ページングといえば検索結果のページングと、東洋経済のようなサイト(こういうやつ 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
にするほうが多いみたいです。