├── spec
├── fixtures
│ ├── src
│ │ ├── feed.xslt.xml
│ │ ├── witherb.md
│ │ ├── _collection
│ │ │ ├── 2018-01-01-collection-doc.md
│ │ │ └── 2018-01-02-collection-category-doc.md
│ │ ├── _posts
│ │ │ ├── 2015-01-12-a-draft.md
│ │ │ ├── 2015-05-12-pre.html
│ │ │ ├── 2016-04-25-author-reference.md
│ │ │ ├── 2015-02-12-strip-newlines.md
│ │ │ ├── 2015-08-08-stuck-in-the-middle.html
│ │ │ ├── 2014-03-02-march-the-second.md
│ │ │ ├── 2015-01-18-jekyll-last-modified-at.md
│ │ │ ├── 2013-12-12-dec-the-second.md
│ │ │ ├── 2014-03-04-march-the-fourth.md
│ │ │ ├── 2015-05-12-liquid.md
│ │ │ └── 2015-05-18-author-detail.md
│ │ ├── _data
│ │ │ └── authors.yml
│ │ └── _layouts
│ │ │ ├── helper.erb
│ │ │ └── some_default.html
│ ├── config
│ │ └── initializers.rb
│ └── bridgetown.config.yml
├── spec_helper.rb
└── bridgetown-feed_spec.rb
├── .rspec
├── script
├── bootstrap
├── test
├── cibuild
├── release
└── fmt
├── lib
├── bridgetown-feed
│ ├── version.rb
│ ├── builder.rb
│ ├── generator.rb
│ └── feed.liquid
└── bridgetown-feed.rb
├── Gemfile
├── Rakefile
├── .gitignore
├── .rubocop.yml
├── LICENSE.txt
├── bridgetown-feed.gemspec
├── CHANGELOG.md
└── README.md
/spec/fixtures/src/feed.xslt.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format progress
3 |
--------------------------------------------------------------------------------
/script/bootstrap:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | bundle install
4 |
--------------------------------------------------------------------------------
/script/test:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ex
3 |
4 | bundle exec rspec "$@"
5 |
--------------------------------------------------------------------------------
/script/cibuild:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | set -e
4 |
5 | script/fmt
6 | script/test
7 |
--------------------------------------------------------------------------------
/spec/fixtures/src/witherb.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: helper
3 | title: I'm a page
4 | ---
5 |
6 | Page content here
--------------------------------------------------------------------------------
/spec/fixtures/src/_collection/2018-01-01-collection-doc.md:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 | Look at me! I'm a collection!
5 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2015-01-12-a-draft.md:
--------------------------------------------------------------------------------
1 | ---
2 | published: false
3 | ---
4 |
5 | This is a draft.
6 |
--------------------------------------------------------------------------------
/spec/fixtures/config/initializers.rb:
--------------------------------------------------------------------------------
1 | Bridgetown.configure do
2 | timezone "UTC"
3 | init :"bridgetown-feed"
4 | end
5 |
--------------------------------------------------------------------------------
/script/release:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Tag and push a release.
3 |
4 | set -e
5 |
6 | script/cibuild
7 | bundle exec rake release
8 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2015-05-12-pre.html:
--------------------------------------------------------------------------------
1 | ---
2 | author: Pat
3 | lang: en
4 | ---
5 |
6 |
Line 1
7 | Line 2
8 | Line 3
9 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2016-04-25-author-reference.md:
--------------------------------------------------------------------------------
1 | ---
2 | excerpt: ""
3 | author: garthdb
4 | ---
5 |
6 | # April the twenty-fifth?
7 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2015-02-12-strip-newlines.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | The plugin
4 | will properly
5 | strip newlines.
6 | ---
7 |
--------------------------------------------------------------------------------
/lib/bridgetown-feed/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Bridgetown
4 | module Feed
5 | VERSION = "4.0.0"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_data/authors.yml:
--------------------------------------------------------------------------------
1 | garthdb:
2 | name: Garth
3 | twitter: garthdb
4 | uri: "http://garthdb.com"
5 | email: example@mail.com
6 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2015-08-08-stuck-in-the-middle.html:
--------------------------------------------------------------------------------
1 | ---
2 | feed:
3 | excerpt_only: true
4 | ---
5 |
6 | This content should not be in feed.
7 |
--------------------------------------------------------------------------------
/spec/fixtures/bridgetown.config.yml:
--------------------------------------------------------------------------------
1 | defaults:
2 | -
3 | scope:
4 | path: ""
5 | type: pages
6 | values:
7 | layout: some_default
8 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_collection/2018-01-02-collection-category-doc.md:
--------------------------------------------------------------------------------
1 | ---
2 | category: news
3 | ---
4 |
5 | Look at me! I'm a collection doc in a category!
6 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 | gemspec
5 |
6 | gem "bridgetown", ENV.fetch("BRIDGETOWN_VERSION", "2.0.0.beta6")
7 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2014-03-02-march-the-second.md:
--------------------------------------------------------------------------------
1 | ---
2 | image: https://cdn.example.org/absolute.png?h=188&w=250
3 | category: news
4 | ---
5 |
6 | March the second!
7 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 |
6 | RSpec::Core::RakeTask.new(:spec)
7 |
8 | task :default => :spec
9 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2015-01-18-jekyll-last-modified-at.md:
--------------------------------------------------------------------------------
1 | ---
2 | last_modified_at: 2015-05-12T13:27:59+00:00
3 | ---
4 |
5 | Please don't modify this file. It's modified time is important.
6 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2013-12-12-dec-the-second.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: https://example.com/foo
3 | excerpt: "Foo"
4 | image: "/image.png"
5 | category: news
6 | ---
7 |
8 | # December the twelfth, actually.
9 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_layouts/helper.erb:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 |
5 | <%= feed_meta %>
6 |
7 |
8 | THIS IS MY ERB LAYOUT
9 | {{ content }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_layouts/some_default.html:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 |
5 | {% feed_meta %}
6 |
7 |
8 | THIS IS MY LAYOUT
9 | {{ content }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2014-03-04-march-the-fourth.md:
--------------------------------------------------------------------------------
1 | ---
2 | tags:
3 | - '"/>'
4 | image:
5 | path: "/object-image.png"
6 | categories: updates bridgetown
7 | ---
8 |
9 | March the fourth!
10 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2015-05-12-liquid.md:
--------------------------------------------------------------------------------
1 | ---
2 | template_engine: liquid
3 | ---
4 |
5 | {% capture liquidstring %}
6 | Liquid is not rendered.
7 | {% endcapture %}
8 | {{ liquidstring | replace:'not ','' }}
9 |
--------------------------------------------------------------------------------
/spec/fixtures/src/_posts/2015-05-18-author-detail.md:
--------------------------------------------------------------------------------
1 | ---
2 | excerpt: ""
3 | author:
4 | name: Ben
5 | uri: "http://ben.balter.com"
6 | email: ben@example.com
7 | ---
8 |
9 | # December the twelfth, actually.
10 |
--------------------------------------------------------------------------------
/script/fmt:
--------------------------------------------------------------------------------
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
11 |
--------------------------------------------------------------------------------
/lib/bridgetown-feed.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bridgetown"
4 | require "fileutils"
5 | require "bridgetown-feed/builder"
6 | require "bridgetown-feed/generator"
7 |
8 | Bridgetown.initializer :"bridgetown-feed" do |config|
9 | config.builder BridgetownFeed::Builder
10 | end
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /.bundle/
3 | /.yardoc
4 | /Gemfile.lock
5 | /_yardoc/
6 | /coverage/
7 | /doc/
8 | /pkg/
9 | /spec/reports/
10 | /tmp/
11 | *.bundle
12 | *.so
13 | *.o
14 | *.a
15 | mkmf.log
16 | *.gem
17 | Gemfile.lock
18 | spec/dest
19 | .bundle
20 | .bridgetown-metadata
21 | .bridgetown-cache
22 | .ruby-version
23 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | require: rubocop-bridgetown
2 |
3 | inherit_gem:
4 | rubocop-bridgetown: .rubocop.yml
5 |
6 | AllCops:
7 | TargetRubyVersion: 2.5
8 |
9 | Exclude:
10 | - .gitignore
11 | - .rspec
12 | - .rubocop.yml
13 | - .travis.yml
14 |
15 | - Rakefile
16 | - "*.gemspec"
17 | - Gemfile.lock
18 | - History.markdown
19 | - LICENSE.txt
20 | - README.md
21 |
22 | - script/**/*
23 | - vendor/**/*
24 | - spec/**/*
25 |
26 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bridgetown"
4 | require "typhoeus"
5 | require "nokogiri"
6 | require "rss"
7 |
8 | Bridgetown.begin!
9 |
10 | require File.expand_path("../lib/bridgetown-feed", __dir__)
11 |
12 | Bridgetown.logger.log_level = :error
13 |
14 | RSpec.configure do |config|
15 | config.run_all_when_everything_filtered = true
16 | config.filter_run :focus
17 | config.order = "random"
18 |
19 | ROOT_DIR = File.expand_path("fixtures", __dir__)
20 | SOURCE_DIR = File.join(ROOT_DIR, "src")
21 | DEST_DIR = File.expand_path("dest", __dir__)
22 |
23 | def root_dir(*files)
24 | File.join(ROOT_DIR, *files)
25 | end
26 |
27 | def source_dir(*files)
28 | File.join(SOURCE_DIR, *files)
29 | end
30 |
31 | def dest_dir(*files)
32 | File.join(DEST_DIR, *files)
33 | end
34 |
35 | def make_context(registers = {})
36 | Liquid::Context.new({}, {}, { :site => site }.merge(registers))
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/bridgetown-feed/builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BridgetownFeed
4 | class Builder < Bridgetown::Builder
5 | include Bridgetown::Filters::URLFilters
6 |
7 | Context = Struct.new(:registers)
8 |
9 | def build
10 | @context = Context.new({ site: site })
11 | helper "feed_meta", :generate_link_tag
12 | liquid_tag "feed_meta", :generate_link_tag
13 | end
14 |
15 | def generate_link_tag(*)
16 | attrs = attributes.map { |k, v| %(#{k}="#{v}") }.join(" ")
17 | tag_output = ""
18 | tag_output.respond_to?(:html_safe) ? tag_output.html_safe : tag_output
19 | end
20 |
21 | private
22 |
23 | def config
24 | @config ||= site.config
25 | end
26 |
27 | def metadata
28 | @metadata ||= site.data["site_metadata"]
29 | end
30 |
31 | def attributes
32 | {
33 | type: "application/atom+xml",
34 | rel: "alternate",
35 | href: absolute_url(path),
36 | title: title,
37 | }.keep_if { |_, v| v }
38 | end
39 |
40 | def path
41 | config.dig("feed", "path") || "feed.xml"
42 | end
43 |
44 | def title
45 | metadata["title"] || metadata["name"]
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020-present Jared White and Bridgetown contributors
4 | Copyright (c) 2015-2020 Ben Balter and jekyll-feed contributors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining
7 | a copy of this software and associated documentation files (the
8 | "Software"), to deal in the Software without restriction, including
9 | without limitation the rights to use, copy, modify, merge, publish,
10 | distribute, sublicense, and/or sell copies of the Software, and to
11 | permit persons to whom the Software is furnished to do so, subject to
12 | the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be
15 | included in all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/bridgetown-feed.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/bridgetown-feed/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "bridgetown-feed"
7 | spec.version = Bridgetown::Feed::VERSION
8 | spec.author = "Bridgetown Team"
9 | spec.email = "maintainers@bridgetownrb.com"
10 | spec.summary = "A Bridgetown plugin to generate an Atom feed of your Bridgetown posts"
11 | spec.homepage = "https://github.com/bridgetownrb/bridgetown-feed"
12 | spec.license = "MIT"
13 |
14 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r!^(test|script|spec|features)/!) }
15 | spec.test_files = spec.files.grep(%r!^spec/!)
16 | spec.require_paths = ["lib"]
17 |
18 | spec.required_ruby_version = ">= 3.1.0"
19 |
20 | spec.add_dependency "bridgetown", ">= 1.2.0"
21 |
22 | spec.add_development_dependency "bundler"
23 | spec.add_development_dependency "rss"
24 | spec.add_development_dependency "nokogiri", "~> 1.6"
25 | spec.add_development_dependency "rake", "~> 13.0"
26 | spec.add_development_dependency "rspec", "~> 3.0"
27 | spec.add_development_dependency "rubocop-bridgetown", "~> 0.2"
28 | spec.add_development_dependency "typhoeus", ">= 0.7", "< 2.0"
29 | end
30 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Unreleased
4 |
5 | ...
6 |
7 | ## 4.0.0 / 2025-09-16
8 |
9 | * Enable use with Bridgetown 2.0 (@michaelherold)
10 | * Add option to specify the feeds icon and logo (@jbennett)
11 |
12 | ## 3.1.2 / 2024-03-02
13 |
14 | * Fix: as readme promises, use `id` from a post's front matter if present
15 |
16 | ## 3.1.1 / 2024-02-02
17 |
18 | * Remove duplicate variable assignment (@jbennett)
19 |
20 | ## 3.1.0 / 2024-02-01
21 |
22 | * Add an option to set the post_limit (@jbennett)
23 |
24 | ## 3.0.0 / 2022-10-08
25 |
26 | * Upgrade to initializers system in Bridgetown 1.2
27 |
28 | ## 2.1.0 / 2021-10-26
29 |
30 | * Update test suite and ensure generated pages have the right permalink
31 | * Switch from `site.pages` to `site.generated_pages` due to Bridgetown 1.0 API change
32 |
33 | ## 2.0.1 / 2021-06-04
34 |
35 | * Fix bug where resources' relative URLs weren't included properly
36 |
37 | ## 2.0.0 / 2021-04-17
38 |
39 | * New release with helper to support Ruby templates like ERB
40 |
41 | ## 1.1.3 / 2020-11-05
42 |
43 | * Add `template_engine: liquid` to the feed XML so it plays well with Bridgetown 0.18+
44 |
45 | ## 1.1.2 / 2020-05-01
46 |
47 | Update to require a minimum Ruby version of 2.5.
48 |
49 | ## 1.1.1 / 2020-04-19
50 |
51 | Update to use `_data/site_metadata.yml` in line with the rest of the ecosystem.
52 |
53 | ## 1.0.0 / 2020-04-09
54 |
55 | Use Bridgetown gem and rename to bridgetown-feed.
--------------------------------------------------------------------------------
/lib/bridgetown-feed/generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BridgetownFeed
4 | class Generator < Bridgetown::Generator
5 | # Matches all whitespace that follows
6 | # 1. A '>', which closes an XML tag or
7 | # 2. A '}', which closes a Liquid tag
8 | # We will strip all of this whitespace to minify the template
9 | MINIFY_REGEX = %r!(?<=>|})\s+!.freeze
10 |
11 | priority :lowest
12 |
13 | # Main plugin action, called by Bridgetown-core
14 | def generate(site)
15 | @site = site
16 | collections.each do |name, meta|
17 | Bridgetown.logger.info "Bridgetown Feed:", "Generating feed for #{name}"
18 | (meta["categories"] + [nil]).each do |category|
19 | path = feed_path(collection: name, category: category)
20 | next if file_exists?(path)
21 |
22 | @site.generated_pages << make_page(path, collection: name, category: category)
23 | end
24 | end
25 | end
26 |
27 | private
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.liquid", __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 | Bridgetown::GeneratedPage.new(@site, __dir__, "", file_path, from_plugin: true).tap do |file|
89 | file.content = feed_template
90 | file.data.merge!(
91 | "layout" => "none",
92 | "permalink" => file_path,
93 | "template_engine" => "liquid",
94 | "sitemap" => false,
95 | "xsl" => file_exists?("feed.xslt.xml"),
96 | "collection" => collection,
97 | "category" => category
98 | )
99 | file.output
100 | end
101 | end
102 |
103 | # Special case the "posts" collection, which, for ease of use and backwards
104 | # compatability, can be configured via top-level keys or directly as a collection
105 | def normalize_posts_meta(hash)
106 | hash["posts"] ||= {}
107 | hash["posts"]["path"] ||= config["path"]
108 | hash["posts"]["categories"] ||= config["categories"]
109 | config["path"] ||= hash["posts"]["path"]
110 | hash
111 | end
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/lib/bridgetown-feed/feed.liquid:
--------------------------------------------------------------------------------
1 |
2 | {% if page.xsl %}
3 |
4 | {% endif %}
5 |
6 | Bridgetown
7 |
8 |
9 | {{ site.time | date_to_xmlschema }}
10 | {{ page.url | absolute_url | xml_escape }}
11 |
12 | {% if site.feed.icon and site.feed.icon != empty %}
13 | {{ site.feed.icon | absolute_url }}
14 | {% endif %}
15 | {% if site.feed.logo and site.feed.logo != empty %}
16 | {{ site.feed.logo | absolute_url }}
17 | {% endif %}
18 |
19 | {% assign title = site.metadata.title | default: site.metadata.name %}
20 | {% if page.collection != "posts" %}
21 | {% assign collection = page.collection | capitalize %}
22 | {% assign title = title | append: " | " | append: collection %}
23 | {% endif %}
24 | {% if page.category %}
25 | {% assign category = page.category | capitalize %}
26 | {% assign title = title | append: " | " | append: category %}
27 | {% endif %}
28 |
29 | {% if title %}
30 | {{ title | smartify | xml_escape }}
31 | {% endif %}
32 |
33 | {% if site.metadata.description %}
34 | {{ site.metadata.description | xml_escape }}
35 | {% endif %}
36 |
37 | {% if site.metadata.author %}
38 |
39 | {{ site.metadata.author.name | default: site.metadata.author | xml_escape }}
40 | {% if site.metadata.author.email %}
41 | {{ site.metadata.author.email | xml_escape }}
42 | {% endif %}
43 | {% if site.metadata.author.uri %}
44 | {{ site.metadata.author.uri | xml_escape }}
45 | {% endif %}
46 |
47 | {% endif %}
48 |
49 | {% assign feed_collection = collections[page.collection] %}
50 | {% find posts where feed_collection.resources, draft != true %}
51 | {% if page.category %}
52 | {% assign posts = posts | where: "category",page.category %}
53 | {% endif %}
54 | {% assign post_limit = site.feed.collections[page.collection].post_limit | default: site.feed.post_limit | default: 10 %}
55 | {% for post in posts limit: post_limit %}
56 | {% assign post_id = post.data.id | default: post.id %}
57 |
58 | {{ post.title | smartify | strip_html | normalize_whitespace | xml_escape }}
59 |
60 | {{ post.date | date_to_xmlschema }}
61 | {{ post.last_modified_at | default: post.date | date_to_xmlschema }}
62 | {{ post_id | absolute_url | xml_escape }}
63 | {% assign excerpt_only = post.feed.excerpt_only | default: site.feed.excerpt_only %}
64 | {% unless excerpt_only %}
65 | {{ post.content | strip | xml_escape }}
66 | {% endunless %}
67 |
68 | {% assign post_author = post.author | default: post.authors[0] | default: site.metadata.author %}
69 | {% assign post_author = site.data.authors[post_author] | default: post_author %}
70 | {% assign post_author_email = post_author.email | default: nil %}
71 | {% assign post_author_uri = post_author.uri | default: nil %}
72 | {% assign post_author_name = post_author.name | default: post_author %}
73 |
74 |
75 | {{ post_author_name | default: "" | xml_escape }}
76 | {% if post_author_email %}
77 | {{ post_author_email | xml_escape }}
78 | {% endif %}
79 | {% if post_author_uri %}
80 | {{ post_author_uri | xml_escape }}
81 | {% endif %}
82 |
83 |
84 | {% if post.category %}
85 |
86 | {% endif %}
87 |
88 | {% for tag in post.tags %}
89 |
90 | {% endfor %}
91 |
92 | {% if post.excerpt and post.excerpt != empty %}
93 | {{ post.excerpt | strip_html | normalize_whitespace | xml_escape }}
94 | {% endif %}
95 |
96 | {% assign post_image = post.image.path | default: post.image %}
97 | {% if post_image %}
98 | {% unless post_image contains "://" %}
99 | {% assign post_image = post_image | absolute_url %}
100 | {% endunless %}
101 |
102 |
103 | {% endif %}
104 |
105 | {% endfor %}
106 |
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bridgetown Feed plugin
2 |
3 | A Bridgetown plugin to generate an Atom (RSS-like) feed of your Bridgetown posts and other collection documents.
4 |
5 | ## Installation for Bridgetown 1.2+
6 |
7 | Run this command to add this plugin to your site's Gemfile:
8 |
9 | ```shell
10 | $ bundle add bridgetown-feed
11 | ```
12 |
13 | Or simply add this line to your Gemfile:
14 |
15 | ```ruby
16 | gem 'bridgetown-feed'
17 | ```
18 |
19 | And then add the initializer to your configuration in `config/initializers.rb`:
20 |
21 | ```ruby
22 | Bridgetown.configure do
23 | # existing config here
24 |
25 | init :"bridgetown-feed"
26 | end
27 | ```
28 |
29 | (For Bridgetown 1.1 or earlier, [read these instructions](https://github.com/bridgetownrb/bridgetown-feed/tree/v2.1.0).)
30 |
31 | ## Usage
32 |
33 | The plugin exposes a helper tag to expose the appropriate meta tags to support automated discovery of your feed.
34 |
35 | Simply place `feed_meta` someplace in your layout's `` section to output the necessary metadata.
36 |
37 | ```erb
38 |
39 | <%= feed_meta %>
40 | ```
41 |
42 | ```liquid
43 |
44 | {% feed_meta %}
45 | ```
46 |
47 | The plugin will automatically generate an Atom feed at `/feed.xml`.
48 |
49 | ### Optional configuration options
50 |
51 | The plugin will automatically use any of the following metadata variables if they are present in your site's `_data/site_metadata.yml` file.
52 |
53 | * `title` or `name` - The title of the site, e.g., "My awesome site"
54 | * `description` - A longer description of what your site is about, e.g., "Where I blog about Bridgetown and other awesome things"
55 | * `author` - Global author information (see below)
56 |
57 | In addition it looks for these `bridgetown.config.yml` settings:
58 |
59 | * `url` - The URL to your site, e.g., `https://example.com`.
60 |
61 | ### Already have a feed path?
62 |
63 | Do you already have an existing feed someplace other than `/feed.xml`, but are on a host like GitHub Pages that doesn't support machine-friendly redirects? If you simply swap out `bridgetown-feed` for your existing template, your existing subscribers won't continue to get updates. Instead, you can specify a non-default path via your site's config.
64 |
65 | ```yml
66 | feed:
67 | path: atom.xml
68 | ```
69 |
70 | To note, you shouldn't have to do this unless you already have a feed you're using, and you can't or wish not to redirect existing subscribers.
71 |
72 | ### Optional front matter
73 |
74 | The plugin will use the following post metadata, automatically generated by Bridgetown, which you can override via a post's YAML front matter:
75 |
76 | * `date`
77 | * `title`
78 | * `excerpt`
79 | * `id`
80 | * `category`
81 | * `tags`
82 |
83 | Additionally, the plugin will use the following values, if present in a post's YAML front matter:
84 |
85 | * `image` - URL of an image that is representative of the post (can also be passed as `image.path`)
86 |
87 | * `author` - The author of the post, e.g., "Dr. Bridgetown". If none is given, feed readers will look to the feed author as defined in `_data/site_metadata.yml`. Like the feed author, this can also be an object or a reference to an author in `_data/authors.yml` (see below).
88 |
89 | ### Author information
90 |
91 | *TL;DR: In most cases, put `author: [your name]` in the document's front matter, for sites with multiple authors. If you need something more complicated, read on.*
92 |
93 | There are several ways to convey author-specific information. Author information is found in the following order of priority:
94 |
95 | 1. An `author` object, in the documents's front matter, e.g.:
96 |
97 | ```yml
98 | author:
99 | name: Issac Asimov
100 | ```
101 |
102 | 2. An `author` object, in the site's `_data/site_metadata.yml`, e.g.:
103 |
104 | ```yml
105 | author:
106 | name: Issac Asimov
107 | ```
108 |
109 | 3. `site.data.authors[author]`, if an author is specified in the document's front matter, and a corresponding key exists in `site.data.authors`. E.g., you have the following in the document's front matter:
110 |
111 | ```yml
112 | author: iasimov
113 | ```
114 |
115 | And you have the following in `_data/authors.yml`:
116 |
117 | ```yml
118 | iasimov:
119 | picture: /images/marina.jpg
120 | name: Issac Asimov
121 |
122 | jwhite:
123 | picture: /images/jared.jpg
124 | name: Jared White
125 | ```
126 |
127 | In the above example, the author `iasimov`'s name will be resolved to `Issac Asimov`. This allows you to centralize author information in a single `_data/authors.yml` file for site with many authors that require more than just the author's username.
128 |
129 | *Pro-tip: If `authors` is present in the document's front matter as an array (and `author` is not), the plugin will use the first author listed.*
130 |
131 | 4. An author in the document's front matter (the simplest way), e.g.:
132 |
133 | ```yml
134 | author: marina
135 | ```
136 |
137 | 5. An author in the site's `_data/site_metadata.yml`, e.g.:
138 |
139 | ```yml
140 | author: marina
141 | ```
142 |
143 | The author keys the plugin can read are `name`, `email`, and `uri` (for linking to an author's website).
144 |
145 | ### SmartyPants
146 |
147 | The plugin uses [Bridgetown's `smartify` filter](https://www.bridgetownrb.com/docs/liquid/filters) for processing the site title and post titles. This will translate plain ASCII punctuation into "smart" typographic punctuation. This will not render or strip any Markdown you may be using in a title.
148 |
149 | Bridgetown's `smartify` filter uses [kramdown](https://kramdown.gettalong.org/options.html) as a processor. Accordingly, if you do not want "smart" typographic punctuation, disabling them in kramdown in your `bridgetown.config.yml` will disable them in your feed. For example:
150 |
151 | ```yml
152 | kramdown:
153 | smart_quotes: apos,apos,quot,quot
154 | typographic_symbols: {hellip: ...}
155 | ```
156 |
157 | ### Custom styling
158 |
159 | Want to style what your feed looks like in the browser? Simply add an XSLT at `/feed.xslt.xml` and Bridgetown Feed will link to the stylesheet.
160 |
161 | ## Categories
162 |
163 | Bridgetown Feed can generate feeds for each category. Simply define which categories you'd like feeds for in your config:
164 |
165 | ```yml
166 | feed:
167 | categories:
168 | - news
169 | - updates
170 | ```
171 |
172 | ## Collections
173 |
174 | Bridgetown 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:
175 |
176 | ```yml
177 | feed:
178 | collections:
179 | - changes
180 | ```
181 |
182 | 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:
183 |
184 | ```yml
185 | feed:
186 | collections:
187 | changes:
188 | path: "/changes.xml"
189 | ```
190 |
191 | Finally, collections can also have category feeds which are outputted as `/feed//.xml`. Specify categories like so:
192 |
193 | ```yml
194 | feed:
195 | collections:
196 | changes:
197 | path: "/changes.xml"
198 | categories:
199 | - news
200 | - updates
201 | ```
202 |
203 | ## Excerpt Only flag
204 |
205 | Optional flag `excerpt_only` allows you to exclude post content from the Atom feed. Default value is `false` for backward compatibility.
206 |
207 | When it's set to `true` in `bridgetown.config.yml`, all posts in feed will be without `` tags.
208 |
209 | ```yml
210 | feed:
211 | excerpt_only: true
212 | ```
213 |
214 | The same flag can be used directly in post file. It will be disable `` tag for selected post.
215 | Settings in post file has higher priority than in config file.
216 |
217 | ## Post Limit
218 |
219 | Optional flag `post_limit` allows you to set a limit to the number of posts shown in the feed. Default value is `10`.
220 |
221 | When it is set in `bridgetown.config.yml`, all collections will be limited:
222 |
223 | ```yml
224 | feed:
225 | post_limit: 25
226 | ```
227 |
228 | The same flag can also be set on a collection:
229 |
230 | ```yml
231 | feed:
232 | collections:
233 | changes:
234 | post_limit: 25
235 | ```
236 |
237 | ## Icons
238 |
239 | You can optionally specify feed's icon and logo relative paths:
240 |
241 | ```yml
242 | feed:
243 | icon: "/images/icon.png"
244 | logo: "/images/logo.png"
245 | ```
246 |
247 | ## Testing
248 |
249 | * Run `bundle exec rspec` to run the test suite
250 | * Or run `script/cibuild` to validate with Rubocop and test with rspec together
251 |
252 | ## Contributing
253 |
254 | 1. Fork it (https://github.com/bridgetownrb/bridgetown-feed/fork)
255 | 2. Create your feature branch (`git checkout -b my-new-feature`)
256 | 3. Commit your changes (`git commit -am 'Add some feature'`)
257 | 4. Push to the branch (`git push origin my-new-feature`)
258 | 5. Create a new Pull Request
259 |
--------------------------------------------------------------------------------
/spec/bridgetown-feed_spec.rb:
--------------------------------------------------------------------------------
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 | "collections" => {
16 | "my_collection" => { "output" => true },
17 | "other_things" => { "output" => false },
18 | },
19 | }, overrides)).tap do |conf|
20 | conf.run_initializers! context: :static
21 | end
22 | end
23 | let(:metadata_overrides) { {} }
24 | let(:metadata_defaults) do
25 | {
26 | "name" => "My awesome site",
27 | "author" => {
28 | "name" => "Dr. Bridgetown",
29 | }
30 | }
31 | end
32 | let(:site) { Bridgetown::Site.new(config) }
33 | let(:contents) { File.read(dest_dir("feed.xml")) }
34 | let(:context) { make_context(:site => site) }
35 | let(:feed_meta) { Liquid::Template.parse("{% feed_meta %}").render!(context, {}) }
36 | before(:each) do
37 | metadata = metadata_defaults.merge(metadata_overrides).to_yaml.sub("---\n", "")
38 | File.write(source_dir("_data/site_metadata.yml"), metadata)
39 | site.process
40 | FileUtils.rm(source_dir("_data/site_metadata.yml"))
41 | end
42 |
43 | it "has no layout" do
44 | expect(contents).not_to match(%r!\ATHIS IS MY LAYOUT!)
45 | end
46 |
47 | it "creates a feed.xml file" do
48 | expect(Pathname.new(dest_dir("feed.xml"))).to exist
49 | end
50 |
51 | it "doesn't have multiple new lines or trailing whitespace" do
52 | expect(contents).to_not match %r!\s+\n!
53 | expect(contents).to_not match %r!\n{2,}!
54 | end
55 |
56 | it "puts all the posts in the feed.xml file" do
57 | expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth/"
58 | expect(contents).to match "http://example.org/news/2014/03/02/march-the-second/"
59 | expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second/"
60 | expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle/"
61 | expect(contents).to_not match "http://example.org/2016/02/09/a-draft/"
62 | end
63 |
64 | it "does not include assets or any static files that aren't .html" do
65 | expect(contents).not_to match "http://example.org/images/hubot.png"
66 | expect(contents).not_to match "http://example.org/feeds/atom.xml"
67 | end
68 |
69 | it "preserves linebreaks in preformatted text in posts" do
70 | expect(contents).to match "Line 1\nLine 2\nLine 3"
71 | end
72 |
73 | it "supports post author name as an object" do
74 | expect(contents).to match %r!\s*Ben\s*ben@example\.com\s*http://ben\.balter\.com\s*!
75 | end
76 |
77 | it "supports post author name as a string" do
78 | expect(contents).to match %r!\s*Pat\s*!
79 | end
80 |
81 | it "does not output author tag no author is provided" do
82 | expect(contents).not_to match %r!\s*\s*!
83 | end
84 |
85 | it "does use author reference with data from _data/authors.yml" do
86 | expect(contents).to match %r!\s*Garth\s*example@mail\.com\s*http://garthdb\.com\s*!
87 | end
88 |
89 | it "converts markdown posts to HTML" do
90 | expect(contents).to match %r!<p>March the second\!</p>!
91 | end
92 |
93 | it "uses last_modified_at where available" do
94 | expect(contents).to match %r!2015-05-12T13:27:59\+00:00!
95 | end
96 |
97 | it "replaces newlines in posts to spaces" do
98 | expect(contents).to match 'The plugin will properly strip newlines.'
99 | end
100 |
101 | it "renders Liquid inside posts" do
102 | expect(contents).to match "Liquid is rendered."
103 | expect(contents).not_to match "Liquid is not rendered."
104 | end
105 |
106 | context "images" do
107 | let(:image1) { 'http://example.org/image.png' }
108 | let(:image2) { 'https://cdn.example.org/absolute.png?h=188&w=250' }
109 | let(:image3) { 'http://example.org/object-image.png' }
110 |
111 | it "includes the item image" do
112 | expect(contents).to include(%())
113 | expect(contents).to include(%())
114 | expect(contents).to include(%())
115 | end
116 |
117 | it "included media content for mail templates (Mailchimp)" do
118 | expect(contents).to include(%())
119 | expect(contents).to include(%())
120 | expect(contents).to include(%())
121 | end
122 | end
123 |
124 | context "erb helper" do
125 | it "outputs link tag" do
126 | page = site.collections.pages.resources.find { |item| item.data.title == "I'm a page" }
127 | expect(page.output).to include(%())
128 | end
129 | end
130 |
131 | context "parsing" do
132 | let(:feed) { RSS::Parser.parse(contents) }
133 |
134 | it "outputs an RSS feed" do
135 | expect(feed.feed_type).to eql("atom")
136 | expect(feed.feed_version).to eql("1.0")
137 | expect(feed.encoding).to eql("UTF-8")
138 | expect(feed.lang).to be_nil
139 | expect(feed.valid?).to eql(true)
140 | end
141 |
142 | it "outputs the link" do
143 | expect(feed.link.href).to eql("http://example.org/feed.xml")
144 | end
145 |
146 | it "outputs the generator" do
147 | expect(feed.generator.content).to eql("Bridgetown")
148 | expect(feed.generator.version).to eql(Bridgetown::VERSION)
149 | end
150 |
151 | it "includes the items" do
152 | expect(feed.items.count).to eql(10)
153 | end
154 |
155 | it "includes item contents" do
156 | post = feed.items.last
157 | expect(post.title.content).to eql("Dec The Second")
158 | expect(post.link.href).to eql("http://example.org/news/2013/12/12/dec-the-second/")
159 | expect(post.published.content).to eql(Time.parse("2013-12-12"))
160 | end
161 |
162 | it "includes the item's excerpt" do
163 | post = feed.items.last
164 | expect(post.summary.content).to eql("Foo")
165 | end
166 |
167 | it "doesn't include the item's excerpt if blank" do
168 | post = feed.items.first
169 | expect(post.summary).to be_nil
170 | end
171 |
172 | it "has the correct item ids" do
173 | expect(feed.items.first.id.content).to eql("repo://posts.collection/_posts/2016-04-25-author-reference.md")
174 | expect(feed.items.last.id.content).to eql("https://example.com/foo")
175 | end
176 |
177 | context "with site.lang set" do
178 | lang = "en_US"
179 | let(:overrides) { { "lang" => lang } }
180 | it "outputs a valid feed" do
181 | expect(feed.feed_type).to eql("atom")
182 | expect(feed.feed_version).to eql("1.0")
183 | expect(feed.encoding).to eql("UTF-8")
184 | expect(feed.valid?).to eql(true)
185 | end
186 |
187 | it "outputs the correct language" do
188 | expect(feed.lang).to eql(lang)
189 | end
190 |
191 | it "sets the language of entries" do
192 | post = feed.items.first
193 | expect(post.lang).to eql(lang)
194 | end
195 |
196 | it "renders the feed meta" do
197 | expected = %r!!
198 | expect(contents).to match(expected)
199 | end
200 | end
201 |
202 | context "with site.title set" do
203 | let(:site_title) { "My Site Title" }
204 | let(:metadata_overrides) { { "title" => site_title } }
205 |
206 | it "uses site.title for the title" do
207 | expect(feed.title.content).to eql(site_title)
208 | end
209 | end
210 |
211 | context "with site.name set" do
212 | let(:site_name) { "My Site Name" }
213 | let(:metadata_overrides) { { "name" => site_name } }
214 |
215 | it "uses site.name for the title" do
216 | expect(feed.title.content).to eql(site_name)
217 | end
218 | end
219 |
220 | context "with site.name and site.title set" do
221 | let(:site_title) { "My Site Title" }
222 | let(:site_name) { "My Site Name" }
223 | let(:metadata_overrides) { { "title" => site_title, "name" => site_name } }
224 |
225 | it "uses site.title for the title, dropping site.name" do
226 | expect(feed.title.content).to eql(site_title)
227 | end
228 | end
229 | end
230 |
231 | context "smartify" do
232 | let(:site_title) { "Pat's Site" }
233 | let(:metadata_overrides) { { "title" => site_title } }
234 | let(:feed) { RSS::Parser.parse(contents) }
235 |
236 | it "processes site title with SmartyPants" do
237 | expect(feed.title.content).to eql("Pat’s Site")
238 | end
239 | end
240 |
241 | context "validation" do
242 | it "validates" do
243 | skip "Typhoeus couldn't find the 'libcurl' module on Windows" if Gem.win_platform?
244 | # See https://validator.w3.org/docs/api.html
245 | url = "https://validator.w3.org/feed/check.cgi?output=soap12"
246 | response = Typhoeus.post(url, :body => { :rawdata => contents }, :accept_encoding => "gzip")
247 | pending "Something went wrong with the W3 validator" unless response.success?
248 | result = Nokogiri::XML(response.body)
249 | result.remove_namespaces!
250 |
251 | result.css("warning").each do |warning|
252 | # Quiet a warning that results from us passing the feed as a string
253 | next if warning.css("text").text =~ %r!Self reference doesn't match document location!
254 |
255 | # Quiet expected warning that results from blank summary test case
256 | next if warning.css("text").text =~ %r!(content|summary) should not be blank!
257 |
258 | # Quiet expected warning about multiple posts with same updated time
259 | next if warning.css("text").text =~ %r!Two entries with the same value for atom:updated!
260 |
261 | warn "Validation warning: #{warning.css("text").text} on line #{warning.css("line").text} column #{warning.css("column").text}"
262 | end
263 |
264 | errors = result.css("error").map do |error|
265 | "Validation error: #{error.css("text").text} on line #{error.css("line").text} column #{error.css("column").text}"
266 | end
267 |
268 | expect(result.css("validity").text).to eql("true"), errors.join("\n")
269 | end
270 | end
271 |
272 | context "with a baseurl" do
273 | let(:overrides) do
274 | { "base_path" => "/bass" }
275 | end
276 |
277 | it "correctly adds the baseurl to the posts" do
278 | expect(contents).to match "http://example.org/bass/updates/bridgetown/2014/03/04/march-the-fourth/"
279 | expect(contents).to match "http://example.org/bass/news/2014/03/02/march-the-second/"
280 | expect(contents).to match "http://example.org/bass/news/2013/12/12/dec-the-second/"
281 | end
282 |
283 | it "renders the feed meta" do
284 | expected = 'href="http://example.org/bass/feed.xml"'
285 | expect(feed_meta).to include(expected)
286 | end
287 | end
288 |
289 | context "feed meta" do
290 | it "renders the feed meta" do
291 | expected = ''
292 | expect(feed_meta).to eql(expected)
293 | end
294 |
295 | context "with a blank site name" do
296 | let(:config) do
297 | Bridgetown.configuration(
298 | "source" => source_dir,
299 | "destination" => dest_dir,
300 | "url" => "http://example.org"
301 | )
302 | end
303 | let(:metadata_defaults) { {} }
304 |
305 | it "does not output blank title" do
306 | expect(feed_meta).not_to include("title=")
307 | end
308 | end
309 | end
310 |
311 | context "changing the feed path" do
312 | let(:overrides) do
313 | {
314 | "feed" => {
315 | "path" => "atom.xml",
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 "changing the file path via collection meta" do
331 | let(:overrides) do
332 | {
333 | "feed" => {
334 | "collections" => {
335 | "posts" => {
336 | "path" => "atom.xml",
337 | },
338 | },
339 | },
340 | }
341 | end
342 |
343 | it "should write to atom.xml" do
344 | expect(Pathname.new(dest_dir("atom.xml"))).to exist
345 | end
346 |
347 | it "renders the feed meta with custom feed path" do
348 | expected = 'href="http://example.org/atom.xml"'
349 | expect(feed_meta).to include(expected)
350 | end
351 | end
352 |
353 | context "feed stylesheet" do
354 | it "includes the stylesheet" do
355 | expect(contents).to include('')
356 | end
357 | end
358 |
359 | context "with site.lang set" do
360 | let(:overrides) { { "lang" => "en-US" } }
361 |
362 | it "should set the language" do
363 | expect(contents).to match 'type="text/html" hreflang="en-US" />'
364 | end
365 | end
366 |
367 | context "with post.lang set" do
368 | it "should set the language for that entry" do
369 | expect(contents).to match ''
370 | expect(contents).to match ''
371 | end
372 | end
373 |
374 | context "categories" do
375 | context "with top-level post categories" do
376 | let(:overrides) do
377 | {
378 | "feed" => { "categories" => ["news"] },
379 | }
380 | end
381 | let(:news_feed) { File.read(dest_dir("feed/news.xml")) }
382 |
383 | it "outputs the primary feed" do
384 | expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth/"
385 | expect(contents).to match "http://example.org/news/2014/03/02/march-the-second/"
386 | expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second/"
387 | expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle/"
388 | expect(contents).to_not match "http://example.org/2016/02/09/a-draft/"
389 | end
390 |
391 | it "outputs the category feed" do
392 | expect(news_feed).to match 'My awesome site | News'
393 | expect(news_feed).to match "http://example.org/news/2014/03/02/march-the-second/"
394 | expect(news_feed).to match "http://example.org/news/2013/12/12/dec-the-second/"
395 | expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth/"
396 | expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle/"
397 | end
398 | end
399 |
400 | context "with collection-level post categories" do
401 | let(:overrides) do
402 | {
403 | "feed" => {
404 | "collections" => {
405 | "posts" => {
406 | "categories" => ["news"],
407 | },
408 | },
409 | },
410 | }
411 | end
412 | let(:news_feed) { File.read(dest_dir("feed/news.xml")) }
413 |
414 | it "outputs the primary feed" do
415 | expect(contents).to match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth/"
416 | expect(contents).to match "http://example.org/news/2014/03/02/march-the-second/"
417 | expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second/"
418 | expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle/"
419 | expect(contents).to_not match "http://example.org/2016/02/09/a-draft/"
420 | end
421 |
422 | it "outputs the category feed" do
423 | expect(news_feed).to match 'My awesome site | News'
424 | expect(news_feed).to match "http://example.org/news/2014/03/02/march-the-second/"
425 | expect(news_feed).to match "http://example.org/news/2013/12/12/dec-the-second/"
426 | expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth/"
427 | expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle/"
428 | end
429 | end
430 | end
431 |
432 | context "collections" do
433 | let(:collection_feed) { File.read(dest_dir("feed/collection.xml")) }
434 |
435 | context "when initialized as an array" do
436 | let(:overrides) do
437 | {
438 | "collections" => {
439 | "collection" => {
440 | "output" => true,
441 | },
442 | },
443 | "feed" => { "collections" => ["collection"] },
444 | }
445 | end
446 |
447 | it "outputs the collection feed" do
448 | expect(collection_feed).to match 'My awesome site | Collection'
449 | expect(collection_feed).to match "http://example.org/collection/collection-doc/"
450 | expect(collection_feed).to match "http://example.org/collection/collection-category-doc/"
451 | expect(collection_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth/"
452 | expect(collection_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle/"
453 | end
454 | end
455 |
456 | context "with categories" do
457 | let(:overrides) do
458 | {
459 | "collections" => {
460 | "collection" => {
461 | "output" => true,
462 | },
463 | },
464 | "feed" => {
465 | "collections" => {
466 | "collection" => {
467 | "categories" => ["news"],
468 | },
469 | },
470 | },
471 | }
472 | end
473 | let(:news_feed) { File.read(dest_dir("feed/collection/news.xml")) }
474 |
475 | it "outputs the collection category feed" do
476 | expect(news_feed).to match 'My awesome site | Collection | News'
477 | expect(news_feed).to match "http://example.org/collection/collection-category-doc/"
478 | expect(news_feed).to_not match "http://example.org/collection/collection-doc/"
479 | expect(news_feed).to_not match "http://example.org/updates/bridgetown/2014/03/04/march-the-fourth/"
480 | expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle/"
481 | end
482 | end
483 |
484 | context "with a custom path" do
485 | let(:overrides) do
486 | {
487 | "collections" => {
488 | "collection" => {
489 | "output" => true,
490 | },
491 | },
492 | "feed" => {
493 | "collections" => {
494 | "collection" => {
495 | "categories" => ["news"],
496 | "path" => "custom.xml",
497 | },
498 | },
499 | },
500 | }
501 | end
502 |
503 | it "should write to the custom path" do
504 | expect(Pathname.new(dest_dir("custom.xml"))).to exist
505 | expect(Pathname.new(dest_dir("feed/collection.xml"))).to_not exist
506 | expect(Pathname.new(dest_dir("feed/collection/news.xml"))).to exist
507 | end
508 | end
509 | end
510 |
511 | context "excerpt_only flag" do
512 | context "backward compatibility for no excerpt_only flag" do
513 | it "should be in contents" do
514 | expect(contents).to match ' { "excerpt_only" => true } }
521 | end
522 |
523 | it "should not set any contents" do
524 | expect(contents).to_not match ' { "excerpt_only" => false } }
531 | end
532 |
533 | it "should be in contents" do
534 | expect(contents).to match ' { "excerpt_only" => false } }
541 | end
542 |
543 | it "should not be in contents" do
544 | expect(contents).to_not match "This content should not be in feed."
545 | end
546 | end
547 | end
548 |
549 | context "post_limit override" do
550 | it "limit the number of posts by default" do
551 | expect(contents.scan(" {
558 | "collections" => {
559 | "posts" => {
560 | "post_limit": "1"
561 | },
562 | },
563 | },
564 | }
565 | end
566 |
567 | it "should limit the number of posts" do
568 | expect(contents.scan(" {
576 | "post_limit": 1
577 | },
578 | }
579 | end
580 |
581 | it "should limit the number of posts" do
582 | expect(contents.scan("