better_seo 0.13.0 β 1.0.0.1
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/CHANGELOG.md +121 -3
- data/README.md +299 -181
- data/docs/00_OVERVIEW.md +472 -0
- data/docs/01_CORE_AND_CONFIGURATION.md +913 -0
- data/docs/02_META_TAGS_AND_OPEN_GRAPH.md +251 -0
- data/docs/03_STRUCTURED_DATA.md +140 -0
- data/docs/04_SITEMAP_AND_ROBOTS.md +131 -0
- data/docs/05_RAILS_INTEGRATION.md +175 -0
- data/docs/06_I18N_PAGE_GENERATOR.md +233 -0
- data/docs/07_IMAGE_OPTIMIZATION.md +260 -0
- data/docs/DEPENDENCIES.md +383 -0
- data/docs/README.md +180 -0
- data/docs/TESTING_STRATEGY.md +663 -0
- data/lib/better_seo/analytics/google_analytics.rb +83 -0
- data/lib/better_seo/analytics/google_tag_manager.rb +74 -0
- data/lib/better_seo/configuration.rb +316 -0
- data/lib/better_seo/dsl/base.rb +86 -0
- data/lib/better_seo/dsl/meta_tags.rb +55 -0
- data/lib/better_seo/dsl/open_graph.rb +109 -0
- data/lib/better_seo/dsl/twitter_cards.rb +131 -0
- data/lib/better_seo/errors.rb +31 -0
- data/lib/better_seo/generators/amp_generator.rb +83 -0
- data/lib/better_seo/generators/breadcrumbs_generator.rb +126 -0
- data/lib/better_seo/generators/canonical_url_manager.rb +106 -0
- data/lib/better_seo/generators/meta_tags_generator.rb +100 -0
- data/lib/better_seo/generators/open_graph_generator.rb +110 -0
- data/lib/better_seo/generators/robots_txt_generator.rb +102 -0
- data/lib/better_seo/generators/twitter_cards_generator.rb +102 -0
- data/lib/better_seo/image/optimizer.rb +143 -0
- data/lib/better_seo/rails/helpers/controller_helpers.rb +118 -0
- data/lib/better_seo/rails/helpers/seo_helper.rb +176 -0
- data/lib/better_seo/rails/helpers/structured_data_helper.rb +123 -0
- data/lib/better_seo/rails/model_helpers.rb +62 -0
- data/lib/better_seo/rails/railtie.rb +22 -0
- data/lib/better_seo/sitemap/builder.rb +65 -0
- data/lib/better_seo/sitemap/generator.rb +57 -0
- data/lib/better_seo/sitemap/sitemap_index.rb +73 -0
- data/lib/better_seo/sitemap/url_entry.rb +157 -0
- data/lib/better_seo/structured_data/article.rb +55 -0
- data/lib/better_seo/structured_data/base.rb +73 -0
- data/lib/better_seo/structured_data/breadcrumb_list.rb +49 -0
- data/lib/better_seo/structured_data/event.rb +207 -0
- data/lib/better_seo/structured_data/faq_page.rb +55 -0
- data/lib/better_seo/structured_data/generator.rb +75 -0
- data/lib/better_seo/structured_data/how_to.rb +96 -0
- data/lib/better_seo/structured_data/local_business.rb +94 -0
- data/lib/better_seo/structured_data/organization.rb +67 -0
- data/lib/better_seo/structured_data/person.rb +51 -0
- data/lib/better_seo/structured_data/product.rb +123 -0
- data/lib/better_seo/structured_data/recipe.rb +135 -0
- data/lib/better_seo/validators/seo_recommendations.rb +165 -0
- data/lib/better_seo/validators/seo_validator.rb +195 -0
- data/lib/better_seo/version.rb +1 -1
- data/lib/better_seo.rb +5 -0
- data/lib/generators/better_seo/install_generator.rb +21 -0
- data/lib/generators/better_seo/templates/README +29 -0
- data/lib/generators/better_seo/templates/better_seo.rb +40 -0
- metadata +69 -2
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
# Step 01: Core e Sistema di Configurazione
|
|
2
|
+
|
|
3
|
+
**Versione Target**: 0.2.0
|
|
4
|
+
**Durata Stimata**: 1-2 settimane
|
|
5
|
+
**PrioritΓ **: π΄ CRITICA (Foundation)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Obiettivi dello Step
|
|
10
|
+
|
|
11
|
+
Creare il sistema fondamentale di BetterSeo:
|
|
12
|
+
|
|
13
|
+
1. β
Sistema di configurazione YAML/JSON
|
|
14
|
+
2. β
DSL base pattern per builder
|
|
15
|
+
3. β
Custom errors e validazione
|
|
16
|
+
4. β
Rails Railtie per integrazione automatica
|
|
17
|
+
5. β
Configuration loader con environment support
|
|
18
|
+
6. β
Test suite completa per core
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## File da Creare/Modificare
|
|
23
|
+
|
|
24
|
+
### File Principali
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
lib/better_seo.rb # Entry point principale
|
|
28
|
+
lib/better_seo/version.rb # GiΓ esistente
|
|
29
|
+
lib/better_seo/configuration.rb # Sistema configurazione
|
|
30
|
+
lib/better_seo/errors.rb # Custom errors
|
|
31
|
+
lib/better_seo/dsl/base.rb # DSL base pattern
|
|
32
|
+
lib/better_seo/rails/railtie.rb # Rails integration
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### File di Test
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
spec/configuration_spec.rb
|
|
39
|
+
spec/dsl/base_spec.rb
|
|
40
|
+
spec/errors_spec.rb
|
|
41
|
+
spec/rails/railtie_spec.rb
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### File di Supporto
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
lib/better_seo/support/hash_with_indifferent_access.rb # Helper per config
|
|
48
|
+
lib/better_seo/support/deep_merge.rb # Deep merge YAML
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Dipendenze Gem
|
|
54
|
+
|
|
55
|
+
Aggiungere al `better_seo.gemspec`:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
spec.add_dependency "activesupport", ">= 6.1" # Per HashWithIndifferentAccess, Concerns
|
|
59
|
+
spec.add_dependency "yaml", "~> 0.2" # YAML parsing (built-in Ruby)
|
|
60
|
+
|
|
61
|
+
# Development dependencies
|
|
62
|
+
spec.add_development_dependency "rails", ">= 6.1"
|
|
63
|
+
spec.add_development_dependency "rspec-rails", "~> 6.0"
|
|
64
|
+
spec.add_development_dependency "simplecov", "~> 0.22" # Code coverage
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Implementazione Dettagliata
|
|
70
|
+
|
|
71
|
+
### 1. Entry Point (`lib/better_seo.rb`)
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# frozen_string_literal: true
|
|
75
|
+
|
|
76
|
+
require "active_support"
|
|
77
|
+
require "active_support/core_ext"
|
|
78
|
+
require "yaml"
|
|
79
|
+
|
|
80
|
+
require_relative "better_seo/version"
|
|
81
|
+
require_relative "better_seo/errors"
|
|
82
|
+
require_relative "better_seo/configuration"
|
|
83
|
+
require_relative "better_seo/dsl/base"
|
|
84
|
+
|
|
85
|
+
# Rails integration (caricato solo se Rails Γ¨ presente)
|
|
86
|
+
require_relative "better_seo/rails/railtie" if defined?(Rails::Railtie)
|
|
87
|
+
|
|
88
|
+
module BetterSeo
|
|
89
|
+
class Error < StandardError; end
|
|
90
|
+
|
|
91
|
+
class << self
|
|
92
|
+
# Accesso alla configurazione singleton
|
|
93
|
+
def configuration
|
|
94
|
+
@configuration ||= Configuration.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Block-style configuration
|
|
98
|
+
def configure
|
|
99
|
+
yield(configuration) if block_given?
|
|
100
|
+
configuration.validate!
|
|
101
|
+
configuration
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Reset configuration (utile per test)
|
|
105
|
+
def reset_configuration!
|
|
106
|
+
@configuration = Configuration.new
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Shortcut per check feature enabled
|
|
110
|
+
def enabled?(feature)
|
|
111
|
+
configuration.public_send("#{feature}_enabled?")
|
|
112
|
+
rescue NoMethodError
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### 2. Configuration System (`lib/better_seo/configuration.rb`)
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# frozen_string_literal: true
|
|
125
|
+
|
|
126
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
127
|
+
|
|
128
|
+
module BetterSeo
|
|
129
|
+
class Configuration
|
|
130
|
+
# Default values
|
|
131
|
+
DEFAULT_CONFIG = {
|
|
132
|
+
site_name: nil,
|
|
133
|
+
default_locale: :en,
|
|
134
|
+
available_locales: [:en],
|
|
135
|
+
|
|
136
|
+
# Meta tags defaults
|
|
137
|
+
meta_tags: {
|
|
138
|
+
default_title: nil,
|
|
139
|
+
title_separator: " | ",
|
|
140
|
+
append_site_name: true,
|
|
141
|
+
default_description: nil,
|
|
142
|
+
default_keywords: [],
|
|
143
|
+
default_author: nil
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
# Open Graph defaults
|
|
147
|
+
open_graph: {
|
|
148
|
+
enabled: true,
|
|
149
|
+
site_name: nil,
|
|
150
|
+
default_type: "website",
|
|
151
|
+
default_locale: "en_US",
|
|
152
|
+
default_image: {
|
|
153
|
+
url: nil,
|
|
154
|
+
width: 1200,
|
|
155
|
+
height: 630
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
# Twitter Cards defaults
|
|
160
|
+
twitter: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
site: nil,
|
|
163
|
+
creator: nil,
|
|
164
|
+
card_type: "summary_large_image"
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
# Structured Data defaults
|
|
168
|
+
structured_data: {
|
|
169
|
+
enabled: true,
|
|
170
|
+
organization: {},
|
|
171
|
+
website: {}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
# Sitemap defaults
|
|
175
|
+
sitemap: {
|
|
176
|
+
enabled: false,
|
|
177
|
+
output_path: "public/sitemap.xml",
|
|
178
|
+
host: nil,
|
|
179
|
+
compress: false,
|
|
180
|
+
ping_search_engines: false,
|
|
181
|
+
defaults: {
|
|
182
|
+
changefreq: "weekly",
|
|
183
|
+
priority: 0.5
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
# Robots.txt defaults
|
|
188
|
+
robots: {
|
|
189
|
+
enabled: false,
|
|
190
|
+
output_path: "public/robots.txt",
|
|
191
|
+
user_agents: {
|
|
192
|
+
"*" => {
|
|
193
|
+
allow: ["/"],
|
|
194
|
+
disallow: [],
|
|
195
|
+
crawl_delay: nil
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
# Image optimization defaults
|
|
201
|
+
images: {
|
|
202
|
+
enabled: false,
|
|
203
|
+
webp: {
|
|
204
|
+
enabled: true,
|
|
205
|
+
quality: 80
|
|
206
|
+
},
|
|
207
|
+
sizes: {
|
|
208
|
+
thumbnail: { width: 150, height: 150 },
|
|
209
|
+
small: { width: 300 },
|
|
210
|
+
medium: { width: 600 },
|
|
211
|
+
large: { width: 1200 },
|
|
212
|
+
og_image: { width: 1200, height: 630, crop: true }
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
# I18n settings
|
|
217
|
+
i18n: {
|
|
218
|
+
load_path: "config/locales/seo/**/*.yml",
|
|
219
|
+
auto_reload: false
|
|
220
|
+
}
|
|
221
|
+
}.freeze
|
|
222
|
+
|
|
223
|
+
attr_accessor :site_name, :default_locale, :available_locales
|
|
224
|
+
attr_reader :meta_tags, :open_graph, :twitter, :structured_data,
|
|
225
|
+
:sitemap, :robots, :images, :i18n
|
|
226
|
+
|
|
227
|
+
def initialize
|
|
228
|
+
# Deep dup per evitare shared state
|
|
229
|
+
@config = deep_dup(DEFAULT_CONFIG)
|
|
230
|
+
|
|
231
|
+
# Initialize nested configurations
|
|
232
|
+
@meta_tags = NestedConfiguration.new(@config[:meta_tags])
|
|
233
|
+
@open_graph = NestedConfiguration.new(@config[:open_graph])
|
|
234
|
+
@twitter = NestedConfiguration.new(@config[:twitter])
|
|
235
|
+
@structured_data = NestedConfiguration.new(@config[:structured_data])
|
|
236
|
+
@sitemap = NestedConfiguration.new(@config[:sitemap])
|
|
237
|
+
@robots = NestedConfiguration.new(@config[:robots])
|
|
238
|
+
@images = NestedConfiguration.new(@config[:images])
|
|
239
|
+
@i18n = NestedConfiguration.new(@config[:i18n])
|
|
240
|
+
|
|
241
|
+
# Top-level attributes
|
|
242
|
+
@site_name = @config[:site_name]
|
|
243
|
+
@default_locale = @config[:default_locale]
|
|
244
|
+
@available_locales = @config[:available_locales]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Load configuration from YAML file
|
|
248
|
+
def load_from_file(path, environment: nil)
|
|
249
|
+
unless File.exist?(path)
|
|
250
|
+
raise ConfigurationError, "Configuration file not found: #{path}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
yaml_content = YAML.load_file(path)
|
|
254
|
+
|
|
255
|
+
# Se c'Γ¨ environment, carica quella sezione
|
|
256
|
+
config_hash = if environment && yaml_content[environment.to_s]
|
|
257
|
+
yaml_content[environment.to_s]
|
|
258
|
+
else
|
|
259
|
+
yaml_content
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
merge_hash!(config_hash)
|
|
263
|
+
self
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Load from hash
|
|
267
|
+
def load_from_hash(hash)
|
|
268
|
+
merge_hash!(hash)
|
|
269
|
+
self
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Validate configuration
|
|
273
|
+
def validate!
|
|
274
|
+
errors = []
|
|
275
|
+
|
|
276
|
+
# Validate locales
|
|
277
|
+
unless available_locales.is_a?(Array) && available_locales.any?
|
|
278
|
+
errors << "available_locales must be a non-empty array"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
unless available_locales.include?(default_locale)
|
|
282
|
+
errors << "default_locale must be included in available_locales"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Validate sitemap
|
|
286
|
+
if sitemap.enabled && sitemap.host.nil?
|
|
287
|
+
errors << "sitemap.host is required when sitemap is enabled"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Validate meta tags lengths
|
|
291
|
+
if meta_tags.default_title && meta_tags.default_title.length > 60
|
|
292
|
+
errors << "meta_tags.default_title should be max 60 characters"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if meta_tags.default_description && meta_tags.default_description.length > 160
|
|
296
|
+
errors << "meta_tags.default_description should be max 160 characters"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
raise ValidationError, errors.join(", ") if errors.any?
|
|
300
|
+
|
|
301
|
+
true
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Feature enabled checks
|
|
305
|
+
def sitemap_enabled?
|
|
306
|
+
sitemap.enabled == true
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def robots_enabled?
|
|
310
|
+
robots.enabled == true
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def images_enabled?
|
|
314
|
+
images.enabled == true
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def open_graph_enabled?
|
|
318
|
+
open_graph.enabled == true
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def twitter_enabled?
|
|
322
|
+
twitter.enabled == true
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def structured_data_enabled?
|
|
326
|
+
structured_data.enabled == true
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Convert to hash
|
|
330
|
+
def to_h
|
|
331
|
+
{
|
|
332
|
+
site_name: site_name,
|
|
333
|
+
default_locale: default_locale,
|
|
334
|
+
available_locales: available_locales,
|
|
335
|
+
meta_tags: meta_tags.to_h,
|
|
336
|
+
open_graph: open_graph.to_h,
|
|
337
|
+
twitter: twitter.to_h,
|
|
338
|
+
structured_data: structured_data.to_h,
|
|
339
|
+
sitemap: sitemap.to_h,
|
|
340
|
+
robots: robots.to_h,
|
|
341
|
+
images: images.to_h,
|
|
342
|
+
i18n: i18n.to_h
|
|
343
|
+
}
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
private
|
|
347
|
+
|
|
348
|
+
def merge_hash!(hash)
|
|
349
|
+
hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
|
|
350
|
+
|
|
351
|
+
# Merge top-level attributes
|
|
352
|
+
@site_name = hash[:site_name] if hash.key?(:site_name)
|
|
353
|
+
@default_locale = hash[:default_locale] if hash.key?(:default_locale)
|
|
354
|
+
@available_locales = hash[:available_locales] if hash.key?(:available_locales)
|
|
355
|
+
|
|
356
|
+
# Merge nested configurations
|
|
357
|
+
@meta_tags.merge!(hash[:meta_tags]) if hash[:meta_tags]
|
|
358
|
+
@open_graph.merge!(hash[:open_graph]) if hash[:open_graph]
|
|
359
|
+
@twitter.merge!(hash[:twitter]) if hash[:twitter]
|
|
360
|
+
@structured_data.merge!(hash[:structured_data]) if hash[:structured_data]
|
|
361
|
+
@sitemap.merge!(hash[:sitemap]) if hash[:sitemap]
|
|
362
|
+
@robots.merge!(hash[:robots]) if hash[:robots]
|
|
363
|
+
@images.merge!(hash[:images]) if hash[:images]
|
|
364
|
+
@i18n.merge!(hash[:i18n]) if hash[:i18n]
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def deep_dup(hash)
|
|
368
|
+
hash.transform_values do |value|
|
|
369
|
+
value.is_a?(Hash) ? deep_dup(value) : value.dup rescue value
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Nested configuration object
|
|
374
|
+
class NestedConfiguration
|
|
375
|
+
def initialize(hash = {})
|
|
376
|
+
@data = hash.with_indifferent_access
|
|
377
|
+
define_accessors!
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def merge!(other_hash)
|
|
381
|
+
@data.deep_merge!(other_hash.with_indifferent_access)
|
|
382
|
+
define_accessors!
|
|
383
|
+
self
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def to_h
|
|
387
|
+
@data.deep_dup
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def [](key)
|
|
391
|
+
@data[key]
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def []=(key, value)
|
|
395
|
+
@data[key] = value
|
|
396
|
+
define_accessor(key)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def method_missing(method_name, *args, &block)
|
|
400
|
+
method_str = method_name.to_s
|
|
401
|
+
|
|
402
|
+
if method_str.end_with?("=")
|
|
403
|
+
key = method_str.chomp("=").to_sym
|
|
404
|
+
@data[key] = args.first
|
|
405
|
+
define_accessor(key)
|
|
406
|
+
elsif @data.key?(method_name)
|
|
407
|
+
@data[method_name]
|
|
408
|
+
else
|
|
409
|
+
super
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
414
|
+
@data.key?(method_name.to_s.chomp("=").to_sym) || super
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private
|
|
418
|
+
|
|
419
|
+
def define_accessors!
|
|
420
|
+
@data.keys.each { |key| define_accessor(key) }
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def define_accessor(key)
|
|
424
|
+
return if respond_to?(key, true)
|
|
425
|
+
|
|
426
|
+
singleton_class.class_eval do
|
|
427
|
+
define_method(key) { @data[key] }
|
|
428
|
+
define_method("#{key}=") { |value| @data[key] = value }
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### 3. Custom Errors (`lib/better_seo/errors.rb`)
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
# frozen_string_literal: true
|
|
442
|
+
|
|
443
|
+
module BetterSeo
|
|
444
|
+
# Base error class
|
|
445
|
+
class Error < StandardError; end
|
|
446
|
+
|
|
447
|
+
# Configuration errors
|
|
448
|
+
class ConfigurationError < Error; end
|
|
449
|
+
class ValidationError < Error; end
|
|
450
|
+
|
|
451
|
+
# DSL errors
|
|
452
|
+
class DSLError < Error; end
|
|
453
|
+
class InvalidBuilderError < DSLError; end
|
|
454
|
+
|
|
455
|
+
# Generator errors
|
|
456
|
+
class GeneratorError < Error; end
|
|
457
|
+
class TemplateNotFoundError < GeneratorError; end
|
|
458
|
+
|
|
459
|
+
# Validator errors
|
|
460
|
+
class ValidatorError < Error; end
|
|
461
|
+
class InvalidDataError < ValidatorError; end
|
|
462
|
+
|
|
463
|
+
# Image errors
|
|
464
|
+
class ImageError < Error; end
|
|
465
|
+
class ImageConversionError < ImageError; end
|
|
466
|
+
class ImageValidationError < ImageError; end
|
|
467
|
+
|
|
468
|
+
# I18n errors
|
|
469
|
+
class I18nError < Error; end
|
|
470
|
+
class MissingTranslationError < I18nError; end
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### 4. DSL Base Pattern (`lib/better_seo/dsl/base.rb`)
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
# frozen_string_literal: true
|
|
480
|
+
|
|
481
|
+
module BetterSeo
|
|
482
|
+
module DSL
|
|
483
|
+
class Base
|
|
484
|
+
attr_reader :config
|
|
485
|
+
|
|
486
|
+
def initialize
|
|
487
|
+
@config = {}
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Generic setter method
|
|
491
|
+
def set(key, value)
|
|
492
|
+
@config[key] = value
|
|
493
|
+
self
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Generic getter method
|
|
497
|
+
def get(key)
|
|
498
|
+
@config[key]
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Block evaluation
|
|
502
|
+
def evaluate(&block)
|
|
503
|
+
instance_eval(&block) if block_given?
|
|
504
|
+
self
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Build final configuration
|
|
508
|
+
def build
|
|
509
|
+
validate!
|
|
510
|
+
@config.dup
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Convert to hash
|
|
514
|
+
def to_h
|
|
515
|
+
@config.dup
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Merge another config
|
|
519
|
+
def merge!(other)
|
|
520
|
+
if other.is_a?(Hash)
|
|
521
|
+
@config.merge!(other)
|
|
522
|
+
elsif other.respond_to?(:to_h)
|
|
523
|
+
@config.merge!(other.to_h)
|
|
524
|
+
else
|
|
525
|
+
raise DSLError, "Cannot merge #{other.class}"
|
|
526
|
+
end
|
|
527
|
+
self
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
protected
|
|
531
|
+
|
|
532
|
+
# Override in subclasses for validation
|
|
533
|
+
def validate!
|
|
534
|
+
true
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Dynamic method handling
|
|
538
|
+
def method_missing(method_name, *args, &block)
|
|
539
|
+
method_str = method_name.to_s
|
|
540
|
+
|
|
541
|
+
if method_str.end_with?("=")
|
|
542
|
+
# Setter: title = "value"
|
|
543
|
+
key = method_str.chomp("=").to_sym
|
|
544
|
+
set(key, args.first)
|
|
545
|
+
elsif block_given?
|
|
546
|
+
# Nested block: open_graph do ... end
|
|
547
|
+
nested_builder = self.class.new
|
|
548
|
+
nested_builder.evaluate(&block)
|
|
549
|
+
set(method_name, nested_builder.build)
|
|
550
|
+
elsif args.any?
|
|
551
|
+
# Setter without =: title "value"
|
|
552
|
+
set(method_name, args.first)
|
|
553
|
+
else
|
|
554
|
+
# Getter
|
|
555
|
+
get(method_name)
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
560
|
+
true
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### 5. Rails Railtie (`lib/better_seo/rails/railtie.rb`)
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
# frozen_string_literal: true
|
|
573
|
+
|
|
574
|
+
require "rails/railtie"
|
|
575
|
+
|
|
576
|
+
module BetterSeo
|
|
577
|
+
module Rails
|
|
578
|
+
class Railtie < ::Rails::Railtie
|
|
579
|
+
# Auto-load helpers in Rails views
|
|
580
|
+
initializer "better_seo.helpers" do
|
|
581
|
+
ActiveSupport.on_load(:action_view) do
|
|
582
|
+
# Will be implemented in Step 05
|
|
583
|
+
# include BetterSeo::Rails::Helpers::MetaTagsHelper
|
|
584
|
+
# include BetterSeo::Rails::Helpers::OpenGraphHelper
|
|
585
|
+
# include BetterSeo::Rails::Helpers::StructuredDataHelper
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Auto-load concerns in Rails controllers
|
|
590
|
+
initializer "better_seo.concerns" do
|
|
591
|
+
ActiveSupport.on_load(:action_controller) do
|
|
592
|
+
# Will be implemented in Step 05
|
|
593
|
+
# include BetterSeo::Rails::Concerns::SeoAware
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Load configuration from Rails config
|
|
598
|
+
initializer "better_seo.configuration" do |app|
|
|
599
|
+
config_file = app.root.join("config", "better_seo.yml")
|
|
600
|
+
|
|
601
|
+
if config_file.exist?
|
|
602
|
+
BetterSeo.configure do |config|
|
|
603
|
+
config.load_from_file(config_file, environment: ::Rails.env)
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Sync with Rails i18n if not configured
|
|
608
|
+
if BetterSeo.configuration.available_locales == [:en]
|
|
609
|
+
BetterSeo.configuration.available_locales = I18n.available_locales
|
|
610
|
+
BetterSeo.configuration.default_locale = I18n.default_locale
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Add i18n load paths
|
|
615
|
+
initializer "better_seo.i18n" do |app|
|
|
616
|
+
seo_locales_path = app.root.join("config", "locales", "seo", "**", "*.yml")
|
|
617
|
+
I18n.load_path += Dir[seo_locales_path]
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Rake tasks
|
|
621
|
+
rake_tasks do
|
|
622
|
+
# Will be loaded in future steps
|
|
623
|
+
# load "tasks/better_seo/sitemap.rake"
|
|
624
|
+
# load "tasks/better_seo/robots.rake"
|
|
625
|
+
# load "tasks/better_seo/images.rake"
|
|
626
|
+
# load "tasks/better_seo/i18n.rake"
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Test Suite
|
|
636
|
+
|
|
637
|
+
### 1. Configuration Test (`spec/configuration_spec.rb`)
|
|
638
|
+
|
|
639
|
+
```ruby
|
|
640
|
+
# frozen_string_literal: true
|
|
641
|
+
|
|
642
|
+
require "spec_helper"
|
|
643
|
+
|
|
644
|
+
RSpec.describe BetterSeo::Configuration do
|
|
645
|
+
subject(:config) { described_class.new }
|
|
646
|
+
|
|
647
|
+
describe "#initialize" do
|
|
648
|
+
it "sets default values" do
|
|
649
|
+
expect(config.default_locale).to eq(:en)
|
|
650
|
+
expect(config.available_locales).to eq([:en])
|
|
651
|
+
expect(config.meta_tags.default_title).to be_nil
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
it "initializes nested configurations" do
|
|
655
|
+
expect(config.meta_tags).to be_a(BetterSeo::Configuration::NestedConfiguration)
|
|
656
|
+
expect(config.open_graph).to be_a(BetterSeo::Configuration::NestedConfiguration)
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
describe "#load_from_hash" do
|
|
661
|
+
let(:custom_config) do
|
|
662
|
+
{
|
|
663
|
+
site_name: "My Site",
|
|
664
|
+
default_locale: :it,
|
|
665
|
+
available_locales: [:it, :en],
|
|
666
|
+
meta_tags: {
|
|
667
|
+
default_title: "Custom Title",
|
|
668
|
+
default_description: "Custom Description"
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it "merges custom configuration" do
|
|
674
|
+
config.load_from_hash(custom_config)
|
|
675
|
+
|
|
676
|
+
expect(config.site_name).to eq("My Site")
|
|
677
|
+
expect(config.default_locale).to eq(:it)
|
|
678
|
+
expect(config.meta_tags.default_title).to eq("Custom Title")
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
describe "#load_from_file" do
|
|
683
|
+
let(:yaml_path) { "spec/fixtures/config/better_seo.yml" }
|
|
684
|
+
|
|
685
|
+
before do
|
|
686
|
+
allow(File).to receive(:exist?).with(yaml_path).and_return(true)
|
|
687
|
+
allow(YAML).to receive(:load_file).with(yaml_path).and_return({
|
|
688
|
+
"production" => {
|
|
689
|
+
"site_name" => "Production Site",
|
|
690
|
+
"sitemap" => { "enabled" => true, "host" => "https://example.com" }
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
it "loads configuration from YAML file" do
|
|
696
|
+
config.load_from_file(yaml_path, environment: :production)
|
|
697
|
+
|
|
698
|
+
expect(config.site_name).to eq("Production Site")
|
|
699
|
+
expect(config.sitemap.enabled).to be true
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
it "raises error if file not found" do
|
|
703
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
704
|
+
|
|
705
|
+
expect {
|
|
706
|
+
config.load_from_file("nonexistent.yml")
|
|
707
|
+
}.to raise_error(BetterSeo::ConfigurationError, /not found/)
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
describe "#validate!" do
|
|
712
|
+
context "with valid configuration" do
|
|
713
|
+
before do
|
|
714
|
+
config.load_from_hash({
|
|
715
|
+
available_locales: [:it, :en],
|
|
716
|
+
default_locale: :it
|
|
717
|
+
})
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
it "returns true" do
|
|
721
|
+
expect(config.validate!).to be true
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
context "with invalid configuration" do
|
|
726
|
+
it "raises error when default_locale not in available_locales" do
|
|
727
|
+
config.default_locale = :fr
|
|
728
|
+
|
|
729
|
+
expect {
|
|
730
|
+
config.validate!
|
|
731
|
+
}.to raise_error(BetterSeo::ValidationError, /default_locale/)
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
it "raises error when sitemap enabled without host" do
|
|
735
|
+
config.sitemap.enabled = true
|
|
736
|
+
config.sitemap.host = nil
|
|
737
|
+
|
|
738
|
+
expect {
|
|
739
|
+
config.validate!
|
|
740
|
+
}.to raise_error(BetterSeo::ValidationError, /sitemap.host/)
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
it "raises error when title too long" do
|
|
744
|
+
config.meta_tags.default_title = "A" * 80
|
|
745
|
+
|
|
746
|
+
expect {
|
|
747
|
+
config.validate!
|
|
748
|
+
}.to raise_error(BetterSeo::ValidationError, /60 characters/)
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
describe "feature checks" do
|
|
754
|
+
it "checks if sitemap is enabled" do
|
|
755
|
+
expect(config.sitemap_enabled?).to be false
|
|
756
|
+
|
|
757
|
+
config.sitemap.enabled = true
|
|
758
|
+
expect(config.sitemap_enabled?).to be true
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
it "checks if images optimization is enabled" do
|
|
762
|
+
expect(config.images_enabled?).to be false
|
|
763
|
+
|
|
764
|
+
config.images.enabled = true
|
|
765
|
+
expect(config.images_enabled?).to be true
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
describe "#to_h" do
|
|
770
|
+
it "converts configuration to hash" do
|
|
771
|
+
hash = config.to_h
|
|
772
|
+
|
|
773
|
+
expect(hash).to be_a(Hash)
|
|
774
|
+
expect(hash[:default_locale]).to eq(:en)
|
|
775
|
+
expect(hash[:meta_tags]).to be_a(Hash)
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### 2. DSL Base Test (`spec/dsl/base_spec.rb`)
|
|
782
|
+
|
|
783
|
+
```ruby
|
|
784
|
+
# frozen_string_literal: true
|
|
785
|
+
|
|
786
|
+
require "spec_helper"
|
|
787
|
+
|
|
788
|
+
RSpec.describe BetterSeo::DSL::Base do
|
|
789
|
+
subject(:builder) { described_class.new }
|
|
790
|
+
|
|
791
|
+
describe "#set and #get" do
|
|
792
|
+
it "sets and gets values" do
|
|
793
|
+
builder.set(:title, "My Title")
|
|
794
|
+
expect(builder.get(:title)).to eq("My Title")
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
describe "method_missing for setters" do
|
|
799
|
+
it "sets values using method calls" do
|
|
800
|
+
builder.title "My Title"
|
|
801
|
+
expect(builder.get(:title)).to eq("My Title")
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
it "sets values using assignment" do
|
|
805
|
+
builder.title = "My Title"
|
|
806
|
+
expect(builder.get(:title)).to eq("My Title")
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
describe "nested blocks" do
|
|
811
|
+
it "supports nested configuration blocks" do
|
|
812
|
+
builder.evaluate do
|
|
813
|
+
title "Main Title"
|
|
814
|
+
|
|
815
|
+
open_graph do
|
|
816
|
+
title "OG Title"
|
|
817
|
+
image "https://example.com/image.jpg"
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
expect(builder.get(:title)).to eq("Main Title")
|
|
822
|
+
expect(builder.get(:open_graph)).to eq({
|
|
823
|
+
title: "OG Title",
|
|
824
|
+
image: "https://example.com/image.jpg"
|
|
825
|
+
})
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
describe "#build" do
|
|
830
|
+
it "returns configuration hash" do
|
|
831
|
+
builder.title "Title"
|
|
832
|
+
builder.description "Description"
|
|
833
|
+
|
|
834
|
+
result = builder.build
|
|
835
|
+
|
|
836
|
+
expect(result).to eq({
|
|
837
|
+
title: "Title",
|
|
838
|
+
description: "Description"
|
|
839
|
+
})
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
describe "#merge!" do
|
|
844
|
+
it "merges hash configuration" do
|
|
845
|
+
builder.title "Original"
|
|
846
|
+
|
|
847
|
+
builder.merge!(description: "Added", keywords: ["seo", "ruby"])
|
|
848
|
+
|
|
849
|
+
expect(builder.get(:title)).to eq("Original")
|
|
850
|
+
expect(builder.get(:description)).to eq("Added")
|
|
851
|
+
expect(builder.get(:keywords)).to eq(["seo", "ruby"])
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
it "merges another builder" do
|
|
855
|
+
other = described_class.new
|
|
856
|
+
other.title "Other Title"
|
|
857
|
+
other.description "Other Description"
|
|
858
|
+
|
|
859
|
+
builder.merge!(other)
|
|
860
|
+
|
|
861
|
+
expect(builder.get(:title)).to eq("Other Title")
|
|
862
|
+
expect(builder.get(:description)).to eq("Other Description")
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## Checklist Completamento
|
|
871
|
+
|
|
872
|
+
### Implementazione
|
|
873
|
+
- [ ] `lib/better_seo.rb` implementato con configure block
|
|
874
|
+
- [ ] `lib/better_seo/configuration.rb` con nested config support
|
|
875
|
+
- [ ] `lib/better_seo/errors.rb` con tutte le custom errors
|
|
876
|
+
- [ ] `lib/better_seo/dsl/base.rb` con builder pattern
|
|
877
|
+
- [ ] `lib/better_seo/rails/railtie.rb` con auto-configuration
|
|
878
|
+
- [ ] Dipendenze gem aggiunte al gemspec
|
|
879
|
+
|
|
880
|
+
### Testing
|
|
881
|
+
- [ ] `spec/configuration_spec.rb` con test coverage > 90%
|
|
882
|
+
- [ ] `spec/dsl/base_spec.rb` con test coverage > 90%
|
|
883
|
+
- [ ] `spec/errors_spec.rb` con test per ogni error class
|
|
884
|
+
- [ ] `spec/rails/railtie_spec.rb` con test integration Rails
|
|
885
|
+
- [ ] SimpleCov configurato per code coverage
|
|
886
|
+
- [ ] Tutti i test passano (green)
|
|
887
|
+
|
|
888
|
+
### Documentazione
|
|
889
|
+
- [ ] Commenti YARD su metodi pubblici
|
|
890
|
+
- [ ] README aggiornato con esempio configurazione base
|
|
891
|
+
- [ ] CHANGELOG.md aggiornato per v0.2.0
|
|
892
|
+
|
|
893
|
+
### Quality Assurance
|
|
894
|
+
- [ ] Rubocop pass (0 offenses)
|
|
895
|
+
- [ ] No deprecation warnings
|
|
896
|
+
- [ ] Memory leaks check
|
|
897
|
+
- [ ] Performance benchmark (overhead < 1ms)
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## Prossimi Passi
|
|
902
|
+
|
|
903
|
+
Una volta completato questo step:
|
|
904
|
+
|
|
905
|
+
1. β
Sistema di configurazione funzionante
|
|
906
|
+
2. β
DSL base pronto per estensioni
|
|
907
|
+
3. β
Rails integration automatica
|
|
908
|
+
4. β **Procedere con Step 02**: Meta Tags e Open Graph
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
**Status**: π TODO
|
|
913
|
+
**Ultima modifica**: 2025-10-22
|