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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -2
- data/README.md +1164 -13
- data/lib/better_seo/version.rb +1 -1
- data/lib/better_seo.rb +41 -2
- metadata +49 -6
data/README.md
CHANGED
|
@@ -1,38 +1,1175 @@
|
|
|
1
1
|
# BetterSeo
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
[](https://github.com/yourusername/better_seo)
|
|
6
|
+
[](https://github.com/yourusername/better_seo)
|
|
7
|
+
[](https://www.ruby-lang.org)
|
|
8
|
+
[](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
|
-
|
|
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
|
-
|
|
91
|
+
Or install it yourself as:
|
|
12
92
|
|
|
13
93
|
```bash
|
|
14
|
-
|
|
94
|
+
gem install better_seo
|
|
15
95
|
```
|
|
16
96
|
|
|
17
|
-
|
|
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
|
-
|
|
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 "quotes" & <script>alert("xss")</script></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
|
-
|
|
855
|
+
#### URL Entry Details
|
|
24
856
|
|
|
25
|
-
|
|
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.
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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)
|