better_seo 0.1.0 → 0.5.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.
data/README.md CHANGED
@@ -1,38 +1,1175 @@
1
1
  # BetterSeo
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ A comprehensive SEO gem for Ruby and Rails applications. BetterSeo provides a clean, fluent DSL for managing meta tags, Open Graph, Twitter Cards, structured data, sitemaps, and more.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/better_seo`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ [![Tests](https://img.shields.io/badge/tests-384%20passing-brightgreen)](https://github.com/yourusername/better_seo)
6
+ [![Coverage](https://img.shields.io/badge/coverage-99.71%25-brightgreen)](https://github.com/yourusername/better_seo)
7
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0.0-red)](https://www.ruby-lang.org)
8
+ [![Rails](https://img.shields.io/badge/rails-%3E%3D%206.1-red)](https://rubyonrails.org)
9
+
10
+ ## Features
11
+
12
+ ### ✅ Implemented (v0.5.0)
13
+
14
+ - **Core Configuration System**
15
+ - Singleton configuration with block-style setup
16
+ - Nested configuration objects
17
+ - Feature flags for enabling/disabling modules
18
+ - Validation with detailed error messages
19
+ - i18n support with multiple locales
20
+
21
+ - **DSL Builders**
22
+ - **Meta Tags DSL**: title, description, keywords, author, canonical, robots, viewport, charset
23
+ - **Open Graph DSL**: Complete OG protocol support including articles, images, videos, audio
24
+ - **Twitter Cards DSL**: All card types (summary, summary_large_image, app, player)
25
+ - Fluent interface with method chaining
26
+ - Automatic validation (title/description length, required fields)
27
+
28
+ - **HTML Generators**
29
+ - **MetaTagsGenerator**: Converts DSL to HTML meta tags
30
+ - **OpenGraphGenerator**: Converts DSL to Open Graph meta tags
31
+ - **TwitterCardsGenerator**: Converts DSL to Twitter Card meta tags
32
+ - HTML entity escaping for security
33
+ - Integration with DSL builders
34
+
35
+ - **Rails Integration**
36
+ - **View Helpers**: `seo_meta_tags`, `seo_open_graph_tags`, `seo_twitter_tags`, `seo_tags`
37
+ - Support for hash configuration and DSL blocks
38
+ - Automatic HTML safety with `raw` helper
39
+ - Integration with global configuration defaults
40
+
41
+ - **Sitemap Generation**
42
+ - **XML Sitemap Builder**: Fluent API for building sitemaps
43
+ - **Sitemap Generator**: Generate from blocks, arrays, or model collections
44
+ - **URL Entry**: Full sitemap.org protocol support (loc, lastmod, changefreq, priority)
45
+ - **Dynamic Generation**: Lambda support for dynamic attributes
46
+ - **File Writing**: Write sitemaps directly to files
47
+ - **Rails Integration**: Controller actions and Rake tasks
48
+ - **Validation**: Automatic URL validation (format, protocol)
49
+ - **Method Chaining**: Fluent interface for adding multiple URLs
50
+
51
+ ### 🚧 Planned
52
+
53
+ - **Advanced Generators** (v0.6.0)
54
+ - Schema.org JSON-LD generator
55
+ - Breadcrumbs generator
56
+ - AMP HTML generator
57
+
58
+ - **Advanced Rails Integration** (v0.6.0)
59
+ - Controller helpers for setting page SEO
60
+ - Railtie for automatic initialization
61
+ - Generator for initializer file
62
+
63
+ - **Advanced Sitemap Features** (v0.6.0)
64
+ - Multi-language sitemap support (hreflang)
65
+ - Sitemap index for large sites (50,000+ URLs)
66
+ - Image/video sitemap extensions
67
+ - News sitemap support
68
+
69
+ - **Advanced Features** (v0.6.0+)
70
+ - robots.txt generator
71
+ - Image optimization with WebP conversion
72
+ - Structured data builders (Organization, Person, Product, etc.)
73
+ - SEO validators and recommendations
6
74
 
7
75
  ## Installation
8
76
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
77
+ ### For Production Use (when published to RubyGems)
78
+
79
+ Add this line to your application's Gemfile:
80
+
81
+ ```ruby
82
+ gem 'better_seo', '~> 0.5.0'
83
+ ```
84
+
85
+ And then execute:
86
+
87
+ ```bash
88
+ bundle install
89
+ ```
10
90
 
11
- Install the gem and add to the application's Gemfile by executing:
91
+ Or install it yourself as:
12
92
 
13
93
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
94
+ gem install better_seo
15
95
  ```
16
96
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
97
+ ### For Development (from source)
98
+
99
+ Add this line to your application's Gemfile:
100
+
101
+ ```ruby
102
+ gem 'better_seo', git: 'https://github.com/alessiobussolari/better_seo.git', tag: 'v0.5.0'
103
+ ```
104
+
105
+ Or clone and build locally:
18
106
 
19
107
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
108
+ git clone https://github.com/alessiobussolari/better_seo.git
109
+ cd better_seo
110
+ gem build better_seo.gemspec
111
+ gem install better_seo-0.5.0.gem
112
+ ```
113
+
114
+ ## Quick Start
115
+
116
+ ### 1. Configuration
117
+
118
+ Create an initializer (Rails) or configure at app startup:
119
+
120
+ ```ruby
121
+ # config/initializers/better_seo.rb
122
+ BetterSeo.configure do |config|
123
+ config.site_name = "My Awesome Site"
124
+ config.default_locale = :en
125
+ config.available_locales = [:en, :it, :fr]
126
+
127
+ # Configure defaults for meta tags
128
+ config.meta_tags.default_title = "My Awesome Site"
129
+ config.meta_tags.title_separator = " | "
130
+ config.meta_tags.append_site_name = true
131
+ config.meta_tags.default_description = "The best site on the internet"
132
+ config.meta_tags.default_keywords = ["awesome", "site", "seo"]
133
+
134
+ # Configure Open Graph defaults
135
+ config.open_graph.site_name = "My Awesome Site"
136
+ config.open_graph.default_type = "website"
137
+ config.open_graph.default_locale = "en_US"
138
+
139
+ # Configure Twitter Cards defaults
140
+ config.twitter.site = "@mysite"
141
+ config.twitter.creator = "@myhandle"
142
+ config.twitter.card_type = "summary_large_image"
143
+ end
144
+ ```
145
+
146
+ ### 2. Using DSL Builders
147
+
148
+ #### Meta Tags
149
+
150
+ ```ruby
151
+ meta = BetterSeo::DSL::MetaTags.new
152
+
153
+ meta.evaluate do
154
+ title "My Page Title"
155
+ description "This is an amazing page about Ruby and SEO"
156
+ keywords "ruby", "seo", "meta tags"
157
+ author "John Doe"
158
+ canonical "https://example.com/my-page"
159
+ robots index: true, follow: true, noarchive: true
160
+ viewport # uses default: "width=device-width, initial-scale=1.0"
161
+ charset # uses default: "UTF-8"
162
+ end
163
+
164
+ # Get the configuration
165
+ config = meta.build
166
+ # => {
167
+ # title: "My Page Title",
168
+ # description: "This is an amazing page...",
169
+ # keywords: ["ruby", "seo", "meta tags"],
170
+ # ...
171
+ # }
172
+ ```
173
+
174
+ #### Open Graph
175
+
176
+ ```ruby
177
+ og = BetterSeo::DSL::OpenGraph.new
178
+
179
+ og.evaluate do
180
+ title "My OG Title"
181
+ description "Description for social media"
182
+ type "article"
183
+ url "https://example.com/article"
184
+ image "https://example.com/image.jpg"
185
+ site_name "My Site"
186
+ locale "en_US"
187
+ locale_alternate "it_IT", "fr_FR"
188
+
189
+ # For article type
190
+ article do
191
+ author "John Doe"
192
+ published_time "2024-01-01T00:00:00Z"
193
+ modified_time "2024-01-02T00:00:00Z"
194
+ section "Technology"
195
+ tag "Ruby", "SEO", "OpenGraph"
196
+ end
197
+ end
198
+
199
+ config = og.build
200
+ ```
201
+
202
+ #### Twitter Cards
203
+
204
+ ```ruby
205
+ twitter = BetterSeo::DSL::TwitterCards.new
206
+
207
+ twitter.evaluate do
208
+ card "summary_large_image"
209
+ site "@mysite" # @ prefix added automatically
210
+ creator "myhandle" # @ prefix added automatically
211
+ title "Twitter Card Title"
212
+ description "Description for Twitter"
213
+ image "https://example.com/twitter-image.jpg"
214
+ image_alt "Image description for accessibility"
215
+ end
216
+
217
+ config = twitter.build
218
+ ```
219
+
220
+ #### Method Chaining
221
+
222
+ All DSL builders support fluent interface:
223
+
224
+ ```ruby
225
+ meta = BetterSeo::DSL::MetaTags.new
226
+ .title("Chained Title")
227
+ .description("Chained Description")
228
+ .keywords("ruby", "rails", "seo")
229
+ .author("Jane Doe")
230
+ .canonical("https://example.com/page")
231
+
232
+ og = BetterSeo::DSL::OpenGraph.new
233
+ .title("OG Title")
234
+ .type("article")
235
+ .url("https://example.com")
236
+ .image("https://example.com/og.jpg")
237
+
238
+ twitter = BetterSeo::DSL::TwitterCards.new
239
+ .card("summary_large_image")
240
+ .site("@mysite")
241
+ .title("Twitter Title")
242
+ .description("Twitter Description")
243
+ .image("https://example.com/twitter.jpg")
244
+ ```
245
+
246
+ ### 3. HTML Generation
247
+
248
+ Once you've built your SEO configuration with DSL builders, use generators to convert them to HTML tags:
249
+
250
+ #### Meta Tags Generator
251
+
252
+ ```ruby
253
+ # Build configuration with DSL
254
+ meta = BetterSeo::DSL::MetaTags.new
255
+ meta.title("My Page Title")
256
+ meta.description("Page description for SEO")
257
+ meta.keywords("ruby", "seo", "rails")
258
+ meta.canonical("https://example.com/page")
259
+ meta.robots(index: true, follow: true)
260
+
261
+ # Generate HTML tags
262
+ generator = BetterSeo::Generators::MetaTagsGenerator.new(meta.build)
263
+ html = generator.generate
264
+
265
+ # Output:
266
+ # <meta charset="UTF-8">
267
+ # <meta name="viewport" content="width=device-width, initial-scale=1.0">
268
+ # <title>My Page Title</title>
269
+ # <meta name="description" content="Page description for SEO">
270
+ # <meta name="keywords" content="ruby, seo, rails">
271
+ # <link rel="canonical" href="https://example.com/page">
272
+ # <meta name="robots" content="index, follow">
273
+ ```
274
+
275
+ #### Open Graph Generator
276
+
277
+ ```ruby
278
+ # Build configuration with DSL
279
+ og = BetterSeo::DSL::OpenGraph.new
280
+ og.title("Article Title")
281
+ og.type("article")
282
+ og.url("https://example.com/article")
283
+ og.image(url: "https://example.com/og.jpg", width: 1200, height: 630)
284
+
285
+ # Generate HTML tags
286
+ generator = BetterSeo::Generators::OpenGraphGenerator.new(og.build)
287
+ html = generator.generate
288
+
289
+ # Output:
290
+ # <meta property="og:title" content="Article Title">
291
+ # <meta property="og:type" content="article">
292
+ # <meta property="og:url" content="https://example.com/article">
293
+ # <meta property="og:image" content="https://example.com/og.jpg">
294
+ # <meta property="og:image:width" content="1200">
295
+ # <meta property="og:image:height" content="630">
296
+ ```
297
+
298
+ #### Twitter Cards Generator
299
+
300
+ ```ruby
301
+ # Build configuration with DSL
302
+ twitter = BetterSeo::DSL::TwitterCards.new
303
+ twitter.card("summary_large_image")
304
+ twitter.site("@mysite")
305
+ twitter.title("Twitter Card Title")
306
+ twitter.description("Description for Twitter")
307
+ twitter.image("https://example.com/twitter.jpg")
308
+
309
+ # Generate HTML tags
310
+ generator = BetterSeo::Generators::TwitterCardsGenerator.new(twitter.build)
311
+ html = generator.generate
312
+
313
+ # Output:
314
+ # <meta name="twitter:card" content="summary_large_image">
315
+ # <meta name="twitter:site" content="@mysite">
316
+ # <meta name="twitter:title" content="Twitter Card Title">
317
+ # <meta name="twitter:description" content="Description for Twitter">
318
+ # <meta name="twitter:image" content="https://example.com/twitter.jpg">
319
+ ```
320
+
321
+ #### Complete Example
322
+
323
+ ```ruby
324
+ # Build all SEO tags for a page
325
+ meta = BetterSeo::DSL::MetaTags.new.evaluate do
326
+ title "My Awesome Page"
327
+ description "This page is about Ruby SEO"
328
+ keywords "ruby", "seo", "meta tags"
329
+ end
330
+
331
+ og = BetterSeo::DSL::OpenGraph.new.evaluate do
332
+ title "My Awesome Page"
333
+ type "article"
334
+ url "https://example.com/page"
335
+ image "https://example.com/og.jpg"
336
+ end
337
+
338
+ twitter = BetterSeo::DSL::TwitterCards.new.evaluate do
339
+ card "summary_large_image"
340
+ site "@mysite"
341
+ title "My Awesome Page"
342
+ image "https://example.com/twitter.jpg"
343
+ end
344
+
345
+ # Generate all HTML
346
+ meta_html = BetterSeo::Generators::MetaTagsGenerator.new(meta.build).generate
347
+ og_html = BetterSeo::Generators::OpenGraphGenerator.new(og.build).generate
348
+ twitter_html = BetterSeo::Generators::TwitterCardsGenerator.new(twitter.build).generate
349
+
350
+ # Combine and render in your view
351
+ all_tags = [meta_html, og_html, twitter_html].join("\n")
352
+ ```
353
+
354
+ #### Security Features
355
+
356
+ All generators automatically escape HTML entities to prevent XSS attacks:
357
+
358
+ ```ruby
359
+ meta = BetterSeo::DSL::MetaTags.new
360
+ meta.title('Title with "quotes" & <script>alert("xss")</script>')
361
+
362
+ generator = BetterSeo::Generators::MetaTagsGenerator.new(meta.build)
363
+ html = generator.generate
364
+
365
+ # Output:
366
+ # <title>Title with &quot;quotes&quot; &amp; &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</title>
367
+ # All dangerous characters are properly escaped
368
+ ```
369
+
370
+ ### 4. Validation
371
+
372
+ All DSL builders include automatic validation:
373
+
374
+ ```ruby
375
+ meta = BetterSeo::DSL::MetaTags.new
376
+ meta.title("A" * 80) # Too long (max 60 chars recommended)
377
+ meta.build
378
+ # => BetterSeo::ValidationError: Title too long (80 chars, max 60 recommended)
379
+
380
+ og = BetterSeo::DSL::OpenGraph.new
381
+ og.title("Title")
382
+ og.build
383
+ # => BetterSeo::ValidationError: og:type is required, og:image is required, og:url is required
384
+
385
+ twitter = BetterSeo::DSL::TwitterCards.new
386
+ twitter.card("invalid_type")
387
+ twitter.build
388
+ # => BetterSeo::ValidationError: Invalid card type: invalid_type. Valid types: summary, summary_large_image, app, player
389
+ ```
390
+
391
+ ### 5. Rails Integration
392
+
393
+ BetterSeo provides view helpers for easy integration in Rails applications.
394
+
395
+ #### Setup
396
+
397
+ Include the helpers in your `ApplicationHelper`:
398
+
399
+ ```ruby
400
+ # app/helpers/application_helper.rb
401
+ module ApplicationHelper
402
+ include BetterSeo::Rails::Helpers::SeoHelper
403
+ end
404
+ ```
405
+
406
+ Or include them globally in `ApplicationController`:
407
+
408
+ ```ruby
409
+ # app/controllers/application_controller.rb
410
+ class ApplicationController < ActionController::Base
411
+ helper BetterSeo::Rails::Helpers::SeoHelper
412
+ end
413
+ ```
414
+
415
+ #### Using View Helpers
416
+
417
+ ##### Single Tag Group Helpers
418
+
419
+ ```erb
420
+ <%# app/views/layouts/application.html.erb %>
421
+ <head>
422
+ <%= seo_meta_tags do |meta|
423
+ meta.title "My Page Title"
424
+ meta.description "Page description"
425
+ meta.keywords "ruby", "rails", "seo"
426
+ meta.canonical request.original_url
427
+ meta.robots index: true, follow: true
428
+ end %>
429
+
430
+ <%= seo_open_graph_tags do |og|
431
+ og.title "My Page Title"
432
+ og.type "website"
433
+ og.url request.original_url
434
+ og.image image_url("og-image.jpg")
435
+ og.site_name "My Site"
436
+ end %>
437
+
438
+ <%= seo_twitter_tags do |twitter|
439
+ twitter.card "summary_large_image"
440
+ twitter.site "@mysite"
441
+ twitter.title "My Page Title"
442
+ twitter.description "Page description"
443
+ twitter.image image_url("twitter-image.jpg")
444
+ end %>
445
+ </head>
446
+ ```
447
+
448
+ ##### All-in-One Helper
449
+
450
+ ```erb
451
+ <%# Generate all SEO tags at once %>
452
+ <head>
453
+ <%= seo_tags do |seo|
454
+ seo.meta do |meta|
455
+ meta.title @page_title || "Default Title"
456
+ meta.description @page_description
457
+ meta.keywords @page_keywords if @page_keywords
458
+ meta.canonical request.original_url
459
+ end
460
+
461
+ seo.og do |og|
462
+ og.title @page_title || "Default Title"
463
+ og.type "article"
464
+ og.url request.original_url
465
+ og.image @og_image || image_url("default-og.jpg")
466
+ end
467
+
468
+ seo.twitter do |twitter|
469
+ twitter.card "summary_large_image"
470
+ twitter.site "@mysite"
471
+ twitter.title @page_title || "Default Title"
472
+ twitter.image @twitter_image || image_url("default-twitter.jpg")
473
+ end
474
+ end %>
475
+ </head>
476
+ ```
477
+
478
+ #### Controller Integration
479
+
480
+ Set SEO data in your controllers:
481
+
482
+ ```ruby
483
+ class ArticlesController < ApplicationController
484
+ def show
485
+ @article = Article.find(params[:id])
486
+
487
+ # Set SEO variables for the view
488
+ @page_title = @article.title
489
+ @page_description = @article.excerpt
490
+ @page_keywords = @article.tags.pluck(:name)
491
+ @og_image = url_for(@article.cover_image) if @article.cover_image.attached?
492
+ end
493
+ end
494
+ ```
495
+
496
+ Then use them in your layout:
497
+
498
+ ```erb
499
+ <head>
500
+ <%= seo_tags do |seo|
501
+ seo.meta do |meta|
502
+ meta.title @page_title if @page_title
503
+ meta.description @page_description if @page_description
504
+ meta.keywords(*@page_keywords) if @page_keywords
505
+ meta.canonical request.original_url
506
+ end
507
+
508
+ seo.og do |og|
509
+ og.title @page_title || "Default Title"
510
+ og.type "article"
511
+ og.url request.original_url
512
+ og.image @og_image if @og_image
513
+ end
514
+
515
+ seo.twitter do |twitter|
516
+ twitter.card "summary_large_image"
517
+ twitter.title @page_title || "Default Title"
518
+ twitter.description @page_description if @page_description
519
+ end
520
+ end %>
521
+ </head>
522
+ ```
523
+
524
+ #### Hash Configuration
525
+
526
+ You can also pass hash configurations directly:
527
+
528
+ ```erb
529
+ <%= seo_meta_tags(
530
+ title: "My Page",
531
+ description: "Description",
532
+ keywords: ["ruby", "rails"]
533
+ ) %>
534
+
535
+ <%= seo_open_graph_tags(
536
+ title: "My Page",
537
+ type: "article",
538
+ url: request.original_url,
539
+ image: image_url("og.jpg")
540
+ ) %>
541
+
542
+ <%= seo_twitter_tags(
543
+ card: "summary",
544
+ title: "My Page",
545
+ description: "Description"
546
+ ) %>
547
+ ```
548
+
549
+ #### Partial Integration
550
+
551
+ Create reusable SEO partials:
552
+
553
+ ```erb
554
+ <%# app/views/shared/_seo.html.erb %>
555
+ <%= seo_tags do |seo|
556
+ seo.meta do |meta|
557
+ meta.title local_assigns[:title] || "Default Title"
558
+ meta.description local_assigns[:description]
559
+ meta.canonical request.original_url
560
+ end
561
+
562
+ seo.og do |og|
563
+ og.title local_assigns[:title] || "Default Title"
564
+ og.type local_assigns[:og_type] || "website"
565
+ og.url request.original_url
566
+ og.image local_assigns[:og_image] || image_url("default-og.jpg")
567
+ end
568
+
569
+ seo.twitter do |twitter|
570
+ twitter.card "summary_large_image"
571
+ twitter.title local_assigns[:title] || "Default Title"
572
+ end
573
+ end %>
574
+ ```
575
+
576
+ Then use it in your views:
577
+
578
+ ```erb
579
+ <%# app/views/articles/show.html.erb %>
580
+ <%= render "shared/seo",
581
+ title: @article.title,
582
+ description: @article.excerpt,
583
+ og_type: "article",
584
+ og_image: url_for(@article.cover_image) %>
585
+ ```
586
+
587
+ ### 6. Sitemap Generation
588
+
589
+ BetterSeo provides a comprehensive sitemap generation system with support for XML sitemaps, dynamic content, and model collections.
590
+
591
+ #### Basic Sitemap Generation
592
+
593
+ Generate a simple sitemap using the block syntax:
594
+
595
+ ```ruby
596
+ xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
597
+ sitemap.add_url("https://example.com")
598
+ sitemap.add_url("https://example.com/about")
599
+ sitemap.add_url("https://example.com/contact")
600
+ end
601
+
602
+ puts xml
603
+ # <?xml version="1.0" encoding="UTF-8"?>
604
+ # <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
605
+ # <url>
606
+ # <loc>https://example.com</loc>
607
+ # <changefreq>weekly</changefreq>
608
+ # <priority>0.5</priority>
609
+ # </url>
610
+ # ...
611
+ # </urlset>
612
+ ```
613
+
614
+ #### URL Entry with Full Attributes
615
+
616
+ Add URLs with all sitemap attributes (lastmod, changefreq, priority):
617
+
618
+ ```ruby
619
+ xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
620
+ sitemap.add_url(
621
+ "https://example.com",
622
+ lastmod: Date.today,
623
+ changefreq: "daily",
624
+ priority: 1.0
625
+ )
626
+
627
+ sitemap.add_url(
628
+ "https://example.com/blog",
629
+ lastmod: "2024-01-15",
630
+ changefreq: "weekly",
631
+ priority: 0.8
632
+ )
633
+
634
+ sitemap.add_url(
635
+ "https://example.com/about",
636
+ changefreq: "monthly",
637
+ priority: 0.5
638
+ )
639
+ end
640
+ ```
641
+
642
+ **Valid changefreq values**: `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `never`
643
+
644
+ **Priority range**: 0.0 to 1.0 (default: 0.5)
645
+
646
+ #### Method Chaining
647
+
648
+ The builder supports fluent method chaining:
649
+
650
+ ```ruby
651
+ xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
652
+ sitemap
653
+ .add_url("https://example.com", priority: 1.0)
654
+ .add_url("https://example.com/about", priority: 0.8)
655
+ .add_url("https://example.com/contact", priority: 0.6)
656
+ end
657
+ ```
658
+
659
+ #### Generate from Array
660
+
661
+ Create a sitemap from an array of URLs:
662
+
663
+ ```ruby
664
+ urls = [
665
+ "https://example.com",
666
+ "https://example.com/about",
667
+ "https://example.com/contact",
668
+ "https://example.com/blog"
669
+ ]
670
+
671
+ xml = BetterSeo::Sitemap::Generator.generate_from(
672
+ urls,
673
+ changefreq: "weekly",
674
+ priority: 0.7
675
+ )
676
+ ```
677
+
678
+ #### Generate from Model Collection
679
+
680
+ Generate sitemaps dynamically from your Rails models:
681
+
682
+ ```ruby
683
+ # Simple example with Post model
684
+ xml = BetterSeo::Sitemap::Generator.generate_from_collection(
685
+ Post.published,
686
+ url: ->(post) { "https://example.com/posts/#{post.slug}" },
687
+ lastmod: ->(post) { post.updated_at },
688
+ changefreq: "weekly",
689
+ priority: 0.8
690
+ )
691
+ ```
692
+
693
+ **Dynamic attributes with lambdas**:
694
+
695
+ ```ruby
696
+ xml = BetterSeo::Sitemap::Generator.generate_from_collection(
697
+ Article.all,
698
+ url: ->(article) { "https://example.com/articles/#{article.slug}" },
699
+ lastmod: ->(article) { article.updated_at },
700
+ changefreq: ->(article) do
701
+ article.featured? ? "daily" : "weekly"
702
+ end,
703
+ priority: ->(article) do
704
+ article.featured? ? 0.9 : 0.6
705
+ end
706
+ )
707
+ ```
708
+
709
+ #### Rails Routes Integration
710
+
711
+ Generate sitemap from Rails routes:
712
+
713
+ ```ruby
714
+ # config/routes.rb
715
+ Rails.application.routes.draw do
716
+ # Your routes...
717
+
718
+ # Sitemap endpoint
719
+ get '/sitemap.xml', to: 'sitemaps#index', defaults: { format: 'xml' }
720
+ end
721
+
722
+ # app/controllers/sitemaps_controller.rb
723
+ class SitemapsController < ApplicationController
724
+ def index
725
+ @sitemap_xml = generate_sitemap
726
+ render xml: @sitemap_xml
727
+ end
728
+
729
+ private
730
+
731
+ def generate_sitemap
732
+ BetterSeo::Sitemap::Generator.generate do |sitemap|
733
+ # Static pages
734
+ sitemap.add_url(root_url, priority: 1.0, changefreq: "daily")
735
+ sitemap.add_url(about_url, priority: 0.8, changefreq: "monthly")
736
+ sitemap.add_url(contact_url, priority: 0.7, changefreq: "monthly")
737
+
738
+ # Dynamic content from models
739
+ Post.published.find_each do |post|
740
+ sitemap.add_url(
741
+ post_url(post),
742
+ lastmod: post.updated_at,
743
+ changefreq: "weekly",
744
+ priority: 0.8
745
+ )
746
+ end
747
+
748
+ Category.all.find_each do |category|
749
+ sitemap.add_url(
750
+ category_url(category),
751
+ lastmod: category.updated_at,
752
+ changefreq: "weekly",
753
+ priority: 0.7
754
+ )
755
+ end
756
+ end
757
+ end
758
+ end
759
+ ```
760
+
761
+ #### Write Sitemap to File
762
+
763
+ Save sitemap directly to a file:
764
+
765
+ ```ruby
766
+ # In a Rake task or script
767
+ BetterSeo::Sitemap::Generator.write_to_file('public/sitemap.xml') do |sitemap|
768
+ sitemap.add_url("https://example.com", priority: 1.0)
769
+
770
+ Post.published.find_each do |post|
771
+ sitemap.add_url(
772
+ "https://example.com/posts/#{post.slug}",
773
+ lastmod: post.updated_at,
774
+ changefreq: "weekly",
775
+ priority: 0.8
776
+ )
777
+ end
778
+ end
779
+
780
+ # Returns the file path: "public/sitemap.xml"
781
+ ```
782
+
783
+ #### Rake Task for Sitemap Generation
784
+
785
+ Create a Rake task to regenerate your sitemap:
786
+
787
+ ```ruby
788
+ # lib/tasks/sitemap.rake
789
+ namespace :sitemap do
790
+ desc "Generate sitemap.xml"
791
+ task generate: :environment do
792
+ file_path = BetterSeo::Sitemap::Generator.write_to_file('public/sitemap.xml') do |sitemap|
793
+ # Add static pages
794
+ sitemap.add_url("#{ENV['SITE_URL']}", priority: 1.0, changefreq: "daily")
795
+ sitemap.add_url("#{ENV['SITE_URL']}/about", priority: 0.8)
796
+ sitemap.add_url("#{ENV['SITE_URL']}/contact", priority: 0.7)
797
+
798
+ # Add dynamic content
799
+ Post.published.find_each do |post|
800
+ sitemap.add_url(
801
+ "#{ENV['SITE_URL']}/posts/#{post.slug}",
802
+ lastmod: post.updated_at,
803
+ changefreq: "weekly",
804
+ priority: 0.8
805
+ )
806
+ end
807
+ end
808
+
809
+ puts "Sitemap generated at #{file_path}"
810
+ end
811
+ end
812
+
813
+ # Run with: rake sitemap:generate
814
+ ```
815
+
816
+ #### Using the Builder Directly
817
+
818
+ For more control, use the Builder class directly:
819
+
820
+ ```ruby
821
+ builder = BetterSeo::Sitemap::Builder.new
822
+
823
+ # Add URLs
824
+ builder.add_url("https://example.com", priority: 1.0)
825
+ builder.add_url("https://example.com/about", priority: 0.8)
826
+
827
+ # Add multiple URLs at once
828
+ builder.add_urls(
829
+ ["https://example.com/blog", "https://example.com/contact"],
830
+ changefreq: "weekly",
831
+ priority: 0.7
832
+ )
833
+
834
+ # Remove a URL
835
+ builder.remove_url("https://example.com/contact")
836
+
837
+ # Check size
838
+ puts builder.size # => 3
839
+
840
+ # Iterate over URLs
841
+ builder.each do |url|
842
+ puts "#{url.loc} - Priority: #{url.priority}"
843
+ end
844
+
845
+ # Generate XML
846
+ xml = builder.to_xml
847
+
848
+ # Validate all URLs
849
+ builder.validate! # Raises ValidationError if any URL is invalid
850
+
851
+ # Clear all URLs
852
+ builder.clear
21
853
  ```
22
854
 
23
- ## Usage
855
+ #### URL Entry Details
24
856
 
25
- TODO: Write usage instructions here
857
+ Work with individual URL entries:
858
+
859
+ ```ruby
860
+ entry = BetterSeo::Sitemap::UrlEntry.new(
861
+ "https://example.com/page",
862
+ lastmod: Date.today,
863
+ changefreq: "daily",
864
+ priority: 0.8
865
+ )
866
+
867
+ # Access attributes
868
+ entry.loc # => "https://example.com/page"
869
+ entry.lastmod # => "2024-01-15"
870
+ entry.changefreq # => "daily"
871
+ entry.priority # => 0.8
872
+
873
+ # Update attributes
874
+ entry.lastmod = Date.new(2024, 1, 20)
875
+ entry.changefreq = "weekly"
876
+ entry.priority = 0.9
877
+
878
+ # Generate XML for single entry
879
+ entry.to_xml
880
+ # <url>
881
+ # <loc>https://example.com/page</loc>
882
+ # <lastmod>2024-01-20</lastmod>
883
+ # <changefreq>weekly</changefreq>
884
+ # <priority>0.9</priority>
885
+ # </url>
886
+
887
+ # Convert to hash
888
+ entry.to_h
889
+ # {
890
+ # loc: "https://example.com/page",
891
+ # lastmod: "2024-01-20",
892
+ # changefreq: "weekly",
893
+ # priority: 0.9
894
+ # }
895
+
896
+ # Validate
897
+ entry.validate! # Raises ValidationError if invalid
898
+ ```
899
+
900
+ #### Advanced: Multi-Model Sitemap
901
+
902
+ Combine multiple models in a single sitemap:
903
+
904
+ ```ruby
905
+ xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
906
+ # Homepage
907
+ sitemap.add_url("https://example.com", priority: 1.0, changefreq: "daily")
908
+
909
+ # Blog posts
910
+ Post.published.find_each do |post|
911
+ sitemap.add_url(
912
+ "https://example.com/posts/#{post.slug}",
913
+ lastmod: post.updated_at,
914
+ changefreq: post.featured? ? "daily" : "weekly",
915
+ priority: post.featured? ? 0.9 : 0.7
916
+ )
917
+ end
918
+
919
+ # Categories
920
+ Category.all.find_each do |category|
921
+ sitemap.add_url(
922
+ "https://example.com/categories/#{category.slug}",
923
+ lastmod: category.updated_at,
924
+ changefreq: "weekly",
925
+ priority: 0.6
926
+ )
927
+ end
928
+
929
+ # Static pages
930
+ %w[about contact privacy terms].each do |page|
931
+ sitemap.add_url(
932
+ "https://example.com/#{page}",
933
+ changefreq: "monthly",
934
+ priority: 0.5
935
+ )
936
+ end
937
+ end
938
+ ```
939
+
940
+ #### Validation
941
+
942
+ All URLs are automatically validated when generating:
943
+
944
+ ```ruby
945
+ # This will raise BetterSeo::ValidationError
946
+ xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
947
+ sitemap.add_url("") # Error: Location is required
948
+ sitemap.add_url("not-a-valid-url") # Error: Invalid URL format
949
+ sitemap.add_url("ftp://example.com") # Error: Must be HTTP/HTTPS
950
+ end
951
+
952
+ # Validate manually
953
+ builder = BetterSeo::Sitemap::Builder.new
954
+ builder.add_url("https://example.com")
955
+ builder.validate! # Returns true if all URLs valid
956
+ ```
957
+
958
+ #### Complete Example: Production Sitemap
959
+
960
+ ```ruby
961
+ # app/services/sitemap_generator_service.rb
962
+ class SitemapGeneratorService
963
+ def self.generate
964
+ BetterSeo::Sitemap::Generator.write_to_file(Rails.root.join('public', 'sitemap.xml')) do |sitemap|
965
+ add_static_pages(sitemap)
966
+ add_blog_posts(sitemap)
967
+ add_categories(sitemap)
968
+ add_products(sitemap) if defined?(Product)
969
+ end
970
+ end
971
+
972
+ private_class_method def self.add_static_pages(sitemap)
973
+ sitemap.add_url(Rails.application.routes.url_helpers.root_url, priority: 1.0, changefreq: "daily")
974
+ sitemap.add_url(Rails.application.routes.url_helpers.about_url, priority: 0.8, changefreq: "monthly")
975
+ sitemap.add_url(Rails.application.routes.url_helpers.contact_url, priority: 0.7, changefreq: "monthly")
976
+ end
977
+
978
+ private_class_method def self.add_blog_posts(sitemap)
979
+ Post.published.find_each do |post|
980
+ sitemap.add_url(
981
+ Rails.application.routes.url_helpers.post_url(post),
982
+ lastmod: post.updated_at,
983
+ changefreq: post.frequently_updated? ? "daily" : "weekly",
984
+ priority: calculate_post_priority(post)
985
+ )
986
+ end
987
+ end
988
+
989
+ private_class_method def self.add_categories(sitemap)
990
+ Category.all.find_each do |category|
991
+ sitemap.add_url(
992
+ Rails.application.routes.url_helpers.category_url(category),
993
+ lastmod: category.posts.maximum(:updated_at),
994
+ changefreq: "weekly",
995
+ priority: 0.6
996
+ )
997
+ end
998
+ end
999
+
1000
+ private_class_method def self.add_products(sitemap)
1001
+ Product.available.find_each do |product|
1002
+ sitemap.add_url(
1003
+ Rails.application.routes.url_helpers.product_url(product),
1004
+ lastmod: product.updated_at,
1005
+ changefreq: "daily",
1006
+ priority: product.featured? ? 0.95 : 0.75
1007
+ )
1008
+ end
1009
+ end
1010
+
1011
+ private_class_method def self.calculate_post_priority(post)
1012
+ base_priority = 0.7
1013
+ base_priority += 0.2 if post.featured?
1014
+ base_priority += 0.1 if post.comments_count > 10
1015
+ [base_priority, 1.0].min
1016
+ end
1017
+ end
1018
+
1019
+ # Call from rake task or controller:
1020
+ # SitemapGeneratorService.generate
1021
+ ```
1022
+
1023
+ ## Configuration Reference
1024
+
1025
+ ### Global Configuration
1026
+
1027
+ ```ruby
1028
+ BetterSeo.configure do |config|
1029
+ # Site-wide settings
1030
+ config.site_name = "My Site"
1031
+ config.default_locale = :en
1032
+ config.available_locales = [:en, :it, :fr, :de, :es]
1033
+
1034
+ # Meta tags configuration
1035
+ config.meta_tags.default_title = "Default Title"
1036
+ config.meta_tags.title_separator = " | "
1037
+ config.meta_tags.append_site_name = true
1038
+ config.meta_tags.default_description = "Default description"
1039
+ config.meta_tags.default_keywords = ["keyword1", "keyword2"]
1040
+ config.meta_tags.default_author = "Your Name"
1041
+
1042
+ # Open Graph configuration
1043
+ config.open_graph.enabled = true
1044
+ config.open_graph.site_name = "My Site"
1045
+ config.open_graph.default_type = "website"
1046
+ config.open_graph.default_locale = "en_US"
1047
+ config.open_graph.default_image.url = "https://example.com/default-og.jpg"
1048
+ config.open_graph.default_image.width = 1200
1049
+ config.open_graph.default_image.height = 630
1050
+
1051
+ # Twitter Cards configuration
1052
+ config.twitter.enabled = true
1053
+ config.twitter.site = "@mysite"
1054
+ config.twitter.creator = "@myhandle"
1055
+ config.twitter.card_type = "summary_large_image"
1056
+
1057
+ # Structured Data configuration
1058
+ config.structured_data.enabled = true
1059
+ config.structured_data.organization = {
1060
+ name: "My Organization",
1061
+ url: "https://example.com"
1062
+ }
1063
+
1064
+ # Sitemap configuration (planned)
1065
+ config.sitemap.enabled = false
1066
+ config.sitemap.host = "https://example.com"
1067
+ config.sitemap.output_path = "public/sitemap.xml"
1068
+
1069
+ # Robots.txt configuration (planned)
1070
+ config.robots.enabled = false
1071
+ config.robots.output_path = "public/robots.txt"
1072
+
1073
+ # Image optimization configuration (planned)
1074
+ config.images.enabled = false
1075
+ config.images.webp.enabled = true
1076
+ config.images.webp.quality = 80
1077
+ end
1078
+ ```
1079
+
1080
+ ### Checking Configuration
1081
+
1082
+ ```ruby
1083
+ # Access configuration
1084
+ BetterSeo.configuration.site_name
1085
+ # => "My Site"
1086
+
1087
+ # Check if features are enabled
1088
+ BetterSeo.enabled?(:open_graph)
1089
+ # => true
1090
+
1091
+ BetterSeo.enabled?(:sitemap)
1092
+ # => false
1093
+
1094
+ # Reset configuration (useful for testing)
1095
+ BetterSeo.reset_configuration!
1096
+ ```
26
1097
 
27
1098
  ## Development
28
1099
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1100
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
1101
+
1102
+ ```bash
1103
+ # Install dependencies
1104
+ bundle install
1105
+
1106
+ # Run tests
1107
+ bundle exec rspec
1108
+
1109
+ # Run tests with coverage
1110
+ bundle exec rspec --format documentation
1111
+
1112
+ # Check code coverage
1113
+ open coverage/index.html
1114
+ ```
30
1115
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1116
+ ### Running Tests
1117
+
1118
+ The gem uses RSpec with SimpleCov for test coverage. We maintain **100% code coverage**.
1119
+
1120
+ ```bash
1121
+ # Run all tests
1122
+ bundle exec rspec
1123
+
1124
+ # Run specific test file
1125
+ bundle exec rspec spec/dsl/meta_tags_spec.rb
1126
+
1127
+ # Run with documentation format
1128
+ bundle exec rspec --format documentation
1129
+ ```
1130
+
1131
+ Current test statistics:
1132
+ - **286 tests** passing
1133
+ - **100% code coverage** (562/562 lines)
1134
+ - **3 DSL builders** fully tested
1135
+ - **3 HTML generators** fully tested
1136
+ - **1 Rails view helper module** fully tested
1137
+ - **1 core configuration system** fully tested
1138
+
1139
+ ## Architecture
1140
+
1141
+ ```
1142
+ lib/better_seo/
1143
+ ├── version.rb # Gem version
1144
+ ├── errors.rb # Custom error classes
1145
+ ├── configuration.rb # Main configuration class
1146
+ ├── dsl/
1147
+ │ ├── base.rb # Base DSL builder class
1148
+ │ ├── meta_tags.rb # Meta tags DSL
1149
+ │ ├── open_graph.rb # Open Graph DSL
1150
+ │ └── twitter_cards.rb # Twitter Cards DSL
1151
+ ├── generators/
1152
+ │ ├── meta_tags_generator.rb # HTML meta tags generator
1153
+ │ ├── open_graph_generator.rb # Open Graph tags generator
1154
+ │ └── twitter_cards_generator.rb # Twitter Cards generator
1155
+ ├── rails/
1156
+ │ └── helpers/
1157
+ │ └── seo_helper.rb # Rails view helpers
1158
+ └── (planned)
1159
+ ├── validators/ # SEO validators
1160
+ └── sitemap/ # Sitemap generation
1161
+ ```
32
1162
 
33
1163
  ## Contributing
34
1164
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/better_seo. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/better_seo/blob/main/CODE_OF_CONDUCT.md).
1165
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/better_seo.
1166
+
1167
+ 1. Fork it
1168
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
1169
+ 3. Write tests (we maintain 100% coverage)
1170
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
1171
+ 5. Push to the branch (`git push origin feature/my-new-feature`)
1172
+ 6. Create new Pull Request
36
1173
 
37
1174
  ## License
38
1175
 
@@ -40,4 +1177,18 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
1177
 
41
1178
  ## Code of Conduct
42
1179
 
43
- Everyone interacting in the BetterSeo project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/better_seo/blob/main/CODE_OF_CONDUCT.md).
1180
+ Everyone interacting in the BetterSeo project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/yourusername/better_seo/blob/main/CODE_OF_CONDUCT.md).
1181
+
1182
+ ## Roadmap
1183
+
1184
+ See [docs/00_OVERVIEW.md](docs/00_OVERVIEW.md) for the complete implementation roadmap.
1185
+
1186
+ ### Version History
1187
+
1188
+ - **v0.1.0** - Core configuration system
1189
+ - **v0.2.0** - DSL builders (Meta Tags, Open Graph, Twitter Cards)
1190
+ - **v0.3.0** - HTML generators (Meta Tags, Open Graph, Twitter Cards)
1191
+ - **v0.4.0** - Rails view helpers integration ← **Current**
1192
+ - **v0.5.0** - Sitemap generation (planned)
1193
+ - **v0.6.0** - Advanced features (planned)
1194
+ - **v1.0.0** - Stable release (planned)