Skip to content
This repository

Port the extension to the new sitemap structure #41

Merged
merged 9 commits into from about 2 years ago

2 participants

Ben Hollis Thomas Reynolds
Ben Hollis
Owner

The new sitemap stuff is really cool - it lets me fix #39 easily because the page list gets rebuilt completely whenever stuff changes.

I'd definitely like comments on the approaches I took here - specifically how I'm using manipulate_resource_list and how I made BlogArticle into a module that just gets mixed into Resource. I was also pretty uncomfortable with what I had to do in TagPages and CalendarPages to set instance variables in the views using provides_metadata_for_path, especially since each time the sitemap is rebuilt a new block is added to that list and they just build up. I'd love to come up with a better way of handling that.

All tests pass assuming middleman/middleman#367 is pulled.

Thomas Reynolds
Owner

Great!

Thomas Reynolds tdreyno merged commit a4fe6d4 into from
Thomas Reynolds tdreyno closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
8 features/next_previous.feature
... ... @@ -0,0 +1,8 @@
  1 +Feature: Next and previous article
  2 +
  3 + Scenario: Articles know their next and previous article
  4 + Given the Server is running at "calendar-app"
  5 + When I go to "/blog/2011-01-01-new-article.html"
  6 + Then I should see "Next: /blog/2011-01-02-another-article.html"
  7 + When I go to "/blog/2011-01-02-another-article.html"
  8 + Then I should see "Previous: /blog/2011-01-01-new-article.html"
22 features/tags.feature
@@ -26,3 +26,25 @@ Feature: Tag pages
26 26 And the file "tags/bar.html" should contain "Tag: bar"
27 27 And the file "tags/bar.html" should contain "/2011-01-01-new-article.html"
28 28 And the file "tags/bar.html" should not contain "/2011-01-02-another-article.html"
  29 +
  30 + Scenario: Adding a tag to a post in preview adds a tag page
  31 + Given the Server is running at "tags-app"
  32 + When I go to "/tags/bar.html"
  33 + Then I should see "/2011-01-01-new-article.html"
  34 + When I go to "/tags/newtag.html"
  35 + Then I should see "Not Found"
  36 + And the file "source/blog/2011-01-01-new-article.html.markdown" has the contents
  37 + """
  38 + ---
  39 + title: "Newest Article"
  40 + date: 2011-01-01
  41 + tags: newtag
  42 + ---
  43 +
  44 + Newer Article Content
  45 + """
  46 + When I go to "/tags/bar.html"
  47 + Then I should see "Not Found"
  48 + When I go to "/tags/newtag.html"
  49 + Then I should see "/2011-01-01-new-article.html"
  50 +
4 fixtures/calendar-app/source/layout.erb
@@ -5,7 +5,9 @@
5 5 <body>
6 6 <% if is_blog_article? %>
7 7 <%= yield %>
8   - <%= current_article.url %>
  8 + Url: <%= current_article.url %>
  9 + Previous: <%= current_article.previous_article.try(:url) %>
  10 + Next: <%= current_article.next_article.try(:url) %>
9 11 <% else %>
10 12 <%= yield %>
11 13 <% end %>
117 lib/middleman-blog/blog_article.rb
@@ -2,47 +2,24 @@
2 2
3 3 module Middleman
4 4 module Blog
5   - # A class encapsulating the properties of a blog article.
6   - # Access the underlying page object with {#page}.
7   - class BlogArticle
8   - # The {http://rubydoc.info/github/middleman/middleman/master/Middleman/Sitemap/Page Page} associated with this article.
9   - # @return [Middleman::Sitemap::Page]
10   - attr_reader :page
11   -
12   - # The date for this article, set from the filename
13   - # (and optionally refined by frontmatter)
14   - # @return [DateTime]
15   - attr_reader :date
16   -
17   - # The title of the article, set from frontmatter
  5 + # A module that adds blog-article methods to Resources.
  6 + module BlogArticle
  7 + # The "slug" of the article that shows up in its URL.
18 8 # @return [String]
19   - attr_reader :title
20   -
21   - # @private
22   - def initialize(app, page)
23   - @app = app
24   - @page = page
25   -
26   - self.update!
27   - end
28   -
29   - # @private
30   - def update!
31   - data, content = @app.frontmatter(@page.relative_path)
32   -
33   - @title = data["title"]
34   - @_raw = content
  9 + attr_accessor :slug
35 10
36   - find_date
  11 + # Render this resource
  12 + # @return [String]
  13 + def render(opts={}, locs={}, &block)
  14 + opts[:layout] = app.blog_layout
37 15
38   - @_body = nil
39   - @_summary = nil
  16 + super(opts, locs, &block)
40 17 end
41 18
42   - # The permalink url for this blog article.
  19 + # The title of the article, set from frontmatter
43 20 # @return [String]
44   - def url
45   - @page.url
  21 + def title
  22 + data["title"]
46 23 end
47 24
48 25 # The body of this article, in HTML. This is for
@@ -52,9 +29,9 @@ def url
52 29 # @return [String]
53 30 def body
54 31 @_body ||= begin
55   - all_content = @page.render(:layout => false)
  32 + all_content = render(:layout => false)
56 33
57   - if all_content =~ @app.blog_summary_separator
  34 + if all_content =~ app.blog_summary_separator
58 35 all_content.sub!($1, "")
59 36 end
60 37
@@ -69,21 +46,19 @@ def body
69 46 # @return [String]
70 47 def summary
71 48 @_summary ||= begin
72   - sum = if @_raw =~ @app.blog_summary_separator
73   - @_raw.split(@app.blog_summary_separator).first
  49 + all_content = render(:layout => false)
  50 + if all_content =~ app.blog_summary_separator
  51 + all_content.split(app.blog_summary_separator).first
74 52 else
75   - @_raw.match(/(.{1,#{@app.blog_summary_length}}.*?)(\n|\Z)/m).to_s
  53 + all_content.match(/(.{1,#{app.blog_summary_length}}.*?)(\n|\Z)/m).to_s
76 54 end
77   -
78   - engine = ::Tilt[@page.source_file].new { sum }
79   - engine.render
80 55 end
81 56 end
82 57
83 58 # A list of tags for this article, set from frontmatter.
84 59 # @return [Array<String>] (never nil)
85 60 def tags
86   - article_tags = @page.data["tags"]
  61 + article_tags = data["tags"]
87 62
88 63 if article_tags.is_a? String
89 64 article_tags.split(',').map(&:strip)
@@ -92,44 +67,56 @@ def tags
92 67 end
93 68 end
94 69
95   - private
96   -
97 70 # Attempt to figure out the date of the post. The date should be
98 71 # present in the source path, but users may also provide a date
99 72 # in the frontmatter in order to provide a time of day for sorting
100 73 # reasons.
101   - def find_date
102   - frontmatter_date = @page.data["date"]
  74 + #
  75 + # @return [DateTime]
  76 + def date
  77 + return @_date if @_date
  78 +
  79 + frontmatter_date = data["date"]
103 80
104 81 # First get the date from frontmatter
105 82 if frontmatter_date.is_a?(String)
106   - @date = DateTime.parse(frontmatter_date)
  83 + @_date = DateTime.parse(frontmatter_date)
107 84 else
108   - @date = frontmatter_date
  85 + @_date = frontmatter_date
109 86 end
110 87
111 88 # Next figure out the date from the filename
112   - if @app.blog_sources.include?(":year") &&
113   - @app.blog_sources.include?(":month") &&
114   - @app.blog_sources.include?(":day")
115   -
116   - matcher = Regexp.escape(@app.blog_sources).
117   - sub(":year", "(\\d{4})").
118   - sub(":month", "(\\d{2})").
119   - sub(":day", "(\\d{2})").
120   - sub(":title", "(.*)")
121   - matcher = /#{matcher}/
122   - date_parts = matcher.match(@page.path).captures
  89 + if app.blog_sources.include?(":year") &&
  90 + app.blog_sources.include?(":month") &&
  91 + app.blog_sources.include?(":day")
  92 +
  93 + date_parts = @app.blog.path_matcher.match(path).captures
123 94
124 95 filename_date = Date.new(date_parts[0].to_i, date_parts[1].to_i, date_parts[2].to_i)
125   - if @date
126   - raise "The date in #{@page.path}'s filename doesn't match the date in its frontmatter" unless @date.to_date == filename_date
  96 + if @_date
  97 + raise "The date in #{path}'s filename doesn't match the date in its frontmatter" unless @_date.to_date == filename_date
127 98 else
128   - @date = filename_date.to_datetime
  99 + @_date = filename_date.to_datetime
129 100 end
130 101 end
131 102
132   - raise "Blog post #{@page.path} needs a date in its filename or frontmatter" unless @date
  103 + raise "Blog post #{path} needs a date in its filename or frontmatter" unless @_date
  104 +
  105 + @_date
  106 + end
  107 +
  108 + # The previous (chronologically earlier) article before this one
  109 + # or nil if this is the first article.
  110 + # @return [Middleman::Sitemap::Resource]
  111 + def previous_article
  112 + app.blog.articles.find {|a| a.date < self.date }
  113 + end
  114 +
  115 + # The next (chronologically later) article after this one
  116 + # or nil if this is the most recent article.
  117 + # @return [Middleman::Sitemap::Resource]
  118 + def next_article
  119 + app.blog.articles.reverse.find {|a| a.date > self.date }
133 120 end
134 121 end
135 122 end
97 lib/middleman-blog/blog_data.rb
@@ -4,81 +4,86 @@ module Blog
4 4 # for the articles by various dimensions. Accessed via "blog" in
5 5 # templates.
6 6 class BlogData
  7 + # A regex for matching blog article source paths
  8 + # @return [Regex]
  9 + attr_reader :path_matcher
7 10
8 11 # @private
9 12 def initialize(app)
10 13 @app = app
11 14
12   - # A map from path to BlogArticle
13   - @_articles = {}
  15 + # A list of resources corresponding to blog articles
  16 + @_articles = []
  17 +
  18 + matcher = Regexp.escape(@app.blog_sources).
  19 + sub(/^\//, "").
  20 + sub(":year", "(\\d{4})").
  21 + sub(":month", "(\\d{2})").
  22 + sub(":day", "(\\d{2})").
  23 + sub(":title", "(.*)")
  24 +
  25 + @path_matcher = /^#{matcher}/
14 26 end
15 27
16 28 # A list of all blog articles, sorted by date
17   - # @return [Array<Middleman::Extensions::Blog::BlogArticle>]
  29 + # @return [Array<Middleman::Sitemap::Resource>]
18 30 def articles
19   - @_sorted_articles ||= begin
20   - @_articles.values.sort do |a, b|
21   - b.date <=> a.date
22   - end
  31 + @_articles.sort do |a,b|
  32 + b.date <=> a.date
23 33 end
24 34 end
25 35
26 36 # The BlogArticle for the given path, or nil if one doesn't exist.
27   - # @return [Middleman::Extensions::Blog::BlogArticle]
  37 + # @return [Middleman::Sitemap::Resource]
28 38 def article(path)
29   - @_articles[path.to_s]
  39 + article = @app.sitemap.find_resource_by_path(path.to_s)
  40 + if article && article.is_a?(BlogArticle)
  41 + article
  42 + else
  43 + nil
  44 + end
30 45 end
31 46
32 47 # Returns a map from tag name to an array
33 48 # of BlogArticles associated with that tag.
34   - # @return [Hash<String, Array<Middleman::Extensions::Blog::BlogArticle>>]
  49 + # @return [Hash<String, Array<Middleman::Sitemap::Resource>>]
35 50 def tags
36   - @tags ||= begin
37   - tags = {}
38   - @_articles.values.each do |article|
  51 + tags = {}
  52 + @_articles.each do |article|
39 53 article.tags.each do |tag|
40   - tags[tag] ||= []
41   - tags[tag] << article
42   - end
  54 + tags[tag] ||= []
  55 + tags[tag] << article
43 56 end
44   -
45   - tags
46 57 end
  58 +
  59 + tags
47 60 end
48 61
49   - # Notify the blog store that a particular file has updated
50   - # @private
51   - def touch_file(file)
52   - output_path = @app.sitemap.file_to_path(file)
53   - if @app.sitemap.exists?(output_path)
54   - if @_articles.has_key?(output_path)
55   - @_articles[output_path].update!
56   - else
57   - @_articles[output_path] = BlogArticle.new(@app, @app.sitemap.page(output_path))
58   - end
  62 + # Updates' blog articles destination paths to be the
  63 + # permalink.
  64 + # @return [void]
  65 + def manipulate_resource_list(resources)
  66 + @_articles = []
59 67
60   - self.update_data
61   - end
62   - end
  68 + resources.each do |resource|
  69 + if resource.path =~ path_matcher
  70 + resource.extend BlogArticle
  71 + resource.slug = $4
  72 +
  73 + # compute output path:
  74 + # substitute date parts to path pattern
  75 + resource.destination_path = @app.blog_permalink.
  76 + sub(':year', resource.date.year.to_s).
  77 + sub(':month', resource.date.month.to_s.rjust(2,'0')).
  78 + sub(':day', resource.date.day.to_s.rjust(2,'0')).
  79 + sub(':title', resource.slug)
63 80
64   - # Notify the blog store that a file has been removed
65   - # @private
66   - def remove_file(file)
67   - output_path = @app.sitemap.file_to_path(file)
  81 + resource.destination_path = Middleman::Util.normalize_path(resource.destination_path)
68 82
69   - if @_articles.has_key?(output_path)
70   - @_articles.delete(output_path)
71   - self.update_data
  83 + @_articles << resource
  84 + end
72 85 end
73 86 end
74   -
75   - protected
76   - # Clear cached data
77   - # @private
78   - def update_data
79   - @_sorted_articles = nil
80   - @tags = nil
81   - end
82 87 end
83 88 end
84 89 end
97 lib/middleman-blog/calendar_pages.rb
... ... @@ -0,0 +1,97 @@
  1 +module Middleman
  2 + module Blog
  3 +
  4 + # A sitemap plugin that adds month/day/year pages to the sitemap
  5 + # based on the dates of blog articles.
  6 + class CalendarPages
  7 + def initialize(app)
  8 + @app = app
  9 + end
  10 +
  11 + # Update the main sitemap resource list
  12 + # @return [void]
  13 + def manipulate_resource_list(resources)
  14 + new_resources = []
  15 + # Set up date pages if the appropriate templates have been specified
  16 + @app.blog.articles.group_by {|a| a.date.year }.each do |year, year_articles|
  17 + if @app.respond_to? :blog_year_template
  18 + @app.ignore @app.blog_year_template
  19 +
  20 + path = Middleman::Util.normalize_path(@app.blog_year_path(year))
  21 +
  22 + p = ::Middleman::Sitemap::Resource.new(
  23 + @app.sitemap,
  24 + path
  25 + )
  26 + p.proxy_to(@app.blog_year_template)
  27 +
  28 + set_locals_year = Proc.new do
  29 + @year = year
  30 + @articles = year_articles
  31 + end
  32 +
  33 + @app.sitemap.provides_metadata_for_path path do |path|
  34 + { :blocks => set_locals_year }
  35 + end
  36 +
  37 + new_resources << p
  38 + end
  39 +
  40 + year_articles.group_by {|a| a.date.month }.each do |month, month_articles|
  41 + if @app.respond_to? :blog_month_template
  42 + @app.ignore @app.blog_month_template
  43 +
  44 + path = Middleman::Util.normalize_path(@app.blog_month_path(year, month))
  45 +
  46 + p = ::Middleman::Sitemap::Resource.new(
  47 + @app.sitemap,
  48 + path
  49 + )
  50 + p.proxy_to(@app.blog_month_template)
  51 +
  52 + set_locals_month = Proc.new do
  53 + @year = year
  54 + @month = month
  55 + @articles = month_articles
  56 + end
  57 +
  58 + @app.sitemap.provides_metadata_for_path path do |path|
  59 + { :blocks => [ set_locals_month ] }
  60 + end
  61 +
  62 + new_resources << p
  63 + end
  64 +
  65 + month_articles.group_by {|a| a.date.day }.each do |day, day_articles|
  66 + if @app.respond_to? :blog_day_template
  67 + @app.ignore @app.blog_day_template
  68 +
  69 + path = Middleman::Util.normalize_path(@app.blog_day_path(year, month, day))
  70 + p = ::Middleman::Sitemap::Resource.new(
  71 + @app.sitemap,
  72 + path
  73 + )
  74 + p.proxy_to(@app.blog_month_template)
  75 +
  76 + set_locals_day = Proc.new do
  77 + @year = year
  78 + @month = month
  79 + @day = day
  80 + @articles = day_articles
  81 + end
  82 +
  83 + @app.sitemap.provides_metadata_for_path path do |path|
  84 + { :blocks => [ set_locals_day ] }
  85 + end
  86 +
  87 + new_resources << p
  88 + end
  89 + end
  90 + end
  91 + end
  92 +
  93 + resources + new_resources
  94 + end
  95 + end
  96 + end
  97 +end
129 lib/middleman-blog/extension.rb
... ... @@ -1,19 +1,21 @@
1 1 require 'middleman-blog/blog_data'
2 2 require 'middleman-blog/blog_article'
  3 +require 'middleman-blog/calendar_pages'
  4 +require 'middleman-blog/tag_pages'
3 5
4 6 module Middleman
5 7 module Blog
6 8 class << self
7 9 def registered(app)
8   - app.set :blog_permalink, ":year/:month/:day/:title.html"
  10 + app.set :blog_permalink, "/:year/:month/:day/:title.html"
9 11 app.set :blog_sources, ":year-:month-:day-:title.html"
10 12 app.set :blog_taglink, "tags/:tag.html"
11 13 app.set :blog_layout, "layout"
12 14 app.set :blog_summary_separator, /(READMORE)/
13 15 app.set :blog_summary_length, 250
14   - app.set :blog_year_link, ":year.html"
15   - app.set :blog_month_link, ":year/:month.html"
16   - app.set :blog_day_link, ":year/:month/:day.html"
  16 + app.set :blog_year_link, "/:year.html"
  17 + app.set :blog_month_link, "/:year/:month.html"
  18 + app.set :blog_day_link, "/:year/:month/:day.html"
17 19 app.set :blog_default_extension, ".markdown"
18 20
19 21 app.send :include, Helpers
@@ -31,102 +33,35 @@ def registered(app)
31 33 set :blog_day_template, blog_calendar_template
32 34 end
33 35
34   - matcher = Regexp.escape(blog_sources).
35   - sub(/^\//, "").
36   - sub(":year", "(\\d{4})").
37   - sub(":month", "(\\d{2})").
38   - sub(":day", "(\\d{2})").
39   - sub(":title", "(.*)")
40   -
41   - path_matcher = /^#{matcher}/
42   - file_matcher = /^#{source_dir}\/#{matcher}/
43   -
44   - sitemap.reroute do |destination, page|
45   - if page.path =~ path_matcher
46   - # This doesn't allow people to omit one part!
47   - year = $1
48   - month = $2
49   - day = $3
50   - title = $4
51   -
52   - # compute output path:
53   - # substitute date parts to path pattern
54   - # get date from frontmatter, path
55   - blog_permalink.
56   - sub(':year', year).
57   - sub(':month', month).
58   - sub(':day', day).
59   - sub(':title', title)
60   - else
61   - destination
  36 + app.ready do
  37 + sitemap.register_resource_list_manipulator(
  38 + :blog_articles,
  39 + blog,
  40 + false
  41 + )
  42 +
  43 + if defined? blog_tag_template
  44 + ignore blog_tag_template
  45 +
  46 + sitemap.register_resource_list_manipulator(
  47 + :blog_tags,
  48 + TagPages.new(self),
  49 + false
  50 + )
62 51 end
63   - end
64 52
65   - frontmatter_changed file_matcher do |file|
66   - blog.touch_file(file)
67   - end
  53 + if defined? blog_year_template ||
  54 + defined? blog_month_template ||
  55 + defined? blog_day_template
68 56
69   - self.files.deleted file_matcher do |file|
70   - self.blog.remove_file(file)
71   - end
72   -
73   - provides_metadata file_matcher do
74   - {
75   - :options => {
76   - :layout => blog_layout
77   - }
78   - }
79   - end
80   - end
81   -
82   - app.ready do
83   - # Set up tag pages if the tag template has been specified
84   - if defined? blog_tag_template
85   - page blog_tag_template, :ignore => true
86   -
87   - blog.tags.each do |tag, articles|
88   - page tag_path(tag), :proxy => blog_tag_template do
89   - @tag = tag
90   - @articles = articles
91   - end
92   - end
93   - end
94   -
95   - # Set up date pages if the appropriate templates have been specified
96   - blog.articles.group_by {|a| a.date.year }.each do |year, year_articles|
97   - if defined? blog_year_template
98   - page blog_year_template, :ignore => true
99   -
100   - page blog_year_path(year), :proxy => blog_year_template do
101   - @year = year
102   - @articles = year_articles
103   - end
  57 + sitemap.register_resource_list_manipulator(
  58 + :blog_calendar,
  59 + CalendarPages.new(self),
  60 + false
  61 + )
104 62 end
105   -
106   - year_articles.group_by {|a| a.date.month }.each do |month, month_articles|
107   - if defined? blog_month_template
108   - page blog_month_template, :ignore => true
109 63
110   - page blog_month_path(year, month), :proxy => blog_month_template do
111   - @year = year
112   - @month = month
113   - @articles = month_articles
114   - end
115   - end
116   -
117   - month_articles.group_by {|a| a.date.day }.each do |day, day_articles|
118   - if defined? blog_day_template
119   - page blog_day_template, :ignore => true
120   -
121   - page blog_day_path(year, month, day), :proxy => blog_day_template do
122   - @year = year
123   - @month = month
124   - @day = day
125   - @articles = day_articles
126   - end
127   - end
128   - end
129   - end
  64 + sitemap.rebuild_resource_list!(:registered_new)
130 65 end
131 66 end
132 67 end
@@ -148,8 +83,8 @@ def is_blog_article?
148 83 !current_article.nil?
149 84 end
150 85
151   - # Get a {BlogArticle} representing the current article.
152   - # @return [BlogArticle]
  86 + # Get a {Resource} with mixed in {BlogArticle} methods representing the current article.
  87 + # @return [Middleman::Sitemap::Resource]
153 88 def current_article
154 89 blog.article(current_page.path)
155 90 end
39 lib/middleman-blog/tag_pages.rb
... ... @@ -0,0 +1,39 @@
  1 +module Middleman
  2 + module Blog
  3 +
  4 + # A sitemap plugin that adds tag pages to the sitemap
  5 + # based on the tags of blog articles.
  6 + class TagPages
  7 + def initialize(app)
  8 + @app = app
  9 + end
  10 +
  11 + # Update the main sitemap resource list
  12 + # @return [void]
  13 + def manipulate_resource_list(resources)
  14 + resources + @app.blog.tags.map do |tag, articles|
  15 + path = @app.tag_path(tag)
  16 +
  17 + p = ::Middleman::Sitemap::Resource.new(
  18 + @app.sitemap,
  19 + path
  20 + )
  21 + p.proxy_to(@app.blog_tag_template)
  22 +
  23 + set_locals = Proc.new do
  24 + @tag = tag
  25 + @articles = articles
  26 + end
  27 +
  28 + # TODO: how to keep from adding duplicates here?
  29 + # How could we better set locals?
  30 + @app.sitemap.provides_metadata_for_path path do |path|
  31 + { :blocks => [ set_locals ] }
  32 + end
  33 +
  34 + p
  35 + end
  36 + end
  37 + end
  38 + end
  39 +end

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.