bridgetown-feed 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +27 -0
  5. data/Gemfile +11 -0
  6. data/History.markdown +5 -0
  7. data/LICENSE.txt +23 -0
  8. data/README.md +201 -0
  9. data/Rakefile +8 -0
  10. data/bridgetown-feed.gemspec +28 -0
  11. data/lib/bridgetown-feed.rb +12 -0
  12. data/lib/bridgetown-feed/feed.xml +96 -0
  13. data/lib/bridgetown-feed/generator.rb +111 -0
  14. data/lib/bridgetown-feed/meta-tag.rb +37 -0
  15. data/lib/bridgetown-feed/page-without-a-file.rb +9 -0
  16. data/lib/bridgetown-feed/version.rb +7 -0
  17. data/script/bootstrap +3 -0
  18. data/script/cibuild +7 -0
  19. data/script/fmt +10 -0
  20. data/script/release +7 -0
  21. data/script/test +4 -0
  22. data/spec/bridgetown-feed_spec.rb +525 -0
  23. data/spec/fixtures/bridgetown.config.yml +9 -0
  24. data/spec/fixtures/src/_collection/2018-01-01-collection-doc.md +4 -0
  25. data/spec/fixtures/src/_collection/2018-01-02-collection-category-doc.md +5 -0
  26. data/spec/fixtures/src/_data/authors.yml +5 -0
  27. data/spec/fixtures/src/_layouts/some_default.html +11 -0
  28. data/spec/fixtures/src/_posts/2013-12-12-dec-the-second.md +7 -0
  29. data/spec/fixtures/src/_posts/2014-03-02-march-the-second.md +6 -0
  30. data/spec/fixtures/src/_posts/2014-03-04-march-the-fourth.md +9 -0
  31. data/spec/fixtures/src/_posts/2015-01-12-a-draft.md +5 -0
  32. data/spec/fixtures/src/_posts/2015-01-18-jekyll-last-modified-at.md +5 -0
  33. data/spec/fixtures/src/_posts/2015-02-12-strip-newlines.md +6 -0
  34. data/spec/fixtures/src/_posts/2015-05-12-liquid.md +7 -0
  35. data/spec/fixtures/src/_posts/2015-05-12-pre.html +8 -0
  36. data/spec/fixtures/src/_posts/2015-05-18-author-detail.md +9 -0
  37. data/spec/fixtures/src/_posts/2015-08-08-stuck-in-the-middle.html +6 -0
  38. data/spec/fixtures/src/_posts/2016-04-25-author-reference.md +6 -0
  39. data/spec/fixtures/src/feed.xslt.xml +0 -0
  40. data/spec/spec_helper.rb +35 -0
  41. metadata +210 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownFeed
4
+ class Generator < Bridgetown::Generator
5
+ priority :lowest
6
+
7
+ # Main plugin action, called by Bridgetown-core
8
+ def generate(site)
9
+ @site = site
10
+ collections.each do |name, meta|
11
+ Bridgetown.logger.info "Bridgetown Feed:", "Generating feed for #{name}"
12
+ (meta["categories"] + [nil]).each do |category|
13
+ path = feed_path(:collection => name, :category => category)
14
+ next if file_exists?(path)
15
+
16
+ @site.pages << make_page(path, :collection => name, :category => category)
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # Matches all whitespace that follows
24
+ # 1. A '>', which closes an XML tag or
25
+ # 2. A '}', which closes a Liquid tag
26
+ # We will strip all of this whitespace to minify the template
27
+ MINIFY_REGEX = %r!(?<=>|})\s+!.freeze
28
+
29
+ # Returns the plugin's config or an empty hash if not set
30
+ def config
31
+ @config ||= @site.config["feed"] || {}
32
+ end
33
+
34
+ # Determines the destination path of a given feed
35
+ #
36
+ # collection - the name of a collection, e.g., "posts"
37
+ # category - a category within that collection, e.g., "news"
38
+ #
39
+ # Will return "/feed.xml", or the config-specified default feed for posts
40
+ # Will return `/feed/category.xml` for post categories
41
+ # WIll return `/feed/collection.xml` for other collections
42
+ # Will return `/feed/collection/category.xml` for other collection categories
43
+ def feed_path(collection: "posts", category: nil)
44
+ prefix = collection == "posts" ? "/feed" : "/feed/#{collection}"
45
+ return "#{prefix}/#{category}.xml" if category
46
+
47
+ collections.dig(collection, "path") || "#{prefix}.xml"
48
+ end
49
+
50
+ # Returns a hash representing all collections to be processed and their metadata
51
+ # in the form of { collection_name => { categories = [...], path = "..." } }
52
+ def collections
53
+ return @collections if defined?(@collections)
54
+
55
+ @collections = if config["collections"].is_a?(Array)
56
+ config["collections"].map { |c| [c, {}] }.to_h
57
+ elsif config["collections"].is_a?(Hash)
58
+ config["collections"]
59
+ else
60
+ {}
61
+ end
62
+
63
+ @collections = normalize_posts_meta(@collections)
64
+ @collections.each_value do |meta|
65
+ meta["categories"] = (meta["categories"] || []).to_set
66
+ end
67
+
68
+ @collections
69
+ end
70
+
71
+ # Path to feed.xml template file
72
+ def feed_source_path
73
+ @feed_source_path ||= File.expand_path "feed.xml", __dir__
74
+ end
75
+
76
+ def feed_template
77
+ @feed_template ||= File.read(feed_source_path).gsub(MINIFY_REGEX, "")
78
+ end
79
+
80
+ # Checks if a file already exists in the site source
81
+ def file_exists?(file_path)
82
+ File.exist? @site.in_source_dir(file_path)
83
+ end
84
+
85
+ # Generates contents for a file
86
+
87
+ def make_page(file_path, collection: "posts", category: nil)
88
+ PageWithoutAFile.new(@site, __dir__, "", file_path).tap do |file|
89
+ file.content = feed_template
90
+ file.data.merge!(
91
+ "layout" => nil,
92
+ "sitemap" => false,
93
+ "xsl" => file_exists?("feed.xslt.xml"),
94
+ "collection" => collection,
95
+ "category" => category
96
+ )
97
+ file.output
98
+ end
99
+ end
100
+
101
+ # Special case the "posts" collection, which, for ease of use and backwards
102
+ # compatability, can be configured via top-level keys or directly as a collection
103
+ def normalize_posts_meta(hash)
104
+ hash["posts"] ||= {}
105
+ hash["posts"]["path"] ||= config["path"]
106
+ hash["posts"]["categories"] ||= config["categories"]
107
+ config["path"] ||= hash["posts"]["path"]
108
+ hash
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownFeed
4
+ class MetaTag < Liquid::Tag
5
+ # Use Bridgetown's native relative_url filter
6
+ include Bridgetown::Filters::URLFilters
7
+
8
+ def render(context)
9
+ @context = context
10
+ attrs = attributes.map { |k, v| %(#{k}="#{v}") }.join(" ")
11
+ "<link #{attrs} />"
12
+ end
13
+
14
+ private
15
+
16
+ def config
17
+ @config ||= @context.registers[:site].config
18
+ end
19
+
20
+ def attributes
21
+ {
22
+ :type => "application/atom+xml",
23
+ :rel => "alternate",
24
+ :href => absolute_url(path),
25
+ :title => title,
26
+ }.keep_if { |_, v| v }
27
+ end
28
+
29
+ def path
30
+ config.dig("feed", "path") || "feed.xml"
31
+ end
32
+
33
+ def title
34
+ config["title"] || config["name"]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownFeed
4
+ class PageWithoutAFile < Bridgetown::Page
5
+ def read_yaml(*)
6
+ @data ||= {}
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bridgetown
4
+ module Feed
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ #! /bin/bash
2
+
3
+ bundle install
@@ -0,0 +1,7 @@
1
+ #! /bin/bash
2
+
3
+ set -e
4
+
5
+ script/test
6
+ script/fmt
7
+ bundle exec rake build
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Rubocop $(bundle exec rubocop --version)"
5
+ bundle exec rubocop -D -E $@
6
+ success=$?
7
+ if ((success != 0)); then
8
+ echo -e "\nTry running \`script/fmt -a\` to automatically fix errors"
9
+ fi
10
+ exit $success
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ # Tag and push a release.
3
+
4
+ set -e
5
+
6
+ script/cibuild
7
+ bundle exec rake release
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ set -ex
3
+
4
+ bundle exec rspec "$@"
@@ -0,0 +1,525 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe(BridgetownFeed) do
6
+ let(:overrides) { {} }
7
+ let(:config) do
8
+ Bridgetown.configuration(Bridgetown::Utils.deep_merge_hashes({
9
+ "full_rebuild" => true,
10
+ "root_dir" => root_dir,
11
+ "source" => source_dir,
12
+ "destination" => dest_dir,
13
+ "show_drafts" => true,
14
+ "url" => "http://example.org",
15
+ "name" => "My awesome site",
16
+ "author" => {
17
+ "name" => "Dr. Bridgetown",
18
+ },
19
+ "collections" => {
20
+ "my_collection" => { "output" => true },
21
+ "other_things" => { "output" => false },
22
+ },
23
+ }, overrides))
24
+ end
25
+ let(:site) { Bridgetown::Site.new(config) }
26
+ let(:contents) { File.read(dest_dir("feed.xml")) }
27
+ let(:context) { make_context(:site => site) }
28
+ let(:feed_meta) { Liquid::Template.parse("{% feed_meta %}").render!(context, {}) }
29
+ before(:each) do
30
+ site.process
31
+ end
32
+
33
+ it "has no layout" do
34
+ expect(contents).not_to match(%r!\ATHIS IS MY LAYOUT!)
35
+ end
36
+
37
+ it "creates a feed.xml file" do
38
+ expect(Pathname.new(dest_dir("feed.xml"))).to exist
39
+ end
40
+
41
+ it "doesn't have multiple new lines or trailing whitespace" do
42
+ expect(contents).to_not match %r!\s+\n!
43
+ expect(contents).to_not match %r!\n{2,}!
44
+ end
45
+
46
+ it "puts all the posts in the feed.xml file" do
47
+ expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
48
+ expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html"
49
+ expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html"
50
+ expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html"
51
+ expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html"
52
+ end
53
+
54
+ it "does not include assets or any static files that aren't .html" do
55
+ expect(contents).not_to match "http://example.org/images/hubot.png"
56
+ expect(contents).not_to match "http://example.org/feeds/atom.xml"
57
+ end
58
+
59
+ it "preserves linebreaks in preformatted text in posts" do
60
+ expect(contents).to match "Line 1\nLine 2\nLine 3"
61
+ end
62
+
63
+ it "supports post author name as an object" do
64
+ expect(contents).to match %r!<author>\s*<name>Ben</name>\s*<email>ben@example\.com</email>\s*<uri>http://ben\.balter\.com</uri>\s*</author>!
65
+ end
66
+
67
+ it "supports post author name as a string" do
68
+ expect(contents).to match %r!<author>\s*<name>Pat</name>\s*</author>!
69
+ end
70
+
71
+ it "does not output author tag no author is provided" do
72
+ expect(contents).not_to match %r!<author>\s*<name></name>\s*</author>!
73
+ end
74
+
75
+ it "does use author reference with data from _data/authors.yml" do
76
+ expect(contents).to match %r!<author>\s*<name>Garth</name>\s*<email>example@mail\.com</email>\s*<uri>http://garthdb\.com</uri>\s*</author>!
77
+ end
78
+
79
+ it "converts markdown posts to HTML" do
80
+ expect(contents).to match %r!&lt;p&gt;March the second\!&lt;/p&gt;!
81
+ end
82
+
83
+ it "uses last_modified_at where available" do
84
+ expect(contents).to match %r!<updated>2015-05-12T13:27:59\+00:00</updated>!
85
+ end
86
+
87
+ it "replaces newlines in posts to spaces" do
88
+ expect(contents).to match '<title type="html">The plugin will properly strip newlines.</title>'
89
+ end
90
+
91
+ it "renders Liquid inside posts" do
92
+ expect(contents).to match "Liquid is rendered."
93
+ expect(contents).not_to match "Liquid is not rendered."
94
+ end
95
+
96
+ context "images" do
97
+ let(:image1) { 'http://example.org/image.png' }
98
+ let(:image2) { 'https://cdn.example.org/absolute.png?h=188&amp;w=250' }
99
+ let(:image3) { 'http://example.org/object-image.png' }
100
+
101
+ it "includes the item image" do
102
+ expect(contents).to include(%(<media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="#{image1}" />))
103
+ expect(contents).to include(%(<media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="#{image2}" />))
104
+ expect(contents).to include(%(<media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="#{image3}" />))
105
+ end
106
+
107
+ it "included media content for mail templates (Mailchimp)" do
108
+ expect(contents).to include(%(<media:content medium="image" url="#{image1}" xmlns:media="http://search.yahoo.com/mrss/" />))
109
+ expect(contents).to include(%(<media:content medium="image" url="#{image2}" xmlns:media="http://search.yahoo.com/mrss/" />))
110
+ expect(contents).to include(%(<media:content medium="image" url="#{image3}" xmlns:media="http://search.yahoo.com/mrss/" />))
111
+ end
112
+ end
113
+
114
+ context "parsing" do
115
+ let(:feed) { RSS::Parser.parse(contents) }
116
+
117
+ it "outputs an RSS feed" do
118
+ expect(feed.feed_type).to eql("atom")
119
+ expect(feed.feed_version).to eql("1.0")
120
+ expect(feed.encoding).to eql("UTF-8")
121
+ expect(feed.lang).to be_nil
122
+ expect(feed.valid?).to eql(true)
123
+ end
124
+
125
+ it "outputs the link" do
126
+ expect(feed.link.href).to eql("http://example.org/feed.xml")
127
+ end
128
+
129
+ it "outputs the generator" do
130
+ expect(feed.generator.content).to eql("Bridgetown")
131
+ expect(feed.generator.version).to eql(Bridgetown::VERSION)
132
+ end
133
+
134
+ it "includes the items" do
135
+ expect(feed.items.count).to eql(10)
136
+ end
137
+
138
+ it "includes item contents" do
139
+ post = feed.items.last
140
+ expect(post.title.content).to eql("Dec The Second")
141
+ expect(post.link.href).to eql("http://example.org/news/2013/12/12/dec-the-second.html")
142
+ expect(post.published.content).to eql(Time.parse("2013-12-12"))
143
+ end
144
+
145
+ it "includes the item's excerpt" do
146
+ post = feed.items.last
147
+ expect(post.summary.content).to eql("Foo")
148
+ end
149
+
150
+ it "doesn't include the item's excerpt if blank" do
151
+ post = feed.items.first
152
+ expect(post.summary).to be_nil
153
+ end
154
+
155
+ context "with site.lang set" do
156
+ lang = "en_US"
157
+ let(:overrides) { { "lang" => lang } }
158
+ it "outputs a valid feed" do
159
+ expect(feed.feed_type).to eql("atom")
160
+ expect(feed.feed_version).to eql("1.0")
161
+ expect(feed.encoding).to eql("UTF-8")
162
+ expect(feed.valid?).to eql(true)
163
+ end
164
+
165
+ it "outputs the correct language" do
166
+ expect(feed.lang).to eql(lang)
167
+ end
168
+
169
+ it "sets the language of entries" do
170
+ post = feed.items.first
171
+ expect(post.lang).to eql(lang)
172
+ end
173
+
174
+ it "renders the feed meta" do
175
+ expected = %r!<link href="http://example.org/" rel="alternate" type="text/html" hreflang="#{lang}" />!
176
+ expect(contents).to match(expected)
177
+ end
178
+ end
179
+
180
+ context "with site.title set" do
181
+ let(:site_title) { "My Site Title" }
182
+ let(:overrides) { { "title" => site_title } }
183
+
184
+ it "uses site.title for the title" do
185
+ expect(feed.title.content).to eql(site_title)
186
+ end
187
+ end
188
+
189
+ context "with site.name set" do
190
+ let(:site_name) { "My Site Name" }
191
+ let(:overrides) { { "name" => site_name } }
192
+
193
+ it "uses site.name for the title" do
194
+ expect(feed.title.content).to eql(site_name)
195
+ end
196
+ end
197
+
198
+ context "with site.name and site.title set" do
199
+ let(:site_title) { "My Site Title" }
200
+ let(:site_name) { "My Site Name" }
201
+ let(:overrides) { { "title" => site_title, "name" => site_name } }
202
+
203
+ it "uses site.title for the title, dropping site.name" do
204
+ expect(feed.title.content).to eql(site_title)
205
+ end
206
+ end
207
+ end
208
+
209
+ context "smartify" do
210
+ let(:site_title) { "Pat's Site" }
211
+ let(:overrides) { { "title" => site_title } }
212
+ let(:feed) { RSS::Parser.parse(contents) }
213
+
214
+ it "processes site title with SmartyPants" do
215
+ expect(feed.title.content).to eql("Pat’s Site")
216
+ end
217
+ end
218
+
219
+ context "validation" do
220
+ it "validates" do
221
+ skip "Typhoeus couldn't find the 'libcurl' module on Windows" if Gem.win_platform?
222
+ # See https://validator.w3.org/docs/api.html
223
+ url = "https://validator.w3.org/feed/check.cgi?output=soap12"
224
+ response = Typhoeus.post(url, :body => { :rawdata => contents }, :accept_encoding => "gzip")
225
+ pending "Something went wrong with the W3 validator" unless response.success?
226
+ result = Nokogiri::XML(response.body)
227
+ result.remove_namespaces!
228
+
229
+ result.css("warning").each do |warning|
230
+ # Quiet a warning that results from us passing the feed as a string
231
+ next if warning.css("text").text =~ %r!Self reference doesn't match document location!
232
+
233
+ # Quiet expected warning that results from blank summary test case
234
+ next if warning.css("text").text =~ %r!(content|summary) should not be blank!
235
+
236
+ # Quiet expected warning about multiple posts with same updated time
237
+ next if warning.css("text").text =~ %r!Two entries with the same value for atom:updated!
238
+
239
+ warn "Validation warning: #{warning.css("text").text} on line #{warning.css("line").text} column #{warning.css("column").text}"
240
+ end
241
+
242
+ errors = result.css("error").map do |error|
243
+ "Validation error: #{error.css("text").text} on line #{error.css("line").text} column #{error.css("column").text}"
244
+ end
245
+
246
+ expect(result.css("validity").text).to eql("true"), errors.join("\n")
247
+ end
248
+ end
249
+
250
+ context "with a baseurl" do
251
+ let(:overrides) do
252
+ { "baseurl" => "/bass" }
253
+ end
254
+
255
+ it "correctly adds the baseurl to the posts" do
256
+ expect(contents).to match "http://example.org/bass/updates/bridgetown/2014/03/04/march-the-fourth.html"
257
+ expect(contents).to match "http://example.org/bass/news/2014/03/02/march-the-second.html"
258
+ expect(contents).to match "http://example.org/bass/news/2013/12/12/dec-the-second.html"
259
+ end
260
+
261
+ it "renders the feed meta" do
262
+ expected = 'href="http://example.org/bass/feed.xml"'
263
+ expect(feed_meta).to include(expected)
264
+ end
265
+ end
266
+
267
+ context "feed meta" do
268
+ it "renders the feed meta" do
269
+ expected = '<link type="application/atom+xml" rel="alternate" href="http://example.org/feed.xml" title="My awesome site" />'
270
+ expect(feed_meta).to eql(expected)
271
+ end
272
+
273
+ context "with a blank site name" do
274
+ let(:config) do
275
+ Bridgetown.configuration(
276
+ "source" => source_dir,
277
+ "destination" => dest_dir,
278
+ "url" => "http://example.org"
279
+ )
280
+ end
281
+
282
+ it "does not output blank title" do
283
+ expect(feed_meta).not_to include("title=")
284
+ end
285
+ end
286
+ end
287
+
288
+ context "changing the feed path" do
289
+ let(:overrides) do
290
+ {
291
+ "feed" => {
292
+ "path" => "atom.xml",
293
+ },
294
+ }
295
+ end
296
+
297
+ it "should write to atom.xml" do
298
+ expect(Pathname.new(dest_dir("atom.xml"))).to exist
299
+ end
300
+
301
+ it "renders the feed meta with custom feed path" do
302
+ expected = 'href="http://example.org/atom.xml"'
303
+ expect(feed_meta).to include(expected)
304
+ end
305
+ end
306
+
307
+ context "changing the file path via collection meta" do
308
+ let(:overrides) do
309
+ {
310
+ "feed" => {
311
+ "collections" => {
312
+ "posts" => {
313
+ "path" => "atom.xml",
314
+ },
315
+ },
316
+ },
317
+ }
318
+ end
319
+
320
+ it "should write to atom.xml" do
321
+ expect(Pathname.new(dest_dir("atom.xml"))).to exist
322
+ end
323
+
324
+ it "renders the feed meta with custom feed path" do
325
+ expected = 'href="http://example.org/atom.xml"'
326
+ expect(feed_meta).to include(expected)
327
+ end
328
+ end
329
+
330
+ context "feed stylesheet" do
331
+ it "includes the stylesheet" do
332
+ expect(contents).to include('<?xml-stylesheet type="text/xml" href="http://example.org/feed.xslt.xml"?>')
333
+ end
334
+ end
335
+
336
+ context "with site.lang set" do
337
+ let(:overrides) { { "lang" => "en-US" } }
338
+
339
+ it "should set the language" do
340
+ expect(contents).to match 'type="text/html" hreflang="en-US" />'
341
+ end
342
+ end
343
+
344
+ context "with post.lang set" do
345
+ it "should set the language for that entry" do
346
+ expect(contents).to match '<entry xml:lang="en">'
347
+ expect(contents).to match '<entry>'
348
+ end
349
+ end
350
+
351
+ context "categories" do
352
+ context "with top-level post categories" do
353
+ let(:overrides) do
354
+ {
355
+ "feed" => { "categories" => ["news"] },
356
+ }
357
+ end
358
+ let(:news_feed) { File.read(dest_dir("feed/news.xml")) }
359
+
360
+ it "outputs the primary feed" do
361
+ expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
362
+ expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html"
363
+ expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html"
364
+ expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html"
365
+ expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html"
366
+ end
367
+
368
+ it "outputs the category feed" do
369
+ expect(news_feed).to match '<title type="html">My awesome site | News</title>'
370
+ expect(news_feed).to match "http://example.org/news/2014/03/02/march-the-second.html"
371
+ expect(news_feed).to match "http://example.org/news/2013/12/12/dec-the-second.html"
372
+ expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
373
+ expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
374
+ end
375
+ end
376
+
377
+ context "with collection-level post categories" do
378
+ let(:overrides) do
379
+ {
380
+ "feed" => {
381
+ "collections" => {
382
+ "posts" => {
383
+ "categories" => ["news"],
384
+ },
385
+ },
386
+ },
387
+ }
388
+ end
389
+ let(:news_feed) { File.read(dest_dir("feed/news.xml")) }
390
+
391
+ it "outputs the primary feed" do
392
+ expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
393
+ expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html"
394
+ expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html"
395
+ expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html"
396
+ expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html"
397
+ end
398
+
399
+ it "outputs the category feed" do
400
+ expect(news_feed).to match '<title type="html">My awesome site | News</title>'
401
+ expect(news_feed).to match "http://example.org/news/2014/03/02/march-the-second.html"
402
+ expect(news_feed).to match "http://example.org/news/2013/12/12/dec-the-second.html"
403
+ expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
404
+ expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
405
+ end
406
+ end
407
+ end
408
+
409
+ context "collections" do
410
+ let(:collection_feed) { File.read(dest_dir("feed/collection.xml")) }
411
+
412
+ context "when initialized as an array" do
413
+ let(:overrides) do
414
+ {
415
+ "collections" => {
416
+ "collection" => {
417
+ "output" => true,
418
+ },
419
+ },
420
+ "feed" => { "collections" => ["collection"] },
421
+ }
422
+ end
423
+
424
+ it "outputs the collection feed" do
425
+ expect(collection_feed).to match '<title type="html">My awesome site | Collection</title>'
426
+ expect(collection_feed).to match "http://example.org/collection/2018-01-01-collection-doc.html"
427
+ expect(collection_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html"
428
+ expect(collection_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
429
+ expect(collection_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
430
+ end
431
+ end
432
+
433
+ context "with categories" do
434
+ let(:overrides) do
435
+ {
436
+ "collections" => {
437
+ "collection" => {
438
+ "output" => true,
439
+ },
440
+ },
441
+ "feed" => {
442
+ "collections" => {
443
+ "collection" => {
444
+ "categories" => ["news"],
445
+ },
446
+ },
447
+ },
448
+ }
449
+ end
450
+ let(:news_feed) { File.read(dest_dir("feed/collection/news.xml")) }
451
+
452
+ it "outputs the collection category feed" do
453
+ expect(news_feed).to match '<title type="html">My awesome site | Collection | News</title>'
454
+ expect(news_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html"
455
+ expect(news_feed).to_not match "http://example.org/collection/2018-01-01-collection-doc.html"
456
+ expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth.html"
457
+ expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html"
458
+ end
459
+ end
460
+
461
+ context "with a custom path" do
462
+ let(:overrides) do
463
+ {
464
+ "collections" => {
465
+ "collection" => {
466
+ "output" => true,
467
+ },
468
+ },
469
+ "feed" => {
470
+ "collections" => {
471
+ "collection" => {
472
+ "categories" => ["news"],
473
+ "path" => "custom.xml",
474
+ },
475
+ },
476
+ },
477
+ }
478
+ end
479
+
480
+ it "should write to the custom path" do
481
+ expect(Pathname.new(dest_dir("custom.xml"))).to exist
482
+ expect(Pathname.new(dest_dir("feed/collection.xml"))).to_not exist
483
+ expect(Pathname.new(dest_dir("feed/collection/news.xml"))).to exist
484
+ end
485
+ end
486
+ end
487
+
488
+ context "excerpt_only flag" do
489
+ context "backward compatibility for no excerpt_only flag" do
490
+ it "should be in contents" do
491
+ expect(contents).to match '<content '
492
+ end
493
+ end
494
+
495
+ context "when site.excerpt_only flag is true" do
496
+ let(:overrides) do
497
+ { "feed" => { "excerpt_only" => true } }
498
+ end
499
+
500
+ it "should not set any contents" do
501
+ expect(contents).to_not match '<content '
502
+ end
503
+ end
504
+
505
+ context "when site.excerpt_only flag is false" do
506
+ let(:overrides) do
507
+ { "feed" => { "excerpt_only" => false } }
508
+ end
509
+
510
+ it "should be in contents" do
511
+ expect(contents).to match '<content '
512
+ end
513
+ end
514
+
515
+ context "when post.excerpt_only flag is true" do
516
+ let(:overrides) do
517
+ { "feed" => { "excerpt_only" => false } }
518
+ end
519
+
520
+ it "should not be in contents" do
521
+ expect(contents).to_not match "This content should not be in feed.</content>"
522
+ end
523
+ end
524
+ end
525
+ end