better_seo 0.14.0 β†’ 1.0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +30 -0
  3. data/.rubocop_todo.yml +360 -0
  4. data/CHANGELOG.md +105 -0
  5. data/README.md +280 -180
  6. data/docs/00_OVERVIEW.md +472 -0
  7. data/docs/01_CORE_AND_CONFIGURATION.md +913 -0
  8. data/docs/02_META_TAGS_AND_OPEN_GRAPH.md +251 -0
  9. data/docs/03_STRUCTURED_DATA.md +140 -0
  10. data/docs/04_SITEMAP_AND_ROBOTS.md +131 -0
  11. data/docs/05_RAILS_INTEGRATION.md +175 -0
  12. data/docs/06_I18N_PAGE_GENERATOR.md +233 -0
  13. data/docs/07_IMAGE_OPTIMIZATION.md +260 -0
  14. data/docs/DEPENDENCIES.md +383 -0
  15. data/docs/README.md +180 -0
  16. data/docs/TESTING_STRATEGY.md +663 -0
  17. data/lib/better_seo/analytics/google_analytics.rb +83 -0
  18. data/lib/better_seo/analytics/google_tag_manager.rb +74 -0
  19. data/lib/better_seo/configuration.rb +322 -0
  20. data/lib/better_seo/dsl/base.rb +86 -0
  21. data/lib/better_seo/dsl/meta_tags.rb +55 -0
  22. data/lib/better_seo/dsl/open_graph.rb +105 -0
  23. data/lib/better_seo/dsl/twitter_cards.rb +129 -0
  24. data/lib/better_seo/errors.rb +31 -0
  25. data/lib/better_seo/generators/amp_generator.rb +77 -0
  26. data/lib/better_seo/generators/breadcrumbs_generator.rb +127 -0
  27. data/lib/better_seo/generators/canonical_url_manager.rb +100 -0
  28. data/lib/better_seo/generators/meta_tags_generator.rb +101 -0
  29. data/lib/better_seo/generators/open_graph_generator.rb +110 -0
  30. data/lib/better_seo/generators/robots_txt_generator.rb +96 -0
  31. data/lib/better_seo/generators/twitter_cards_generator.rb +102 -0
  32. data/lib/better_seo/image/optimizer.rb +145 -0
  33. data/lib/better_seo/rails/helpers/controller_helpers.rb +120 -0
  34. data/lib/better_seo/rails/helpers/seo_helper.rb +172 -0
  35. data/lib/better_seo/rails/helpers/structured_data_helper.rb +123 -0
  36. data/lib/better_seo/rails/model_helpers.rb +62 -0
  37. data/lib/better_seo/rails/railtie.rb +22 -0
  38. data/lib/better_seo/sitemap/builder.rb +65 -0
  39. data/lib/better_seo/sitemap/generator.rb +57 -0
  40. data/lib/better_seo/sitemap/sitemap_index.rb +73 -0
  41. data/lib/better_seo/sitemap/url_entry.rb +155 -0
  42. data/lib/better_seo/structured_data/article.rb +55 -0
  43. data/lib/better_seo/structured_data/base.rb +74 -0
  44. data/lib/better_seo/structured_data/breadcrumb_list.rb +49 -0
  45. data/lib/better_seo/structured_data/event.rb +205 -0
  46. data/lib/better_seo/structured_data/faq_page.rb +55 -0
  47. data/lib/better_seo/structured_data/generator.rb +75 -0
  48. data/lib/better_seo/structured_data/how_to.rb +96 -0
  49. data/lib/better_seo/structured_data/local_business.rb +94 -0
  50. data/lib/better_seo/structured_data/organization.rb +67 -0
  51. data/lib/better_seo/structured_data/person.rb +51 -0
  52. data/lib/better_seo/structured_data/product.rb +123 -0
  53. data/lib/better_seo/structured_data/recipe.rb +134 -0
  54. data/lib/better_seo/validators/seo_recommendations.rb +165 -0
  55. data/lib/better_seo/validators/seo_validator.rb +205 -0
  56. data/lib/better_seo/version.rb +1 -1
  57. data/lib/better_seo.rb +1 -0
  58. data/lib/generators/better_seo/install_generator.rb +21 -0
  59. data/lib/generators/better_seo/templates/README +29 -0
  60. data/lib/generators/better_seo/templates/better_seo.rb +40 -0
  61. metadata +57 -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