├── .gitignore ├── Gem.gemspec ├── Gemfile ├── LICENSE ├── README.md ├── lib ├── jekyll-menus.rb └── jekyll │ ├── menus.rb │ └── menus │ ├── drops.rb │ ├── drops │ ├── all.rb │ ├── item.rb │ └── menu.rb │ ├── hook.rb │ ├── utils.rb │ └── version.rb └── spec ├── fixture ├── _config.yml ├── _data │ └── menus.yml ├── _includes │ └── _menus.html ├── _layouts │ └── default.html ├── array-of-strings.md ├── empty.md ├── hello │ └── array-of-strings.md ├── key-hash.md └── string.md ├── lib └── jekyll │ └── menus.rb └── rspec └── helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /vendor/bundle 3 | /spec/fixture/_site 4 | /Gemfile.lock 5 | /*.gem 6 | -------------------------------------------------------------------------------- /Gem.gemspec: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | $LOAD_PATH.unshift(File.expand_path("../lib", __FILE__)) 6 | require "jekyll/menus/version" 7 | 8 | Gem::Specification.new do |spec| 9 | spec.authors = ["Jordon Bedwell"] 10 | spec.version = Jekyll::Menus::VERSION 11 | spec.homepage = "http://github.com/forestryio/jekyll-menus/" 12 | spec.description = "Menus (site navigation) for your Jekyll website" 13 | spec.summary = "Menus (navigation) for your very own Jekyll website." 14 | spec.files = %W(Gemfile) + Dir["lib/**/*"] 15 | spec.required_ruby_version = ">= 2.4.0" 16 | spec.email = ["jordon@envygeeks.io"] 17 | spec.require_paths = ["lib"] 18 | spec.name = "jekyll-menus" 19 | spec.has_rdoc = false 20 | spec.license = "MIT" 21 | 22 | spec.add_runtime_dependency("jekyll", ">= 3.6", "< 5.0" ) 23 | spec.add_development_dependency( 24 | "rspec", ">= 3", "< 4" 25 | ) 26 | end 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | group :development do 5 | gem "pry" 6 | end 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Forestry.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 19 | USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Menus 2 | 3 | A robust, simple-to-use menu plugin for Jekyll that allows for infinitely nested menus. 4 | 5 | ## Installation 6 | 7 | To install and use Jekyll Menus, you should have Ruby, and either [RubyGems](https://jekyllrb.com/docs/installation/#install-with-rubygems), or we recommend using [Bundler](https://bundler.io/#getting-started). Bundler is what Jekyll will prefer you to use by default if you `jekyll new`. 8 | 9 | ### Using Bundler 10 | 11 | You can add our gem to the `jekyll_plugins` group in your `Gemfile`: 12 | 13 | ```ruby 14 | group :jekyll_plugins do 15 | gem "jekyll-menus" 16 | end 17 | ``` 18 | 19 | And then install from shell. 20 | 21 | ```sh 22 | bundle install 23 | # --path vendor/bundle 24 | ``` 25 | 26 | ***If you are using Jekyll Docker, you do not need to perform this step, Jekyll Docker will perform it on your behalf when you launch the image, you only need to perform this step if you are working directly on your system.*** 27 | 28 | ### Using RubyGems 29 | 30 | ```sh 31 | sudo gem install jekyll-menus 32 | sudo gem update jekyll-menus 33 | ``` 34 | 35 | Once installed, add the Gem to your `_config.yml`: 36 | 37 | ```yaml 38 | plugins: 39 | - jekyll-menus 40 | ``` 41 | 42 | ***Note in earlier versions of Jekyll, `plugins` should instead be `gems`*** 43 | 44 | ## Usage 45 | 46 | Jekyll Menus allows you to create menus by attaching posts and pages to menus through their front matter, or by defining custom menu items via `_data/menus.yml`. 47 | 48 | Jekyll Menus adds a new option to the site variable called `site.menus`, which can be looped over just like pages, posts, and other content: 49 | 50 | ```liquid 51 | 58 | ``` 59 | 60 | ## Menus via Front Matter 61 | 62 | The easiest way to use Jekyll Menus is to start building menus using your existing posts and pages. This can be done by adding a `menus` variable to your front matter: 63 | 64 | ```markdown 65 | --- 66 | title: Homepage 67 | menus: header 68 | --- 69 | ``` 70 | 71 | This will create the `header` menu with a single item, the homepage. The `url`, `title`, and `identifier` for the homepage menu item will be automatically generated from the pages title, file path, and permalink. 72 | 73 | You can optionally set any of the available [menu item variables](#menu-items) yourself to customize the appearance and functionality of your menus. For example, to set a custom title and weight: 74 | 75 | ```markdown 76 | --- 77 | title: Homepage 78 | menus: 79 | header: 80 | title: Home 81 | weight: 1 82 | --- 83 | ``` 84 | 85 | ## Custom Menu Items via `_data/menus.yml` 86 | 87 | The other option for configuring menus is creating menus using `_data/menus.yml`. In this scenario, you can add custom menu items to external content, or site content that isn’t handled by Jekyll. 88 | 89 | In this file, you provide the menu key and an array of custom menu items. Custom menu items in the data file must have `url`, `title`, and `identifier` variable: 90 | 91 | ```markdown 92 | --- 93 | header: 94 | - url: /api 95 | title: API Documentation 96 | identifier: api 97 | --- 98 | ``` 99 | 100 | ## Sub-menus 101 | 102 | Jekyll Menus supports infinitely nested menu items using the `identifier` variable. Any menu item can be used as a parent menu by using its identifier as the menu. 103 | 104 | For example, in `_data/menus.yml`: 105 | 106 | ```yaml 107 | header: 108 | - url: /api 109 | title: API Documentation 110 | identifier: api 111 | ``` 112 | 113 | In a content file called `/api-support.html`: 114 | 115 | ```markdown 116 | --- 117 | title: Get API Support 118 | menus: api 119 | --- 120 | ``` 121 | 122 | Which can then be used in your templates by looping over the menu item’s `children` variable: 123 | 124 | ```liquid 125 | 141 | ``` 142 | 143 | You can also do this [recursively using a re-usable include](#recursive-menus), allowing for easily managed infinitely nested menus. 144 | 145 | ## Variables 146 | 147 | Jekyll Menus has the following variables: 148 | 149 | ### Menus 150 | 151 | | Variable | Description | 152 | |---|---| 153 | | menu.menu | Returns a JSON object with all of the menu’s items. | 154 | | menu.identifier | The unique identifier for the current menu, generated from the menu key. Allows for nested menu items. | 155 | | menu.parent | The parent menu. Resolves to the site.menus object for top-level menus. | 156 | 157 | ### Menu Items 158 | 159 | | Variable | Description | 160 | |---|---| 161 | | item.title | The display title of the menu item. Automatically set as the post or page title if no value is provided in front matter. | 162 | | item.url | The URL the menu item links to. Automatically set to the post or page URL if no value is provided in front matter. | 163 | | item.weight | Handles the order of menu items through a weighted system, starting with 1 being first. | 164 | | item.identifier | The unique identifier for the current menu item. Allows for nested menu items. Automatically resolved to the page’s file path and filename if not provided in front matter. | 165 | | item.parent | The parent menu. | 166 | | item.children | An array of any child menu items. Used to create sub-menus. | 167 | 168 | ## Custom Variables 169 | 170 | Menu items also support custom variables, which you add to each menu item in the front matter or data file. 171 | 172 | For example, adding a `pre` or `post` variable to add text or HTML to your menu items: 173 | 174 | ```markdown 175 | --- 176 | title: Homepage 177 | menus: 178 | header: 179 | pre: 180 | post: " · " 181 | --- 182 | ``` 183 | 184 | ## Recursive Menus 185 | 186 | If you’re looking to build an infinitely nested menu (or a menu that is nested more than once up to a limit) then you should set up a reusable menu include that will handle this for you. 187 | 188 | In `_includes/menu.html` : 189 | 190 | ```liquid 191 | {% if menu %} 192 | 203 | {% endif %} 204 | ``` 205 | 206 | In `_layouts/default.html` (or any layout file): 207 | 208 | ```liquid 209 | 210 | 211 |
212 | 216 |
217 | {{ content }} 218 | 219 | 220 | ``` 221 | -------------------------------------------------------------------------------- /lib/jekyll-menus.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | require "jekyll/menus" 6 | -------------------------------------------------------------------------------- /lib/jekyll/menus.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | class Menus 7 | autoload :Utils, "jekyll/menus/utils" 8 | autoload :Drops, "jekyll/menus/drops" 9 | 10 | def initialize(site) 11 | @site = site 12 | end 13 | 14 | # 15 | 16 | def menus 17 | Utils.deep_merge(_data_menus, Utils.deep_merge( 18 | _page_menus, _collection_menus 19 | )) 20 | end 21 | 22 | # 23 | 24 | def to_liquid_drop 25 | Drops::All.new( 26 | menus 27 | ) 28 | end 29 | 30 | # 31 | 32 | def _data_menus 33 | out = {} 34 | 35 | if @site.data["menus"] && @site.data["menus"].is_a?(Hash) 36 | then @site.data["menus"].each do |key, menu| 37 | if menu.is_a?(Hash) || menu.is_a?(Array) 38 | (menu = [menu].flatten).each do |item| 39 | _validate_config_menu_item( 40 | item 41 | ) 42 | 43 | item["_frontmatter"] = false 44 | end 45 | 46 | else 47 | _throw_invalid_menu_entry( 48 | menu 49 | ) 50 | end 51 | 52 | merge = { key => menu } 53 | out = Utils.deep_merge( 54 | out, merge 55 | ) 56 | end 57 | end 58 | 59 | out 60 | end 61 | 62 | # 63 | 64 | def _page_menus 65 | out = {} 66 | 67 | @site.pages.select { |p| p.data.keys.grep(/menus?/).size > 0 }.each_with_object({}) do |page| 68 | [page.data["menus"], page.data["menu"]].flatten.compact.map do |menu| 69 | out = _front_matter_menu(menu, page, out) 70 | end 71 | end 72 | 73 | out 74 | end 75 | 76 | # 77 | 78 | def _collection_menus 79 | out = {} 80 | 81 | @site.collections.each do |collection, pages| 82 | pages.docs.select { |p| p.data.keys.grep(/menus?/).size > 0 }.each_with_object({}) do |page| 83 | [page.data["menus"], page.data["menu"]].flatten.compact.map do |menu| 84 | out = _front_matter_menu(menu, page, out) 85 | end 86 | end 87 | end 88 | 89 | out 90 | end 91 | 92 | # 93 | 94 | def _front_matter_menu(menu, page, out={}) 95 | # -- 96 | # menu: key 97 | # menu: 98 | # - key1 99 | # - key2 100 | # -- 101 | 102 | if menu.is_a?(Array) || menu.is_a?(String) 103 | _simple_front_matter_menu(menu, **{ 104 | :mergeable => out, :page => page 105 | }) 106 | 107 | # 108 | 109 | elsif menu.is_a?(Hash) 110 | menu.each do |key, item| 111 | out[key] ||= [] 112 | 113 | # -- 114 | # menu: 115 | # key: identifier 116 | # -- 117 | 118 | if item.is_a?(String) 119 | out[key] << _fill_front_matter_menu({ "identifier" => item }, **{ 120 | :page => page 121 | }) 122 | 123 | # -- 124 | # menu: 125 | # key: 126 | # url: /url 127 | # -- 128 | 129 | elsif item.is_a?(Hash) 130 | out[key] << _fill_front_matter_menu(item, **{ 131 | :page => page 132 | }) 133 | 134 | # -- 135 | # menu: 136 | # key: 137 | # - url: /url 138 | # -- 139 | 140 | else 141 | _throw_invalid_menu_entry( 142 | item 143 | ) 144 | end 145 | end 146 | 147 | # -- 148 | # menu: 149 | # key: 3 150 | # -- 151 | 152 | else 153 | _throw_invalid_menu_entry( 154 | menu 155 | ) 156 | end 157 | 158 | out 159 | end 160 | 161 | # 162 | 163 | private 164 | def _simple_front_matter_menu(menu, mergeable: nil, page: nil) 165 | if menu.is_a?(Array) 166 | then menu.each do |item| 167 | if !item.is_a?(String) 168 | _throw_invalid_menu_entry( 169 | item 170 | ) 171 | 172 | else 173 | _simple_front_matter_menu(item, { 174 | :mergeable => mergeable, :page => page 175 | }) 176 | end 177 | end 178 | 179 | else 180 | mergeable[menu] ||= [] 181 | mergeable[menu] << _fill_front_matter_menu(nil, **{ 182 | :page => page 183 | }) 184 | end 185 | end 186 | 187 | # 188 | 189 | private 190 | def _fill_front_matter_menu(val, page: nil) 191 | raise ArgumentError, "Kwd 'page' is required." unless page 192 | val ||= {} 193 | 194 | val["url"] ||= page.url 195 | val["identifier"] ||= slug(page) 196 | val["_frontmatter"] = page.relative_path # `page.url` can be changed with permalink frontmatter 197 | val["title"] ||= page.data["title"] 198 | val["weight"] ||= -1 199 | val 200 | end 201 | 202 | # 203 | 204 | private 205 | def slug(page) 206 | ext = page.data["ext"] || page.ext 207 | out = File.join(File.dirname(page.path), File.basename(page.path, ext)) 208 | out.tr("^a-z0-9-_\\/", "").gsub(/\/|\-+/, "_").gsub( 209 | /^_+/, "" 210 | ) 211 | end 212 | 213 | # 214 | 215 | private 216 | def _validate_config_menu_item(item) 217 | if !item.is_a?(Hash) || !item.values_at("url", "title", "identifier").compact.size == 3 218 | _throw_invalid_menu_entry( 219 | item 220 | ) 221 | else 222 | item["weight"] ||= -1 223 | end 224 | end 225 | 226 | # 227 | 228 | private 229 | def _throw_invalid_menu_entry(data) 230 | raise RuntimeError, "Invalid menu item given: #{ 231 | data.inspect 232 | }" 233 | end 234 | end 235 | end 236 | 237 | require "jekyll/menus/hook" 238 | -------------------------------------------------------------------------------- /lib/jekyll/menus/drops.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | class Menus 7 | module Drops 8 | autoload :Menu, "jekyll/menus/drops/menu" 9 | autoload :All, "jekyll/menus/drops/all" 10 | autoload :Item, "jekyll/menus/drops/item" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/jekyll/menus/drops/all.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | class Menus 7 | module Drops 8 | class All < Liquid::Drop 9 | def initialize(menus) 10 | @menus = menus 11 | end 12 | 13 | # 14 | 15 | def find 16 | to_a.find do |menu| 17 | yield menu 18 | end 19 | end 20 | 21 | # 22 | 23 | def to_a 24 | @menus.keys.map do |identifier| 25 | self[ 26 | identifier 27 | ] 28 | end 29 | end 30 | 31 | # 32 | 33 | def each 34 | to_a.each do |drop| 35 | yield drop 36 | end 37 | end 38 | 39 | # 40 | 41 | def [](key) 42 | if @menus.key?(key) 43 | then Menu.new(@menus[key], 44 | key, self 45 | ) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/jekyll/menus/drops/item.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | class Menus 7 | module Drops 8 | class Item < Liquid::Drop 9 | def initialize(item, parent) 10 | @parent = parent 11 | @item = 12 | item 13 | end 14 | 15 | # 16 | 17 | def children 18 | out = @parent.find { |menu| menu.identifier == @item["identifier"] } 19 | 20 | if out 21 | return out.to_a 22 | end 23 | end 24 | 25 | # 26 | 27 | def url 28 | @item[ 29 | "url" 30 | ] 31 | end 32 | 33 | # 34 | 35 | def title 36 | @item[ 37 | "title" 38 | ] 39 | end 40 | 41 | # 42 | 43 | def identifier 44 | @item[ 45 | "identifier" 46 | ] 47 | end 48 | 49 | # 50 | 51 | def weight 52 | @item[ 53 | "weight" 54 | ] 55 | end 56 | 57 | # 58 | 59 | def before_method(method) 60 | if @item.has_key?(method.to_s) 61 | return @item[ 62 | method.to_s 63 | ] 64 | end 65 | end 66 | 67 | alias_method :liquid_method_missing, :before_method 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/jekyll/menus/drops/menu.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | class Menus 7 | module Drops 8 | class Menu < Liquid::Drop 9 | attr_reader :parent, :identifier, :menu 10 | def initialize(menu, identifier, parent) 11 | @parent = parent 12 | @identifier = identifier 13 | @menu = menu 14 | end 15 | 16 | # 17 | 18 | def find 19 | to_a.find do |item| 20 | yield item 21 | end 22 | end 23 | 24 | # 25 | 26 | def select 27 | to_a.select do |item| 28 | yield item 29 | end 30 | end 31 | 32 | # 33 | 34 | def to_a 35 | @menu.map { |item| Item.new(item, parent) }.sort_by( 36 | &:weight 37 | ) 38 | end 39 | 40 | # 41 | 42 | def each 43 | to_a.each do |drop| 44 | yield drop 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/jekyll/menus/hook.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | module Drops 7 | class SiteDrop 8 | attr_accessor :menus 9 | end 10 | end 11 | end 12 | 13 | Jekyll::Hooks.register :site, :pre_render do |site, payload| 14 | payload.site.menus = Jekyll::Menus.new(site).to_liquid_drop 15 | end 16 | -------------------------------------------------------------------------------- /lib/jekyll/menus/utils.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | class Menus 7 | module Utils module_function 8 | def deep_merge(old, _new) 9 | return old | _new if old.is_a?(Array) 10 | 11 | old.merge(_new) do |_, o, n| 12 | (o.is_a?(Hash) && n.is_a?(Hash)) || (o.is_a?(Array) && 13 | n.is_a?(Array)) ? deep_merge(o, n) : n 14 | end 15 | end 16 | 17 | def deep_merge!(old, _new) 18 | old.replace(deep_merge( 19 | old, _new 20 | )) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/jekyll/menus/version.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 Forestry.io - MIT License 3 | # Encoding: utf-8 4 | 5 | module Jekyll 6 | class Menus 7 | VERSION = "0.6.1" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixture/_config.yml: -------------------------------------------------------------------------------- 1 | gems: 2 | - jekyll-menus 3 | -------------------------------------------------------------------------------- /spec/fixture/_data/menus.yml: -------------------------------------------------------------------------------- 1 | social: 2 | - title: Github 3 | url: https://github.com/envygeeks 4 | identifier: github 5 | header: 6 | title: Github 7 | url: https://github.com 8 | identifier: hello 9 | -------------------------------------------------------------------------------- /spec/fixture/_includes/_menus.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /spec/fixture/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 10 |
11 | 12 | {{ content }} 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixture/array-of-strings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Array of Strings 3 | layout: default 4 | menu: 5 | - header 6 | - footer 7 | --- 8 | 9 | A Page 10 | -------------------------------------------------------------------------------- /spec/fixture/empty.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Should not show up 3 | --- 4 | 5 | Broken 6 | -------------------------------------------------------------------------------- /spec/fixture/hello/array-of-strings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Array of Strings 3 | layout: default 4 | menu: 5 | - header 6 | - footer 7 | --- 8 | 9 | A Page 10 | -------------------------------------------------------------------------------- /spec/fixture/key-hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Key Hash 3 | layout: default 4 | menu: 5 | header: 6 | weight: -100 7 | url: /key 8 | --- 9 | 10 | A Page 11 | -------------------------------------------------------------------------------- /spec/fixture/string.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: String 3 | layout: default 4 | menu: header 5 | --- 6 | 7 | A Page 8 | -------------------------------------------------------------------------------- /spec/lib/jekyll/menus.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forestryio/jekyll-menus/288acfacfaa14a94fbfd6738b10f9311da3a436c/spec/lib/jekyll/menus.rb -------------------------------------------------------------------------------- /spec/rspec/helper.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forestryio/jekyll-menus/288acfacfaa14a94fbfd6738b10f9311da3a436c/spec/rspec/helper.rb --------------------------------------------------------------------------------