perron 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/Gemfile.lock +3 -1
  4. data/README.md +70 -42
  5. data/app/helpers/meta_tags_helper.rb +1 -1
  6. data/lib/generators/content/templates/root.erb.tt +0 -1
  7. data/lib/generators/perron/install_generator.rb +2 -1
  8. data/lib/generators/perron/templates/initializer.rb.tt +5 -2
  9. data/lib/perron/{site/collection.rb → collection.rb} +7 -1
  10. data/lib/perron/configuration.rb +4 -0
  11. data/lib/perron/{site/data.rb → data.rb} +14 -2
  12. data/lib/perron/html_processor/syntax_highlight.rb +30 -0
  13. data/lib/perron/html_processor.rb +12 -8
  14. data/lib/perron/markdown.rb +6 -5
  15. data/lib/perron/metatags.rb +33 -59
  16. data/lib/perron/{site/resource → resource}/class_methods.rb +5 -1
  17. data/lib/perron/resource/metadata.rb +67 -0
  18. data/lib/perron/{site/resource → resource}/publishable.rb +5 -5
  19. data/lib/perron/{site/resource → resource}/related.rb +1 -1
  20. data/lib/perron/{site/resource → resource}/separator.rb +5 -5
  21. data/lib/perron/{site/resource → resource}/slug.rb +6 -3
  22. data/lib/perron/{site/resource.rb → resource.rb} +34 -10
  23. data/lib/perron/root.rb +1 -1
  24. data/lib/perron/site.rb +3 -4
  25. data/lib/perron/version.rb +1 -1
  26. data/lib/perron.rb +1 -0
  27. data/perron.gemspec +1 -1
  28. metadata +18 -16
  29. /data/lib/perron/{site/data → data}/proxy.rb +0 -0
  30. /data/lib/perron/{site/resource → resource}/configuration.rb +0 -0
  31. /data/lib/perron/{site/resource → resource}/core.rb +0 -0
  32. /data/lib/perron/{site/resource → resource}/related/stop_words.rb +0 -0
  33. /data/lib/perron/{site/resource → resource}/renderer.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2aa4c7af63f95aef3bc25c9ef97a4da02d75486ec3ee975f69a980f66cb34557
4
- data.tar.gz: f1c62fff6d0ba6c121da85f463dc32fd5656c884d059d0f2955c6a95a4c0bf3a
3
+ metadata.gz: 1757232a5830a600f00f1517b0aa26c0f95cc55005c32cdb7865282b522cefec
4
+ data.tar.gz: ba9a09ab06d80c8cc266c210d51770c5577d079dbfab6226f058c1f39a974505
5
5
  SHA512:
6
- metadata.gz: 175ecbdc5dc5468e28e0abe13926b578fb6669d078b594da1dea91ab8cce68c3a7f63f12d3be4f0de757d270c5d3a95ce96db925ea9c3cbe238e1dd72069aaa1
7
- data.tar.gz: b65f95000da601c8ea2f6dcea2e1cf9edb907d366ff783b3ba37e4b2c2d0ff0222cf98821f7537690895087c39ad0c902397c6325e309e27019914101072b6df
6
+ metadata.gz: 82e81dbe7b346d47d539ab50b098c1b2542241d8864e3ed0cddf3e565b39785ae6710718a3db158cacaa27e67ea436960b88b012851ec5e85d7c91d3d0baa718
7
+ data.tar.gz: bdc619b82756afede00259a7f89970bac482a2e7204f2b8a66ad80b798467604b934462fe69d93e9d70e359abc298b0f72fcbf6842dde0258b7aaabe14a5042c
data/Gemfile CHANGED
@@ -14,4 +14,5 @@ group :development, :test do
14
14
  gem "rake", "~> 13.3.0"
15
15
  gem "minitest", "~> 5.25", ">= 5.25.5"
16
16
  gem "debug", "~> 1.11.0"
17
+ gem "rouge", "~> 4.6.0"
17
18
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- perron (0.10.0)
4
+ perron (0.11.0)
5
5
  csv
6
6
  json
7
7
  psych
@@ -204,6 +204,7 @@ GEM
204
204
  regexp_parser (2.10.0)
205
205
  reline (0.6.1)
206
206
  io-console (~> 0.5)
207
+ rouge (4.6.0)
207
208
  rubocop (1.75.8)
208
209
  json (~> 2.3)
209
210
  language_server-protocol (~> 3.17.0.2)
@@ -267,6 +268,7 @@ DEPENDENCIES
267
268
  perron!
268
269
  rails (~> 7.2.0)
269
270
  rake (~> 13.3.0)
271
+ rouge (~> 4.6.0)
270
272
  standard (~> 1.50.0)
271
273
 
272
274
  BUNDLED WITH
data/README.md CHANGED
@@ -13,7 +13,7 @@ A Rails-based static site generator.
13
13
  </a>
14
14
 
15
15
 
16
- ## Getting Started
16
+ ## Getting started
17
17
 
18
18
  ### Installation
19
19
 
@@ -47,14 +47,14 @@ Perron can operate in two modes, configured via `config.mode`. This allows you t
47
47
  | **Asset Handling** | Via Perron | Via Asset Pipeline |
48
48
 
49
49
 
50
- ## Creating Content
50
+ ## Collections
51
51
 
52
- Perron is, just like Rails, designed with convention over configuration in mind. Content is stored in `app/content/*/*.{erb,md}`. Content is backed by a class, located in `app/models/content/` that inherits from `Perron::Resource`.
52
+ Perron is, just like Rails, designed with convention over configuration in mind. Content is stored in `app/content/*/*.{erb,md,*}` and backed by a class, located in `app/models/content/` that inherits from `Perron::Resource`.
53
53
 
54
54
  The controllers are located in `app/controllers/content/`. To make them available, create a route: `resources :posts, module: :content, only: %w[index show]`.
55
55
 
56
56
 
57
- ### Collections
57
+ ### Create content
58
58
 
59
59
  ```bash
60
60
  bin/rails generate content Post
@@ -69,29 +69,52 @@ This will create the following files:
69
69
  * Adds route: `resources :posts, module: :content, only: %w[index show]`
70
70
 
71
71
 
72
- ### Setting a Root Page
72
+ ### Setting a root page
73
73
 
74
- To set a root page, include `Perron::Root` in your `Content::PagesController` and add a `app/content/pages/root.[md,erb]` file (make sure to set `slug: "/"` in its frontmatter).
74
+ To set a root page, include `Perron::Root` in your `Content::PagesController` and add a `app/content/pages/root.{md,erb,*}` file. This is automatically added for you when you create a `Page` collection.
75
75
 
76
- This is automatically added when you create a `Page` collection.
77
76
 
78
-
79
- ## Markdown Support
77
+ ## Markdown support
80
78
 
81
79
  Perron supports markdown with the `markdownify` helper.
82
80
 
83
- There are no markdown gems bundled by default, so you'll need to add one of these:
81
+ There are no markdown gems bundled by default, so you'll need to add one of these to your `Gemfile`:
84
82
 
85
- - CommonMarker
86
- - Kramdown
87
- - Redcarpet
83
+ - `commonmarker`
84
+ - `kramdown`
85
+ - `redcarpet`
88
86
 
89
87
  ```bash
90
88
  bundle add {commonmarker,kramdown,redcarpet}
91
89
  ```
92
90
 
91
+ ### Configuration
92
+
93
+ To pass options to the parser, set `markdown_options` in `config/initializers/perron.rb`. The options hash is passed directly to the chosen library.
94
+
95
+ **Commonmarker**
96
+ ```ruby
97
+ # Options are passed as keyword arguments.
98
+ Perron.configuration.markdown_options = { options: [:HARDBREAKS], extensions: [:table] }
99
+ ```
100
+
101
+ **Kramdown**
102
+ ```ruby
103
+ # Options are passed as a standard hash.
104
+ Perron.configuration.markdown_options = { input: "GFM", smart_quotes: "apos,quot" }
105
+ ```
106
+
107
+ **Redcarpet**
108
+ ```ruby
109
+ # Options are nested under :renderer_options and :markdown_options.
110
+ Perron.configuration.markdown_options = {
111
+ renderer_options: { hard_wrap: true },
112
+ markdown_options: { tables: true, autolink: true }
113
+ }
114
+ ```
115
+
93
116
 
94
- ## HTML Transformations
117
+ ## HTML transformations
95
118
 
96
119
  Perron can post-process the HTML generated from your Markdown content.
97
120
 
@@ -100,19 +123,20 @@ Perron can post-process the HTML generated from your Markdown content.
100
123
 
101
124
  Apply transformations by passing an array of processor names or classes to the `markdownify` helper via the `process` option.
102
125
  ```erb
103
- <%= markdownify @resource.content, process: %w[target_blank lazy_load_images] %>
126
+ <%= markdownify @resource.content, process: %w[lazy_load_images syntax_highlight target_blank] %>
104
127
  ```
105
128
 
106
129
 
107
- ### Available Processors
130
+ ### Available processors
108
131
 
109
132
  The following processors are built-in and can be activated by passing their string name:
110
133
 
111
134
  - `target_blank`: Adds `target="_blank"` to all external links;
112
135
  - `lazy_load_images`: Adds `loading="lazy"` to all `<img>` tags.
136
+ - `syntax_highlight`: Applies syntax highlighting to fenced code blocks (e.g., \`\`\`ruby). This requires adding the `rouge` gem to your Gemfile (`bundle add rouge`). You will also need to include a Rouge CSS theme for colors to appear.
113
137
 
114
138
 
115
- ### Creating Your Own
139
+ ### Creating your own processors
116
140
 
117
141
  You can create your own processor by defining a class that inherits from `Perron::HtmlProcessor::Base` and implements a `process` method.
118
142
  Then, pass the class constant directly in the `process` array.
@@ -184,19 +208,8 @@ Check out our amazing features:
184
208
  <% end %>
185
209
  ```
186
210
 
187
- **Result:**
188
- ```html
189
- <p>Check out our amazing features:</p>
190
- <ul>
191
- <li>Rails based</li>
192
- <li>SEO friendly</li>
193
- <li>Markdown first</li>
194
- <li>ERB support</li>
195
- </ul>
196
- ```
197
-
198
211
 
199
- ## Data Files
212
+ ## Data files
200
213
 
201
214
  Perron can consume structured data from YML, JSON, or CSV files, making them available within your templates.
202
215
  This is useful for populating features, team members, or any other repeated data structure.
@@ -211,12 +224,12 @@ To use a data file, instantiate `Perron::Site.data` with the basename of the fil
211
224
  <% end %>
212
225
  ```
213
226
 
214
- ### File Location and Formats
227
+ ### File location and formats
215
228
 
216
229
  By default, Perron looks up `app/content/data/` for files with a `.yml`, `.json`, or `.csv` extension.
217
230
  For a `features` call, it would find `features.yml`, `features.json`, or `features.csv`. You can also provide a path to any data file, via `Perron::Data.new("path/to/data.json")`.
218
231
 
219
- ### Accessing Data
232
+ ### Accessing data
220
233
 
221
234
  The wrapper object provides flexible, read-only access to each record's attributes. Both dot notation and hash-like key access are supported.
222
235
  ```ruby
@@ -224,6 +237,22 @@ feature.name
224
237
  feature[:name]
225
238
  ```
226
239
 
240
+ ### Rendering
241
+
242
+ You can render data collections directly using Rails-like partial rendering. When you call `render` on a data collection, Perron will automatically render a partial for each item.
243
+ ```erb
244
+ <%= render Perron::Site.data.features %>
245
+ ```
246
+
247
+ This expects a partial at `app/views/features/_feature.html.erb` that will be rendered once for each feature in your data file. The individual record is made available as a local variable matching the singular form of the collection name.
248
+ ```erb
249
+ <!-- app/views/features/_feature.html.erb -->
250
+ <div class="feature">
251
+ <h4><%= feature.name %></h4>
252
+ <p><%= feature.description %></p>
253
+ </div>
254
+ ```
255
+
227
256
 
228
257
  ## Feeds
229
258
 
@@ -299,7 +328,7 @@ Or exclude certain tags:
299
328
 
300
329
  Values are determined with the following precedence, from highest to lowest:
301
330
 
302
- #### 1. Controller Action
331
+ #### 1. Controller action
303
332
 
304
333
  Define a `@metadata` instance variable in your controller:
305
334
  ```ruby
@@ -314,10 +343,9 @@ class Content::PostsController < ApplicationController
314
343
  end
315
344
  ```
316
345
 
317
- #### 2. Page Frontmatter
346
+ #### 2. Page frontmatter
318
347
 
319
348
  Add values to the YAML frontmatter in content files:
320
-
321
349
  ```yaml
322
350
  ---
323
351
  title: My Awesome Post
@@ -331,7 +359,7 @@ Your content here…
331
359
 
332
360
  #### 3. Collection configuration
333
361
 
334
- Set site-wide defaults in the initializer:
362
+ Set collection defaults in the resource model:
335
363
  ```ruby
336
364
  class Content::Post < Perron::Resource
337
365
  # …
@@ -341,7 +369,7 @@ class Content::Post < Perron::Resource
341
369
  end
342
370
  ```
343
371
 
344
- #### 4. Default Values
372
+ #### 4. Default values
345
373
 
346
374
  Set site-wide defaults in the initializer:
347
375
  ```ruby
@@ -354,13 +382,13 @@ end
354
382
  ```
355
383
 
356
384
 
357
- ## Related Resources
385
+ ## Related resources
358
386
 
359
387
  The `related_resources` method allows to find and display a list of similar resources
360
- from the same collection. Similarity is calculated using the **TF-IDF** algorithm on the content of each resource.
388
+ from the sme collection. Similarity is calculated using the **[TF-IDF](https://en.wikipedia.org/wiki/Tf%E2%80%93idf)** algorithm on the content of each resource.
361
389
 
362
390
 
363
- ### Basic Usage
391
+ ### Basic usage
364
392
 
365
393
  To get a list of the 5 most similar resources, call the method on any resource instance.
366
394
  ```ruby
@@ -372,9 +400,9 @@ To get a list of the 5 most similar resources, call the method on any resource i
372
400
  ```
373
401
 
374
402
 
375
- ## XML Sitemap
403
+ ## XML sitemap
376
404
 
377
- A sitemap is an XML file that lists all the pages of a website to help search engines discover and index content more efficiently, typically containing URLs, last modification dates, change frequency, and priority values.
405
+ A sitemap is a XML file that lists all the pages of a website to help search engines discover and index content more efficiently, typically containing URLs, last modification dates, change frequency, and priority values.
378
406
 
379
407
  Enable it with the following line in the Perron configuration:
380
408
  ```ruby
@@ -409,7 +437,7 @@ sitemap_change_frequency: :daily
409
437
  ```
410
438
 
411
439
 
412
- ## Building Your Static Site
440
+ ## Building your static site
413
441
 
414
442
  When in `standalone` mode and you're ready to generate your static site, run:
415
443
  ```bash
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MetaTagsHelper
4
- def meta_tags(options = {}) = Perron::Metatags.new(resource).render(options)
4
+ def meta_tags(options = {}) = Perron::Metatags.new(resource.metadata).render(options)
5
5
 
6
6
  private
7
7
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- slug: "/"
3
2
  ---
4
3
 
5
4
  Find me in `app/content/pages/root.erb`
@@ -18,7 +18,8 @@ module Perron
18
18
  def add_markdown_gems
19
19
  append_to_file "Gemfile", <<~RUBY
20
20
 
21
- # Perron can use one of the following gems. Uncomment your preferred choice and run `bundle install`
21
+ # Perron supports Markdown rendering using one of the following gems.
22
+ # Uncomment your preferred choice and run `bundle install`
22
23
  # gem "commonmarker"
23
24
  # gem "kramdown"
24
25
  # gem "redcarpet"
@@ -3,14 +3,17 @@ Perron.configure do |config|
3
3
 
4
4
  # config.site_name = "Helptail"
5
5
 
6
- # The build mode for Perron. Can be :standalone or :integrated.
6
+ # The build mode for Perron. Can be :standalone or :integrated
7
7
  # config.mode = :standalone
8
8
 
9
- # In `integrated` mode, the root is skipped by default. Set to `true` to enable.
9
+ # In `integrated` mode, the root is skipped by default. Set to `true` to enable
10
10
  # config.include_root = false
11
11
 
12
12
  # config.default_url_options = {host: "helptail.com", protocol: "https", trailing_slash: true}
13
13
 
14
+ # The options hash is passed directly to the chosen library
15
+ # config.markdown_options = {}
16
+
14
17
  # Set default meta values
15
18
  # Examples:
16
19
  # - `config.metadata.description = "Put your routine tasks on autopilot"`
@@ -5,7 +5,7 @@ module Perron
5
5
  attr_reader :name
6
6
 
7
7
  def initialize(name)
8
- @name = name
8
+ @name = name.inquiry
9
9
  @collection_path = File.join(Perron.configuration.input, name)
10
10
 
11
11
  raise Errors::CollectionNotFoundError, "No such collection: #{name}" unless File.exist?(@collection_path) && File.directory?(@collection_path)
@@ -29,5 +29,11 @@ module Perron
29
29
 
30
30
  raise Errors::ResourceNotFoundError, "Resource not found with slug: #{slug}"
31
31
  end
32
+
33
+ def find_by_file_name(file_name, resource_class = Resource)
34
+ resource_class.new(
35
+ Perron.configuration.allowed_extensions.lazy.map { File.join(@collection_path, [file_name, it].join(".")) }.find { File.exist?(it) }
36
+ )
37
+ end
32
38
  end
33
39
  end
@@ -28,12 +28,16 @@ module Perron
28
28
  @config.exclude_from_public = %w[assets storage]
29
29
  @config.excluded_assets = %w[action_cable actioncable actiontext activestorage rails-ujs trix turbo]
30
30
 
31
+ @config.view_unpublished = Rails.env.development?
32
+
31
33
  @config.default_url_options = {
32
34
  host: ENV.fetch("PERRON_HOST", "localhost:3000"),
33
35
  protocol: ENV.fetch("PERRON_PROTOCOL", "http"),
34
36
  trailing_slash: ENV.fetch("PERRON_TRAILING_SLASH", "true") == "true"
35
37
  }
36
38
 
39
+ @config.markdown_options = {}
40
+
37
41
  @config.sitemap = ActiveSupport::OrderedOptions.new
38
42
  @config.sitemap.enabled = false
39
43
  @config.sitemap.priority = 0.5
@@ -5,6 +5,7 @@ require "csv"
5
5
  module Perron
6
6
  class Data < SimpleDelegator
7
7
  def initialize(identifier)
8
+ @identifier = identifier
8
9
  @file_path = self.class.path_for!(identifier)
9
10
  @records = records
10
11
 
@@ -47,7 +48,7 @@ module Perron
47
48
  raise Errors::DataParseError, "Data in `#{@file_path}` must be an array of objects."
48
49
  end
49
50
 
50
- data.map { Item.new(it) }
51
+ data.map { Item.new(it, identifier: @identifier) }
51
52
  end
52
53
 
53
54
  def rendered_from(path)
@@ -97,12 +98,23 @@ module Perron
97
98
  private_constant :HelperContext
98
99
 
99
100
  class Item
100
- def initialize(attributes)
101
+ def initialize(attributes, identifier:)
101
102
  @attributes = attributes.transform_keys(&:to_sym)
103
+ @identifier = identifier
102
104
  end
103
105
 
104
106
  def [](key) = @attributes[key.to_sym]
105
107
 
108
+ def to_partial_path
109
+ @to_partial_path ||= begin
110
+ identifier = @identifier.to_s
111
+ collection = File.extname(identifier).present? ? File.basename(identifier, ".*") : identifier
112
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.singularize(File.basename(collection)))
113
+
114
+ File.join("content", collection, element)
115
+ end
116
+ end
117
+
106
118
  def method_missing(method_name, *arguments, &block)
107
119
  return super if !@attributes.key?(method_name) || arguments.any? || block
108
120
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rouge"
4
+ require "perron/html_processor/base"
5
+
6
+ module Perron
7
+ class HtmlProcessor
8
+ class SyntaxHighlight < HtmlProcessor::Base
9
+ def process
10
+ @html.css('pre > code[class*="language-"]').each do |code_block|
11
+ language = code_block[:class][/(?<=language-)\S+/]
12
+
13
+ next if language.blank?
14
+
15
+ code_block.parent.replace(
16
+ highlight(code_block.text, with: language)
17
+ )
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def highlight(code_block, with:)
24
+ lexer = Rouge::Lexer.find(with) || Rouge::Lexers::PlainText.new
25
+
26
+ Rouge::Formatters::HTMLPygments.new(::Rouge::Formatters::HTML.new).format(lexer.lex(code_block))
27
+ end
28
+ end
29
+ end
30
+ end
@@ -11,11 +11,9 @@ module Perron
11
11
  end
12
12
 
13
13
  def process
14
- document = Nokogiri::HTML::DocumentFragment.parse(@html)
15
-
16
- @processors.each { it.new(document).process }
17
-
18
- document.to_html
14
+ Nokogiri::HTML::DocumentFragment.parse(@html).tap do |document|
15
+ @processors.each { it.new(document).process }
16
+ end.to_html
19
17
  end
20
18
 
21
19
  private
@@ -23,7 +21,13 @@ module Perron
23
21
  BUILT_IN = {
24
22
  "target_blank" => Perron::HtmlProcessor::TargetBlank,
25
23
  "lazy_load_images" => Perron::HtmlProcessor::LazyLoadImages
26
- }
24
+ }.tap do |processors|
25
+ require "rouge"
26
+ require "perron/html_processor/syntax_highlight"
27
+
28
+ processors["syntax_highlight"] = Perron::HtmlProcessor::SyntaxHighlight
29
+ rescue LoadError
30
+ end
27
31
 
28
32
  def find_by(identifier)
29
33
  case identifier
@@ -43,8 +47,8 @@ module Perron
43
47
 
44
48
  return processor if processor
45
49
 
46
- raise Perron::Errors::ProcessorNotFoundError,
47
- "Could not find processor `#{name}`. It is not a Perron-included processor and the constant `#{name.camelize}` could not be found."
50
+ raise Perron::Errors::ProcessorNotFoundError, "The `syntax_highlight` processor requires `rouge`. Run `bundle add rouge` to add it to your Gemfile." if name.inquiry.syntax_highlight?
51
+ raise Perron::Errors::ProcessorNotFoundError, "Could not find processor `#{name}`. It is not a Perron-included processor and the constant `#{name.camelize}` could not be found."
48
52
  end
49
53
  end
50
54
  end
@@ -18,7 +18,7 @@ module Perron
18
18
  end
19
19
 
20
20
  def markdown_parser
21
- if defined?(::CommonMarker)
21
+ if defined?(::Commonmarker)
22
22
  CommonMarkerParser.new
23
23
  elsif defined?(::Kramdown)
24
24
  KramdownParser.new
@@ -31,17 +31,18 @@ module Perron
31
31
  end
32
32
 
33
33
  class CommonMarkerParser
34
- def parse(text) = CommonMarker.render_html(text, :DEFAULT)
34
+ def parse(text) = Commonmarker.to_html(text, **Perron.configuration.markdown_options)
35
35
  end
36
36
 
37
37
  class KramdownParser
38
- def parse(text) = Kramdown::Document.new(text).to_html
38
+ def parse(text) = Kramdown::Document.new(text, Perron.configuration.markdown_options).to_html
39
39
  end
40
40
 
41
41
  class RedcarpetParser
42
42
  def parse(text)
43
- renderer = Redcarpet::Render::HTML.new(filter_html: true)
44
- markdown = Redcarpet::Markdown.new(renderer, autolink: true, tables: true)
43
+ options = Perron.configuration.markdown_options
44
+ renderer = Redcarpet::Render::HTML.new(options.fetch(:renderer_options, {}))
45
+ markdown = Redcarpet::Markdown.new(renderer, options.fetch(:markdown_options, {}))
45
46
 
46
47
  markdown.render(text)
47
48
  end
@@ -4,9 +4,8 @@ module Perron
4
4
  class Metatags
5
5
  include ActionView::Helpers::TagHelper
6
6
 
7
- def initialize(resource)
8
- @resource = resource
9
- @config = Perron.configuration
7
+ def initialize(data)
8
+ @data = data
10
9
  end
11
10
 
12
11
  def render(options = {})
@@ -19,75 +18,50 @@ module Perron
19
18
 
20
19
  private
21
20
 
22
- FRONTMATTER_KEY_MAP = {
23
- "locale" => %w[og:locale],
24
- "image" => %w[og:image twitter:image],
25
- "author" => %w[og:author]
26
- }.freeze
27
-
28
21
  def tags
29
- @tags ||= begin
30
- frontmatter = @resource&.metadata&.stringify_keys || {}
31
- collection_config = @resource&.collection&.configuration&.metadata || {}
32
- site_config = @config.metadata
33
-
34
- title = frontmatter["title"] || collection_config["title"] || site_config["title"] || @config.site_name || Rails.application.name.underscore.camelize
35
- type = frontmatter["type"] || collection_config["type"] || site_config["type"]
36
- description = frontmatter["description"] || collection_config["description"] || site_config["description"]
37
- logo = frontmatter["logo"] || collection_config["logo"] || site_config["logo"]
38
- author = frontmatter["author"] || collection_config["author"] || site_config["author"]
39
- locale = frontmatter["locale"] || collection_config["locale"] || site_config["locale"]
40
-
41
- image = frontmatter["image"] || collection_config["image"] || site_config["image"]
42
- og_image = frontmatter["og:image"] || collection_config["og:image"] || site_config["og:image"] || image
43
- twitter_image = frontmatter["twitter:image"] || collection_config["twitter:image"] || site_config["twitter:image"] || og_image
44
-
45
- {
46
- title: title_tag(title),
47
- description: meta_tag(name: "description", content: description),
48
- article_published: meta_tag(property: "article:published_time", content: @resource&.published_at),
49
-
50
- og_title: meta_tag(property: "og:title", content: frontmatter["og:title"] || title),
51
- og_type: meta_tag(property: "og:type", content: frontmatter["og:type"] || type),
52
- og_url: meta_tag(property: "og:url", content: canonical_url),
53
- og_image: meta_tag(property: "og:image", content: og_image),
54
-
55
- og_description: meta_tag(property: "og:description", content: frontmatter["og:description"] || description),
56
- og_site_name: meta_tag(property: "og:site_name", content: @config.site_name),
57
- og_logo: meta_tag(property: "og:logo", content: frontmatter["og:logo"] || logo),
58
- og_author: meta_tag(property: "og:author", content: frontmatter["og:author"] || author),
59
- og_locale: meta_tag(property: "og:locale", content: frontmatter["og:locale"] || locale),
60
-
61
- twitter_card: meta_tag(name: "twitter:card", content: frontmatter["twitter:card"] || "summary_large_image"),
62
- twitter_title: meta_tag(name: "twitter:title", content: frontmatter["twitter:title"] || title),
63
- twitter_description: meta_tag(name: "twitter:description", content: frontmatter["twitter:description"] || description),
64
- twitter_image: meta_tag(name: "twitter:image", content: twitter_image)
65
- }
66
- end
22
+ @tags ||= {
23
+ title: title_tag(@data[:title]),
24
+ canonical: link_tag(rel: "canonical", href: @data[:canonical_url]),
25
+
26
+ description: meta_tag(name: "description", content: @data[:description]),
27
+ article_published: meta_tag(property: "article:published_time", content: @data[:article_published_time]),
28
+
29
+ og_title: meta_tag(property: "og:title", content: @data[:og_title]),
30
+ og_type: meta_tag(property: "og:type", content: @data[:og_type]),
31
+ og_url: meta_tag(property: "og:url", content: @data[:og_url]),
32
+ og_image: meta_tag(property: "og:image", content: @data[:og_image]),
33
+ og_description: meta_tag(property: "og:description", content: @data[:og_description]),
34
+ og_site_name: meta_tag(property: "og:site_name", content: @data[:og_site_name]),
35
+ og_logo: meta_tag(property: "og:logo", content: @data[:og_logo]),
36
+ og_author: meta_tag(property: "og:author", content: @data[:og_author]),
37
+ og_locale: meta_tag(property: "og:locale", content: @data[:og_locale]),
38
+
39
+ twitter_card: meta_tag(name: "twitter:card", content: @data[:twitter_card]),
40
+ twitter_title: meta_tag(name: "twitter:title", content: @data[:twitter_title]),
41
+ twitter_description: meta_tag(name: "twitter:description", content: @data[:twitter_description]),
42
+ twitter_image: meta_tag(name: "twitter:image", content: @data[:twitter_image])
43
+ }
67
44
  end
68
45
 
69
46
  def title_tag(content)
47
+ config = Perron.configuration
70
48
  resource_title = content.to_s.strip
71
- title_suffix = Perron.configuration.metadata.title_suffix&.strip
72
-
49
+ title_suffix = config.metadata.title_suffix&.strip
73
50
  suffix = (title_suffix if title_suffix.present? && resource_title != title_suffix)
74
51
 
75
- tag.title([resource_title, suffix].compact.join(Perron.configuration.metadata.title_separator))
52
+ tag.title([resource_title, suffix].compact.join(config.metadata.title_separator))
76
53
  end
77
54
 
78
- def meta_tag(attributes)
79
- return if attributes[:content].blank?
55
+ def link_tag(attributes)
56
+ return if attributes[:href].blank?
80
57
 
81
- tag.meta(**attributes)
58
+ tag.link(**attributes)
82
59
  end
83
60
 
84
- def canonical_url
85
- url_options = @config.default_url_options
86
- base_url = "#{url_options[:protocol]}://#{url_options[:host]}"
87
- url = URI.join(base_url, @resource&.path).to_s
88
- has_extension = URI(url).path.split("/").last&.include?(".")
61
+ def meta_tag(attributes)
62
+ return if attributes[:content].blank?
89
63
 
90
- url.then { (url_options[:trailing_slash] && !it.end_with?("/") && !has_extension) ? "#{it}/" : it }
64
+ tag.meta(**attributes)
91
65
  end
92
66
  end
93
67
  end
@@ -30,13 +30,17 @@ module Perron
30
30
 
31
31
  def collection = Collection.new(collection_name)
32
32
 
33
+ def root
34
+ collection_name.pages? && collection.find_by_file_name("root", name.constantize)
35
+ end
36
+
33
37
  def model_name
34
38
  @model_name ||= ActiveModel::Name.new(self, nil, name.demodulize.to_s)
35
39
  end
36
40
 
37
41
  private
38
42
 
39
- def collection_name = name.demodulize.underscore.pluralize
43
+ def collection_name = name.demodulize.underscore.pluralize.inquiry
40
44
  end
41
45
  end
42
46
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ class Metadata
6
+ def initialize(resource:, frontmatter:, collection:)
7
+ @resource = resource
8
+ @frontmatter = frontmatter&.deep_symbolize_keys || {}
9
+ @collection = collection
10
+ @config = Perron.configuration
11
+ end
12
+
13
+ def data
14
+ @data ||= ActiveSupport::OrderedOptions
15
+ .new
16
+ .merge(apply_fallbacks_and_defaults(to: merged_site_collection_resource_frontmatter))
17
+ end
18
+
19
+ private
20
+
21
+ def merged_site_collection_resource_frontmatter = site_data.merge(collection_data).merge(@frontmatter)
22
+
23
+ def apply_fallbacks_and_defaults(to:)
24
+ to[:title] ||= @config.site_name || Rails.application.name.underscore.camelize
25
+
26
+ to[:canonical_url] ||= canonical_url
27
+
28
+ to[:og_image] ||= to[:image]
29
+ to[:twitter_image] ||= to[:og_image]
30
+
31
+ to[:og_title] ||= to[:title]
32
+ to[:twitter_title] ||= to[:title]
33
+ to[:og_description] ||= to[:description]
34
+ to[:twitter_description] ||= to[:description]
35
+ to[:og_type] ||= to[:type]
36
+ to[:og_logo] ||= to[:logo]
37
+ to[:og_author] ||= to[:author]
38
+ to[:og_locale] ||= to[:locale]
39
+
40
+ to[:og_site_name] = @config.site_name
41
+ to[:twitter_card] ||= "summary_large_image"
42
+ to[:og_url] = canonical_url
43
+ to[:article_published_time] = @resource.published_at
44
+
45
+ to.compact
46
+ end
47
+
48
+ def canonical_url
49
+ return @frontmatter[:canonical_url] if @frontmatter[:canonical_url]
50
+ return Rails.application.routes.url_helpers.root_url(**Perron.configuration.default_url_options) if @resource.slug == "/"
51
+
52
+ Rails.application.routes.url_helpers.polymorphic_url(
53
+ @resource,
54
+ **Perron.configuration.default_url_options
55
+ )
56
+ end
57
+
58
+ def site_data
59
+ @config.metadata.except(:title_separator, :title_suffix).deep_symbolize_keys || {}
60
+ end
61
+
62
+ def collection_data
63
+ @collection&.configuration&.metadata&.deep_symbolize_keys || {}
64
+ end
65
+ end
66
+ end
67
+ end
@@ -7,10 +7,10 @@ module Perron
7
7
 
8
8
  included do
9
9
  def published?
10
- return true if Rails.env.development?
10
+ return true if Perron.configuration.view_unpublished
11
11
 
12
- return false if metadata.draft == true
13
- return false if metadata.published == false
12
+ return false if frontmatter.draft == true
13
+ return false if frontmatter.published == false
14
14
  return false if publication_date&.after?(Time.current)
15
15
 
16
16
  true
@@ -20,8 +20,8 @@ module Perron
20
20
 
21
21
  def publication_date
22
22
  @publication_date ||= begin
23
- from_meta = metadata.published_at.present? ? begin
24
- Time.zone.parse(metadata.published_at.to_s)
23
+ from_meta = frontmatter.published_at.present? ? begin
24
+ Time.zone.parse(frontmatter.published_at.to_s)
25
25
  rescue
26
26
  nil
27
27
  end : nil
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "perron/site/resource/related/stop_words"
3
+ require "perron/resource/related/stop_words"
4
4
 
5
5
  module Perron
6
6
  module Site
@@ -9,9 +9,9 @@ module Perron
9
9
  parsed(content)
10
10
  end
11
11
 
12
- def metadata
13
- @metadata_with_dot_access ||= ActiveSupport::OrderedOptions.new.tap do |options|
14
- @metadata.each { |key, value| options[key] = value }
12
+ def frontmatter
13
+ @frontmatter_with_dot_access ||= ActiveSupport::OrderedOptions.new.tap do |options|
14
+ @frontmatter.each { |key, value| options[key] = value }
15
15
  end
16
16
  end
17
17
 
@@ -19,10 +19,10 @@ module Perron
19
19
 
20
20
  def parsed(content)
21
21
  if content =~ /\A---\s*(.*?)\s*---\s*(.*)/m
22
- @metadata = YAML.safe_load($1, permitted_classes: [Date, Time]) || {}
22
+ @frontmatter = YAML.safe_load($1, permitted_classes: [Date, Time]) || {}
23
23
  @content = $2.strip
24
24
  else
25
- @metadata = {}
25
+ @frontmatter = {}
26
26
  @content = content
27
27
  end
28
28
  end
@@ -7,13 +7,16 @@ module Perron
7
7
  class Slug
8
8
  using Perron::SuffixStripping
9
9
 
10
- def initialize(resource)
10
+ def initialize(resource, frontmatter)
11
11
  @resource = resource
12
- @metadata = resource.metadata
12
+ @frontmatter = frontmatter
13
13
  end
14
14
 
15
15
  def create
16
- @metadata.slug.presence || @resource.filename.sub(/^[\d-]+-/, "").delete_suffixes(dot_prepended_allowed_extensions)
16
+ return "/" if Perron.configuration.allowed_extensions.any? { @resource.filename == "root.#{it}" }
17
+
18
+ @frontmatter.slug.presence ||
19
+ @resource.filename.sub(/^[\d-]+-/, "").delete_suffixes(dot_prepended_allowed_extensions)
17
20
  end
18
21
 
19
22
  private
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "perron/site/resource/configuration"
4
- require "perron/site/resource/core"
5
- require "perron/site/resource/class_methods"
6
- require "perron/site/resource/publishable"
7
- require "perron/site/resource/related"
8
- require "perron/site/resource/renderer"
9
- require "perron/site/resource/slug"
10
- require "perron/site/resource/separator"
3
+ require "perron/resource/configuration"
4
+ require "perron/resource/core"
5
+ require "perron/resource/class_methods"
6
+ require "perron/resource/metadata"
7
+ require "perron/resource/publishable"
8
+ require "perron/resource/related"
9
+ require "perron/resource/renderer"
10
+ require "perron/resource/slug"
11
+ require "perron/resource/separator"
11
12
 
12
13
  module Perron
13
14
  class Resource
@@ -29,7 +30,7 @@ module Perron
29
30
 
30
31
  def filename = File.basename(@file_path)
31
32
 
32
- def slug = Perron::Resource::Slug.new(self).create
33
+ def slug = Perron::Resource::Slug.new(self, frontmatter).create
33
34
  alias_method :path, :slug
34
35
  alias_method :to_param, :slug
35
36
 
@@ -41,11 +42,30 @@ module Perron
41
42
  Perron::Resource::Renderer.erb(page_content, {resource: self})
42
43
  end
43
44
 
44
- def metadata = Perron::Resource::Separator.new(raw_content).metadata
45
+ def metadata
46
+ Perron::Resource::Metadata.new(
47
+ resource: self,
48
+ frontmatter: frontmatter,
49
+ collection: collection
50
+ ).data
51
+ end
45
52
 
46
53
  def raw_content = File.read(@file_path)
47
54
  alias_method :raw, :raw_content
48
55
 
56
+ def to_partial_path
57
+ @to_partial_path ||= begin
58
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self.class.model_name))
59
+ collection = ActiveSupport::Inflector.tableize(self.class.model_name)
60
+
61
+ File.join("content", collection, element)
62
+ end
63
+ end
64
+
65
+ def root?
66
+ collection.name.inquiry.pages? && File.basename(filename) == "root"
67
+ end
68
+
49
69
  def collection = Collection.new(self.class.model_name.collection)
50
70
 
51
71
  def related_resources(limit: 5) = Perron::Site::Resource::Related.new(self).find(limit:)
@@ -53,6 +73,10 @@ module Perron
53
73
 
54
74
  private
55
75
 
76
+ def frontmatter
77
+ @frontmatter ||= Perron::Resource::Separator.new(raw_content).frontmatter
78
+ end
79
+
56
80
  def erb_processing?
57
81
  @file_path.ends_with?(".erb") || metadata.erb == true
58
82
  end
data/lib/perron/root.rb CHANGED
@@ -5,7 +5,7 @@ module Perron
5
5
  include ActiveSupport::Concern
6
6
 
7
7
  def root
8
- @resource = Content::Page.find("/")
8
+ @resource = Content::Page.root
9
9
 
10
10
  render :show
11
11
  end
data/lib/perron/site.rb CHANGED
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "perron/site/builder"
4
- require "perron/site/collection"
5
- require "perron/site/resource"
6
- require "perron/site/data"
7
- require "perron/site/data/proxy"
4
+ require "perron/collection"
5
+ require "perron/data"
6
+ require "perron/data/proxy"
8
7
 
9
8
  module Perron
10
9
  module Site
@@ -1,3 +1,3 @@
1
1
  module Perron
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.0"
3
3
  end
data/lib/perron.rb CHANGED
@@ -5,6 +5,7 @@ require "perron/configuration"
5
5
  require "perron/errors"
6
6
  require "perron/root"
7
7
  require "perron/site"
8
+ require "perron/resource"
8
9
  require "perron/markdown"
9
10
  require "perron/feeds"
10
11
  require "perron/metatags"
data/perron.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
 
9
9
  spec.summary = "Rails-based static site generator"
10
10
  spec.description = "Perron is a Rails-based static site generator that follows Rails conventions. It allows you to create content collections with markdown or ERB, configure SEO metadata, and build production-ready static sites while leveraging your existing Rails knowledge with familiar patterns and minimal configuration."
11
- spec.homepage = "https://railsdesigner.com/perron/"
11
+ spec.homepage = "https://perron.railsdesigner.com/"
12
12
  spec.license = "MIT"
13
13
 
14
14
  spec.metadata["homepage_uri"] = spec.homepage
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perron
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer Developers
@@ -96,17 +96,32 @@ files:
96
96
  - lib/generators/perron/templates/README.md.tt
97
97
  - lib/generators/perron/templates/initializer.rb.tt
98
98
  - lib/perron.rb
99
+ - lib/perron/collection.rb
99
100
  - lib/perron/configuration.rb
101
+ - lib/perron/data.rb
102
+ - lib/perron/data/proxy.rb
100
103
  - lib/perron/engine.rb
101
104
  - lib/perron/errors.rb
102
105
  - lib/perron/feeds.rb
103
106
  - lib/perron/html_processor.rb
104
107
  - lib/perron/html_processor/base.rb
105
108
  - lib/perron/html_processor/lazy_load_images.rb
109
+ - lib/perron/html_processor/syntax_highlight.rb
106
110
  - lib/perron/html_processor/target_blank.rb
107
111
  - lib/perron/markdown.rb
108
112
  - lib/perron/metatags.rb
109
113
  - lib/perron/refinements/delete_suffixes.rb
114
+ - lib/perron/resource.rb
115
+ - lib/perron/resource/class_methods.rb
116
+ - lib/perron/resource/configuration.rb
117
+ - lib/perron/resource/core.rb
118
+ - lib/perron/resource/metadata.rb
119
+ - lib/perron/resource/publishable.rb
120
+ - lib/perron/resource/related.rb
121
+ - lib/perron/resource/related/stop_words.rb
122
+ - lib/perron/resource/renderer.rb
123
+ - lib/perron/resource/separator.rb
124
+ - lib/perron/resource/slug.rb
110
125
  - lib/perron/root.rb
111
126
  - lib/perron/site.rb
112
127
  - lib/perron/site/builder.rb
@@ -118,27 +133,14 @@ files:
118
133
  - lib/perron/site/builder/paths.rb
119
134
  - lib/perron/site/builder/public_files.rb
120
135
  - lib/perron/site/builder/sitemap.rb
121
- - lib/perron/site/collection.rb
122
- - lib/perron/site/data.rb
123
- - lib/perron/site/data/proxy.rb
124
- - lib/perron/site/resource.rb
125
- - lib/perron/site/resource/class_methods.rb
126
- - lib/perron/site/resource/configuration.rb
127
- - lib/perron/site/resource/core.rb
128
- - lib/perron/site/resource/publishable.rb
129
- - lib/perron/site/resource/related.rb
130
- - lib/perron/site/resource/related/stop_words.rb
131
- - lib/perron/site/resource/renderer.rb
132
- - lib/perron/site/resource/separator.rb
133
- - lib/perron/site/resource/slug.rb
134
136
  - lib/perron/tasks/perron.rake
135
137
  - lib/perron/version.rb
136
138
  - perron.gemspec
137
- homepage: https://railsdesigner.com/perron/
139
+ homepage: https://perron.railsdesigner.com/
138
140
  licenses:
139
141
  - MIT
140
142
  metadata:
141
- homepage_uri: https://railsdesigner.com/perron/
143
+ homepage_uri: https://perron.railsdesigner.com/
142
144
  source_code_uri: https://github.com/Rails-Designer/perron/
143
145
  rdoc_options: []
144
146
  require_paths:
File without changes
File without changes
File without changes