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 +4 -4
- data/.gitattributes +3 -0
- data/.gitignore +1 -3
- data/CHANGELOG.md +6 -0
- data/Gemfile +0 -5
- data/features/alias.feature +41 -0
- data/features/summary.feature +21 -2
- data/fixtures/alias-app/config.rb +10 -0
- data/fixtures/alias-app/source/2024-03-14-pi-day.html.markdown +7 -0
- data/fixtures/alias-app/source/index.html +1 -0
- data/fixtures/alias-prefix-app/config.rb +11 -0
- data/fixtures/alias-prefix-app/source/blog/2024-01-15-prefix-test.html.markdown +7 -0
- data/fixtures/alias-prefix-app/source/index.html +1 -0
- data/fixtures/summary-app/source/layout.erb +6 -0
- data/lib/middleman-blog/alias_pages.rb +161 -0
- data/lib/middleman-blog/blog_article.rb +11 -4
- data/lib/middleman-blog/blog_data.rb +1 -1
- data/lib/middleman-blog/commands/article.rb +3 -1
- data/lib/middleman-blog/custom_pages.rb +1 -1
- data/lib/middleman-blog/extension.rb +16 -0
- data/lib/middleman-blog/tag_pages.rb +1 -1
- data/lib/middleman-blog/truncate_html.rb +14 -0
- data/lib/middleman-blog/uri_templates.rb +6 -6
- data/lib/middleman-blog/version.rb +1 -1
- data/spec/alias_spec.rb +116 -0
- data/spec/uri_templates_spec.rb +12 -0
- metadata +22 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c5a345f50c066864dda5b42af46442cf9ca240c0facdfd53b9ef51286b1bdb7
|
|
4
|
+
data.tar.gz: 198ba52cd58f09e38f62ed9853834eaafbb3cd39aeb939facc7c4b37e20c99cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d57a33a414d34a975ef901a81ab3bfd89765125701b52a856924a63c6cfb50b35050a2b7389ace9fb96fe4a74ae6c6aeba4ce99288e859f22866748deffc150
|
|
7
|
+
data.tar.gz: d8f216cf40da81e347a54456258cd6671d729b98838b44b22ef65a8ca6c8ecd3c1509bae8e8d561b815b18b53a3d43471c8e20ea737c57d3fffb10fcec046365
|
data/.gitattributes
ADDED
data/.gitignore
CHANGED
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"
|
data/features/summary.feature
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Feature: Article summary generation
|
|
2
|
-
Scenario: Article has
|
|
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
|
-
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
<h1>Blog with Prefix</h1>
|
|
@@ -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
|
-
|
|
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,
|
|
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\-_?]+/,
|
|
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(
|
|
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,}/,
|
|
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!('?')
|
data/spec/alias_spec.rb
ADDED
|
@@ -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
|
data/spec/uri_templates_spec.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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.
|
|
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
|