From af6272fcb86039bc814263524b6e9d3a5fce210f Mon Sep 17 00:00:00 2001 From: Samuel Mortenson Date: Mon, 29 May 2017 19:16:46 -0700 Subject: [PATCH 1/4] Generate a feed per-category. --- lib/jekyll-feed/feed.xml | 5 +++++ lib/jekyll-feed/generator.rb | 14 +++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/jekyll-feed/feed.xml b/lib/jekyll-feed/feed.xml index 6f4770ec..37e5b03d 100644 --- a/lib/jekyll-feed/feed.xml +++ b/lib/jekyll-feed/feed.xml @@ -33,6 +33,11 @@ {% assign posts = site.posts | where_exp: "post", "post.draft != true" %} {% for post in posts limit: 10 %} + {% if page.category %} + {% unless post.categories contains page.category %} + {% continue %} + {% endunless %} + {% endif %} {{ post.title | smartify | strip_html | normalize_whitespace | xml_escape }} diff --git a/lib/jekyll-feed/generator.rb b/lib/jekyll-feed/generator.rb index 0ff34ed7..441d633a 100644 --- a/lib/jekyll-feed/generator.rb +++ b/lib/jekyll-feed/generator.rb @@ -6,8 +6,15 @@ class Generator < Jekyll::Generator # Main plugin action, called by Jekyll-core def generate(site) @site = site - return if file_exists?(feed_path) - @site.pages << content_for_file(feed_path, feed_source_path) + unless file_exists?(feed_path) + @site.pages << content_for_file(feed_path, feed_source_path) + end + @site.categories.each do |category| + path = "/feed/" + category.first + ".xml" + unless file_exists?(path) + @site.pages << content_for_file(path, feed_source_path, category.first) + end + end end private @@ -42,12 +49,13 @@ def file_exists?(file_path) end # Generates contents for a file - def content_for_file(file_path, file_source_path) + def content_for_file(file_path, file_source_path, category = false) file = PageWithoutAFile.new(@site, File.dirname(__FILE__), "", file_path) file.content = File.read(file_source_path).gsub(MINIFY_REGEX, "") file.data["layout"] = nil file.data["sitemap"] = false file.data["xsl"] = file_exists?("feed.xslt.xml") + file.data["category"] = category file.output file end From 88fb65f727f7515b560a03b96754cd7c1c9d3a7e Mon Sep 17 00:00:00 2001 From: Ben Balter Date: Tue, 8 May 2018 12:19:23 -0400 Subject: [PATCH 2/4] add support for categories and collections --- README.md | 42 +++++ lib/jekyll-feed/feed.xml | 28 +-- lib/jekyll-feed/generator.rb | 91 +++++++-- .../_collection/2018-01-01-collection-doc.md | 4 + .../2018-01-02-collection-category-doc.md | 5 + .../_posts/2013-12-12-dec-the-second.md | 1 + .../_posts/2014-03-02-march-the-second.md | 1 + .../_posts/2014-03-04-march-the-fourth.md | 1 + spec/jekyll-feed_spec.rb | 175 +++++++++++++++++- 9 files changed, 311 insertions(+), 37 deletions(-) create mode 100644 spec/fixtures/_collection/2018-01-01-collection-doc.md create mode 100644 spec/fixtures/_collection/2018-01-02-collection-category-doc.md diff --git a/README.md b/README.md index 8b5af319..84d68634 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,48 @@ Want to style what your feed looks like in the browser? Simply add an XSLT at `/ Great question. In short, Atom is a better format. Think of it like RSS 3.0. For more information, see [this discussion on why we chose Atom over RSS 2.0](https://github.com/jekyll/jekyll-rss-feed/issues/2). +## Categories + +Jekyll Feed can generate feeds for each category. Simply define which categories you'd like feeds for in your config: + +```yml +feed: + categories: + - news + - updates +``` + +## Collections + +Jekyll Feed can generate feeds for collections other than the Posts collection. This works best for chronological collections (e.g., collections with dates in the filenames). Simply define which collections you'd like feeds for in your config: + +```yml +feed: + collections: + - changes +``` + +By default, collection feeds will be outputted to `/feed/.xml`. If you'd like to customize the output path, specify a collection's custom path as follows: + +```yml +feed: + collections: + changes: + path: "/changes.xml" +``` + +Finally, collections can also have category feeds which are outputted as `/feed//.xml`. Specify categories like so: + +```yml +feed: + collections: + changes: + path: "/changes.xml" + categories: + - news + - updates +``` + ## Contributing 1. Fork it (https://github.com/jekyll/jekyll-feed/fork) diff --git a/lib/jekyll-feed/feed.xml b/lib/jekyll-feed/feed.xml index 424b18d4..cf9f9ed4 100644 --- a/lib/jekyll-feed/feed.xml +++ b/lib/jekyll-feed/feed.xml @@ -7,12 +7,20 @@ {{ site.time | date_to_xmlschema }} - {{ '/' | absolute_url | xml_escape }} + {{ page.url | absolute_url | xml_escape }} - {% if site.title %} - {{ site.title | smartify | xml_escape }} - {% elsif site.name %} - {{ site.name | smartify | xml_escape }} + {% assign title = site.title | default: site.name %} + {% if page.collection != "posts" %} + {% assign collection = page.collection | capitalize %} + {% assign title = title | append: " | " | append: collection %} + {% endif %} + {% if page.category %} + {% assign category = page.category | capitalize %} + {% assign title = title | append: " | " | append: category %} + {% endif %} + + {% if title %} + {{ title | smartify | xml_escape }} {% endif %} {% if site.description %} @@ -31,13 +39,11 @@ {% endif %} - {% assign posts = site.posts | where_exp: "post", "post.draft != true" %} + {% assign posts = site[page.collection] | where_exp: "post", "post.draft != true" | sort: "date" | reverse %} + {% if page.category %} + {% assign posts = posts | where: "category",page.category %} + {% endif %} {% for post in posts limit: 10 %} - {% if page.category %} - {% unless post.categories contains page.category %} - {% continue %} - {% endunless %} - {% endif %} {{ post.title | smartify | strip_html | normalize_whitespace | xml_escape }} diff --git a/lib/jekyll-feed/generator.rb b/lib/jekyll-feed/generator.rb index 73039789..cec0efa3 100644 --- a/lib/jekyll-feed/generator.rb +++ b/lib/jekyll-feed/generator.rb @@ -8,13 +8,12 @@ class Generator < Jekyll::Generator # Main plugin action, called by Jekyll-core def generate(site) @site = site - unless file_exists?(feed_path) - @site.pages << content_for_file(feed_path, feed_source_path) - end - @site.categories.each do |category| - path = "/feed/" + category.first + ".xml" - unless file_exists?(path) - @site.pages << content_for_file(path, feed_source_path, category.first) + collections.each do |name, meta| + Jekyll.logger.info "Jekyll Feed:", "Generating feed for #{name}" + (meta["categories"] + [nil]).each do |category| + path = feed_path(:collection => name, :category => category) + next if file_exists?(path) + @site.pages << make_page(path, :collection => name, :category => category) end end end @@ -27,18 +26,59 @@ def generate(site) # We will strip all of this whitespace to minify the template MINIFY_REGEX = %r!(?<=>|})\s+! - # Path to feed from config, or feed.xml for default - def feed_path - if @site.config["feed"] && @site.config["feed"]["path"] - @site.config["feed"]["path"] + # Returns the plugin's config or an empty hash if not set + def config + @site.config["feed"] || {} + end + + # Determines the destination path of a given feed + # + # collection - the name of a collection, e.g., "posts" + # category - a category within that collection, e.g., "news" + # + # Will return "feed.xml", or the config-specified default feed for posts + # Will return `/feed/category.xml` for post categories + # WIll return `/feed/collection.xml` for other collections + # Will return `/feed/collection/category.xml` for other collection categories + def feed_path(collection: "posts", category: nil) + prefix = collection == "posts" ? "/feed" : "feed/#{collection}" + if category + "#{prefix}/#{category}.xml" + elsif collections[collection]["path"] + collections[collection]["path"] else - "feed.xml" + "#{prefix}.xml" end end + # Returns a hash representing all collections to be processed and their metadata + # in the form of { collection_name => { categories = [...], path = "..." } } + def collections + return @collections if defined?(@collections) + + @collections = if config["collections"].is_a?(Array) + config["collections"].map { |c| [c, {}] }.to_h + elsif config["collections"].is_a?(Hash) + config["collections"] + else + {} + end + + @collections = normalize_posts_meta(@collections) + @collections.each do |_name, meta| + meta["categories"] = (meta["categories"] || []).to_set + end + + @collections + end + # Path to feed.xml template file def feed_source_path - File.expand_path "feed.xml", __dir__ + @feed_source_path ||= File.expand_path "feed.xml", __dir__ + end + + def feed_template + @feed_template ||= File.read(feed_source_path).gsub(MINIFY_REGEX, "") end # Checks if a file already exists in the site source @@ -52,15 +92,28 @@ def file_exists?(file_path) # Generates contents for a file - def content_for_file(file_path, file_source_path, category = false) + def make_page(file_path, collection: "posts", category: nil) file = PageWithoutAFile.new(@site, __dir__, "", file_path) - file.content = File.read(file_source_path).gsub(MINIFY_REGEX, "") - file.data["layout"] = nil - file.data["sitemap"] = false - file.data["xsl"] = file_exists?("feed.xslt.xml") - file.data["category"] = category + file.content = feed_template + file.data.merge!({ + "layout" => nil, + "sitemap" => false, + "xsl" => file_exists?("feed.xslt.xml"), + "collection" => collection, + "category" => category, + }) file.output file end + + # Special case the "posts" collection, which, for ease of use and backwards + # compatability, can be configured via top-level keys or directly as a collection + def normalize_posts_meta(hash) + hash["posts"] ||= {} + hash["posts"]["path"] ||= config["path"] + hash["posts"]["categories"] ||= config["categories"] + config["path"] ||= hash["posts"]["path"] + hash + end end end diff --git a/spec/fixtures/_collection/2018-01-01-collection-doc.md b/spec/fixtures/_collection/2018-01-01-collection-doc.md new file mode 100644 index 00000000..2bacb115 --- /dev/null +++ b/spec/fixtures/_collection/2018-01-01-collection-doc.md @@ -0,0 +1,4 @@ +--- +--- + +Look at me! I'm a collection! diff --git a/spec/fixtures/_collection/2018-01-02-collection-category-doc.md b/spec/fixtures/_collection/2018-01-02-collection-category-doc.md new file mode 100644 index 00000000..e5899e98 --- /dev/null +++ b/spec/fixtures/_collection/2018-01-02-collection-category-doc.md @@ -0,0 +1,5 @@ +--- +category: news +--- + +Look at me! I'm a collection doc in a category! diff --git a/spec/fixtures/_posts/2013-12-12-dec-the-second.md b/spec/fixtures/_posts/2013-12-12-dec-the-second.md index 22604a10..13b97917 100644 --- a/spec/fixtures/_posts/2013-12-12-dec-the-second.md +++ b/spec/fixtures/_posts/2013-12-12-dec-the-second.md @@ -1,6 +1,7 @@ --- excerpt: "Foo" image: "/image.png" +category: news --- # December the twelfth, actually. diff --git a/spec/fixtures/_posts/2014-03-02-march-the-second.md b/spec/fixtures/_posts/2014-03-02-march-the-second.md index e828b051..e33a699d 100644 --- a/spec/fixtures/_posts/2014-03-02-march-the-second.md +++ b/spec/fixtures/_posts/2014-03-02-march-the-second.md @@ -1,5 +1,6 @@ --- image: https://cdn.example.org/absolute.png?h=188&w=250 +category: news --- March the second! diff --git a/spec/fixtures/_posts/2014-03-04-march-the-fourth.md b/spec/fixtures/_posts/2014-03-04-march-the-fourth.md index 216c3722..2401ecc3 100644 --- a/spec/fixtures/_posts/2014-03-04-march-the-fourth.md +++ b/spec/fixtures/_posts/2014-03-04-march-the-fourth.md @@ -3,6 +3,7 @@ tags: - '"/>' image: path: "/object-image.png" +category: updates --- March the fourth! diff --git a/spec/jekyll-feed_spec.rb b/spec/jekyll-feed_spec.rb index 39acdcfb..e042c425 100644 --- a/spec/jekyll-feed_spec.rb +++ b/spec/jekyll-feed_spec.rb @@ -43,9 +43,9 @@ end it "puts all the posts in the feed.xml file" do - expect(contents).to match /http:\/\/example\.org\/2014\/03\/04\/march-the-fourth\.html/ - expect(contents).to match /http:\/\/example\.org\/2014\/03\/02\/march-the-second\.html/ - expect(contents).to match /http:\/\/example\.org\/2013\/12\/12\/dec-the-second\.html/ + expect(contents).to match /http:\/\/example\.org\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(contents).to match /http:\/\/example\.org\/news\/2014\/03\/02\/march-the-second\.html/ + expect(contents).to match /http:\/\/example\.org\/news\/2013\/12\/12\/dec-the-second\.html/ expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html" expect(contents).to_not match /http:\/\/example\.org\/2016\/02\/09\/a-draft\.html/ end @@ -125,7 +125,7 @@ it "includes item contents" do post = feed.items.last expect(post.title.content).to eql("Dec The Second") - expect(post.link.href).to eql("http://example.org/2013/12/12/dec-the-second.html") + expect(post.link.href).to eql("http://example.org/news/2013/12/12/dec-the-second.html") expect(post.published.content).to eql(Time.parse("2013-12-12")) end @@ -239,9 +239,9 @@ end it "correctly adds the baseurl to the posts" do - expect(contents).to match /http:\/\/example\.org\/bass\/2014\/03\/04\/march-the-fourth\.html/ - expect(contents).to match /http:\/\/example\.org\/bass\/2014\/03\/02\/march-the-second\.html/ - expect(contents).to match /http:\/\/example\.org\/bass\/2013\/12\/12\/dec-the-second\.html/ + expect(contents).to match /http:\/\/example\.org\/bass\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(contents).to match /http:\/\/example\.org\/bass\/news\/2014\/03\/02\/march-the-second\.html/ + expect(contents).to match /http:\/\/example\.org\/bass\/news\/2013\/12\/12\/dec-the-second\.html/ end it "renders the feed meta" do @@ -290,6 +290,29 @@ end end + context "changing the file path via collection meta" do + let(:overrides) do + { + "feed" => { + "collections" => { + "posts" => { + "path" => "atom.xml" + } + } + }, + } + end + + it "should write to atom.xml" do + expect(Pathname.new(dest_dir("atom.xml"))).to exist + end + + it "renders the feed meta with custom feed path" do + expected = 'href="http://example.org/atom.xml"' + expect(feed_meta).to include(expected) + end + end + context "feed stylesheet" do it "includes the stylesheet" do expect(contents).to include('') @@ -310,4 +333,142 @@ expect(contents).to match %r!! end end + + context "categories" do + context "with top-level post categories" do + let(:overrides) { + { + "feed" => { "categories" => ["news"] } + } + } + let(:news_feed) { File.read(dest_dir("feed/news.xml")) } + + it "outputs the primary feed" do + expect(contents).to match /http:\/\/example\.org\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(contents).to match /http:\/\/example\.org\/news\/2014\/03\/02\/march-the-second\.html/ + expect(contents).to match /http:\/\/example\.org\/news\/2013\/12\/12\/dec-the-second\.html/ + expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html" + expect(contents).to_not match /http:\/\/example\.org\/2016\/02\/09\/a-draft\.html/ + end + + it "outputs the category feed" do + expect(news_feed).to match "My awesome site | News" + expect(news_feed).to match /http:\/\/example\.org\/news\/2014\/03\/02\/march-the-second\.html/ + expect(news_feed).to match /http:\/\/example\.org\/news\/2013\/12\/12\/dec-the-second\.html/ + expect(news_feed).to_not match /http:\/\/example\.org\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html" + end + end + + context "with collection-level post categories" do + let(:overrides) { + { + "feed" => { + "collections" => { + "posts" => { + "categories" => ["news"] + } + } + } + } + } + let(:news_feed) { File.read(dest_dir("feed/news.xml")) } + + it "outputs the primary feed" do + expect(contents).to match /http:\/\/example\.org\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(contents).to match /http:\/\/example\.org\/news\/2014\/03\/02\/march-the-second\.html/ + expect(contents).to match /http:\/\/example\.org\/news\/2013\/12\/12\/dec-the-second\.html/ + expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html" + expect(contents).to_not match /http:\/\/example\.org\/2016\/02\/09\/a-draft\.html/ + end + + it "outputs the category feed" do + expect(news_feed).to match "My awesome site | News" + expect(news_feed).to match /http:\/\/example\.org\/news\/2014\/03\/02\/march-the-second\.html/ + expect(news_feed).to match /http:\/\/example\.org\/news\/2013\/12\/12\/dec-the-second\.html/ + expect(news_feed).to_not match /http:\/\/example\.org\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html" + end + end + end + + context "collections" do + let(:collection_feed) { File.read(dest_dir("feed/collection.xml")) } + + context "when initialized as an array" do + let(:overrides) { + { + "collections" => { + "collection" => { + "output" => true + } + }, + "feed" => { "collections" => ["collection"] } + } + } + + + it "outputs the collection feed" do + expect(collection_feed).to match "My awesome site | Collection" + expect(collection_feed).to match "http://example.org/collection/2018-01-01-collection-doc.html" + expect(collection_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html" + expect(collection_feed).to_not match /http:\/\/example\.org\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(collection_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html" + end + end + + context "with categories" do + let(:overrides) { + { + "collections" => { + "collection" => { + "output" => true + } + }, + "feed" => { + "collections" => { + "collection" => { + "categories" => ["news"] + } + } + } + } + } + let(:news_feed) { File.read(dest_dir("feed/collection/news.xml")) } + + it "outputs the collection category feed" do + expect(news_feed).to match "My awesome site | Collection | News" + expect(news_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html" + expect(news_feed).to_not match "http://example.org/collection/2018-01-01-collection-doc.html" + expect(news_feed).to_not match /http:\/\/example\.org\/updates\/2014\/03\/04\/march-the-fourth\.html/ + expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html" + end + end + + context "with a custom path" do + let(:overrides) { + { + "collections" => { + "collection" => { + "output" => true + } + }, + "feed" => { + "collections" => { + "collection" => { + "categories" => ["news"], + "path" => "custom.xml" + } + } + } + } + } + + it "should write to the custom path" do + expect(Pathname.new(dest_dir("custom.xml"))).to exist + expect(Pathname.new(dest_dir("feed/collection.xml"))).to_not exist + expect(Pathname.new(dest_dir("feed/collection/news.xml"))).to exist + end + end + end end From 2659efadec987647bcc4d3cd41e35f5ac23070a1 Mon Sep 17 00:00:00 2001 From: Ben Balter Date: Tue, 8 May 2018 17:49:46 -0400 Subject: [PATCH 3/4] guard against a collection not existing --- lib/jekyll-feed/generator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jekyll-feed/generator.rb b/lib/jekyll-feed/generator.rb index cec0efa3..d3ea4ffb 100644 --- a/lib/jekyll-feed/generator.rb +++ b/lib/jekyll-feed/generator.rb @@ -44,7 +44,7 @@ def feed_path(collection: "posts", category: nil) prefix = collection == "posts" ? "/feed" : "feed/#{collection}" if category "#{prefix}/#{category}.xml" - elsif collections[collection]["path"] + elsif collections[collection] && collections[collection]["path"] collections[collection]["path"] else "#{prefix}.xml" From 44bee9a2bce86112871a4e795f186faaa74118b3 Mon Sep 17 00:00:00 2001 From: Ben Balter Date: Wed, 9 May 2018 09:53:05 -0400 Subject: [PATCH 4/4] fix preceeding slash in feed_path for consistency --- lib/jekyll-feed/generator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jekyll-feed/generator.rb b/lib/jekyll-feed/generator.rb index d3ea4ffb..3f3150e6 100644 --- a/lib/jekyll-feed/generator.rb +++ b/lib/jekyll-feed/generator.rb @@ -36,12 +36,12 @@ def config # collection - the name of a collection, e.g., "posts" # category - a category within that collection, e.g., "news" # - # Will return "feed.xml", or the config-specified default feed for posts + # Will return "/feed.xml", or the config-specified default feed for posts # Will return `/feed/category.xml` for post categories # WIll return `/feed/collection.xml` for other collections # Will return `/feed/collection/category.xml` for other collection categories def feed_path(collection: "posts", category: nil) - prefix = collection == "posts" ? "/feed" : "feed/#{collection}" + prefix = collection == "posts" ? "/feed" : "/feed/#{collection}" if category "#{prefix}/#{category}.xml" elsif collections[collection] && collections[collection]["path"]