├── 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("