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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +121 -3
  3. data/README.md +299 -181
  4. data/docs/00_OVERVIEW.md +472 -0
  5. data/docs/01_CORE_AND_CONFIGURATION.md +913 -0
  6. data/docs/02_META_TAGS_AND_OPEN_GRAPH.md +251 -0
  7. data/docs/03_STRUCTURED_DATA.md +140 -0
  8. data/docs/04_SITEMAP_AND_ROBOTS.md +131 -0
  9. data/docs/05_RAILS_INTEGRATION.md +175 -0
  10. data/docs/06_I18N_PAGE_GENERATOR.md +233 -0
  11. data/docs/07_IMAGE_OPTIMIZATION.md +260 -0
  12. data/docs/DEPENDENCIES.md +383 -0
  13. data/docs/README.md +180 -0
  14. data/docs/TESTING_STRATEGY.md +663 -0
  15. data/lib/better_seo/analytics/google_analytics.rb +83 -0
  16. data/lib/better_seo/analytics/google_tag_manager.rb +74 -0
  17. data/lib/better_seo/configuration.rb +316 -0
  18. data/lib/better_seo/dsl/base.rb +86 -0
  19. data/lib/better_seo/dsl/meta_tags.rb +55 -0
  20. data/lib/better_seo/dsl/open_graph.rb +109 -0
  21. data/lib/better_seo/dsl/twitter_cards.rb +131 -0
  22. data/lib/better_seo/errors.rb +31 -0
  23. data/lib/better_seo/generators/amp_generator.rb +83 -0
  24. data/lib/better_seo/generators/breadcrumbs_generator.rb +126 -0
  25. data/lib/better_seo/generators/canonical_url_manager.rb +106 -0
  26. data/lib/better_seo/generators/meta_tags_generator.rb +100 -0
  27. data/lib/better_seo/generators/open_graph_generator.rb +110 -0
  28. data/lib/better_seo/generators/robots_txt_generator.rb +102 -0
  29. data/lib/better_seo/generators/twitter_cards_generator.rb +102 -0
  30. data/lib/better_seo/image/optimizer.rb +143 -0
  31. data/lib/better_seo/rails/helpers/controller_helpers.rb +118 -0
  32. data/lib/better_seo/rails/helpers/seo_helper.rb +176 -0
  33. data/lib/better_seo/rails/helpers/structured_data_helper.rb +123 -0
  34. data/lib/better_seo/rails/model_helpers.rb +62 -0
  35. data/lib/better_seo/rails/railtie.rb +22 -0
  36. data/lib/better_seo/sitemap/builder.rb +65 -0
  37. data/lib/better_seo/sitemap/generator.rb +57 -0
  38. data/lib/better_seo/sitemap/sitemap_index.rb +73 -0
  39. data/lib/better_seo/sitemap/url_entry.rb +157 -0
  40. data/lib/better_seo/structured_data/article.rb +55 -0
  41. data/lib/better_seo/structured_data/base.rb +73 -0
  42. data/lib/better_seo/structured_data/breadcrumb_list.rb +49 -0
  43. data/lib/better_seo/structured_data/event.rb +207 -0
  44. data/lib/better_seo/structured_data/faq_page.rb +55 -0
  45. data/lib/better_seo/structured_data/generator.rb +75 -0
  46. data/lib/better_seo/structured_data/how_to.rb +96 -0
  47. data/lib/better_seo/structured_data/local_business.rb +94 -0
  48. data/lib/better_seo/structured_data/organization.rb +67 -0
  49. data/lib/better_seo/structured_data/person.rb +51 -0
  50. data/lib/better_seo/structured_data/product.rb +123 -0
  51. data/lib/better_seo/structured_data/recipe.rb +135 -0
  52. data/lib/better_seo/validators/seo_recommendations.rb +165 -0
  53. data/lib/better_seo/validators/seo_validator.rb +195 -0
  54. data/lib/better_seo/version.rb +1 -1
  55. data/lib/better_seo.rb +5 -0
  56. data/lib/generators/better_seo/install_generator.rb +21 -0
  57. data/lib/generators/better_seo/templates/README +29 -0
  58. data/lib/generators/better_seo/templates/better_seo.rb +40 -0
  59. metadata +69 -2
@@ -0,0 +1,663 @@
1
+ # BetterSeo - Strategia di Testing
2
+
3
+ Approccio completo per testing di BetterSeo con RSpec.
4
+
5
+ ---
6
+
7
+ ## Obiettivi Testing
8
+
9
+ ### Coverage Goals
10
+
11
+ - **Target**: > 90% code coverage
12
+ - **Minimum**: 85% code coverage per release
13
+ - **Critical paths**: 100% coverage (configuration, generators, validators)
14
+
15
+ ### Quality Goals
16
+
17
+ - ✅ Tutti i test passano (green) prima di merge
18
+ - ✅ Zero flaky tests
19
+ - ✅ Test performance < 30 secondi suite completa
20
+ - ✅ Isolamento completo tra test (no shared state)
21
+
22
+ ---
23
+
24
+ ## Test Pyramid
25
+
26
+ ```
27
+ /\
28
+ / \
29
+ / E2E\ (5%)
30
+ /______\
31
+ / \
32
+ /Integration\ (25%)
33
+ /____________\
34
+ / \
35
+ / Unit Tests \ (70%)
36
+ /_________________\
37
+ ```
38
+
39
+ ### Unit Tests (70%)
40
+ - DSL classes
41
+ - Generators
42
+ - Validators
43
+ - Configuration
44
+ - Helpers individuali
45
+
46
+ ### Integration Tests (25%)
47
+ - Rails integration
48
+ - Generator + Validator flow
49
+ - Controller + Helper interaction
50
+ - Full SEO rendering pipeline
51
+
52
+ ### E2E Tests (5%)
53
+ - Gem installation in dummy Rails app
54
+ - Full workflow da generator a HTML output
55
+ - Rake tasks end-to-end
56
+
57
+ ---
58
+
59
+ ## Setup Testing Environment
60
+
61
+ ### spec/spec_helper.rb
62
+
63
+ ```ruby
64
+ # frozen_string_literal: true
65
+
66
+ require "simplecov"
67
+ SimpleCov.start "rails" do
68
+ add_filter "/spec/"
69
+ add_filter "/vendor/"
70
+
71
+ add_group "DSL", "lib/better_seo/dsl"
72
+ add_group "Generators", "lib/better_seo/generators"
73
+ add_group "Validators", "lib/better_seo/validators"
74
+ add_group "Rails", "lib/better_seo/rails"
75
+ add_group "Images", "lib/better_seo/images"
76
+
77
+ minimum_coverage 90
78
+ end
79
+
80
+ require "better_seo"
81
+ require "rspec"
82
+ require "webmock/rspec"
83
+ require "vcr"
84
+
85
+ # Disable external HTTP requests
86
+ WebMock.disable_net_connect!(allow_localhost: true)
87
+
88
+ # VCR configuration
89
+ VCR.configure do |config|
90
+ config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
91
+ config.hook_into :webmock
92
+ config.configure_rspec_metadata!
93
+ config.default_cassette_options = { record: :new_episodes }
94
+ end
95
+
96
+ RSpec.configure do |config|
97
+ # Enable flags like --only-failures and --next-failure
98
+ config.example_status_persistence_file_path = "spec/examples.txt"
99
+
100
+ # Disable RSpec exposing methods globally
101
+ config.disable_monkey_patching!
102
+
103
+ # Use expect syntax
104
+ config.expect_with :rspec do |c|
105
+ c.syntax = :expect
106
+ end
107
+
108
+ # Reset BetterSeo configuration before each test
109
+ config.before(:each) do
110
+ BetterSeo.reset_configuration!
111
+ end
112
+
113
+ # Random order
114
+ config.order = :random
115
+ Kernel.srand config.seed
116
+ end
117
+ ```
118
+
119
+ ### spec/rails_helper.rb
120
+
121
+ ```ruby
122
+ # frozen_string_literal: true
123
+
124
+ require "spec_helper"
125
+
126
+ ENV["RAILS_ENV"] ||= "test"
127
+
128
+ # Dummy Rails app per testing
129
+ require File.expand_path("../dummy/config/environment", __FILE__)
130
+
131
+ require "rspec/rails"
132
+ require "factory_bot_rails"
133
+
134
+ RSpec.configure do |config|
135
+ config.use_transactional_fixtures = true
136
+ config.infer_spec_type_from_file_location!
137
+ config.filter_rails_from_backtrace!
138
+
139
+ # FactoryBot
140
+ config.include FactoryBot::Syntax::Methods
141
+
142
+ # Rails helpers
143
+ config.include ActionView::Helpers::TagHelper, type: :helper
144
+ config.include ActionView::Context, type: :helper
145
+ end
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Testing Patterns
151
+
152
+ ### 1. Unit Test - DSL Classes
153
+
154
+ ```ruby
155
+ # spec/dsl/meta_tags_spec.rb
156
+ RSpec.describe BetterSeo::DSL::MetaTags do
157
+ subject(:meta) { described_class.new }
158
+
159
+ describe "#title" do
160
+ it "sets title" do
161
+ meta.title "My Title"
162
+ expect(meta.title).to eq("My Title")
163
+ end
164
+
165
+ it "returns self for chaining" do
166
+ expect(meta.title("Title")).to eq(meta)
167
+ end
168
+
169
+ context "when title too long" do
170
+ it "raises validation error on build" do
171
+ meta.title "A" * 80
172
+ expect { meta.build }.to raise_error(BetterSeo::ValidationError)
173
+ end
174
+ end
175
+ end
176
+
177
+ describe "#build" do
178
+ it "returns configuration hash" do
179
+ meta.title "Title"
180
+ meta.description "Description"
181
+
182
+ result = meta.build
183
+
184
+ expect(result).to eq({
185
+ title: "Title",
186
+ description: "Description"
187
+ })
188
+ end
189
+
190
+ it "validates configuration" do
191
+ # Validation logic tested
192
+ end
193
+ end
194
+ end
195
+ ```
196
+
197
+ ### 2. Unit Test - Generators
198
+
199
+ ```ruby
200
+ # spec/generators/meta_tags_generator_spec.rb
201
+ RSpec.describe BetterSeo::Generators::MetaTagsGenerator do
202
+ let(:config) do
203
+ {
204
+ charset: "UTF-8",
205
+ title: "Test Page",
206
+ description: "Test description"
207
+ }
208
+ end
209
+
210
+ subject(:generator) { described_class.new(config) }
211
+
212
+ describe "#generate" do
213
+ subject(:html) { generator.generate }
214
+
215
+ it "generates charset tag" do
216
+ expect(html).to include('<meta charset="UTF-8">')
217
+ end
218
+
219
+ it "generates title tag" do
220
+ expect(html).to include('<title>Test Page</title>')
221
+ end
222
+
223
+ it "generates description tag" do
224
+ expect(html).to include('<meta name="description" content="Test description">')
225
+ end
226
+
227
+ context "with XSS attempt" do
228
+ let(:config) { { title: '<script>alert("xss")</script>' } }
229
+
230
+ it "escapes HTML entities" do
231
+ expect(html).to include('&lt;script&gt;')
232
+ expect(html).not_to include('<script>')
233
+ end
234
+ end
235
+
236
+ context "with unicode characters" do
237
+ let(:config) { { title: "Titolo con àccénti é ümläut" } }
238
+
239
+ it "preserves unicode" do
240
+ expect(html).to include("àccénti é ümläut")
241
+ end
242
+ end
243
+ end
244
+ end
245
+ ```
246
+
247
+ ### 3. Integration Test - Configuration Loading
248
+
249
+ ```ruby
250
+ # spec/integration/configuration_spec.rb
251
+ RSpec.describe "Configuration Loading", type: :integration do
252
+ let(:yaml_content) do
253
+ <<~YAML
254
+ production:
255
+ site_name: "Production Site"
256
+ meta_tags:
257
+ default_title: "Prod Title"
258
+ sitemap:
259
+ enabled: true
260
+ host: "https://prod.example.com"
261
+ YAML
262
+ end
263
+
264
+ let(:config_path) { "tmp/better_seo_test.yml" }
265
+
266
+ before do
267
+ File.write(config_path, yaml_content)
268
+ end
269
+
270
+ after do
271
+ File.delete(config_path) if File.exist?(config_path)
272
+ end
273
+
274
+ it "loads configuration from YAML file" do
275
+ BetterSeo.configure do |config|
276
+ config.load_from_file(config_path, environment: :production)
277
+ end
278
+
279
+ expect(BetterSeo.configuration.site_name).to eq("Production Site")
280
+ expect(BetterSeo.configuration.meta_tags.default_title).to eq("Prod Title")
281
+ expect(BetterSeo.configuration.sitemap.host).to eq("https://prod.example.com")
282
+ end
283
+
284
+ it "validates after loading" do
285
+ expect {
286
+ BetterSeo.configure do |config|
287
+ config.load_from_file(config_path, environment: :production)
288
+ end
289
+ }.not_to raise_error
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### 4. Integration Test - Rails Helpers
295
+
296
+ ```ruby
297
+ # spec/helpers/meta_tags_helper_spec.rb
298
+ RSpec.describe BetterSeo::Rails::Helpers::MetaTagsHelper, type: :helper do
299
+ before do
300
+ BetterSeo.configure do |config|
301
+ config.meta_tags.default_title = "Default Title"
302
+ config.meta_tags.default_description = "Default Description"
303
+ end
304
+ end
305
+
306
+ describe "#seo_meta_tags" do
307
+ it "renders meta tags with defaults" do
308
+ html = helper.seo_meta_tags
309
+
310
+ expect(html).to include('<title>Default Title</title>')
311
+ expect(html).to include('name="description" content="Default Description"')
312
+ end
313
+
314
+ it "accepts block for overrides" do
315
+ html = helper.seo_meta_tags do |meta|
316
+ meta.title "Custom Title"
317
+ end
318
+
319
+ expect(html).to include('<title>Custom Title</title>')
320
+ end
321
+
322
+ it "accepts hash options" do
323
+ html = helper.seo_meta_tags(title: "Hash Title")
324
+
325
+ expect(html).to include('<title>Hash Title</title>')
326
+ end
327
+
328
+ it "returns html_safe string" do
329
+ html = helper.seo_meta_tags
330
+ expect(html).to be_html_safe
331
+ end
332
+ end
333
+
334
+ describe "#render_seo_tags" do
335
+ it "renders all SEO tags when enabled" do
336
+ BetterSeo.configuration.open_graph.enabled = true
337
+ BetterSeo.configuration.twitter.enabled = true
338
+
339
+ html = helper.render_seo_tags
340
+
341
+ expect(html).to include('<title>') # meta tags
342
+ expect(html).to include('property="og:') # open graph
343
+ expect(html).to include('name="twitter:') # twitter cards
344
+ end
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### 5. E2E Test - Full Workflow
350
+
351
+ ```ruby
352
+ # spec/e2e/full_seo_workflow_spec.rb
353
+ RSpec.describe "Full SEO Workflow", type: :feature do
354
+ it "generates complete SEO markup from configuration" do
355
+ # 1. Configure
356
+ BetterSeo.configure do |config|
357
+ config.site_name = "Test Site"
358
+
359
+ config.meta_tags do
360
+ default_title "Test Title"
361
+ default_description "Test Description"
362
+ end
363
+
364
+ config.open_graph do
365
+ site_name "Test Site"
366
+ default_type "website"
367
+ end
368
+ end
369
+
370
+ # 2. Build DSL
371
+ meta_dsl = BetterSeo::DSL::MetaTags.new
372
+ meta_dsl.title "Article Title"
373
+ meta_dsl.description "Article Description"
374
+
375
+ og_dsl = BetterSeo::DSL::OpenGraph.new
376
+ og_dsl.title "Article Title"
377
+ og_dsl.type "article"
378
+
379
+ # 3. Generate HTML
380
+ meta_html = BetterSeo::Generators::MetaTagsGenerator.new(meta_dsl.build).generate
381
+ og_html = BetterSeo::Generators::OpenGraphGenerator.new(og_dsl.build).generate
382
+
383
+ # 4. Verify output
384
+ expect(meta_html).to include('<title>Article Title</title>')
385
+ expect(og_html).to include('property="og:title" content="Article Title"')
386
+ expect(og_html).to include('property="og:type" content="article"')
387
+ end
388
+ end
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Fixtures e Factories
394
+
395
+ ### FactoryBot Factories
396
+
397
+ ```ruby
398
+ # spec/factories/articles.rb
399
+ FactoryBot.define do
400
+ factory :article do
401
+ title { "Test Article" }
402
+ description { "Test Description" }
403
+ published_at { Time.current }
404
+
405
+ trait :with_seo do
406
+ seo_title { "SEO Title" }
407
+ seo_description { "SEO Description" }
408
+ seo_keywords { ["ruby", "seo"] }
409
+ end
410
+
411
+ trait :published do
412
+ published { true }
413
+ end
414
+ end
415
+ end
416
+ ```
417
+
418
+ ### Fixtures Files
419
+
420
+ ```yaml
421
+ # spec/fixtures/config/better_seo.yml
422
+ test:
423
+ site_name: "Test Site"
424
+ default_locale: en
425
+ available_locales:
426
+ - en
427
+ - it
428
+
429
+ meta_tags:
430
+ default_title: "Test Title"
431
+ default_description: "Test Description"
432
+
433
+ sitemap:
434
+ enabled: false
435
+
436
+ robots:
437
+ enabled: false
438
+ ```
439
+
440
+ ---
441
+
442
+ ## Shared Examples
443
+
444
+ ```ruby
445
+ # spec/support/shared_examples/dsl_examples.rb
446
+ RSpec.shared_examples "a DSL builder" do
447
+ it "responds to #build" do
448
+ expect(subject).to respond_to(:build)
449
+ end
450
+
451
+ it "responds to #to_h" do
452
+ expect(subject).to respond_to(:to_h)
453
+ end
454
+
455
+ it "responds to #merge!" do
456
+ expect(subject).to respond_to(:merge!)
457
+ end
458
+
459
+ it "returns self for method chaining" do
460
+ # Assume first_method exists
461
+ expect(subject.send(described_class.instance_methods(false).first, "value")).to eq(subject)
462
+ end
463
+ end
464
+
465
+ # Usage:
466
+ RSpec.describe BetterSeo::DSL::MetaTags do
467
+ it_behaves_like "a DSL builder"
468
+ end
469
+ ```
470
+
471
+ ---
472
+
473
+ ## Custom Matchers
474
+
475
+ ```ruby
476
+ # spec/support/matchers/html_matchers.rb
477
+ RSpec::Matchers.define :have_meta_tag do |name, content|
478
+ match do |html|
479
+ html.include?(%(<meta name="#{name}" content="#{content}">))
480
+ end
481
+
482
+ failure_message do |html|
483
+ "expected HTML to include meta tag '#{name}' with content '#{content}', but got:\n#{html}"
484
+ end
485
+ end
486
+
487
+ # Usage:
488
+ expect(html).to have_meta_tag("description", "My Description")
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Testing Images (Step 07)
494
+
495
+ ```ruby
496
+ # spec/images/converter_spec.rb
497
+ RSpec.describe BetterSeo::Images::Converter do
498
+ let(:source_path) { "spec/fixtures/images/test.jpg" }
499
+ let(:output_path) { "tmp/test.webp" }
500
+
501
+ after do
502
+ File.delete(output_path) if File.exist?(output_path)
503
+ end
504
+
505
+ describe ".to_webp" do
506
+ it "converts JPEG to WebP", :vips do
507
+ result = described_class.to_webp(source_path, output_path)
508
+
509
+ expect(File.exist?(result)).to be true
510
+ expect(result).to end_with(".webp")
511
+ end
512
+
513
+ it "reduces file size" do
514
+ original_size = File.size(source_path)
515
+
516
+ described_class.to_webp(source_path, output_path, quality: 80)
517
+
518
+ webp_size = File.size(output_path)
519
+ expect(webp_size).to be < original_size
520
+ end
521
+ end
522
+ end
523
+ ```
524
+
525
+ ---
526
+
527
+ ## Performance Testing
528
+
529
+ ```ruby
530
+ # spec/performance/generators_performance_spec.rb
531
+ RSpec.describe "Generators Performance" do
532
+ it "generates meta tags in < 1ms" do
533
+ config = { title: "Title", description: "Description" }
534
+ generator = BetterSeo::Generators::MetaTagsGenerator.new(config)
535
+
536
+ time = Benchmark.realtime do
537
+ 1000.times { generator.generate }
538
+ end
539
+
540
+ expect(time / 1000).to be < 0.001 # < 1ms per generation
541
+ end
542
+
543
+ it "generates sitemap for 10k URLs in < 1s" do
544
+ urls = 10_000.times.map do |i|
545
+ { loc: "https://example.com/page-#{i}", priority: 0.5 }
546
+ end
547
+
548
+ generator = BetterSeo::Generators::SitemapGenerator.new(urls)
549
+
550
+ time = Benchmark.realtime { generator.generate }
551
+
552
+ expect(time).to be < 1.0 # < 1 second
553
+ end
554
+ end
555
+ ```
556
+
557
+ ---
558
+
559
+ ## CI/CD Integration
560
+
561
+ ### GitHub Actions
562
+
563
+ ```yaml
564
+ # .github/workflows/ci.yml
565
+ name: CI
566
+
567
+ on: [push, pull_request]
568
+
569
+ jobs:
570
+ test:
571
+ runs-on: ubuntu-latest
572
+
573
+ strategy:
574
+ matrix:
575
+ ruby: ['3.0', '3.1', '3.2', '3.3']
576
+ rails: ['6.1', '7.0', '7.1']
577
+
578
+ steps:
579
+ - uses: actions/checkout@v3
580
+
581
+ - name: Set up Ruby
582
+ uses: ruby/setup-ruby@v1
583
+ with:
584
+ ruby-version: ${{ matrix.ruby }}
585
+ bundler-cache: true
586
+
587
+ - name: Install libvips
588
+ run: sudo apt-get install libvips-dev
589
+
590
+ - name: Run tests
591
+ run: bundle exec rspec
592
+
593
+ - name: Upload coverage
594
+ uses: codecov/codecov-action@v3
595
+ with:
596
+ files: ./coverage/.resultset.json
597
+ ```
598
+
599
+ ---
600
+
601
+ ## Coverage Reports
602
+
603
+ ### SimpleCov Configuration
604
+
605
+ ```ruby
606
+ # spec/spec_helper.rb
607
+ SimpleCov.start "rails" do
608
+ add_filter "/spec/"
609
+ add_filter "/vendor/"
610
+
611
+ add_group "DSL", "lib/better_seo/dsl"
612
+ add_group "Generators", "lib/better_seo/generators"
613
+ add_group "Validators", "lib/better_seo/validators"
614
+ add_group "Rails", "lib/better_seo/rails"
615
+ add_group "Images", "lib/better_seo/images"
616
+
617
+ minimum_coverage 90
618
+ refuse_coverage_drop
619
+
620
+ # Formatters
621
+ formatter SimpleCov::Formatter::MultiFormatter.new([
622
+ SimpleCov::Formatter::HTMLFormatter,
623
+ SimpleCov::Formatter::Console
624
+ ])
625
+ end
626
+ ```
627
+
628
+ ### Viewing Coverage
629
+
630
+ ```bash
631
+ # Run tests
632
+ bundle exec rspec
633
+
634
+ # Open coverage report
635
+ open coverage/index.html
636
+ ```
637
+
638
+ ---
639
+
640
+ ## Best Practices
641
+
642
+ ### ✅ DO
643
+
644
+ - Write tests BEFORE implementation (TDD)
645
+ - Use descriptive test names
646
+ - Test edge cases and error conditions
647
+ - Keep tests isolated (no shared state)
648
+ - Use factories for complex objects
649
+ - Mock external dependencies (HTTP, file system quando possibile)
650
+ - Test both happy path and sad path
651
+
652
+ ### ❌ DON'T
653
+
654
+ - Don't test framework code (Rails, RSpec)
655
+ - Don't write flaky tests
656
+ - Don't share state between tests
657
+ - Don't use sleep in tests
658
+ - Don't skip tests (fix or delete)
659
+ - Don't test implementation details
660
+
661
+ ---
662
+
663
+ **Ultima modifica**: 2025-10-22