公式マニュアルではカスタム記事コレクションという機能を使って、カテゴリーを実現できるとしていますが、この機能だとタグと機能が同じなので、あまり存在意義を感じられません。
カテゴリーをタグやカレンダーより上位のレベルにあるものとして扱うにはどうすればよいのでしょうか?
具体的にいうとURLでいうと下のようにタグやカレンダーページを特定のカテゴリーの下に配置するにはどうすればよいのでしょうか。
http://example.com/カテゴリー名/タグ名/
http://example.com/カテゴリー名/2016/01/
これはこのブログの構成です。タグやカレンダーページをいくつか開いてみると構成がわかると思います。
もくじ
2つの方法がある
- 複数ブログ機能を使う
- middleman-blogを拡張する
1の複数ブログ機能は一つのドメインでディレクトリを分けて、複数のブログを運営できる機能です。
この方法はまだ試していないのですが、私としては完全にブログを分けるのでは、次の条件のうち上の二つを実現できなさそうだったので、1ではなく2を採用しました。
- ホーム画面では全カテゴリーの記事を表示したい
- 各記事のURLはルート直下に配置したい
- サイドバーの最新記事やタグやカレンダーはカテゴリー内の記事のみ対象にしたい
(やりようによっては1の方法でも実現できるかもしれません)
middleman-blogにモンキーパッチをあてる
middleman-blogの挙動を変えるには、単純にカスタム拡張を追加するのでは無理で、middleman-blogのclassにモンキーパッチを当てる必要があります。
カスタム拡張のafter_configurationコールバック内で、middleman-blogの3つのclassをリオープンしてモンキーパッチをあてます。
(リオープンとは宣言済みのクラスを再び宣言して変更を加えるという意味です。)
config.rb
下のコードをconfig.rbに追記します。(別ファイルのカスタム拡張にしてもOK。詳しくはカスタム拡張にて)
activate :blog
よりも後ろに書きます。
config.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
class CategorizedBlog < Middleman::Extension def after_configuration Middleman::Blog::BlogData.class_eval do def articles_categorized_by( category=nil ) category.nil? ? articles : articles.select{|a| category == a.data[:category] } end def tags_categorized_by( category=nil ) tags = [] I18n.t(:category).select{|c| category.nil? || category.to_sym == c }. each do | cate_key, category_val | category_val[:tag].each do | tag_key, label | tag = { tag: tag_key.to_s, label: label, category: cate_key.to_s, articles: [] } articles_categorized_by( category ).each do |article| article.tags.each do | tag_str | if article.data[:category] == tag[:category] && tag_str == tag[:tag] tag[:articles] << article end end end tags << tag if tag[:articles].length > 0 end end tags end end Middleman::Blog::TagPages.class_eval do def link(tag, category: nil) params = { tag: tag } if category.present? params[:category] = category template = Addressable::Template.new( ::Middleman::Util.normalize_path("{category}/"+ @tag_link_template.pattern) ) end apply_uri_template template, params end def manipulate_resource_list(resources) resources + @blog_data.tags_categorized_by().map do |tag| tag_page_resource(tag[:tag], tag[:articles], category: tag[:category]) end end private def tag_page_resource(tag, articles, category: nil) Sitemap::Resource.new(@sitemap, link(tag, category: category)).tap do |p| p.proxy_to(@tag_template) p.add_metadata locals: { 'page_type' => 'tag', 'tagname' => tag, 'category' => category, 'articles' => articles, 'blog_controller' => @blog_controller } end end end Middleman::Blog::CalendarPages.class_eval do def link(year, month=nil, day=nil, category: nil) template = if day @day_link_template elsif month @month_link_template else @year_link_template end params = date_to_params(Date.new(year, month || 1, day || 1)) if category.present? params[:category] = category template = Addressable::Template.new( ::Middleman::Util.normalize_path("{category}/"+ template.pattern) ) end apply_uri_template template, params end def manipulate_resource_list(resources) @blog_data.articles.group_by {|a| a.date.year }.each do |year, year_articles| year_articles.group_by {|a| a.date.month }.each do |month, month_articles| resources << month_page_resource(year, month, month_articles) end end @blog_data.articles.group_by {|p| p.data["category"] }.each do |category, pages| next if category.nil? pages.group_by {|a| a.date.year }.each do |year, year_articles| year_articles.group_by {|a| a.date.month }.each do |month, month_articles| resources << month_page_resource(year, month, month_articles, category: category) end end end resources end private def month_page_resource(year, month, month_articles, category: nil) Sitemap::Resource.new(@sitemap, "#{link(year, month, category: category)}").tap do |p| p.proxy_to(@month_template) p.add_metadata locals: { 'page_type' => 'month', 'year' => year, 'month' => month, 'category' => category, 'articles' => month_articles, 'blog_controller' => @blog_controller } end end end end helpers do def blog_month_path(year, month, blog_name=nil, category: nil) build_url blog_controller(blog_name).calendar_pages.link(year, month, category: category) end def tag_path(tag, blog_name=nil, category: nil) build_url blog_controller(blog_name).tag_pages.link(tag, category: category) end end end ::Middleman::Extensions.register(:categorized_blog, CategorizedBlog) activate :categorized_blog |
after_configurationでモンキーパッチする理由
カスタム拡張ではいくつかのコールバックが使えるのですが、このモンキーパッチはafter_configuration
内で行う必要があります。
manipulate_resource_list
では既存のmiddleman-blogで記事が読み込み済みになってしまっています。
また、config.rbのready
イベント時も同じように読み込み済みになってしまっています。(詳しくはconfig.rb の中でサイトマップを使う)
config.rbのトップコンテキストでリオープンすることも考えられますが、そうすると逆にmiddleman-blogの一部のclassが読み込まれていないのでモンキーパッチできません。
なので、classが全て読み込み済み、記事の読み込み前のafter_configuration
で行う必要があります。
スポンサーリンク
config.rbの解説
BlogData
、TagPages
、CalendarPages
を拡張しています。また、middleman-blogによって作られたhelperメソッド、blog_month_path
とtag_path
を上書きしています。
BlogData
articles_categorized_by
とtags_categorized_by
というメソッドを新たに追加しています。
これは、特定のカテゴリーに属する記事、タグをそれぞれ取得するメソッドです。引数にcategoryをとりますが、引数なしの場合は、全カテゴリーに属するものを返します。これはトップページでは、全カテゴリーに属する情報が必要だけど、インターフェイスを揃えるためです。
このメソッドはconfig.rb内でもtemplate側でも使えるように、template側に渡されるBlogDataというオブジェクトに追加しました。テンプレート側ではblog
でアクセスできます。
TagPages
link
、manipulate_resource_list
、tag_page_resource
というメソッドを変更しています。
具体的にそれぞれのタグごとに
http://example.com/カテゴリー名/タグ名/
とうURLでページを生成するようにしています。テンプレート側にcategory
という名前でカテゴリーの種類を渡しています。
CalendarPages
ほぼTagPagesの拡張内容と同じです。
ただ、私は月ごとのカレンダーページしか必要としていないので、年と日のページ生成部分は削除しています。必要であればmiddleman-blogの元ファイルを参考に追加してください。
また、カレンダーページはカテゴリーごとの
http://example.com/カテゴリー名/2016/01/
というページももちろん作るのですが、
http://example.com/2016/01/
というように、全カテゴリーの記事についても作成するようにしています。これはトップページのサイドバーに表示するカレンダーは全カテゴリーを対象にしたいためです。
blog_month_pathとtag_path
こちらはmiddleman-blogで定義されるヘルパーですが、カテゴリーを引数にとり、カテゴリーが第一階層にくるパスを返すように上書きしています。カテゴリーを与えない場合は、ルート直下に配置されたパスを返します。
ja.ymlでタグとカテゴリーの階層関係を定義
タグとカテゴリーの階層関係をja.yml内で定義しています。
ja.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
ja: category: affiliate: label: アフィリエイト tag: result: 成果報告 serius: シリウス review: レビュー web-app-dev: label: Web・アプリ開発 tag: middleman: Middleman atom: Atom neat: Bourbon Neat html_css: HTML/CSS rails: Rails swift: Swift sketch_app: Sketch App aws: AWS review: レビュー culture: label: カルチャー tag: music: 音楽 supercollider: SuperCollider |
template側の書き方
サイドバーはトップページでは全カテゴリーの記事、カテゴリーページではそのカテゴリーだけの記事が反映されるようにします。
なので、サイドバーのpartialは例えばこんな感じです。(slimで書いています)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
- articles = blog.articles_categorized_by(yield_content(:category)) #subnav .subnav__section .subnav__title i.fa.fa-history |最近の投稿 ol - articles[0...10].each do |article| li.subnav__item = link_to article.title, article, class: "u-title_link" .subnav__section .subnav__title i.fa.fa-tags |タグ ol.subnav__tagcloud.u-clearfix - blog.tags_categorized_by(yield_content(:category)).each do |tag| li = link_to tag[:label], tag_path(tag[:tag], category: tag[:category] ) .subnav__section .subnav__title i.fa.fa-archive |過去の投稿 ol - articles.group_by { |a| a.date.year }.each do |year, year_articles| - year_articles.group_by { |a| a.date.month }.each do |month, month_articles| li.subnav__item = link_to\ "#{year}年#{sprintf('%02d', month)}月 (#{month_articles.size})", blog_month_path(year, month, category: yield_content(:category)), class: "u-title_link" |
さきほど追加したarticles_categorized_by
やtags_categorized_by
でカテゴリーごとの記事やタグを取り出しています。
また、メインのテンプレートでは、category
という変数が渡されているので、メインメニューのどのカテゴリーをアクティブにするかの選択に使用できます。
参考
Middleman 3.0 extensions and execution order
さいごに
いかがでしたでしょうか。これでタグやカレンダーをカテゴリーの下に所属することができます。コードが長くわかりづらいかもしれませんが、middleman-blogの元コードをpryなどでデバッグしていくと動きがわかると思います。
なおバージョンは、
- middleman (3.4.0)
- middleman-blog (3.6.0.beta.2)
を使用しています。