middleman-blog 4.1.0 → 4.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f743fdf5f36a7b8d557f705f36ec9eda56b07a9e1f00aa841e81efea01c79870
4
- data.tar.gz: 6336688644973e8b3311f1b8685a4a41e6c2a4f95217ccdfb89170db983aa017
3
+ metadata.gz: 7c5a345f50c066864dda5b42af46442cf9ca240c0facdfd53b9ef51286b1bdb7
4
+ data.tar.gz: 198ba52cd58f09e38f62ed9853834eaafbb3cd39aeb939facc7c4b37e20c99cf
5
5
  SHA512:
6
- metadata.gz: 96abca8c8cef038c1c97d7f2749b152be8c5c19e6ed9d36b1270a3178ba6912f1ec5d757e1d49a7c99963dc4ca897cc6bb29ed66f083c55874af8ff8dc084124
7
- data.tar.gz: adf5d4268a645634c8350070791051d637f1494b0858f7856a0257267b5a884e0d386b4307c644f8e292098368f124876e03ed334267414a609e95c283a5758a
6
+ metadata.gz: 1d57a33a414d34a975ef901a81ab3bfd89765125701b52a856924a63c6cfb50b35050a2b7389ace9fb96fe4a74ae6c6aeba4ce99288e859f22866748deffc150
7
+ data.tar.gz: d8f216cf40da81e347a54456258cd6671d729b98838b44b22ef65a8ca6c8ecd3c1509bae8e8d561b815b18b53a3d43471c8e20ea737c57d3fffb10fcec046365
data/.gitattributes ADDED
@@ -0,0 +1,3 @@
1
+ # Exclude test features and fixtures from language statistics
2
+ features/** linguist-vendored
3
+ fixtures/** linguist-vendored
data/.gitignore CHANGED
@@ -6,7 +6,6 @@ pkg/*
6
6
  .DS_Store
7
7
  .rbenv-version
8
8
  .ruby-version
9
- .ruby-gemset
10
9
  tmp
11
10
  doc
12
11
  .yardoc
@@ -14,5 +13,4 @@ build/
14
13
  .sass-cache
15
14
  coverage
16
15
  .byebug_history
17
- bin/
18
- .config
16
+ vendor/
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 4.2.0
6
+
7
+ * Add preserve_underscores_in_slugs option to control URL formatting (#393).
8
+ * Fix summary separator behavior: article.body should only show content after separator (#395)
9
+ * Add bulk alias functionality for automatic URL redirects (#396)
10
+
5
11
  ## 4.1.0
6
12
 
7
13
  * Migrate CI to GitHub Actions (#386).
data/Gemfile CHANGED
@@ -19,10 +19,5 @@ gem 'cucumber', require: false
19
19
  gem 'rspec', require: false
20
20
  gem 'timecop', require: false
21
21
 
22
- # Optional dependencies, included for tests
23
- gem 'kramdown'
24
- gem 'rack'
25
- gem 'activesupport', RUBY_VERSION < '3.2' ? '~> 7.0' : '~> 8.0'
26
-
27
22
  # Code Quality
28
23
  gem 'simplecov', require: false
@@ -0,0 +1,41 @@
1
+ Feature: Blog aliases
2
+
3
+ Scenario: Blog articles can have bulk aliases configured
4
+
5
+ Given the Server is running at "alias-app"
6
+
7
+ When I go to "/2024/03/14/pi-day.html"
8
+ Then I should see "This is a test article for pi day."
9
+
10
+ When I go to "/2024-03-14-pi-day.html"
11
+ Then I should see "You are being redirected."
12
+ And I should see "/2024/03/14/pi-day.html"
13
+
14
+ When I go to "/2024/03-14-pi-day"
15
+ Then I should see "You are being redirected."
16
+ And I should see "/2024/03/14/pi-day.html"
17
+
18
+ Scenario: Blog aliases work with prefix configuration
19
+
20
+ Given the Server is running at "alias-prefix-app"
21
+
22
+ When I go to "/blog/2024/01/15/prefix-test.html"
23
+ Then I should see "This article tests prefix functionality"
24
+
25
+ When I go to "/blog/2024-01-15-prefix-test.html"
26
+ Then I should see "You are being redirected."
27
+ And I should see "/blog/2024/01/15/prefix-test.html"
28
+
29
+ When I go to "/blog/archive/2024/prefix-test"
30
+ Then I should see "You are being redirected."
31
+ And I should see "/blog/2024/01/15/prefix-test.html"
32
+
33
+ Scenario: Empty aliases configuration generates no redirects
34
+
35
+ Given the Server is running at "blog-sources-app"
36
+
37
+ When I go to "/2011/01/01/new-article.html"
38
+ Then I should see "/2011/01/01/new-article.html"
39
+
40
+ When I go to "/2011-01-01-new-article.html"
41
+ Then I should see "Not Found"
@@ -1,5 +1,5 @@
1
1
  Feature: Article summary generation
2
- Scenario: Article has no summary separator
2
+ Scenario: Article has standard summary separator
3
3
  Given the Server is running at "summary-app"
4
4
  When I go to "/index.html"
5
5
  Then I should see:
@@ -7,9 +7,20 @@ Feature: Article summary generation
7
7
  <p>Summary from article with separator.
8
8
  </p>
9
9
  """
10
- Then I should not see "Extended part from article with separator."
10
+ And I should not see "Extended part from article with separator."
11
+ When I go to "/2011/01/01/article-with-standard-summary-separator.html"
12
+ Then I should see "Extended part from article with separator."
13
+ And I should not see "READMORE"
14
+ And I should not see "Summary from article with separator."
15
+
16
+ Scenario: Article has no summary separator
17
+ Given the Server is running at "summary-app"
18
+ When I go to "/index.html"
11
19
  Then I should see "<p>Summary from article with no separator.</p>"
12
20
  Then I should not see "Extended part from article with no separator."
21
+ When I go to "/2012/06/19/article-with-no-summary-separator.html"
22
+ And I should see "Summary from article with no separator."
23
+ Then I should see "Extended part from article with no separator."
13
24
 
14
25
  Scenario: Article has custom summary separator
15
26
  Given a fixture app "summary-app"
@@ -28,6 +39,10 @@ Feature: Article summary generation
28
39
  """
29
40
  Then I should not see "Extended part from article with custom separator."
30
41
  Then I should see "Extended part from article with separator."
42
+ When I go to "/2013/05/08/article-with-custom-separator.html"
43
+ And I should see "Extended part from article with custom separator."
44
+ Then I should not see "SPLIT_SUMMARY_BEFORE_THIS"
45
+ Then I should not see "Summary from article with custom separator."
31
46
 
32
47
  Scenario: Article has custom summary separator that's an HTML comment
33
48
  Given a fixture app "summary-app"
@@ -46,6 +61,10 @@ Feature: Article summary generation
46
61
  """
47
62
  Then I should not see "Extended part from article with HTML comment separator."
48
63
  Then I should see "Extended part from article with separator."
64
+ When I go to "/2016/05/21/article-with-comment-separator.html"
65
+ And I should see "Extended part from article with HTML comment separator."
66
+ Then I should not see "<!--more-->"
67
+ Then I should not see "Summary from article with HTML comment separator."
49
68
 
50
69
  Scenario: Using a custom summary generator
51
70
  Given a fixture app "summary-app"
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ activate :blog do |blog|
4
+ blog.sources = ':year-:month-:day-:title.html'
5
+ blog.permalink = ':year/:month/:day/:title.html'
6
+ blog.aliases = [
7
+ ':year-:month-:day-:title.html',
8
+ ':year/:month-:day-:title'
9
+ ]
10
+ end
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: "Test Article"
3
+ date: 2024-03-14
4
+ layout: false
5
+ ---
6
+
7
+ This is a test article for pi day.
@@ -0,0 +1 @@
1
+ <h1>Blog</h1>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ activate :blog do |blog|
4
+ blog.prefix = 'blog'
5
+ blog.sources = ':year-:month-:day-:title.html'
6
+ blog.permalink = ':year/:month/:day/:title.html'
7
+ blog.aliases = [
8
+ ':year-:month-:day-:title.html',
9
+ 'archive/:year/:title'
10
+ ]
11
+ end
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: "Prefix Test Article"
3
+ date: 2024-01-15
4
+ layout: false
5
+ ---
6
+
7
+ This article tests prefix functionality with aliases.
@@ -0,0 +1 @@
1
+ <h1>Blog with Prefix</h1>
@@ -0,0 +1,6 @@
1
+ <!doctype html>
2
+ <html>
3
+ <body>
4
+ <%= yield %>
5
+ </body>
6
+ </html>
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'middleman-core'
4
+ require 'middleman-core/sitemap/resource'
5
+ require 'middleman-blog/uri_templates'
6
+
7
+ module Middleman
8
+ module Blog
9
+ ##
10
+ # A sitemap resource manipulator that adds alias/redirect pages to the sitemap
11
+ # for each blog article based on the configured alias patterns
12
+ ##
13
+ class AliasPages
14
+ include UriTemplates
15
+
16
+ ##
17
+ # Initialise Alias pages
18
+ #
19
+ # @param app [Object] Middleman app
20
+ # @param blog_controller [Object] Blog controller
21
+ ##
22
+ def initialize(app, blog_controller)
23
+ @sitemap = app.sitemap
24
+ @blog_controller = blog_controller
25
+ @blog_data = blog_controller.data
26
+ @alias_patterns = blog_controller.options.aliases || []
27
+ @alias_templates = @alias_patterns.map { |pattern| uri_template(pattern) }
28
+ end
29
+
30
+ ##
31
+ # Update the main sitemap resource list
32
+ #
33
+ # @param resources [Array] Existing resources
34
+ # @return [Array] Resources with alias pages added
35
+ ##
36
+ def manipulate_resource_list(resources)
37
+ return resources if @alias_patterns.empty?
38
+
39
+ alias_resources = []
40
+
41
+ @blog_data.articles.each do |article|
42
+ @alias_templates.each do |template|
43
+ alias_path = generate_alias_path(template, article)
44
+ next if alias_path == article.destination_path # Don't create alias to itself
45
+
46
+ alias_resources << alias_page_resource(alias_path, article)
47
+ end
48
+ end
49
+
50
+ resources + alias_resources
51
+ end
52
+
53
+ private
54
+
55
+ ##
56
+ # Generate an alias path for an article using the given template
57
+ #
58
+ # @param template [Addressable::Template] URI template for the alias
59
+ # @param article [BlogArticle] The blog article
60
+ # @return [String] The generated alias path
61
+ ##
62
+ def generate_alias_path(template, article)
63
+ # Get the same parameters used for the main permalink
64
+ params = permalink_options(article)
65
+ apply_uri_template(template, params)
66
+ end
67
+
68
+ ##
69
+ # Generate permalink options for an article (same as BlogData uses)
70
+ #
71
+ # @param article [BlogArticle] The blog article
72
+ # @return [Hash] Parameters for URL generation
73
+ ##
74
+ def permalink_options(article)
75
+ # Get variables from all alias templates
76
+ all_variables = @alias_templates.flat_map(&:variables).uniq
77
+
78
+ # Allow any frontmatter data to be substituted into the alias URL
79
+ page_data = article.metadata[:page] || {}
80
+ params = page_data.slice(*all_variables.map(&:to_sym))
81
+
82
+ params.each do |k, v|
83
+ params[k] = safe_parameterize(v)
84
+ end
85
+
86
+ params
87
+ .merge(date_to_params(article.date))
88
+ .merge(lang: article.lang.to_s, locale: article.locale.to_s, title: article.slug)
89
+ end
90
+
91
+ ##
92
+ # Create an alias page resource that redirects to the main article
93
+ #
94
+ # @param alias_path [String] The path for the alias
95
+ # @param article [BlogArticle] The target article
96
+ # @return [Sitemap::Resource] A redirect resource
97
+ ##
98
+ def alias_page_resource(alias_path, article)
99
+ target_url = article.destination_path
100
+ # Ensure target URL starts with '/' for absolute URLs
101
+ target_url = "/#{target_url}" unless target_url.start_with?('/')
102
+ AliasResource.new(@sitemap, alias_path, target_url, article)
103
+ end
104
+ end
105
+
106
+ ##
107
+ # A resource that generates redirect HTML for alias pages
108
+ ##
109
+ class AliasResource < ::Middleman::Sitemap::Resource
110
+ def initialize(store, path, target_url, alias_resource)
111
+ @target_url = target_url
112
+ @alias_resource = alias_resource
113
+ super(store, path)
114
+ end
115
+
116
+ def source_file
117
+ @alias_resource.source_file
118
+ end
119
+
120
+ def template?
121
+ false
122
+ end
123
+
124
+ def render(*args, &block)
125
+ %[
126
+ <html>
127
+ <head>
128
+ <link rel="canonical" href="#{@target_url}" />
129
+ <meta name="robots" content="noindex,follow" />
130
+ <meta http-equiv="cache-control" content="no-cache" />
131
+ <script>
132
+ // Attempt to keep search and hash
133
+ window.location.replace("#{@target_url}"+window.location.search+window.location.hash);
134
+ </script>
135
+ <meta http-equiv=refresh content="0; url=#{@target_url}" />
136
+ </head>
137
+ <body>
138
+ <a href="#{@target_url}">You are being redirected.</a>
139
+ </body>
140
+ </html>
141
+ ]
142
+ end
143
+
144
+ def binary?
145
+ false
146
+ end
147
+
148
+ def raw_data
149
+ @alias_resource.raw_data
150
+ end
151
+
152
+ def ignored?
153
+ false
154
+ end
155
+
156
+ def metadata
157
+ @alias_resource.metadata
158
+ end
159
+ end
160
+ end
161
+ end
@@ -57,7 +57,14 @@ module Middleman
57
57
 
58
58
  content = super(opts, locs, &block)
59
59
 
60
- content.sub!(blog_options.summary_separator, '') unless opts[:keep_separator]
60
+ # Handle summary separator: if not keeping separator and separator exists,
61
+ # return only content after separator
62
+ if blog_options.summary_separator && !opts[:keep_separator]
63
+ if content.match?(blog_options.summary_separator)
64
+ require 'middleman-blog/truncate_html'
65
+ content = TruncateHTML.content_after_separator(content, blog_options.summary_separator)
66
+ end
67
+ end
61
68
 
62
69
  content
63
70
  end
@@ -238,13 +245,13 @@ module Middleman
238
245
  ##
239
246
  def slug
240
247
  if data['slug']
241
- Blog::UriTemplates.safe_parameterize(data['slug'])
248
+ Blog::UriTemplates.safe_parameterize(data['slug'], preserve_underscores: blog_options.preserve_underscores_in_slugs)
242
249
 
243
250
  elsif blog_data.source_template.variables.include?('title')
244
- Blog::UriTemplates.safe_parameterize(path_part('title'))
251
+ Blog::UriTemplates.safe_parameterize(path_part('title'), preserve_underscores: blog_options.preserve_underscores_in_slugs)
245
252
 
246
253
  elsif title
247
- Blog::UriTemplates.safe_parameterize(title)
254
+ Blog::UriTemplates.safe_parameterize(title, preserve_underscores: blog_options.preserve_underscores_in_slugs)
248
255
 
249
256
  else
250
257
  raise "Can't generate a slug for #{path} because it has no :title in its path pattern or title/slug in its frontmatter."
@@ -209,7 +209,7 @@ module Middleman
209
209
  params = resource.metadata[:page].slice(*@permalink_template.variables.map(&:to_sym))
210
210
 
211
211
  params.each do |k, v|
212
- params[k] = safe_parameterize(v)
212
+ params[k] = safe_parameterize(v, preserve_underscores: @options.preserve_underscores_in_slugs)
213
213
  end
214
214
 
215
215
  params
@@ -67,7 +67,6 @@ module Middleman
67
67
  @content = options[:content] || ''
68
68
  @date = options[:date] ? ::Time.zone.parse(options[:date]) : Time.zone.now
69
69
  @locale = options[:locale] || (::I18n.default_locale if defined? ::I18n)
70
- @slug = safe_parameterize(title)
71
70
  @tags = options[:tags]&.split(/\s*,\s*/) || []
72
71
  @title = title
73
72
 
@@ -90,6 +89,9 @@ module Middleman
90
89
  throw msg
91
90
  end
92
91
 
92
+ # Generate slug after we have access to blog options
93
+ @slug = safe_parameterize(title, preserve_underscores: blog_inst.options.preserve_underscores_in_slugs)
94
+
93
95
  path_template = blog_inst.data.source_template
94
96
  params = date_to_params(@date).merge(locale: @locale.to_s, title: @slug)
95
97
  article_path = apply_uri_template path_template, params
@@ -23,7 +23,7 @@ module Middleman
23
23
  #
24
24
  # @param [String] value
25
25
  def link(value)
26
- apply_uri_template @link_template, property => safe_parameterize(value)
26
+ apply_uri_template @link_template, property => safe_parameterize(value, preserve_underscores: @blog_controller.options.preserve_underscores_in_slugs)
27
27
  end
28
28
 
29
29
  def manipulate_resource_list(resources)
@@ -41,8 +41,10 @@ module Middleman
41
41
  option :publish_future_dated, false, 'Whether articles with a date in the future should be considered published'
42
42
  option :custom_collections, {}, 'Hash of custom frontmatter properties to collect articles on and their options (link, template)'
43
43
  option :preserve_locale, false, 'Use the global Middleman I18n.locale instead of the lang in the article\'s frontmatter'
44
+ option :preserve_underscores_in_slugs, false, 'Whether to preserve underscores in article slugs instead of converting them to dashes'
44
45
  option :new_article_template, File.expand_path('commands/article.tt', __dir__), 'Path (relative to project root) to an ERb template that will be used to generate new articles from the "middleman article" command.'
45
46
  option :default_extension, '.markdown', 'Default template extension for articles (used by "middleman article")'
47
+ option :aliases, [], 'Array of URL patterns that should redirect to the main permalink (e.g., [":year-:month-:day-:title.html"])'
46
48
 
47
49
  # @return [BlogData] blog data for this blog, which has all information about the blog articles
48
50
  attr_reader :data
@@ -62,6 +64,9 @@ module Middleman
62
64
  # @return [Hash<CustomPages>] custom pages handlers for this blog, indexed by property name
63
65
  attr_reader :custom_pages
64
66
 
67
+ # @return [AliasPages] alias page handler for this blog
68
+ attr_reader :alias_pages
69
+
65
70
  # Helpers for use within templates and layouts.
66
71
  self.defined_helpers = [Middleman::Blog::Helpers]
67
72
 
@@ -94,6 +99,11 @@ module Middleman
94
99
  options.custom_collections.each_value do |opts|
95
100
  opts[:link] = File.join(options.prefix, opts[:link])
96
101
  end
102
+
103
+ # Apply prefix to alias patterns if specified
104
+ unless options.aliases.nil? || options.aliases.empty?
105
+ options.aliases = options.aliases.map { |alias_pattern| File.join(options.prefix, alias_pattern) }
106
+ end
97
107
  end
98
108
 
99
109
  def after_configuration
@@ -157,6 +167,12 @@ module Middleman
157
167
  @app.sitemap.register_resource_list_manipulator(:"blog_#{name}_paginate", @paginator)
158
168
  end
159
169
 
170
+ unless options.aliases.nil? || options.aliases.empty?
171
+ require 'middleman-blog/alias_pages'
172
+ @alias_pages = Blog::AliasPages.new(@app, self)
173
+ @app.sitemap.register_resource_list_manipulator(:"blog_#{name}_aliases", @alias_pages)
174
+ end
175
+
160
176
  logger.info "== Blog Sources: #{options.sources} (:prefix + :sources)"
161
177
  end
162
178
 
@@ -33,7 +33,7 @@ module Middleman
33
33
  # @return [String] Safe Tag URL
34
34
  ##
35
35
  def link(tag)
36
- apply_uri_template @tag_link_template, tag: safe_parameterize(tag)
36
+ apply_uri_template @tag_link_template, tag: safe_parameterize(tag, preserve_underscores: @blog_controller.options.preserve_underscores_in_slugs)
37
37
  end
38
38
 
39
39
  ##
@@ -15,6 +15,20 @@ module TruncateHTML
15
15
  doc.inner_html
16
16
  end
17
17
 
18
+ def self.content_after_separator(text, separator)
19
+ text = text.encode('UTF-8') if text.respond_to?(:encode)
20
+ parts = text.split(separator, 2)
21
+
22
+ if parts.length >= 2
23
+ # Take the last part (which should be after the separator)
24
+ content_after = parts.last
25
+ doc = Nokogiri::HTML::DocumentFragment.parse content_after
26
+ doc.inner_html
27
+ else
28
+ text
29
+ end
30
+ end
31
+
18
32
  def self.truncate_at_length(text, max_length, ellipsis = '...')
19
33
  ellipsis_length = ellipsis.length
20
34
  text = text.encode('UTF-8') if text.respond_to?(:encode)
@@ -54,13 +54,13 @@ module Middleman
54
54
  # Reimplementation of this, preserves un-transliterate-able multibyte chars.
55
55
  #
56
56
  # @see http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize
57
- def safe_parameterize(str, sep = '-')
57
+ def safe_parameterize(str, separator: '-', preserve_underscores: false)
58
58
  # Remove ending ?
59
59
  str = str.to_s.gsub(/\?$/, '')
60
60
 
61
61
  # Reimplementation of http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize that preserves un-transliterate-able multibyte chars.
62
62
  parameterized_string = ::ActiveSupport::Inflector.transliterate(str.to_s).downcase
63
- parameterized_string.gsub!(/[^a-z0-9\-_?]+/, sep)
63
+ parameterized_string.gsub!(/[^a-z0-9\-_?]+/, separator)
64
64
 
65
65
  # Check for multibytes and sub back in
66
66
  parameterized_string.chars.to_a.each_with_index do |char, i|
@@ -69,16 +69,16 @@ module Middleman
69
69
  parameterized_string[i] = str[i]
70
70
  end
71
71
 
72
- re_sep = ::Regexp.escape(sep)
72
+ re_sep = ::Regexp.escape(separator)
73
73
 
74
74
  # No more than one of the separator in a row.
75
- parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
75
+ parameterized_string.gsub!(/#{re_sep}{2,}/, separator)
76
76
 
77
77
  # Remove leading/trailing separator.
78
78
  parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/, '')
79
79
 
80
- # Replace all _ with -
81
- parameterized_string.tr!('_', '-')
80
+ # Replace all _ with - (unless preserve_underscores is true)
81
+ parameterized_string.tr!('_', '-') unless preserve_underscores
82
82
 
83
83
  # Delete all ?
84
84
  parameterized_string.delete!('?')
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Middleman
4
4
  module Blog
5
- VERSION = '4.1.0'
5
+ VERSION = '4.2.0'
6
6
  end
7
7
  end
@@ -0,0 +1,116 @@
1
+ require 'middleman-blog/alias_pages'
2
+ require 'middleman-blog/uri_templates'
3
+
4
+ describe 'Middleman::Blog::AliasPages' do
5
+ include Middleman::Blog::UriTemplates
6
+
7
+ let(:mock_app) { double('app') }
8
+ let(:mock_sitemap) { double('sitemap') }
9
+ let(:mock_blog_controller) { double('blog_controller') }
10
+ let(:mock_blog_data) { double('blog_data') }
11
+ let(:mock_article) { double('article') }
12
+ let(:mock_options) { double('options') }
13
+
14
+ before do
15
+ allow(mock_app).to receive(:sitemap).and_return(mock_sitemap)
16
+ allow(mock_blog_controller).to receive(:data).and_return(mock_blog_data)
17
+ allow(mock_blog_controller).to receive(:options).and_return(mock_options)
18
+ allow(mock_article).to receive(:destination_path).and_return('2024/03/14/pi-day.html')
19
+ allow(mock_article).to receive(:date).and_return(Date.new(2024, 3, 14))
20
+ allow(mock_article).to receive(:slug).and_return('pi-day')
21
+ allow(mock_article).to receive(:lang).and_return(:en)
22
+ allow(mock_article).to receive(:locale).and_return(:en)
23
+ allow(mock_article).to receive(:metadata).and_return({ page: {} })
24
+ end
25
+
26
+ describe 'alias URL generation' do
27
+ let(:alias_patterns) { [':year-:month-:day-:title.html', ':year/:month-:day-:title'] }
28
+
29
+ before do
30
+ allow(mock_options).to receive(:aliases).and_return(alias_patterns)
31
+ allow(mock_blog_data).to receive(:articles).and_return([mock_article])
32
+ end
33
+
34
+ context 'when testing alias path generation' do
35
+ let(:alias_pages) { Middleman::Blog::AliasPages.new(mock_app, mock_blog_controller) }
36
+
37
+ it 'generates correct alias paths from patterns' do
38
+ template1 = uri_template(':year-:month-:day-:title.html')
39
+ template2 = uri_template(':year/:month-:day-:title')
40
+
41
+ # Test path generation directly
42
+ alias_path1 = alias_pages.send(:generate_alias_path, template1, mock_article)
43
+ alias_path2 = alias_pages.send(:generate_alias_path, template2, mock_article)
44
+
45
+ expect(alias_path1).to eq('2024-03-14-pi-day.html')
46
+ expect(alias_path2).to eq('2024/03-14-pi-day')
47
+ end
48
+
49
+ it 'filters out aliases that match main permalink' do
50
+ # Test with an alias pattern that would match the main permalink
51
+ template = uri_template(':year/:month/:day/:title.html')
52
+
53
+ alias_path = alias_pages.send(:generate_alias_path, template, mock_article)
54
+ expect(alias_path).to eq('2024/03/14/pi-day.html')
55
+
56
+ # This would be filtered out in manipulate_resource_list because it matches destination_path
57
+ expect(alias_path).to eq(mock_article.destination_path)
58
+ end
59
+ end
60
+
61
+ context 'when testing permalink options generation' do
62
+ let(:alias_pages) { Middleman::Blog::AliasPages.new(mock_app, mock_blog_controller) }
63
+
64
+ it 'generates correct permalink options from article data' do
65
+ params = alias_pages.send(:permalink_options, mock_article)
66
+
67
+ expect(params[:year]).to eq('2024')
68
+ expect(params[:month]).to eq('03')
69
+ expect(params[:day]).to eq('14')
70
+ expect(params[:title]).to eq('pi-day')
71
+ expect(params[:lang]).to eq('en')
72
+ expect(params[:locale]).to eq('en')
73
+ end
74
+ end
75
+ end
76
+
77
+ describe 'empty aliases configuration' do
78
+ before do
79
+ allow(mock_options).to receive(:aliases).and_return([])
80
+ allow(mock_blog_data).to receive(:articles).and_return([mock_article])
81
+ end
82
+
83
+ it 'returns original resources when aliases array is empty' do
84
+ alias_pages = Middleman::Blog::AliasPages.new(mock_app, mock_blog_controller)
85
+ resources = ['existing_resource']
86
+ result = alias_pages.manipulate_resource_list(resources)
87
+
88
+ # Should return the same resources since no aliases are configured
89
+ expect(result).to eq(resources)
90
+ end
91
+ end
92
+
93
+ describe 'alias pattern handling' do
94
+ before do
95
+ allow(mock_blog_data).to receive(:articles).and_return([mock_article])
96
+ end
97
+
98
+ it 'handles multiple alias patterns' do
99
+ patterns = [':year-:month-:day-:title.html', ':year/:month-:day-:title', 'archive/:title']
100
+ allow(mock_options).to receive(:aliases).and_return(patterns)
101
+
102
+ alias_pages = Middleman::Blog::AliasPages.new(mock_app, mock_blog_controller)
103
+
104
+ # Test that all patterns are converted to templates
105
+ expect(alias_pages.instance_variable_get(:@alias_templates).length).to eq(3)
106
+ end
107
+
108
+ it 'handles nil alias patterns gracefully' do
109
+ allow(mock_options).to receive(:aliases).and_return(nil)
110
+
111
+ expect {
112
+ Middleman::Blog::AliasPages.new(mock_app, mock_blog_controller)
113
+ }.not_to raise_error
114
+ end
115
+ end
116
+ end
@@ -26,6 +26,18 @@ describe 'Middleman::Blog::UriTemplates' do
26
26
  it 'can handle numbers' do
27
27
  expect(safe_parameterize(1)).to eq '1'
28
28
  end
29
+
30
+ it 'converts underscores to dashes by default' do
31
+ expect(safe_parameterize('name_of_article')).to eq 'name-of-article'
32
+ end
33
+
34
+ it 'can preserve underscores when requested' do
35
+ expect(safe_parameterize('name_of_article', preserve_underscores: true)).to eq 'name_of_article'
36
+ end
37
+
38
+ it 'still works with mixed content when preserving underscores' do
39
+ expect(safe_parameterize('Some MIXED_content', preserve_underscores: true)).to eq 'some-mixed_content'
40
+ end
29
41
  end
30
42
 
31
43
  describe 'extract_params' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: middleman-blog
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Reynolds
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2025-04-07 00:00:00.000000000 Z
13
+ date: 2025-11-03 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: middleman-core
@@ -64,6 +64,7 @@ extensions: []
64
64
  extra_rdoc_files: []
65
65
  files:
66
66
  - ".editorconfig"
67
+ - ".gitattributes"
67
68
  - ".github/CONTRIBUTING.md"
68
69
  - ".github/ISSUE_TEMPLATE.md"
69
70
  - ".github/workflows/ci.yml"
@@ -75,6 +76,7 @@ files:
75
76
  - LICENSE.md
76
77
  - README.md
77
78
  - Rakefile
79
+ - features/alias.feature
78
80
  - features/article_dirs.feature
79
81
  - features/blog_sources.feature
80
82
  - features/calendar-and-tag.feature
@@ -102,6 +104,12 @@ files:
102
104
  - features/tags.feature
103
105
  - features/tags_multiblog.feature
104
106
  - features/time_zone.feature
107
+ - fixtures/alias-app/config.rb
108
+ - fixtures/alias-app/source/2024-03-14-pi-day.html.markdown
109
+ - fixtures/alias-app/source/index.html
110
+ - fixtures/alias-prefix-app/config.rb
111
+ - fixtures/alias-prefix-app/source/blog/2024-01-15-prefix-test.html.markdown
112
+ - fixtures/alias-prefix-app/source/index.html
105
113
  - fixtures/article-dirs-app/config-directory-indexes.rb
106
114
  - fixtures/article-dirs-app/config-permalink-with-dot.rb
107
115
  - fixtures/article-dirs-app/config.rb
@@ -306,6 +314,7 @@ files:
306
314
  - fixtures/summary-app/source/2013-05-08-article-with-custom-separator.html.markdown
307
315
  - fixtures/summary-app/source/2016-05-21-article-with-comment-separator.html.markdown
308
316
  - fixtures/summary-app/source/index.html.erb
317
+ - fixtures/summary-app/source/layout.erb
309
318
  - fixtures/tags-app/config-directory-indexes.rb
310
319
  - fixtures/tags-app/config-filters.rb
311
320
  - fixtures/tags-app/config-no-tags.rb
@@ -332,6 +341,7 @@ files:
332
341
  - fixtures/time-zone-app/config.rb
333
342
  - fixtures/time-zone-app/source/blog/2013-06-24-hello.html.erb
334
343
  - lib/middleman-blog.rb
344
+ - lib/middleman-blog/alias_pages.rb
335
345
  - lib/middleman-blog/blog_article.rb
336
346
  - lib/middleman-blog/blog_data.rb
337
347
  - lib/middleman-blog/calendar_pages.rb
@@ -347,6 +357,7 @@ files:
347
357
  - lib/middleman-blog/version.rb
348
358
  - lib/middleman_extension.rb
349
359
  - middleman-blog.gemspec
360
+ - spec/alias_spec.rb
350
361
  - spec/spec_helper.rb
351
362
  - spec/uri_templates_spec.rb
352
363
  homepage: https://github.com/middleman/middleman-blog
@@ -368,11 +379,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
368
379
  - !ruby/object:Gem::Version
369
380
  version: '0'
370
381
  requirements: []
371
- rubygems_version: 3.4.10
382
+ rubygems_version: 3.5.16
372
383
  signing_key:
373
384
  specification_version: 4
374
385
  summary: Blog engine for Middleman
375
386
  test_files:
387
+ - features/alias.feature
376
388
  - features/article_dirs.feature
377
389
  - features/blog_sources.feature
378
390
  - features/calendar-and-tag.feature
@@ -400,6 +412,12 @@ test_files:
400
412
  - features/tags.feature
401
413
  - features/tags_multiblog.feature
402
414
  - features/time_zone.feature
415
+ - fixtures/alias-app/config.rb
416
+ - fixtures/alias-app/source/2024-03-14-pi-day.html.markdown
417
+ - fixtures/alias-app/source/index.html
418
+ - fixtures/alias-prefix-app/config.rb
419
+ - fixtures/alias-prefix-app/source/blog/2024-01-15-prefix-test.html.markdown
420
+ - fixtures/alias-prefix-app/source/index.html
403
421
  - fixtures/article-dirs-app/config-directory-indexes.rb
404
422
  - fixtures/article-dirs-app/config-permalink-with-dot.rb
405
423
  - fixtures/article-dirs-app/config.rb
@@ -604,6 +622,7 @@ test_files:
604
622
  - fixtures/summary-app/source/2013-05-08-article-with-custom-separator.html.markdown
605
623
  - fixtures/summary-app/source/2016-05-21-article-with-comment-separator.html.markdown
606
624
  - fixtures/summary-app/source/index.html.erb
625
+ - fixtures/summary-app/source/layout.erb
607
626
  - fixtures/tags-app/config-directory-indexes.rb
608
627
  - fixtures/tags-app/config-filters.rb
609
628
  - fixtures/tags-app/config-no-tags.rb