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 ac0945da..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,7 +39,10 @@ {% 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 %} {{ post.title | smartify | strip_html | normalize_whitespace | xml_escape }} diff --git a/lib/jekyll-feed/generator.rb b/lib/jekyll-feed/generator.rb index 3de90bee..3f3150e6 100644 --- a/lib/jekyll-feed/generator.rb +++ b/lib/jekyll-feed/generator.rb @@ -8,8 +8,14 @@ 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) + 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 private @@ -20,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] && 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 @@ -44,14 +91,29 @@ def file_exists?(file_path) end # Generates contents for a file - def content_for_file(file_path, file_source_path) + + 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.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