├── .github └── workflows │ ├── release.yml │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── Gemfile ├── History.markdown ├── LICENSE.txt ├── README.md ├── Rakefile ├── appveyor.yml ├── jekyll-feed.gemspec ├── lib ├── jekyll-feed.rb └── jekyll-feed │ ├── feed.xml │ ├── generator.rb │ ├── meta-tag.rb │ ├── page-without-a-file.rb │ └── version.rb ├── script ├── bootstrap ├── cibuild ├── fmt ├── release └── test └── spec ├── fixtures ├── _collection │ ├── 2018-01-01-collection-doc.md │ └── 2018-01-02-collection-category-doc.md ├── _config.yml ├── _data │ └── authors.yml ├── _drafts │ └── 2015-01-12-a-draft.md ├── _layouts │ └── some_default.html ├── _posts │ ├── 2013-12-12-dec-the-second.md │ ├── 2014-03-02-march-the-second.md │ ├── 2014-03-04-march-the-fourth.md │ ├── 2015-01-18-jekyll-last-modified-at.md │ ├── 2015-02-12-strip-newlines.md │ ├── 2015-05-12-liquid.md │ ├── 2015-05-12-pre.html │ ├── 2015-05-18-author-detail.md │ ├── 2015-08-08-stuck-in-the-middle.html │ └── 2016-04-25-author-reference.md └── feed.xslt.xml ├── jekyll-feed_spec.rb └── spec_helper.rb /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Gem 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | paths: ["lib/**/version.rb"] 7 | 8 | jobs: 9 | release: 10 | if: "github.repository_owner == 'jekyll'" 11 | name: "Release Gem (Ruby ${{ matrix.ruby_version }})" 12 | runs-on: "ubuntu-latest" 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | ruby_version: ["2.7"] 17 | steps: 18 | - name: Checkout Repository 19 | uses: actions/checkout@v2 20 | - name: "Set up Ruby ${{ matrix.ruby_version }}" 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby_version }} 24 | bundler-cache: true 25 | - name: Build and Publish Gem 26 | uses: ashmaroli/release-gem@dist 27 | with: 28 | gemspec_name: "jekyll-feed" 29 | env: 30 | GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_GEM_PUSH_API_KEY }} 31 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: 'Ruby ${{ matrix.ruby-version }}' 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3'] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | bundler-cache: true 24 | - name: Run tests 25 | run: bash script/cibuild 26 | -------------------------------------------------------------------------------- /.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 | spec/fixtures/.jekyll-metadata 21 | spec/fixtures/.jekyll-cache 22 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: rubocop-jekyll 4 | 5 | inherit_gem: 6 | rubocop-jekyll: .rubocop.yml 7 | 8 | AllCops: 9 | TargetRubyVersion: 2.5 10 | SuggestExtensions: false 11 | Exclude: 12 | - vendor/**/* 13 | 14 | Layout/LineEndStringConcatenationIndentation: 15 | Enabled: true 16 | 17 | Lint/EmptyInPattern: 18 | Enabled: false 19 | 20 | Metrics/AbcSize: 21 | IgnoredMethods: 22 | - generate # in generator.rb 23 | 24 | Naming/InclusiveLanguage: 25 | Enabled: false 26 | Naming/MemoizedInstanceVariableName: 27 | Exclude: 28 | - lib/jekyll-feed/page-without-a-file.rb 29 | 30 | Performance/MapCompact: 31 | Enabled: true 32 | Performance/RedundantEqualityComparisonBlock: 33 | Enabled: true 34 | Performance/RedundantSplitRegexpArgument: 35 | Enabled: true 36 | 37 | Style/InPatternThen: 38 | Enabled: false 39 | Style/MultilineInPatternThen: 40 | Enabled: false 41 | Style/QuotedSymbols: 42 | Enabled: true 43 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-09-17 11:44:51 UTC using RuboCop version 1.18.4. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Cop supports --auto-correct. 11 | Layout/EmptyLines: 12 | Exclude: 13 | - 'spec/jekyll-feed_spec.rb' 14 | 15 | # Offense count: 15 16 | # Cop supports --auto-correct. 17 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 18 | # URISchemes: http, https 19 | Layout/LineLength: 20 | Max: 144 21 | 22 | # Offense count: 3 23 | # Configuration parameters: AllowedMethods. 24 | # AllowedMethods: enums 25 | Lint/ConstantDefinitionInBlock: 26 | Exclude: 27 | - 'spec/jekyll-feed_spec.rb' 28 | - 'spec/spec_helper.rb' 29 | 30 | # Offense count: 3 31 | # Cop supports --auto-correct. 32 | Performance/RegexpMatch: 33 | Exclude: 34 | - 'spec/jekyll-feed_spec.rb' 35 | 36 | # Offense count: 2 37 | # Cop supports --auto-correct. 38 | # Configuration parameters: AutoCorrect. 39 | Performance/StringInclude: 40 | Exclude: 41 | - 'spec/jekyll-feed_spec.rb' 42 | 43 | # Offense count: 2 44 | # Cop supports --auto-correct. 45 | # Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. 46 | # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys 47 | Style/HashSyntax: 48 | Exclude: 49 | - 'spec/jekyll-feed_spec.rb' 50 | 51 | # Offense count: 7 52 | # Cop supports --auto-correct. 53 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 54 | # SupportedStyles: single_quotes, double_quotes 55 | Style/StringLiterals: 56 | Exclude: 57 | - 'spec/jekyll-feed_spec.rb' 58 | 59 | # Offense count: 5 60 | # Cop supports --auto-correct. 61 | # Configuration parameters: EnforcedStyleForMultiline. 62 | # SupportedStylesForMultiline: comma, consistent_comma, no_comma 63 | Style/TrailingCommaInHashLiteral: 64 | Exclude: 65 | - 'spec/jekyll-feed_spec.rb' 66 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec 5 | 6 | gem "jekyll", ENV["JEKYLL_VERSION"] if ENV["JEKYLL_VERSION"] 7 | gem "kramdown-parser-gfm" if ENV["JEKYLL_VERSION"] == "~> 3.9" 8 | gem "rss" if RUBY_VERSION >= "3.0.0" 9 | 10 | install_if -> { Gem.win_platform? } do 11 | gem "tzinfo", "~> 1.2" 12 | gem "tzinfo-data" 13 | end 14 | -------------------------------------------------------------------------------- /History.markdown: -------------------------------------------------------------------------------- 1 | ## HEAD 2 | 3 | ### Development Fixes 4 | 5 | * Upgrade to Rake 13 to fix master (#381) 6 | * feat(CI): Add Ruby 3.2/3.3 to the test matrix (#402) 7 | * Fix style offenses (#414) 8 | 9 | ### Documentation 10 | 11 | * Fix typos in Readme (#384) 12 | 13 | ### Minor Enhancements 14 | 15 | * Icon and logo support with simple tests (#412) 16 | 17 | ## 0.17.0 / 2022-10-14 18 | 19 | ### Documentation 20 | 21 | * Update CI status badge (#363) 22 | 23 | ### Development Fixes 24 | 25 | * Add Ruby 3.1 to the CI matrix (#365) 26 | 27 | ### Minor Enhancements 28 | 29 | * Allow disabling of jekyll-feed while in development (#370) 30 | 31 | ## 0.16.0 / 2022-01-03 32 | 33 | ### Minor Enhancements 34 | 35 | * Add support for `page.description` in front matter to become entry `` (#297) 36 | 37 | ### Bug Fixes 38 | 39 | * Fold private methods into the `:render` method as local variables (#327) 40 | * Check `post.categories` instead of `post.category` (#357) 41 | * Switched xml_escape for `` for post content (#332) 42 | 43 | ### Development Fixes 44 | 45 | * Add Ruby 3.0 to CI (#337) 46 | * Lock RuboCop to v1.18.x (#348) 47 | * Add workflow to release gem via GH Action (#355) 48 | 49 | ### Documentation 50 | 51 | * Use `.atom` extension in documented examples since we write an Atom feed (#359) 52 | 53 | ## 0.15.1 / 2020-10-04 54 | 55 | ### Bug Fixes 56 | 57 | * MetaTag: when encoding for XML special characters, handle non-string objects (#326) 58 | 59 | ## 0.15.0 / 2020-07-10 60 | 61 | ### Minor Enhancements 62 | 63 | * Add support for drafts (#316) 64 | 65 | ## 0.14.0 / 2020-06-24 66 | 67 | ### Minor Enhancements 68 | 69 | * add support for categories (#153) (#233) 70 | * add support for tags (#264) 71 | * Make posts limit configurable (#314) 72 | * XML escape the title field of feed_meta (#306) 73 | 74 | ### Bug Fixes 75 | 76 | * Fix feed link when post title contains HTML (#305) 77 | 78 | ### Development Fixes 79 | 80 | * Use Dir to list source files (#309) 81 | * Require Ruby >=2.4.0 (#307) 82 | 83 | ## 0.13.0 / 2019-11-13 84 | 85 | ### Minor Enhancements 86 | 87 | * Excerpt only flag (#287) 88 | * Add media:content tag (#290) 89 | 90 | ### Development Fixes 91 | 92 | * test: use categories in post (#249) 93 | 94 | ## 0.12.1 / 2019-03-23 95 | 96 | ### Bug Fixes 97 | 98 | * Re-introduce Ruby 2.3 support and test Jekyll 3.7+ (#272) 99 | 100 | ## 0.12.0 / 2019-03-21 101 | 102 | * Allow Jekyll v4 (still alpha) 103 | 104 | ### Development Fixes 105 | 106 | * style: fix offenses in specs (#248) 107 | * dev: update CI and style settings (#258) 108 | * Enable testing for Windows platform (#265) 109 | 110 | ## 0.11.0 / 2018-09-09 111 | 112 | ### Development Fixes 113 | 114 | * Require Ruby 2.3 (#222) 115 | * Refactor to remove redundant calls and variables (#240) 116 | 117 | ### Minor Enhancements 118 | 119 | * Categories and collections (#228) 120 | * Remove check for older version of Jekyll (#234) 121 | 122 | ## 0.10.0 / 2018-06-04 123 | 124 | ### Bug Fixes 125 | 126 | * Escape image URL (#209) 127 | 128 | ### Development Fixes 129 | 130 | * Rubocop 0.55 (#223) 131 | * Bump Rubocop (#230) 132 | 133 | ### Minor Enhancements 134 | 135 | * Support Typhoeus 1.0 (#232) 136 | 137 | ## 0.9.3 / 2018-02-04 138 | 139 | * Define path with __dir (#187) 140 | * Bump Ruby for Travis (#188) 141 | 142 | ### Documentation 143 | 144 | * Fix: Add note about using plugins instead of gems key (#197) 145 | * Add documentation for disabling smartify filter (#205) 146 | * Use `https` in more places. (#165) 147 | 148 | ### Development Fixes 149 | 150 | * Rubocop: Target Ruby 2.2 (#195) 151 | * Test feeds that have a `site.lang` (#164) 152 | * Test against Ruby 2.5 (#201) 153 | 154 | ### Minor Enhancements 155 | 156 | * fix template for posts with post.lang defined (#168) 157 | 158 | ## 0.9.1 / 2017-02-17 159 | 160 | ### Minor Enhancements 161 | 162 | * Update feed.xml (#162) 163 | 164 | ## 0.9.0 / 2017-02-16 165 | 166 | ### Minor Enhancements 167 | 168 | * Use absolute_url to generate the feed_meta url (#150) 169 | * Make feed stylesheet optional (#149) 170 | * Use new `normalize_whitespace` filter (#143) 171 | * Feed entries must contain (#152) 172 | * Remove trailing slash from feed ID (#159) 173 | 174 | ### Development Fixes 175 | 176 | * Simplify minify regular expression (#141) 177 | * Namespace as JekyllFeed (#151) 178 | * rubocop -a (#160) 179 | 180 | ### Bug Fixes 181 | 182 | * Filter out drafts before limit (#154) 183 | 184 | ## 0.8.0 / 2016-10-06 185 | 186 | * Use filters to clean up Liquid template (#134) 187 | 188 | ### Minor Enhancements 189 | 190 | * Don't set @site.config["time"] on feed generation (#138) 191 | 192 | ### pedantry 193 | 194 | * Appease Rubocop (#139) 195 | 196 | ## 0.7.2 / 2016-10-06 197 | 198 | * Support `image.path` when `post.image` is an object (#137) 199 | 200 | ## 0.7.1 / 2016-09-26 201 | 202 | * Assign `url_base` before first usage (#133) 203 | 204 | ## 0.7.0 / 2016-09-06 205 | 206 | * Use type="html" to skirt around double escaping problem (#127) 207 | 208 | ## 0.6.0 / 2016-07-08 209 | 210 | * Cleanup `post_author` logic (#113) 211 | * Add XML stylesheet example with XSLT (#119) 212 | * DRY up and add more doc (#120) 213 | * Use smartify filter (#117) 214 | 215 | ## 0.5.1 / 2016-04-18 216 | 217 | * Fix mangling of whitespace when `site.lang` is set (#110) 218 | 219 | ## 0.5.0 / 2016-04-13 220 | 221 | * Consolidate regexps for stripping whitespace (#82) 222 | * Only test against Jekyll 3 (#99) 223 | * Think about how i18n might work (#75) 224 | * Find author by reference (#106) 225 | * Drop support for Jekyll 2 (#105) 226 | * Add support for post image (#104) 227 | 228 | ### Minor Enhancements 229 | 230 | * Use Module#method_defined? (#83) 231 | * Use site.title for meta tag if available (#100) 232 | 233 | ### Development Fixes 234 | 235 | * Do not require [**jekyll-last-modified-at**](https://github.com/gjtorikian/jekyll-last-modified-at) in tests (#87) 236 | * Add Rubocop (#81) 237 | * Correct typo in tests (#102) 238 | * Simplify testing feed_meta tag (#101) 239 | * Quiet known warnings in tests (#103) 240 | 241 | ## 0.4.0 / 2015-12-30 242 | 243 | * Feed uses `site.title`, or `site.name` if `title` doesn't exist (#72) 244 | * Replace newlines with spaces in `title` and `summary` elements (#67) 245 | * Properly render post content with Jekyll (#73) 246 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-present Ben Balter and jekyll-feed contributors 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Feed plugin 2 | 3 | A Jekyll plugin to generate an Atom (RSS-like) feed of your Jekyll posts 4 | 5 | [![Continuous Integration](https://github.com/jekyll/jekyll-feed/actions/workflows/ruby.yml/badge.svg)](https://github.com/jekyll/jekyll-feed/actions/workflows/ruby.yml) [![Gem Version](https://badge.fury.io/rb/jekyll-feed.svg)](https://badge.fury.io/rb/jekyll-feed) 6 | 7 | ## Installation 8 | 9 | Add this line to your site's Gemfile: 10 | 11 | ```ruby 12 | gem 'jekyll-feed' 13 | ``` 14 | 15 | And then add this line to your site's `_config.yml`: 16 | 17 | ```yml 18 | plugins: 19 | - jekyll-feed 20 | ``` 21 | 22 | :warning: If you are using Jekyll < 3.5.0 use the `gems` key instead of `plugins`. 23 | 24 | ## Usage 25 | 26 | The plugin will automatically generate an Atom feed at `/feed.xml`. 27 | 28 | ### Optional configuration options 29 | 30 | The plugin will automatically use any of the following configuration variables, if they are present in your site's `_config.yml` file. 31 | 32 | * `title` or `name` - The title of the site, e.g., "My awesome site" 33 | * `description` - A longer description of what your site is about, e.g., "Where I blog about Jekyll and other awesome things" 34 | * `url` - The URL to your site, e.g., `https://example.com`. If none is provided, the plugin will try to use `site.github.url`. 35 | * `author` - Global author information (see below) 36 | * `feed.icon` - Icon with 1:1 proportions for readers to use for the blog feed (not supported by all readers; often overridden by a favicon or site icon) 37 | * `feed.logo` - Logo with 2:1 proportions for readers to use for the blog feed (not supported by all readers). 38 | For example, both of the preceding can be expressed: 39 | ```yml 40 | feed: 41 | icon: /images/icon.png 42 | logo: /images/logo.png 43 | ``` 44 | 45 | 46 | ### Already have a feed path? 47 | 48 | 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 `jekyll-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. 49 | 50 | ```yml 51 | feed: 52 | path: /blog/feed.atom 53 | ``` 54 | 55 | 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. 56 | 57 | ### Optional front matter 58 | 59 | The plugin will use the following post metadata, automatically generated by Jekyll, which you can override via a post's YAML front matter: 60 | 61 | * `date` 62 | * `title` 63 | * `excerpt` 64 | * `id` 65 | * `category` 66 | * `tags` 67 | 68 | Additionally, the plugin will use the following values, if present in a post's YAML front matter: 69 | 70 | * `image` - URL of an image that is representative of the post (can also be passed as `image.path`) 71 | 72 | * `author` - The author of the post, e.g., "Dr. Jekyll". If none is given, feed readers will look to the feed author as defined in `_config.yml`. Like the feed author, this can also be an object or a reference to an author in `_data/authors.yml` (see below). 73 | 74 | * `description` - A short description of the post. 75 | 76 | ### Author information 77 | 78 | *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.* 79 | 80 | There are several ways to convey author-specific information. Author information is found in the following order of priority: 81 | 82 | 1. An `author` object, in the document's front matter, e.g.: 83 | 84 | ```yml 85 | author: 86 | twitter: benbalter 87 | ``` 88 | 89 | 2. An `author` object, in the site's `_config.yml`, e.g.: 90 | 91 | ```yml 92 | author: 93 | twitter: benbalter 94 | ``` 95 | 96 | 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: 97 | 98 | ```yml 99 | author: benbalter 100 | ``` 101 | 102 | And you have the following in `_data/authors.yml`: 103 | 104 | ```yml 105 | benbalter: 106 | picture: /img/benbalter.png 107 | twitter: jekyllrb 108 | 109 | potus: 110 | picture: /img/potus.png 111 | twitter: whitehouse 112 | ``` 113 | 114 | In the above example, the author `benbalter`'s Twitter handle will be resolved to `@jekyllrb`. This allows you to centralize author information in a single `_data/authors` file for site with many authors that require more than just the author's username. 115 | 116 | *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.* 117 | 118 | 4. An author in the document's front matter (the simplest way), e.g.: 119 | 120 | ```yml 121 | author: benbalter 122 | ``` 123 | 124 | 5. An author in the site's `_config.yml`, e.g.: 125 | 126 | ```yml 127 | author: benbalter 128 | ``` 129 | 130 | ### Meta tags 131 | 132 | The plugin exposes a helper tag to expose the appropriate meta tags to support automated discovery of your feed. Simply place `{% feed_meta %}` someplace in your template's `` section, to output the necessary metadata. 133 | 134 | ### SmartyPants 135 | 136 | The plugin uses [Jekyll's `smartify` filter](https://jekyllrb.com/docs/templates/) 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. 137 | 138 | Jekyll'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 `_config.yml` will disable them in your feed. For example: 139 | 140 | ```yml 141 | kramdown: 142 | smart_quotes: apos,apos,quot,quot 143 | typographic_symbols: {hellip: ...} 144 | ``` 145 | 146 | ### Custom styling 147 | 148 | Want to style what your feed looks like in the browser? When a XSLT Styleheet file named `feed.xslt.xml` exists at the root of your repository, a link to this stylesheet is added to the generated feed. 149 | 150 | ## Why Atom, and not RSS? 151 | 152 | Great question. In short, Atom is a better format. Think of it like RSS 3.0. For more information, see [this discussion on why we chose Atom over RSS 2.0](https://github.com/jekyll/jekyll-rss-feed/issues/2). 153 | 154 | ## Categories 155 | 156 | Jekyll Feed can generate feeds for each category. Simply define which categories you'd like feeds for in your config: 157 | 158 | ```yml 159 | feed: 160 | categories: 161 | - news 162 | - updates 163 | ``` 164 | 165 | ## Posts limit 166 | 167 | By default the plugin limits the number of posts in the feed to 10. Simply define a new limit in your config: 168 | 169 | ```yml 170 | feed: 171 | posts_limit: 20 172 | ``` 173 | 174 | ## Collections 175 | 176 | Jekyll Feed can generate feeds for collections other than the Posts collection. This works best for chronological collections (e.g., collections with dates in the filenames). Simply define which collections you'd like feeds for in your config: 177 | 178 | ```yml 179 | feed: 180 | collections: 181 | - changes 182 | ``` 183 | 184 | 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: 185 | 186 | ```yml 187 | feed: 188 | collections: 189 | changes: 190 | path: "/changes.atom" 191 | ``` 192 | 193 | Finally, collections can also have category feeds that are outputted as `/feed//.xml`. Specify categories like so: 194 | 195 | ```yml 196 | feed: 197 | collections: 198 | changes: 199 | path: "/changes.atom" 200 | categories: 201 | - news 202 | - updates 203 | ``` 204 | 205 | ## Excerpt Only flag 206 | 207 | Optional flag `excerpt_only` allows you to exclude post content from the Atom feed. Default value is `false` for backward compatibility. 208 | 209 | When in `config.yml` is `true` then all posts in feed will be without `` tags. 210 | 211 | ```yml 212 | feed: 213 | excerpt_only: true 214 | ``` 215 | 216 | The same flag can be used directly in post file. It will disable `` tag for selected post. 217 | Settings in post file have higher priority than in config file. 218 | 219 | ## Tags 220 | 221 | To automatically generate feeds for each tag you apply to your posts you can add a tags setting to your config: 222 | 223 | ```yml 224 | feed: 225 | tags: true 226 | ``` 227 | 228 | If there are tags you don't want included in this auto generation you can exclude them 229 | 230 | ```yml 231 | feed: 232 | tags: 233 | except: 234 | - tag-to-exclude 235 | - another-tag 236 | ``` 237 | 238 | If you wish to change the location of these auto generated feeds (`/feed/by_tag/.xml` by default) you can provide an alternative folder for them to live in. 239 | 240 | ```yml 241 | feed: 242 | tags: 243 | path: "alternative/path/for/tags/feeds/" 244 | ``` 245 | 246 | If you only want to generate feeds for a few tags you can also set this. 247 | 248 | ```yml 249 | feed: 250 | tags: 251 | only: 252 | - tag-to-include 253 | - another-tag 254 | ``` 255 | 256 | Note that if you include a tag that is excluded a feed will not be generated for it. 257 | 258 | ## Skip development 259 | 260 | Use `disable_in_development: true` if you want to turn off feed generation when `jekyll.environment == "development"`, 261 | but don't want to remove the plugin (so you don't accidentally commit the removal). Default value is `false`. 262 | 263 | ```yml 264 | feed: 265 | disable_in_development: true 266 | ``` 267 | 268 | ## Contributing 269 | 270 | 1. Fork it (https://github.com/jekyll/jekyll-feed/fork) 271 | 2. Create your feature branch (`git checkout -b my-new-feature`) 272 | 3. Commit your changes (`git commit -am 'Add some feature'`) 273 | 4. Push to the branch (`git push origin my-new-feature`) 274 | 5. Create a new Pull Request 275 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | clone_depth: 5 3 | build: off 4 | 5 | environment: 6 | NOKOGIRI_USE_SYSTEM_LIBRARIES: true 7 | JEKYLL_VERSION: "~> 3.9" 8 | matrix: 9 | - RUBY_FOLDER_VER: "26" 10 | JEKYLL_VERSION : "~> 3.8.6" 11 | - RUBY_FOLDER_VER: "26" 12 | JEKYLL_VERSION : ">= 4.0.0.pre.alpha1" 13 | - RUBY_FOLDER_VER: "26" 14 | 15 | install: 16 | - SET PATH=C:\Ruby%RUBY_FOLDER_VER%-x64\bin;%PATH% 17 | - bundle install --retry 5 --jobs=%NUMBER_OF_PROCESSORS% --clean --path vendor\bundle 18 | 19 | test_script: 20 | - ruby --version 21 | - gem --version 22 | - bundler --version 23 | - bash ./script/test 24 | 25 | cache: 26 | # If one of the files after the right arrow changes, cache will be invalidated 27 | - 'vendor\bundle -> appveyor.yml, Gemfile, jekyll-feed.gemspec' 28 | -------------------------------------------------------------------------------- /jekyll-feed.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/jekyll-feed/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "jekyll-feed" 7 | spec.version = Jekyll::Feed::VERSION 8 | spec.authors = ["Ben Balter"] 9 | spec.email = ["ben.balter@github.com"] 10 | spec.summary = "A Jekyll plugin to generate an Atom feed of your Jekyll posts" 11 | spec.homepage = "https://github.com/jekyll/jekyll-feed" 12 | spec.license = "MIT" 13 | 14 | spec.files = Dir["lib/**/*"] 15 | spec.extra_rdoc_files = Dir["README.md", "History.markdown", "LICENSE.txt"] 16 | spec.test_files = spec.files.grep(%r!^spec/!) 17 | spec.require_paths = ["lib"] 18 | 19 | spec.required_ruby_version = ">= 2.5.0" 20 | 21 | spec.add_dependency "jekyll", ">= 3.7", "< 5.0" 22 | 23 | spec.add_development_dependency "bundler" 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-jekyll", "~> 0.12.0" 28 | spec.add_development_dependency "typhoeus", ">= 0.7", "< 2.0" 29 | end 30 | -------------------------------------------------------------------------------- /lib/jekyll-feed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "jekyll" 4 | require "fileutils" 5 | require "jekyll-feed/generator" 6 | 7 | module JekyllFeed 8 | autoload :MetaTag, "jekyll-feed/meta-tag" 9 | autoload :PageWithoutAFile, "jekyll-feed/page-without-a-file.rb" 10 | end 11 | 12 | Liquid::Template.register_tag "feed_meta", JekyllFeed::MetaTag 13 | -------------------------------------------------------------------------------- /lib/jekyll-feed/feed.xml: -------------------------------------------------------------------------------- 1 | 2 | {% if page.xsl %} 3 | 4 | {% endif %} 5 | 6 | Jekyll 7 | 8 | 9 | {{ site.time | date_to_xmlschema }} 10 | {{ page.url | absolute_url | xml_escape }} 11 | 12 | {% assign title = site.title | default: site.name %} 13 | {% if page.collection != "posts" %} 14 | {% assign collection = page.collection | capitalize %} 15 | {% assign title = title | append: " | " | append: collection %} 16 | {% endif %} 17 | {% if page.category %} 18 | {% assign category = page.category | capitalize %} 19 | {% assign title = title | append: " | " | append: category %} 20 | {% endif %} 21 | 22 | {% if title %} 23 | {{ title | smartify | xml_escape }} 24 | {% endif %} 25 | 26 | {% if site.description %} 27 | {{ site.description | xml_escape }} 28 | {% endif %} 29 | 30 | {% if site.feed.icon %} 31 | {% assign feed_icon = site.feed.icon %} 32 | {% unless feed_icon contains "://" %} 33 | {% assign feed_icon = feed_icon | absolute_url %} 34 | {% endunless %} 35 | {{ feed_icon | xml_escape }} 36 | {% endif %} 37 | 38 | {% if site.feed.logo %} 39 | {% assign feed_logo = site.feed.logo %} 40 | {% unless feed_logo contains "://" %} 41 | {% assign feed_logo = feed_logo | absolute_url %} 42 | {% endunless %} 43 | {{ feed_logo | xml_escape }} 44 | {% endif %} 45 | 46 | {% if site.author %} 47 | 48 | {{ site.author.name | default: site.author | xml_escape }} 49 | {% if site.author.email %} 50 | {{ site.author.email | xml_escape }} 51 | {% endif %} 52 | {% if site.author.uri %} 53 | {{ site.author.uri | xml_escape }} 54 | {% endif %} 55 | 56 | {% endif %} 57 | 58 | {% if page.tags %} 59 | {% assign posts = site.tags[page.tags] %} 60 | {% else %} 61 | {% assign posts = site[page.collection] %} 62 | {% endif %} 63 | {% if page.category %} 64 | {% assign posts = posts | where: "categories", page.category %} 65 | {% endif %} 66 | {% unless site.show_drafts %} 67 | {% assign posts = posts | where_exp: "post", "post.draft != true" %} 68 | {% endunless %} 69 | {% assign posts = posts | sort: "date" | reverse %} 70 | {% assign posts_limit = site.feed.posts_limit | default: 10 %} 71 | {% for post in posts limit: posts_limit %} 72 | 73 | {% assign post_title = post.title | smartify | strip_html | normalize_whitespace | xml_escape %} 74 | 75 | {{ post_title }} 76 | 77 | {{ post.date | date_to_xmlschema }} 78 | {{ post.last_modified_at | default: post.date | date_to_xmlschema }} 79 | {{ post.id | absolute_url | xml_escape }} 80 | {% assign excerpt_only = post.feed.excerpt_only | default: site.feed.excerpt_only %} 81 | {% unless excerpt_only %} 82 | 83 | {% endunless %} 84 | 85 | {% assign post_author = post.author | default: post.authors[0] | default: site.author %} 86 | {% assign post_author = site.data.authors[post_author] | default: post_author %} 87 | {% assign post_author_email = post_author.email | default: nil %} 88 | {% assign post_author_uri = post_author.uri | default: nil %} 89 | {% assign post_author_name = post_author.name | default: post_author %} 90 | 91 | 92 | {{ post_author_name | default: "" | xml_escape }} 93 | {% if post_author_email %} 94 | {{ post_author_email | xml_escape }} 95 | {% endif %} 96 | {% if post_author_uri %} 97 | {{ post_author_uri | xml_escape }} 98 | {% endif %} 99 | 100 | 101 | {% if post.category %} 102 | 103 | {% elsif post.categories %} 104 | {% for category in post.categories %} 105 | 106 | {% endfor %} 107 | {% endif %} 108 | 109 | {% for tag in post.tags %} 110 | 111 | {% endfor %} 112 | 113 | {% assign post_summary = post.description | default: post.excerpt %} 114 | {% if post_summary and post_summary != empty %} 115 | 116 | {% endif %} 117 | 118 | {% assign post_image = post.image.path | default: post.image %} 119 | {% if post_image %} 120 | {% unless post_image contains "://" %} 121 | {% assign post_image = post_image | absolute_url %} 122 | {% endunless %} 123 | 124 | 125 | {% endif %} 126 | 127 | {% endfor %} 128 | 129 | -------------------------------------------------------------------------------- /lib/jekyll-feed/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllFeed 4 | class Generator < Jekyll::Generator 5 | safe true 6 | priority :lowest 7 | 8 | # Main plugin action, called by Jekyll-core 9 | def generate(site) 10 | @site = site 11 | if disabled_in_development? 12 | Jekyll.logger.info "Jekyll Feed:", "Skipping feed generation in development" 13 | return 14 | end 15 | collections.each do |name, meta| 16 | Jekyll.logger.info "Jekyll Feed:", "Generating feed for #{name}" 17 | (meta["categories"] + [nil]).each do |category| 18 | path = feed_path(:collection => name, :category => category) 19 | next if file_exists?(path) 20 | 21 | @site.pages << make_page(path, :collection => name, :category => category) 22 | end 23 | end 24 | generate_feed_by_tag if config["tags"] && !@site.tags.empty? 25 | end 26 | 27 | private 28 | 29 | # Matches all whitespace that follows 30 | # 1. A '>', which closes an XML tag or 31 | # 2. A '}', which closes a Liquid tag 32 | # We will strip all of this whitespace to minify the template 33 | MINIFY_REGEX = %r!(?<=>|})\s+!.freeze 34 | 35 | # Returns the plugin's config or an empty hash if not set 36 | def config 37 | @config ||= @site.config["feed"] || {} 38 | end 39 | 40 | # Determines the destination path of a given feed 41 | # 42 | # collection - the name of a collection, e.g., "posts" 43 | # category - a category within that collection, e.g., "news" 44 | # 45 | # Will return "/feed.xml", or the config-specified default feed for posts 46 | # Will return `/feed/category.xml` for post categories 47 | # WIll return `/feed/collection.xml` for other collections 48 | # Will return `/feed/collection/category.xml` for other collection categories 49 | def feed_path(collection: "posts", category: nil) 50 | prefix = collection == "posts" ? "/feed" : "/feed/#{collection}" 51 | return "#{prefix}/#{category}.xml" if category 52 | 53 | collections.dig(collection, "path") || "#{prefix}.xml" 54 | end 55 | 56 | # Returns a hash representing all collections to be processed and their metadata 57 | # in the form of { collection_name => { categories = [...], path = "..." } } 58 | def collections 59 | return @collections if defined?(@collections) 60 | 61 | @collections = case config["collections"] 62 | when Array 63 | config["collections"].map { |c| [c, {}] }.to_h 64 | when Hash 65 | config["collections"] 66 | else 67 | {} 68 | end 69 | 70 | @collections = normalize_posts_meta(@collections) 71 | @collections.each_value do |meta| 72 | meta["categories"] = (meta["categories"] || []).to_set 73 | end 74 | 75 | @collections 76 | end 77 | 78 | def generate_feed_by_tag 79 | tags_config = config["tags"] 80 | tags_config = {} unless tags_config.is_a?(Hash) 81 | 82 | except = tags_config["except"] || [] 83 | only = tags_config["only"] || @site.tags.keys 84 | tags_pool = only - except 85 | tags_path = tags_config["path"] || "/feed/by_tag/" 86 | 87 | generate_tag_feed(tags_pool, tags_path) 88 | end 89 | 90 | def generate_tag_feed(tags_pool, tags_path) 91 | tags_pool.each do |tag| 92 | # allow only tags with basic alphanumeric characters and underscore to keep 93 | # feed path simple. 94 | next if %r![^a-zA-Z0-9_]!.match?(tag) 95 | 96 | Jekyll.logger.info "Jekyll Feed:", "Generating feed for posts tagged #{tag}" 97 | path = "#{tags_path}#{tag}.xml" 98 | next if file_exists?(path) 99 | 100 | @site.pages << make_page(path, :tags => tag) 101 | end 102 | end 103 | 104 | # Path to feed.xml template file 105 | def feed_source_path 106 | @feed_source_path ||= File.expand_path "feed.xml", __dir__ 107 | end 108 | 109 | def feed_template 110 | @feed_template ||= File.read(feed_source_path).gsub(MINIFY_REGEX, "") 111 | end 112 | 113 | # Checks if a file already exists in the site source 114 | def file_exists?(file_path) 115 | File.exist? @site.in_source_dir(file_path) 116 | end 117 | 118 | # Generates contents for a file 119 | 120 | def make_page(file_path, collection: "posts", category: nil, tags: nil) 121 | PageWithoutAFile.new(@site, __dir__, "", file_path).tap do |file| 122 | file.content = feed_template 123 | file.data.merge!( 124 | "layout" => nil, 125 | "sitemap" => false, 126 | "xsl" => file_exists?("feed.xslt.xml"), 127 | "collection" => collection, 128 | "category" => category, 129 | "tags" => tags 130 | ) 131 | file.output 132 | end 133 | end 134 | 135 | # Special case the "posts" collection, which, for ease of use and backwards 136 | # compatability, can be configured via top-level keys or directly as a collection 137 | def normalize_posts_meta(hash) 138 | hash["posts"] ||= {} 139 | hash["posts"]["path"] ||= config["path"] 140 | hash["posts"]["categories"] ||= config["categories"] 141 | config["path"] ||= hash["posts"]["path"] 142 | hash 143 | end 144 | 145 | def disabled_in_development? 146 | config && config["disable_in_development"] && Jekyll.env == "development" 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/jekyll-feed/meta-tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllFeed 4 | class MetaTag < Liquid::Tag 5 | # Use Jekyll's native relative_url filter 6 | include Jekyll::Filters::URLFilters 7 | 8 | def render(context) 9 | # Jekyll::Filters::URLFilters requires `@context` to be set in the environment. 10 | @context = context 11 | 12 | config = context.registers[:site].config 13 | path = config.dig("feed", "path") || "feed.xml" 14 | title = config["title"] || config["name"] 15 | 16 | attributes = { 17 | :type => "application/atom+xml", 18 | :rel => "alternate", 19 | :href => absolute_url(path), 20 | } 21 | attributes[:title] = title if title 22 | 23 | attrs = attributes.map { |k, v| "#{k}=#{v.to_s.encode(:xml => :attr)}" }.join(" ") 24 | "" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/jekyll-feed/page-without-a-file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllFeed 4 | class PageWithoutAFile < Jekyll::Page 5 | def read_yaml(*) 6 | @data ||= {} 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/jekyll-feed/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Feed 5 | VERSION = "0.17.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | bundle install 4 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | script/test 6 | script/fmt 7 | bundle exec rake build 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | bundle exec rspec "$@" 5 | -------------------------------------------------------------------------------- /spec/fixtures/_collection/2018-01-01-collection-doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | Look at me! I'm a collection! 5 | -------------------------------------------------------------------------------- /spec/fixtures/_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 | -------------------------------------------------------------------------------- /spec/fixtures/_config.yml: -------------------------------------------------------------------------------- 1 | timezone: UTC 2 | 3 | defaults: 4 | - 5 | scope: 6 | path: "" 7 | type: pages 8 | values: 9 | layout: some_default 10 | -------------------------------------------------------------------------------- /spec/fixtures/_data/authors.yml: -------------------------------------------------------------------------------- 1 | garthdb: 2 | name: Garth 3 | twitter: garthdb 4 | uri: "http://garthdb.com" 5 | email: example@mail.com 6 | -------------------------------------------------------------------------------- /spec/fixtures/_drafts/2015-01-12-a-draft.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | This is a draft. 5 | -------------------------------------------------------------------------------- /spec/fixtures/_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/_posts/2013-12-12-dec-the-second.md: -------------------------------------------------------------------------------- 1 | --- 2 | excerpt: "Foo" 3 | image: "/image.png" 4 | category: news 5 | tags: 6 | - test 7 | --- 8 | 9 | # December the twelfth, actually. 10 | -------------------------------------------------------------------------------- /spec/fixtures/_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 | excerpt: "ignore me" 5 | description: cool post 6 | --- 7 | 8 | March the second! 9 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2014-03-04-march-the-fourth.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sparkling Title 3 | tags: 4 | - '"/>' 5 | - test 6 | image: 7 | path: "/object-image.png" 8 | categories: updates jekyll 9 | --- 10 | 11 | March the fourth! 12 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2015-01-18-jekyll-last-modified-at.md: -------------------------------------------------------------------------------- 1 | --- 2 | last_modified_at: 2015-05-12T13:27:59+00:00 3 | tags: 4 | - test 5 | - fail 6 | --- 7 | 8 | Please don't modify this file. It's modified time is important. 9 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2015-02-12-strip-newlines.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | The plugin 4 | will properly 5 | strip newlines. 6 | --- 7 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2015-05-12-liquid.md: -------------------------------------------------------------------------------- 1 | --- 2 | categories: [first, second, third] 3 | --- 4 | 5 | {% capture liquidstring %} 6 | Liquid is not rendered. 7 | {% endcapture %} 8 | {{ liquidstring | replace:'not ','' }} 9 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2015-05-12-pre.html: -------------------------------------------------------------------------------- 1 | --- 2 | author: Pat 3 | lang: en 4 | tags: 5 | - test 6 | --- 7 | 8 |
Line 1
 9 | Line 2
10 | Line 3
11 | -------------------------------------------------------------------------------- /spec/fixtures/_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 | tags: 8 | - test 9 | - success 10 | --- 11 | 12 | # December the twelfth, actually. 13 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2015-08-08-stuck-in-the-middle.html: -------------------------------------------------------------------------------- 1 | --- 2 | tags: 3 | - test 4 | - fail 5 | feed: 6 | excerpt_only: true 7 | --- 8 | 9 | This content should not be in feed. 10 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2016-04-25-author-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | excerpt: "" 3 | author: garthdb 4 | --- 5 | 6 | # April the twenty-fifth? 7 | -------------------------------------------------------------------------------- /spec/fixtures/feed.xslt.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jekyll/jekyll-feed/43cc5d51e30b59ab8d8ba970e42c8c437e2b233f/spec/fixtures/feed.xslt.xml -------------------------------------------------------------------------------- /spec/jekyll-feed_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe(JekyllFeed) do 6 | let(:overrides) { {} } 7 | let(:config) do 8 | Jekyll.configuration(Jekyll::Utils.deep_merge_hashes({ 9 | "full_rebuild" => true, 10 | "source" => source_dir, 11 | "destination" => dest_dir, 12 | "show_drafts" => false, 13 | "url" => "http://example.org", 14 | "name" => "My awesome site", 15 | "author" => { 16 | "name" => "Dr. Jekyll", 17 | }, 18 | "collections" => { 19 | "my_collection" => { "output" => true }, 20 | "other_things" => { "output" => false }, 21 | }, 22 | }, overrides)) 23 | end 24 | let(:site) { Jekyll::Site.new(config) } 25 | let(:contents) { File.read(dest_dir("feed.xml")) } 26 | let(:context) { make_context(:site => site) } 27 | let(:feed_meta) { Liquid::Template.parse("{% feed_meta %}").render!(context, {}) } 28 | let(:jekyll_env) { "development" } 29 | before(:each) do 30 | allow(Jekyll).to receive(:env).and_return(jekyll_env) 31 | site.process 32 | end 33 | 34 | it "has no layout" do 35 | expect(contents).not_to match(%r!\ATHIS IS MY LAYOUT!) 36 | end 37 | 38 | it "creates a feed.xml file" do 39 | expect(Pathname.new(dest_dir("feed.xml"))).to exist 40 | end 41 | 42 | it "doesn't have multiple new lines or trailing whitespace" do 43 | expect(contents).to_not match %r!\s+\n! 44 | expect(contents).to_not match %r!\n{2,}! 45 | end 46 | 47 | it "puts all the posts in the feed.xml file" do 48 | expect(contents).to match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 49 | expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html" 50 | expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html" 51 | expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html" 52 | expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html" 53 | end 54 | 55 | it "does not include assets or any static files that aren't .html" do 56 | expect(contents).not_to match "http://example.org/images/hubot.png" 57 | expect(contents).not_to match "http://example.org/feeds/atom.xml" 58 | end 59 | 60 | it "preserves linebreaks in preformatted text in posts" do 61 | expect(contents).to match "Line 1\nLine 2\nLine 3" 62 | end 63 | 64 | it "supports post author name as an object" do 65 | expect(contents).to match %r!\s*Ben\s*ben@example\.com\s*http://ben\.balter\.com\s*! 66 | end 67 | 68 | it "supports post author name as a string" do 69 | expect(contents).to match %r!\s*Pat\s*! 70 | end 71 | 72 | it "does not output author tag no author is provided" do 73 | expect(contents).not_to match %r!\s*\s*! 74 | end 75 | 76 | it "does use author reference with data from _data/authors.yml" do 77 | expect(contents).to match %r!\s*Garth\s*example@mail\.com\s*http://garthdb\.com\s*! 78 | end 79 | 80 | it "converts markdown posts to HTML" do 81 | expect(contents).to match %r!<\!\[CDATA\[

March the second\!

\]\]! 82 | end 83 | 84 | it "uses last_modified_at where available" do 85 | expect(contents).to match %r!2015-05-12T13:27:59\+00:00! 86 | end 87 | 88 | it "replaces newlines in posts to spaces" do 89 | expect(contents).to match 'The plugin will properly strip newlines.' 90 | end 91 | 92 | it "strips HTML from link titles" do 93 | expect(contents).to match %r!! 94 | end 95 | 96 | it "renders Liquid inside posts" do 97 | expect(contents).to match "Liquid is rendered." 98 | expect(contents).not_to match "Liquid is not rendered." 99 | end 100 | 101 | context "images" do 102 | let(:image1) { 'http://example.org/image.png' } 103 | let(:image2) { 'https://cdn.example.org/absolute.png?h=188&w=250' } 104 | let(:image3) { 'http://example.org/object-image.png' } 105 | 106 | it "includes the item image" do 107 | expect(contents).to include(%()) 108 | expect(contents).to include(%()) 109 | expect(contents).to include(%()) 110 | end 111 | 112 | it "included media content for mail templates (Mailchimp)" do 113 | expect(contents).to include(%()) 114 | expect(contents).to include(%()) 115 | expect(contents).to include(%()) 116 | end 117 | end 118 | 119 | context "parsing" do 120 | let(:feed) { RSS::Parser.parse(contents) } 121 | 122 | it "outputs an RSS feed" do 123 | expect(feed.feed_type).to eql("atom") 124 | expect(feed.feed_version).to eql("1.0") 125 | expect(feed.encoding).to eql("UTF-8") 126 | expect(feed.lang).to be_nil 127 | expect(feed.valid?).to eql(true) 128 | expect(feed.icon).to be_nil 129 | expect(feed.logo).to be_nil 130 | end 131 | 132 | it "outputs the link" do 133 | expect(feed.link.href).to eql("http://example.org/feed.xml") 134 | end 135 | 136 | it "outputs the generator" do 137 | expect(feed.generator.content).to eql("Jekyll") 138 | expect(feed.generator.version).to eql(Jekyll::VERSION) 139 | end 140 | 141 | it "includes the items" do 142 | expect(feed.items.count).to eql(10) 143 | end 144 | 145 | it "includes item contents" do 146 | post = feed.items.last 147 | expect(post.title.content).to eql("Dec The Second") 148 | expect(post.link.href).to eql("http://example.org/news/2013/12/12/dec-the-second.html") 149 | expect(post.published.content).to eql(Time.parse("2013-12-12")) 150 | end 151 | 152 | it "includes the item's excerpt" do 153 | post = feed.items.last 154 | expect(post.summary.content).to eql("Foo") 155 | end 156 | 157 | it "includes the item's description" do 158 | post = feed.items[-2] 159 | expect(post.summary.content).to eql("cool post") 160 | end 161 | 162 | it "doesn't include the item's excerpt if blank" do 163 | post = feed.items.first 164 | expect(post.summary).to be_nil 165 | end 166 | 167 | context "with site.lang set" do 168 | lang = "en_US" 169 | let(:overrides) { { "lang" => lang } } 170 | it "outputs a valid feed" do 171 | expect(feed.feed_type).to eql("atom") 172 | expect(feed.feed_version).to eql("1.0") 173 | expect(feed.encoding).to eql("UTF-8") 174 | expect(feed.valid?).to eql(true) 175 | end 176 | 177 | it "outputs the correct language" do 178 | expect(feed.lang).to eql(lang) 179 | end 180 | 181 | it "sets the language of entries" do 182 | post = feed.items.first 183 | expect(post.lang).to eql(lang) 184 | end 185 | 186 | it "renders the feed meta" do 187 | expected = %r!! 188 | expect(contents).to match(expected) 189 | end 190 | end 191 | 192 | context "with site.title set" do 193 | let(:site_title) { "My Site Title" } 194 | let(:overrides) { { "title" => site_title } } 195 | 196 | it "uses site.title for the title" do 197 | expect(feed.title.content).to eql(site_title) 198 | end 199 | end 200 | 201 | context "with site.title set as a non-string value" do 202 | class MySiteTitle 203 | def to_s 204 | "My Dynamic Site Title <&>" 205 | end 206 | alias_method :to_liquid, :to_s 207 | end 208 | let(:site_title) { MySiteTitle.new } 209 | let(:overrides) { { "title" => site_title } } 210 | 211 | it "ensures the site.title is the string representation of the object" do 212 | expect(feed.title.content).to eql(site_title.to_s.encode(xml: :text)) 213 | end 214 | end 215 | 216 | context "with site.name set" do 217 | let(:site_name) { "My Site Name" } 218 | let(:overrides) { { "name" => site_name } } 219 | 220 | it "uses site.name for the title" do 221 | expect(feed.title.content).to eql(site_name) 222 | end 223 | end 224 | 225 | context "with site.name and site.title set" do 226 | let(:site_title) { "My Site Title" } 227 | let(:site_name) { "My Site Name" } 228 | let(:overrides) { { "title" => site_title, "name" => site_name } } 229 | 230 | it "uses site.title for the title, dropping site.name" do 231 | expect(feed.title.content).to eql(site_title) 232 | end 233 | end 234 | 235 | context "with site.title has special characters" do 236 | let(:site_title) { "My Site Title <&>" } 237 | let(:overrides) { { "title" => site_title } } 238 | 239 | it "uses encoded site.title for the title" do 240 | expect(feed.title.content).to eql(site_title.encode(xml: :text)) 241 | end 242 | end 243 | 244 | context "with site.icon set" do 245 | let(:site_icon) { "myicon.png" } 246 | let(:overrides) { { "feed" => { "icon" => site_icon } } } 247 | 248 | it "uses site.icon for the icon" do 249 | expect(feed.icon.content).to eql("http://example.org/#{site_icon}") 250 | end 251 | end 252 | 253 | context "with site.logo set" do 254 | let(:site_logo) { "mylogo.png" } 255 | let(:overrides) { { "feed" => { "logo" => site_logo } } } 256 | 257 | it "uses site.logo for the logo" do 258 | expect(feed.logo.content).to eql("http://example.org/#{site_logo}") 259 | end 260 | end 261 | end 262 | 263 | context "smartify" do 264 | let(:site_title) { "Pat's Site" } 265 | let(:overrides) { { "title" => site_title } } 266 | let(:feed) { RSS::Parser.parse(contents) } 267 | 268 | it "processes site title with SmartyPants" do 269 | expect(feed.title.content).to eql("Pat’s Site") 270 | end 271 | end 272 | 273 | context "validation" do 274 | it "validates" do 275 | skip "Typhoeus couldn't find the 'libcurl' module on Windows" if Gem.win_platform? 276 | # See https://validator.w3.org/docs/api.html 277 | url = "https://validator.w3.org/feed/check.cgi?output=soap12" 278 | response = Typhoeus.post(url, :body => { :rawdata => contents }, :accept_encoding => "gzip") 279 | pending "Something went wrong with the W3 validator" unless response.success? 280 | result = Nokogiri::XML(response.body) 281 | result.remove_namespaces! 282 | 283 | result.css("warning").each do |warning| 284 | # Quiet a warning that results from us passing the feed as a string 285 | next if warning.css("text").text =~ %r!Self reference doesn't match document location! 286 | 287 | # Quiet expected warning that results from blank summary test case 288 | next if warning.css("text").text =~ %r!(content|summary) should not be blank! 289 | 290 | # Quiet expected warning about multiple posts with same updated time 291 | next if warning.css("text").text =~ %r!Two entries with the same value for atom:updated! 292 | 293 | warn "Validation warning: #{warning.css("text").text} on line #{warning.css("line").text} column #{warning.css("column").text}" 294 | end 295 | 296 | errors = result.css("error").map do |error| 297 | "Validation error: #{error.css("text").text} on line #{error.css("line").text} column #{error.css("column").text}" 298 | end 299 | 300 | expect(result.css("validity").text).to eql("true"), errors.join("\n") 301 | end 302 | end 303 | 304 | context "with a baseurl" do 305 | let(:overrides) do 306 | { "baseurl" => "/bass" } 307 | end 308 | 309 | it "correctly adds the baseurl to the posts" do 310 | expect(contents).to match "http://example.org/bass/updates/jekyll/2014/03/04/march-the-fourth.html" 311 | expect(contents).to match "http://example.org/bass/news/2014/03/02/march-the-second.html" 312 | expect(contents).to match "http://example.org/bass/news/2013/12/12/dec-the-second.html" 313 | end 314 | 315 | it "renders the feed meta" do 316 | expected = 'href="http://example.org/bass/feed.xml"' 317 | expect(feed_meta).to include(expected) 318 | end 319 | end 320 | 321 | context "feed meta" do 322 | it "renders the feed meta" do 323 | expected = '' 324 | expect(feed_meta).to eql(expected) 325 | end 326 | 327 | context "with a blank site name" do 328 | let(:config) do 329 | Jekyll.configuration( 330 | "source" => source_dir, 331 | "destination" => dest_dir, 332 | "url" => "http://example.org" 333 | ) 334 | end 335 | 336 | it "does not output blank title" do 337 | expect(feed_meta).not_to include("title=") 338 | end 339 | end 340 | end 341 | 342 | context "changing the feed path" do 343 | let(:overrides) do 344 | { 345 | "feed" => { 346 | "path" => "atom.xml", 347 | }, 348 | } 349 | end 350 | 351 | it "should write to atom.xml" do 352 | expect(Pathname.new(dest_dir("atom.xml"))).to exist 353 | end 354 | 355 | it "renders the feed meta with custom feed path" do 356 | expected = 'href="http://example.org/atom.xml"' 357 | expect(feed_meta).to include(expected) 358 | end 359 | end 360 | 361 | context "with 'categories' or 'category' or 'tags' key in the front matter" do 362 | let(:feed) { RSS::Parser.parse(contents) } 363 | let(:entry_with_single_category) { feed.items.find { |i| i.title.content == "March The Second" } } 364 | let(:entry_with_multiple_categories) { feed.items.find { |i| i.title.content == "Liquid" } } 365 | let(:entry_with_multiple_categories_and_tags) { feed.items.find { |i| i.title.content == "Sparkling Title" } } 366 | 367 | it "generates the feed correctly" do 368 | expect(entry_with_single_category.categories.map(&:term)).to eql(%w(news)) 369 | expect(entry_with_multiple_categories.categories.map(&:term)).to eql(%w(first second third)) 370 | expect(entry_with_multiple_categories_and_tags.categories.map(&:term)).to eql(["updates", "jekyll", "\"/>", "test"]) 371 | end 372 | end 373 | 374 | context "changing the file path via collection meta" do 375 | let(:overrides) do 376 | { 377 | "feed" => { 378 | "collections" => { 379 | "posts" => { 380 | "path" => "atom.xml", 381 | }, 382 | }, 383 | }, 384 | } 385 | end 386 | 387 | it "should write to atom.xml" do 388 | expect(Pathname.new(dest_dir("atom.xml"))).to exist 389 | end 390 | 391 | it "renders the feed meta with custom feed path" do 392 | expected = 'href="http://example.org/atom.xml"' 393 | expect(feed_meta).to include(expected) 394 | end 395 | end 396 | 397 | context "feed stylesheet" do 398 | it "includes the stylesheet" do 399 | expect(contents).to include('') 400 | end 401 | end 402 | 403 | context "with site.lang set" do 404 | let(:overrides) { { "lang" => "en-US" } } 405 | 406 | it "should set the language" do 407 | expect(contents).to match 'type="text/html" hreflang="en-US" />' 408 | end 409 | end 410 | 411 | context "with post.lang set" do 412 | it "should set the language for that entry" do 413 | expect(contents).to match '' 414 | expect(contents).to match '' 415 | end 416 | end 417 | 418 | context "categories" do 419 | context "with top-level post categories" do 420 | let(:overrides) do 421 | { 422 | "feed" => { "categories" => %w(news jekyll) }, 423 | } 424 | end 425 | let(:singular_category_feed) { File.read(dest_dir("feed/news.xml")) } 426 | let(:plural_categories_feed) { File.read(dest_dir("feed/jekyll.xml")) } 427 | 428 | it "outputs the primary feed" do 429 | expect(contents).to match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 430 | expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html" 431 | expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html" 432 | expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html" 433 | expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html" 434 | end 435 | 436 | it "outputs the category feeds" do 437 | expect(singular_category_feed).to match 'My awesome site | News' 438 | expect(singular_category_feed).to match "http://example.org/news/2014/03/02/march-the-second.html" 439 | expect(singular_category_feed).to match "March the second!" 440 | expect(plural_categories_feed).to match 'My awesome site | News' 441 | expect(plural_categories_feed).to match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 442 | expect(plural_categories_feed).to match "March the fourth!" 443 | end 444 | end 445 | 446 | context "with collection-level post categories" do 447 | let(:overrides) do 448 | { 449 | "feed" => { 450 | "collections" => { 451 | "posts" => { 452 | "categories" => ["news"], 453 | }, 454 | }, 455 | }, 456 | } 457 | end 458 | let(:news_feed) { File.read(dest_dir("feed/news.xml")) } 459 | 460 | it "outputs the primary feed" do 461 | expect(contents).to match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 462 | expect(contents).to match "http://example.org/news/2014/03/02/march-the-second.html" 463 | expect(contents).to match "http://example.org/news/2013/12/12/dec-the-second.html" 464 | expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html" 465 | expect(contents).to_not match "http://example.org/2016/02/09/a-draft.html" 466 | end 467 | 468 | it "outputs the category feed" do 469 | expect(news_feed).to match 'My awesome site | News' 470 | expect(news_feed).to match "http://example.org/news/2014/03/02/march-the-second.html" 471 | expect(news_feed).to match "http://example.org/news/2013/12/12/dec-the-second.html" 472 | expect(news_feed).to_not match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 473 | expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html" 474 | end 475 | end 476 | end 477 | 478 | context "collections" do 479 | let(:collection_feed) { File.read(dest_dir("feed/collection.xml")) } 480 | 481 | context "when initialized as an array" do 482 | let(:overrides) do 483 | { 484 | "collections" => { 485 | "collection" => { 486 | "output" => true, 487 | }, 488 | }, 489 | "feed" => { "collections" => ["collection"] }, 490 | } 491 | end 492 | 493 | it "outputs the collection feed" do 494 | expect(collection_feed).to match 'My awesome site | Collection' 495 | expect(collection_feed).to match "http://example.org/collection/2018-01-01-collection-doc.html" 496 | expect(collection_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html" 497 | expect(collection_feed).to_not match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 498 | expect(collection_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html" 499 | end 500 | end 501 | 502 | context "with categories" do 503 | let(:overrides) do 504 | { 505 | "collections" => { 506 | "collection" => { 507 | "output" => true, 508 | }, 509 | }, 510 | "feed" => { 511 | "collections" => { 512 | "collection" => { 513 | "categories" => ["news"], 514 | }, 515 | }, 516 | }, 517 | } 518 | end 519 | let(:news_feed) { File.read(dest_dir("feed/collection/news.xml")) } 520 | 521 | it "outputs the collection category feed" do 522 | expect(news_feed).to match 'My awesome site | Collection | News' 523 | expect(news_feed).to match "http://example.org/collection/2018-01-02-collection-category-doc.html" 524 | expect(news_feed).to_not match "http://example.org/collection/2018-01-01-collection-doc.html" 525 | expect(news_feed).to_not match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 526 | expect(news_feed).to_not match "http://example.org/2015/08/08/stuck-in-the-middle.html" 527 | end 528 | end 529 | 530 | context "with a custom path" do 531 | let(:overrides) do 532 | { 533 | "collections" => { 534 | "collection" => { 535 | "output" => true, 536 | }, 537 | }, 538 | "feed" => { 539 | "collections" => { 540 | "collection" => { 541 | "categories" => ["news"], 542 | "path" => "custom.xml", 543 | }, 544 | }, 545 | }, 546 | } 547 | end 548 | 549 | it "should write to the custom path" do 550 | expect(Pathname.new(dest_dir("custom.xml"))).to exist 551 | expect(Pathname.new(dest_dir("feed/collection.xml"))).to_not exist 552 | expect(Pathname.new(dest_dir("feed/collection/news.xml"))).to exist 553 | end 554 | end 555 | end 556 | 557 | context "tags" do 558 | let(:tags_feed_test) { File.read(dest_dir("feed/by_tag/test.xml")) } 559 | let(:tags_feed_fail) { File.read(dest_dir("feed/by_tag/fail.xml")) } 560 | let(:tags_feed_success) { File.read(dest_dir("feed/by_tag/success.xml")) } 561 | 562 | 563 | context "do not set tags setting" do 564 | it "should not write tags feeds" do 565 | expect(Pathname.new(dest_dir("feed/by_tag/test.xml"))).to_not exist 566 | expect(Pathname.new(dest_dir("feed/by_tag/fail.xml"))).to_not exist 567 | expect(Pathname.new(dest_dir("feed/by_tag/success.xml"))).to_not exist 568 | end 569 | end 570 | 571 | context "set tags setting" do 572 | let(:overrides) do 573 | { 574 | "feed" => { 575 | "tags" => true 576 | }, 577 | } 578 | end 579 | 580 | it "should write tags feeds" do 581 | expect(Pathname.new(dest_dir("feed/by_tag/test.xml"))).to exist 582 | expect(Pathname.new(dest_dir("feed/by_tag/fail.xml"))).to exist 583 | expect(Pathname.new(dest_dir("feed/by_tag/success.xml"))).to exist 584 | 585 | expect(tags_feed_test).to match "/2013/12/12/dec-the-second.html" 586 | expect(tags_feed_test).to match "/2014/03/04/march-the-fourth.html" 587 | expect(tags_feed_test).to match "/2015/01/18/jekyll-last-modified-at.html" 588 | expect(tags_feed_test).to match "/2015/05/12/pre.html" 589 | expect(tags_feed_test).to match "/2015/05/18/author-detail.html" 590 | expect(tags_feed_test).to match "/2015/08/08/stuck-in-the-middle.html" 591 | 592 | expect(tags_feed_fail).to match "/2015/01/18/jekyll-last-modified-at.html" 593 | expect(tags_feed_fail).to match "/2015/08/08/stuck-in-the-middle.html" 594 | expect(tags_feed_fail).to_not match "/2013/12/12/dec-the-second.html" 595 | expect(tags_feed_fail).to_not match "/2014/03/02/march-the-second.html" 596 | expect(tags_feed_fail).to_not match "/2014/03/04/march-the-fourth.html" 597 | expect(tags_feed_fail).to_not match "/2015/05/12/pre.html" 598 | expect(tags_feed_fail).to_not match "/2015/05/18/author-detail.html" 599 | 600 | expect(tags_feed_success).to match "2015/05/18/author-detail.html" 601 | expect(tags_feed_success).to_not match "/2013/12/12/dec-the-second.html" 602 | expect(tags_feed_success).to_not match "/2014/03/02/march-the-second.html" 603 | expect(tags_feed_success).to_not match "/2014/03/04/march-the-fourth.html" 604 | expect(tags_feed_success).to_not match "/2015/01/18/jekyll-last-modified-at.html" 605 | expect(tags_feed_success).to_not match "/2015/05/12/pre.html" 606 | expect(tags_feed_success).to_not match "/2015/08/08/stuck-in-the-middle.html" 607 | end 608 | end 609 | 610 | context "set exclusions" do 611 | let(:overrides) do 612 | { 613 | "feed" => { 614 | "tags" => { 615 | "except" => ["fail"] 616 | }, 617 | }, 618 | } 619 | end 620 | 621 | it "should not write fail feed" do 622 | expect(Pathname.new(dest_dir("feed/by_tag/test.xml"))).to exist 623 | expect(Pathname.new(dest_dir("feed/by_tag/fail.xml"))).to_not exist 624 | expect(Pathname.new(dest_dir("feed/by_tag/success.xml"))).to exist 625 | end 626 | end 627 | 628 | context "set inclusions" do 629 | let(:overrides) do 630 | { 631 | "feed" => { 632 | "tags" => { 633 | "only" => ["success"] 634 | }, 635 | }, 636 | } 637 | end 638 | 639 | it "should not write fail feed" do 640 | expect(Pathname.new(dest_dir("feed/by_tag/test.xml"))).to_not exist 641 | expect(Pathname.new(dest_dir("feed/by_tag/fail.xml"))).to_not exist 642 | expect(Pathname.new(dest_dir("feed/by_tag/success.xml"))).to exist 643 | end 644 | end 645 | 646 | context "set alternate path" do 647 | let(:overrides) do 648 | { 649 | "feed" => { 650 | "tags" => { 651 | "path" => "alternate/path/" 652 | }, 653 | }, 654 | } 655 | end 656 | 657 | it "should write feeds to new path" do 658 | expect(Pathname.new(dest_dir("feed/by_tag/test.xml"))).to_not exist 659 | expect(Pathname.new(dest_dir("feed/by_tag/fail.xml"))).to_not exist 660 | expect(Pathname.new(dest_dir("feed/by_tag/success.xml"))).to_not exist 661 | 662 | expect(Pathname.new(dest_dir("alternate/path/test.xml"))).to exist 663 | expect(Pathname.new(dest_dir("alternate/path/fail.xml"))).to exist 664 | expect(Pathname.new(dest_dir("alternate/path/success.xml"))).to exist 665 | end 666 | 667 | context "set to questionable path" do 668 | let(:overrides) do 669 | { 670 | "feed" => { 671 | "tags" => { 672 | "path" => "../../../../../../../questionable/path/" 673 | }, 674 | }, 675 | } 676 | end 677 | 678 | it "should write feeds to sane paths" do 679 | expect(Pathname.new(dest_dir("feed/by_tag/test.xml"))).to_not exist 680 | expect(Pathname.new(dest_dir("feed/by_tag/fail.xml"))).to_not exist 681 | expect(Pathname.new(dest_dir("feed/by_tag/success.xml"))).to_not exist 682 | 683 | expect(Pathname.new(dest_dir("questionable/path/test.xml"))).to exist 684 | expect(Pathname.new(dest_dir("questionable/path/fail.xml"))).to exist 685 | expect(Pathname.new(dest_dir("questionable/path/success.xml"))).to exist 686 | end 687 | end 688 | end 689 | end 690 | 691 | context "excerpt_only flag" do 692 | context "backward compatibility for no excerpt_only flag" do 693 | it "should be in contents" do 694 | expect(contents).to match ' { "excerpt_only" => true } } 701 | end 702 | 703 | it "should not set any contents" do 704 | expect(contents).to_not match ' { "excerpt_only" => false } } 711 | end 712 | 713 | it "should be in contents" do 714 | expect(contents).to match ' { "excerpt_only" => false } } 721 | end 722 | 723 | it "should not be in contents" do 724 | expect(contents).to_not match "This content should not be in feed." 725 | end 726 | end 727 | end 728 | 729 | context "with feed.posts_limit set to 2" do 730 | let(:overrides) do 731 | { "feed" => { "posts_limit" => 2 } } 732 | end 733 | 734 | it "puts the latest 2 the posts in the feed.xml file" do 735 | expect(contents).to_not match "http://example.org/news/2013/12/12/dec-the-second.html" 736 | expect(contents).to_not match "http://example.org/news/2014/03/02/march-the-second.html" 737 | expect(contents).to_not match "http://example.org/updates/jekyll/2014/03/04/march-the-fourth.html" 738 | expect(contents).to_not match "http://example.org/2015/01/18/jekyll-last-modified-at.html" 739 | expect(contents).to_not match "http://example.org/2015/02/12/strip-newlines.html" 740 | expect(contents).to_not match "http://example.org/2015/05/12/liquid.html" 741 | expect(contents).to_not match "http://example.org/2015/05/12/pre.html" 742 | expect(contents).to_not match "http://example.org/2015/05/18/author-detail.html" 743 | 744 | expect(contents).to match "http://example.org/2015/08/08/stuck-in-the-middle.html" 745 | expect(contents).to match "http://example.org/2016/04/25/author-reference.html" 746 | end 747 | end 748 | 749 | context "support drafts" do 750 | context "with disable show_drafts option" do 751 | let(:overrides) do 752 | { "show_drafts" => false } 753 | end 754 | 755 | it "should not be draft post" do 756 | expect(contents).to_not match "a-draft.html" 757 | end 758 | end 759 | 760 | context "with enable show_drafts option" do 761 | let(:overrides) do 762 | { "show_drafts" => true } 763 | end 764 | 765 | it "should be draft post" do 766 | expect(contents).to match "a-draft.html" 767 | end 768 | end 769 | end 770 | 771 | context "with skip_development" do 772 | let(:overrides) do 773 | { 774 | "feed" => { 775 | "disable_in_development" => true 776 | }, 777 | } 778 | end 779 | 780 | context "in production environment" do 781 | let(:jekyll_env) { "production" } 782 | 783 | it "generates a feed as normal" do 784 | expect(Pathname.new(dest_dir("feed.xml"))).to exist 785 | end 786 | end 787 | 788 | context "in development environment" do 789 | let(:jekyll_env) { "development" } 790 | 791 | it "does not generate a feed" do 792 | expect(Pathname.new(dest_dir("feed.xml"))).not_to exist 793 | end 794 | end 795 | end 796 | end 797 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "jekyll" 4 | require "typhoeus" unless Gem.win_platform? 5 | require "nokogiri" 6 | require "rss" 7 | require File.expand_path("../lib/jekyll-feed", __dir__) 8 | 9 | Jekyll.logger.log_level = :error 10 | 11 | RSpec.configure do |config| 12 | config.run_all_when_everything_filtered = true 13 | config.filter_run :focus 14 | config.order = "random" 15 | 16 | SOURCE_DIR = File.expand_path("fixtures", __dir__) 17 | DEST_DIR = File.expand_path("dest", __dir__) 18 | 19 | def source_dir(*files) 20 | File.join(SOURCE_DIR, *files) 21 | end 22 | 23 | def dest_dir(*files) 24 | File.join(DEST_DIR, *files) 25 | end 26 | 27 | def make_context(registers = {}) 28 | Liquid::Context.new({}, {}, { :site => site }.merge(registers)) 29 | end 30 | end 31 | --------------------------------------------------------------------------------