π BetterSeo
A comprehensive, production-ready SEO toolkit for Ruby and Rails applications
BetterSeo provides a clean, fluent DSL for managing meta tags, Open Graph, Twitter Cards, structured data, sitemaps, and advanced SEO tools. Built with 899 passing tests and 94.3% code coverage.
π― Key Features at a Glance
| Feature | Description |
|---|---|
| π·οΈ Meta Tags | Complete DSL for title, description, keywords, robots |
| π± Social Media | Open Graph & Twitter Cards with validation |
| πΊοΈ Sitemaps | XML sitemaps with hreflang, images, videos |
| π Structured Data | 10+ JSON-LD types (Article, Product, Recipe, etc.) |
| π€ Robots.txt | Dynamic robots.txt generation |
| β SEO Validator | Score pages 0-100 with recommendations |
| πΌοΈ Image Optimizer | WebP conversion, resize, compress |
| π Analytics | Google Analytics 4 & Tag Manager |
π Table of Contents
- β¨ Features
- π¦ Installation
- π Quick Start
- π Core Features
- π·οΈ Meta Tags DSL
- π± Open Graph & Twitter Cards
- π― Rails Integration
- πΊοΈ Sitemap Generation
- π Structured Data (JSON-LD)
- π οΈ Advanced Tools
- π Breadcrumbs Generator
- β‘ AMP Support
- π Canonical URL Manager
- π€ Robots.txt Generator
- β SEO Validator & Recommendations
- πΌοΈ Image Optimizer
- π Analytics Integration
- βοΈ Configuration
- π» Development
- π€ Contributing
- π License
β‘ Quick Reference
# 1οΈβ£ Install
gem 'better_seo', '~> 1.0'
# 2οΈβ£ Configure
BetterSeo.configure do |config|
config.site_name = "My Site"
config.meta_tags.default_title = "Default Title"
end
# 3οΈβ£ Use in Views
<%= seo_tags do |seo|
seo.meta { |m| m.title("Page Title").description("Description") }
seo.og { |og| og.type("article").image(image_url) }
seo.twitter { |t| t.card("summary_large_image") }
end %>
# 4οΈβ£ Generate Sitemap
xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
sitemap.add_url("https://example.com", priority: 1.0)
end
# 5οΈβ£ Add Structured Data
<%= article_sd(headline: @post.title, author: @post.author.name) %>β¨ Features
π― Core Capabilities (v1.0.0 - Production Ready)
- βοΈ Singleton configuration with block-style setup
- π§ Nested configuration objects
- π© Feature flags for enabling/disabling modules
- β Validation with detailed error messages
- π i18n support with multiple locales
- Meta Tags: title, description, keywords, author, canonical, robots, viewport, charset
- Open Graph: Complete OG protocol including articles, images, videos, audio
- Twitter Cards: All card types (summary, summary_large_image, app, player)
- π Fluent interface with method chaining
- β Automatic validation (length, required fields)
- π·οΈ MetaTagsGenerator: Converts DSL to HTML meta tags
- π± OpenGraphGenerator: Open Graph meta tags
- π¦ TwitterCardsGenerator: Twitter Card meta tags
- π BreadcrumbsGenerator: HTML breadcrumbs with Schema.org
- β‘ AMP Generator: Accelerated Mobile Pages support
- π Canonical URL Manager: URL normalization and management
- π HTML entity escaping for security
-
View Helpers:
seo_meta_tags,seo_open_graph_tags,seo_twitter_tags,seo_tags - Structured Data Helpers: 10+ helpers for all Schema.org types
-
Controller Helpers:
set_page_title,set_page_description,set_page_keywords, and more -
Model Helpers:
seo_attributesmacro for automatic SEO - Railtie: Automatic initialization and helper injection
-
Generator:
rails generate better_seo:install
- π XML Sitemap Builder with fluent API
- π Multi-language support (hreflang alternates)
- πΌοΈ Image sitemaps with title and caption
- π₯ Video sitemaps with metadata
- π Sitemap Index for 50,000+ URLs
- π Dynamic generation with lambda support
- πΎ File writing capabilities
- β Automatic URL validation
10 comprehensive Schema.org types:
- π’ Organization: Company info with address, social profiles
- π° Article: Blog posts with author, publisher, metadata
- π€ Person: Author profiles with job title, social links
- ποΈ Product: E-commerce with price, ratings, reviews
- π BreadcrumbList: Navigation breadcrumbs
- πͺ LocalBusiness: Physical locations with hours, geo coordinates
- π« Event: Conferences, webinars with dates, tickets
- β FAQPage: Structured FAQ for rich snippets
- π HowTo: Step-by-step guides
- π³ Recipe: Cooking recipes with ingredients, nutrition
β¨ Full Rails integration with dedicated view helpers
- π€ Robots.txt Generator: Control crawler access
- β SEO Validator: Page scoring (0-100) with detailed reports
- π‘ SEO Recommendations: AI-powered suggestions by priority
- πΌοΈ Image Optimizer: WebP conversion, resize, compress
- π Google Analytics 4: GA4 integration
- π·οΈ Google Tag Manager: GTM support with custom events
π¦ Installation
π Production Use (RubyGems)
Add to your Gemfile:
gem 'better_seo', '~> 1.0'Then run:
bundle installOr install directly:
gem install better_seoπ§ Development (from source)
From Git:
gem 'better_seo', git: 'https://github.com/alessiobussolari/better_seo.git', tag: 'v1.0.0'Clone and build locally:
git clone https://github.com/alessiobussolari/better_seo.git
cd better_seo
gem build better_seo.gemspec
gem install better_seo-1.0.0.gemπ Quick Start
1οΈβ£ Configuration
Create an initializer in Rails or configure at app startup:
# config/initializers/better_seo.rb
BetterSeo.configure do |config|
config.site_name = "My Awesome Site"
config.default_locale = :en
config.available_locales = [:en, :it, :fr]
# Configure defaults for meta tags
config.meta_tags.default_title = "My Awesome Site"
config.meta_tags.title_separator = " | "
config.meta_tags.append_site_name = true
config.meta_tags.default_description = "The best site on the internet"
config.meta_tags.default_keywords = ["awesome", "site", "seo"]
# Configure Open Graph defaults
config.open_graph.site_name = "My Awesome Site"
config.open_graph.default_type = "website"
config.open_graph.default_locale = "en_US"
# Configure Twitter Cards defaults
config.twitter.site = "@mysite"
config.twitter.creator = "@myhandle"
config.twitter.card_type = "summary_large_image"
end2οΈβ£ Using DSL Builders
π·οΈ Meta Tags
meta = BetterSeo::DSL::MetaTags.new
meta.evaluate do
title "My Page Title"
description "This is an amazing page about Ruby and SEO"
keywords "ruby", "seo", "meta tags"
author "John Doe"
canonical "https://example.com/my-page"
robots index: true, follow: true, noarchive: true
viewport # uses default: "width=device-width, initial-scale=1.0"
charset # uses default: "UTF-8"
end
# Get the configuration
config = meta.build
# => {
# title: "My Page Title",
# description: "This is an amazing page...",
# keywords: ["ruby", "seo", "meta tags"],
# ...
# }π± Open Graph
og = BetterSeo::DSL::OpenGraph.new
og.evaluate do
title "My OG Title"
description "Description for social media"
type "article"
url "https://example.com/article"
image "https://example.com/image.jpg"
site_name "My Site"
locale "en_US"
locale_alternate "it_IT", "fr_FR"
# For article type
article do
author "John Doe"
published_time "2024-01-01T00:00:00Z"
modified_time "2024-01-02T00:00:00Z"
section "Technology"
tag "Ruby", "SEO", "OpenGraph"
end
end
config = og.buildπ¦ Twitter Cards
twitter = BetterSeo::DSL::TwitterCards.new
twitter.evaluate do
card "summary_large_image"
site "@mysite" # @ prefix added automatically
creator "myhandle" # @ prefix added automatically
title "Twitter Card Title"
description "Description for Twitter"
image "https://example.com/twitter-image.jpg"
image_alt "Image description for accessibility"
end
config = twitter.buildπ Method Chaining
All DSL builders support fluent interface:
meta = BetterSeo::DSL::MetaTags.new
.title("Chained Title")
.description("Chained Description")
.keywords("ruby", "rails", "seo")
.author("Jane Doe")
.canonical("https://example.com/page")
og = BetterSeo::DSL::OpenGraph.new
.title("OG Title")
.type("article")
.url("https://example.com")
.image("https://example.com/og.jpg")
twitter = BetterSeo::DSL::TwitterCards.new
.card("summary_large_image")
.site("@mysite")
.title("Twitter Title")
.description("Twitter Description")
.image("https://example.com/twitter.jpg")3οΈβ£ HTML Generation
Once you've built your SEO configuration with DSL builders, use generators to convert them to HTML tags:
π¨ Meta Tags Generator
# Build configuration with DSL
meta = BetterSeo::DSL::MetaTags.new
meta.title("My Page Title")
meta.description("Page description for SEO")
meta.keywords("ruby", "seo", "rails")
meta.canonical("https://example.com/page")
meta.robots(index: true, follow: true)
# Generate HTML tags
generator = BetterSeo::Generators::MetaTagsGenerator.new(meta.build)
html = generator.generate
# Output:
# <meta charset="UTF-8">
# <meta name="viewport" content="width=device-width, initial-scale=1.0">
# <title>My Page Title</title>
# <meta name="description" content="Page description for SEO">
# <meta name="keywords" content="ruby, seo, rails">
# <link rel="canonical" href="https://example.com/page">
# <meta name="robots" content="index, follow">π± Open Graph Generator
# Build configuration with DSL
og = BetterSeo::DSL::OpenGraph.new
og.title("Article Title")
og.type("article")
og.url("https://example.com/article")
og.image(url: "https://example.com/og.jpg", width: 1200, height: 630)
# Generate HTML tags
generator = BetterSeo::Generators::OpenGraphGenerator.new(og.build)
html = generator.generate
# Output:
# <meta property="og:title" content="Article Title">
# <meta property="og:type" content="article">
# <meta property="og:url" content="https://example.com/article">
# <meta property="og:image" content="https://example.com/og.jpg">
# <meta property="og:image:width" content="1200">
# <meta property="og:image:height" content="630">π¦ Twitter Cards Generator
# Build configuration with DSL
twitter = BetterSeo::DSL::TwitterCards.new
twitter.card("summary_large_image")
twitter.site("@mysite")
twitter.title("Twitter Card Title")
twitter.description("Description for Twitter")
twitter.image("https://example.com/twitter.jpg")
# Generate HTML tags
generator = BetterSeo::Generators::TwitterCardsGenerator.new(twitter.build)
html = generator.generate
# Output:
# <meta name="twitter:card" content="summary_large_image">
# <meta name="twitter:site" content="@mysite">
# <meta name="twitter:title" content="Twitter Card Title">
# <meta name="twitter:description" content="Description for Twitter">
# <meta name="twitter:image" content="https://example.com/twitter.jpg">β¨ Complete Example
# Build all SEO tags for a page
meta = BetterSeo::DSL::MetaTags.new.evaluate do
title "My Awesome Page"
description "This page is about Ruby SEO"
keywords "ruby", "seo", "meta tags"
end
og = BetterSeo::DSL::OpenGraph.new.evaluate do
title "My Awesome Page"
type "article"
url "https://example.com/page"
image "https://example.com/og.jpg"
end
twitter = BetterSeo::DSL::TwitterCards.new.evaluate do
card "summary_large_image"
site "@mysite"
title "My Awesome Page"
image "https://example.com/twitter.jpg"
end
# Generate all HTML
meta_html = BetterSeo::Generators::MetaTagsGenerator.new(meta.build).generate
og_html = BetterSeo::Generators::OpenGraphGenerator.new(og.build).generate
twitter_html = BetterSeo::Generators::TwitterCardsGenerator.new(twitter.build).generate
# Combine and render in your view
all_tags = [meta_html, og_html, twitter_html].join("\n")π Security Features
All generators automatically escape HTML entities to prevent XSS attacks:
meta = BetterSeo::DSL::MetaTags.new
meta.title('Title with "quotes" & <script>alert("xss")</script>')
generator = BetterSeo::Generators::MetaTagsGenerator.new(meta.build)
html = generator.generate
# Output:
# <title>Title with "quotes" & <script>alert("xss")</script></title>
# All dangerous characters are properly escaped4οΈβ£ Validation
All DSL builders include automatic validation:
meta = BetterSeo::DSL::MetaTags.new
meta.title("A" * 80) # Too long (max 60 chars recommended)
meta.build
# => BetterSeo::ValidationError: Title too long (80 chars, max 60 recommended)
og = BetterSeo::DSL::OpenGraph.new
og.title("Title")
og.build
# => BetterSeo::ValidationError: og:type is required, og:image is required, og:url is required
twitter = BetterSeo::DSL::TwitterCards.new
twitter.card("invalid_type")
twitter.build
# => BetterSeo::ValidationError: Invalid card type: invalid_type. Valid types: summary, summary_large_image, app, playerπ Core Features
π― Rails Integration
BetterSeo provides comprehensive view helpers for seamless Rails integration.
π§ Setup
Include the helpers in your ApplicationHelper:
# app/helpers/application_helper.rb
module ApplicationHelper
include BetterSeo::Rails::Helpers::SeoHelper
endOr include them globally in ApplicationController:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper BetterSeo::Rails::Helpers::SeoHelper
endπ¨ Using View Helpers
π·οΈ Single Tag Group Helpers
<%# app/views/layouts/application.html.erb %>
<head>
<%= seo_meta_tags do |meta|
meta.title "My Page Title"
meta.description "Page description"
meta.keywords "ruby", "rails", "seo"
meta.canonical request.original_url
meta.robots index: true, follow: true
end %>
<%= seo_open_graph_tags do |og|
og.title "My Page Title"
og.type "website"
og.url request.original_url
og.image image_url("og-image.jpg")
og.site_name "My Site"
end %>
<%= seo_twitter_tags do |twitter|
twitter.card "summary_large_image"
twitter.site "@mysite"
twitter.title "My Page Title"
twitter.description "Page description"
twitter.image image_url("twitter-image.jpg")
end %>
</head>β¨ All-in-One Helper
<%# Generate all SEO tags at once %>
<head>
<%= seo_tags do |seo|
seo.meta do |meta|
meta.title @page_title || "Default Title"
meta.description @page_description
meta.keywords @page_keywords if @page_keywords
meta.canonical request.original_url
end
seo.og do |og|
og.title @page_title || "Default Title"
og.type "article"
og.url request.original_url
og.image @og_image || image_url("default-og.jpg")
end
seo.twitter do |twitter|
twitter.card "summary_large_image"
twitter.site "@mysite"
twitter.title @page_title || "Default Title"
twitter.image @twitter_image || image_url("default-twitter.jpg")
end
end %>
</head>Controller Integration
Set SEO data in your controllers:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
# Set SEO variables for the view
@page_title = @article.title
@page_description = @article.excerpt
@page_keywords = @article.tags.pluck(:name)
@og_image = url_for(@article.cover_image) if @article.cover_image.attached?
end
endThen use them in your layout:
<head>
<%= seo_tags do |seo|
seo.meta do |meta|
meta.title @page_title if @page_title
meta.description @page_description if @page_description
meta.keywords(*@page_keywords) if @page_keywords
meta.canonical request.original_url
end
seo.og do |og|
og.title @page_title || "Default Title"
og.type "article"
og.url request.original_url
og.image @og_image if @og_image
end
seo.twitter do |twitter|
twitter.card "summary_large_image"
twitter.title @page_title || "Default Title"
twitter.description @page_description if @page_description
end
end %>
</head>Hash Configuration
You can also pass hash configurations directly:
<%= seo_meta_tags(
title: "My Page",
description: "Description",
keywords: ["ruby", "rails"]
) %>
<%= seo_open_graph_tags(
title: "My Page",
type: "article",
url: request.original_url,
image: image_url("og.jpg")
) %>
<%= seo_twitter_tags(
card: "summary",
title: "My Page",
description: "Description"
) %>Partial Integration
Create reusable SEO partials:
<%# app/views/shared/_seo.html.erb %>
<%= seo_tags do |seo|
seo.meta do |meta|
meta.title local_assigns[:title] || "Default Title"
meta.description local_assigns[:description]
meta.canonical request.original_url
end
seo.og do |og|
og.title local_assigns[:title] || "Default Title"
og.type local_assigns[:og_type] || "website"
og.url request.original_url
og.image local_assigns[:og_image] || image_url("default-og.jpg")
end
seo.twitter do |twitter|
twitter.card "summary_large_image"
twitter.title local_assigns[:title] || "Default Title"
end
end %>Then use it in your views:
<%# app/views/articles/show.html.erb %>
<%= render "shared/seo",
title: @article.title,
description: @article.excerpt,
og_type: "article",
og_image: url_for(@article.cover_image) %>πΊοΈ Sitemap Generation
BetterSeo provides a comprehensive sitemap generation system with support for XML sitemaps, dynamic content, and model collections.
π Basic Sitemap Generation
Generate a simple sitemap using the block syntax:
xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
sitemap.add_url("https://example.com")
sitemap.add_url("https://example.com/about")
sitemap.add_url("https://example.com/contact")
end
puts xml
# <?xml version="1.0" encoding="UTF-8"?>
# <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
# <url>
# <loc>https://example.com</loc>
# <changefreq>weekly</changefreq>
# <priority>0.5</priority>
# </url>
# ...
# </urlset>βοΈ URL Entry with Full Attributes
Add URLs with all sitemap attributes (lastmod, changefreq, priority):
xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
sitemap.add_url(
"https://example.com",
lastmod: Date.today,
changefreq: "daily",
priority: 1.0
)
sitemap.add_url(
"https://example.com/blog",
lastmod: "2024-01-15",
changefreq: "weekly",
priority: 0.8
)
sitemap.add_url(
"https://example.com/about",
changefreq: "monthly",
priority: 0.5
)
endValid changefreq values: always, hourly, daily, weekly, monthly, yearly, never
Priority range: 0.0 to 1.0 (default: 0.5)
π Method Chaining
The builder supports fluent method chaining:
xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
sitemap
.add_url("https://example.com", priority: 1.0)
.add_url("https://example.com/about", priority: 0.8)
.add_url("https://example.com/contact", priority: 0.6)
endGenerate from Array
Create a sitemap from an array of URLs:
urls = [
"https://example.com",
"https://example.com/about",
"https://example.com/contact",
"https://example.com/blog"
]
xml = BetterSeo::Sitemap::Generator.generate_from(
urls,
changefreq: "weekly",
priority: 0.7
)Generate from Model Collection
Generate sitemaps dynamically from your Rails models:
# Simple example with Post model
xml = BetterSeo::Sitemap::Generator.generate_from_collection(
Post.published,
url: ->(post) { "https://example.com/posts/#{post.slug}" },
lastmod: ->(post) { post.updated_at },
changefreq: "weekly",
priority: 0.8
)Dynamic attributes with lambdas:
xml = BetterSeo::Sitemap::Generator.generate_from_collection(
Article.all,
url: ->(article) { "https://example.com/articles/#{article.slug}" },
lastmod: ->(article) { article.updated_at },
changefreq: ->(article) do
article.featured? ? "daily" : "weekly"
end,
priority: ->(article) do
article.featured? ? 0.9 : 0.6
end
)Rails Routes Integration
Generate sitemap from Rails routes:
# config/routes.rb
Rails.application.routes.draw do
# Your routes...
# Sitemap endpoint
get '/sitemap.xml', to: 'sitemaps#index', defaults: { format: 'xml' }
end
# app/controllers/sitemaps_controller.rb
class SitemapsController < ApplicationController
def index
@sitemap_xml = generate_sitemap
render xml: @sitemap_xml
end
private
def generate_sitemap
BetterSeo::Sitemap::Generator.generate do |sitemap|
# Static pages
sitemap.add_url(root_url, priority: 1.0, changefreq: "daily")
sitemap.add_url(about_url, priority: 0.8, changefreq: "monthly")
sitemap.add_url(contact_url, priority: 0.7, changefreq: "monthly")
# Dynamic content from models
Post.published.find_each do |post|
sitemap.add_url(
post_url(post),
lastmod: post.updated_at,
changefreq: "weekly",
priority: 0.8
)
end
Category.all.find_each do |category|
sitemap.add_url(
category_url(category),
lastmod: category.updated_at,
changefreq: "weekly",
priority: 0.7
)
end
end
end
endWrite Sitemap to File
Save sitemap directly to a file:
# In a Rake task or script
BetterSeo::Sitemap::Generator.write_to_file('public/sitemap.xml') do |sitemap|
sitemap.add_url("https://example.com", priority: 1.0)
Post.published.find_each do |post|
sitemap.add_url(
"https://example.com/posts/#{post.slug}",
lastmod: post.updated_at,
changefreq: "weekly",
priority: 0.8
)
end
end
# Returns the file path: "public/sitemap.xml"Rake Task for Sitemap Generation
Create a Rake task to regenerate your sitemap:
# lib/tasks/sitemap.rake
namespace :sitemap do
desc "Generate sitemap.xml"
task generate: :environment do
file_path = BetterSeo::Sitemap::Generator.write_to_file('public/sitemap.xml') do |sitemap|
# Add static pages
sitemap.add_url("#{ENV['SITE_URL']}", priority: 1.0, changefreq: "daily")
sitemap.add_url("#{ENV['SITE_URL']}/about", priority: 0.8)
sitemap.add_url("#{ENV['SITE_URL']}/contact", priority: 0.7)
# Add dynamic content
Post.published.find_each do |post|
sitemap.add_url(
"#{ENV['SITE_URL']}/posts/#{post.slug}",
lastmod: post.updated_at,
changefreq: "weekly",
priority: 0.8
)
end
end
puts "Sitemap generated at #{file_path}"
end
end
# Run with: rake sitemap:generateUsing the Builder Directly
For more control, use the Builder class directly:
builder = BetterSeo::Sitemap::Builder.new
# Add URLs
builder.add_url("https://example.com", priority: 1.0)
builder.add_url("https://example.com/about", priority: 0.8)
# Add multiple URLs at once
builder.add_urls(
["https://example.com/blog", "https://example.com/contact"],
changefreq: "weekly",
priority: 0.7
)
# Remove a URL
builder.remove_url("https://example.com/contact")
# Check size
puts builder.size # => 3
# Iterate over URLs
builder.each do |url|
puts "#{url.loc} - Priority: #{url.priority}"
end
# Generate XML
xml = builder.to_xml
# Validate all URLs
builder.validate! # Raises ValidationError if any URL is invalid
# Clear all URLs
builder.clearURL Entry Details
Work with individual URL entries:
entry = BetterSeo::Sitemap::UrlEntry.new(
"https://example.com/page",
lastmod: Date.today,
changefreq: "daily",
priority: 0.8
)
# Access attributes
entry.loc # => "https://example.com/page"
entry.lastmod # => "2024-01-15"
entry.changefreq # => "daily"
entry.priority # => 0.8
# Update attributes
entry.lastmod = Date.new(2024, 1, 20)
entry.changefreq = "weekly"
entry.priority = 0.9
# Generate XML for single entry
entry.to_xml
# <url>
# <loc>https://example.com/page</loc>
# <lastmod>2024-01-20</lastmod>
# <changefreq>weekly</changefreq>
# <priority>0.9</priority>
# </url>
# Convert to hash
entry.to_h
# {
# loc: "https://example.com/page",
# lastmod: "2024-01-20",
# changefreq: "weekly",
# priority: 0.9
# }
# Validate
entry.validate! # Raises ValidationError if invalidAdvanced: Multi-Model Sitemap
Combine multiple models in a single sitemap:
xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
# Homepage
sitemap.add_url("https://example.com", priority: 1.0, changefreq: "daily")
# Blog posts
Post.published.find_each do |post|
sitemap.add_url(
"https://example.com/posts/#{post.slug}",
lastmod: post.updated_at,
changefreq: post.featured? ? "daily" : "weekly",
priority: post.featured? ? 0.9 : 0.7
)
end
# Categories
Category.all.find_each do |category|
sitemap.add_url(
"https://example.com/categories/#{category.slug}",
lastmod: category.updated_at,
changefreq: "weekly",
priority: 0.6
)
end
# Static pages
%w[about contact privacy terms].each do |page|
sitemap.add_url(
"https://example.com/#{page}",
changefreq: "monthly",
priority: 0.5
)
end
endValidation
All URLs are automatically validated when generating:
# This will raise BetterSeo::ValidationError
xml = BetterSeo::Sitemap::Generator.generate do |sitemap|
sitemap.add_url("") # Error: Location is required
sitemap.add_url("not-a-valid-url") # Error: Invalid URL format
sitemap.add_url("ftp://example.com") # Error: Must be HTTP/HTTPS
end
# Validate manually
builder = BetterSeo::Sitemap::Builder.new
builder.add_url("https://example.com")
builder.validate! # Returns true if all URLs validComplete Example: Production Sitemap
# app/services/sitemap_generator_service.rb
class SitemapGeneratorService
def self.generate
BetterSeo::Sitemap::Generator.write_to_file(Rails.root.join('public', 'sitemap.xml')) do |sitemap|
add_static_pages(sitemap)
add_blog_posts(sitemap)
add_categories(sitemap)
add_products(sitemap) if defined?(Product)
end
end
private_class_method def self.add_static_pages(sitemap)
sitemap.add_url(Rails.application.routes.url_helpers.root_url, priority: 1.0, changefreq: "daily")
sitemap.add_url(Rails.application.routes.url_helpers.about_url, priority: 0.8, changefreq: "monthly")
sitemap.add_url(Rails.application.routes.url_helpers.contact_url, priority: 0.7, changefreq: "monthly")
end
private_class_method def self.add_blog_posts(sitemap)
Post.published.find_each do |post|
sitemap.add_url(
Rails.application.routes.url_helpers.post_url(post),
lastmod: post.updated_at,
changefreq: post.frequently_updated? ? "daily" : "weekly",
priority: calculate_post_priority(post)
)
end
end
private_class_method def self.add_categories(sitemap)
Category.all.find_each do |category|
sitemap.add_url(
Rails.application.routes.url_helpers.category_url(category),
lastmod: category.posts.maximum(:updated_at),
changefreq: "weekly",
priority: 0.6
)
end
end
private_class_method def self.add_products(sitemap)
Product.available.find_each do |product|
sitemap.add_url(
Rails.application.routes.url_helpers.product_url(product),
lastmod: product.updated_at,
changefreq: "daily",
priority: product.featured? ? 0.95 : 0.75
)
end
end
private_class_method def self.calculate_post_priority(post)
base_priority = 0.7
base_priority += 0.2 if post.featured?
base_priority += 0.1 if post.comments_count > 10
[base_priority, 1.0].min
end
end
# Call from rake task or controller:
# SitemapGeneratorService.generateπ Structured Data (JSON-LD)
BetterSeo provides comprehensive support for Schema.org structured data using JSON-LD format, helping search engines better understand your content.
π‘ Basic Usage
Create structured data objects and generate JSON-LD script tags:
# Create an Organization
org = BetterSeo::StructuredData::Organization.new
org.name("Acme Corporation")
org.url("https://www.acme.com")
org.logo("https://www.acme.com/logo.png")
org.description("Leading provider of innovative solutions")
# Generate JSON-LD script tag
org.to_script_tag
# <script type="application/ld+json">
# {
# "@context": "https://schema.org",
# "@type": "Organization",
# "name": "Acme Corporation",
# ...
# }
# </script>π Available Types
π’ Organization - Company/organization information:
org = BetterSeo::StructuredData::Organization.new
org.name("Tech Innovations Inc")
org.url("https://techinnovations.com")
org.logo("https://techinnovations.com/logo.png")
org.description("Innovative technology solutions")
org.email("contact@techinnovations.com")
org.telephone("+1-555-0100")
org.address(
street: "123 Tech Boulevard",
city: "San Francisco",
region: "CA",
postal_code: "94105",
country: "US"
)
org.same_as([
"https://twitter.com/techinnovations",
"https://linkedin.com/company/techinnovations"
])
org.founding_date("2015-03-20")π° Article - Blog posts, news articles, content:
article = BetterSeo::StructuredData::Article.new
article.headline("The Future of Web Development")
article.description("An in-depth analysis of emerging trends")
article.image("https://example.com/article-image.jpg")
article.author("Jane Smith") # Or use Person object
article.date_published("2024-01-15T09:00:00Z")
article.date_modified("2024-01-20T14:30:00Z")
article.url("https://example.com/articles/future-of-web-dev")
article.word_count(2500)
article.keywords(["Web Development", "Technology", "Trends"])
article.article_section("Technology")π€ Person - Author profiles, team members:
person = BetterSeo::StructuredData::Person.new
person.name("Dr. Jane Smith")
person.given_name("Jane")
person.family_name("Smith")
person.email("jane@example.com")
person.url("https://janesmith.dev")
person.image("https://janesmith.dev/profile.jpg")
person.job_title("Chief Technology Officer")
person.telephone("+1-555-0199")
person.same_as([
"https://twitter.com/janesmith",
"https://linkedin.com/in/janesmith"
])ποΈ Product - E-commerce products:
product = BetterSeo::StructuredData::Product.new
product.name("Premium Wireless Headphones")
product.description("High-quality wireless headphones with noise cancellation")
product.image("https://example.com/headphones.jpg")
product.brand("AudioTech")
product.sku("HEADPHONES-WL-NC-2024")
product.offers(
price: 299.99,
price_currency: "USD",
availability: "InStock",
url: "https://example.com/products/headphones"
)
product.aggregate_rating(
rating_value: 4.7,
review_count: 342
)π BreadcrumbList - Navigation breadcrumbs:
breadcrumb = BetterSeo::StructuredData::BreadcrumbList.new
breadcrumb
.add_item(name: "Home", url: "https://example.com")
.add_item(name: "Electronics", url: "https://example.com/electronics")
.add_item(name: "Headphones", url: "https://example.com/electronics/headphones")Method Chaining
All structured data classes support fluent method chaining:
org = BetterSeo::StructuredData::Organization.new
.name("Acme Corp")
.url("https://acme.com")
.logo("https://acme.com/logo.png")
.description("Innovation at its finest")Nested Structured Data
Combine multiple structured data objects:
# Create publisher organization
publisher = BetterSeo::StructuredData::Organization.new
publisher.name("Tech Publishing Co")
publisher.logo("https://techpub.com/logo.png")
# Create author person
author = BetterSeo::StructuredData::Person.new
author.name("Jane Smith")
author.email("jane@techpub.com")
author.url("https://janesmith.dev")
# Create article with nested data
article = BetterSeo::StructuredData::Article.new
article.headline("Introduction to Ruby on Rails")
article.description("A comprehensive guide for beginners")
article.image("https://example.com/rails-guide.jpg")
article.author(author) # Nested Person
article.publisher(publisher) # Nested Organization
article.date_published("2024-01-15")
# Generates nested JSON-LD automatically
article.to_script_tagUsing the Generator Helper
The Generator class provides convenient factory methods:
# Create with block
org = BetterSeo::StructuredData::Generator.organization do |o|
o.name("Acme Corp")
o.url("https://acme.com")
o.logo("https://acme.com/logo.png")
end
article = BetterSeo::StructuredData::Generator.article do |a|
a.headline("Amazing Article")
a.author("John Doe")
a.date_published("2024-01-15")
end
person = BetterSeo::StructuredData::Generator.person do |p|
p.name("John Doe")
p.email("john@example.com")
end
# Generate multiple script tags at once
tags = BetterSeo::StructuredData::Generator.generate_script_tags([org, article, person])
# Returns all three script tags joined with newlinesRails Integration
Add structured data to your Rails views:
<%# app/views/articles/show.html.erb %>
<%
author = BetterSeo::StructuredData::Generator.person do |p|
p.name(@article.author.name)
p.email(@article.author.email)
p.url(@article.author.website)
end
article_sd = BetterSeo::StructuredData::Generator.article do |a|
a.headline(@article.title)
a.description(@article.excerpt)
a.image(url_for(@article.cover_image))
a.author(author)
a.date_published(@article.published_at.iso8601)
a.date_modified(@article.updated_at.iso8601)
a.url(article_url(@article))
a.word_count(@article.word_count)
a.keywords(@article.tags.pluck(:name))
end
%>
<%== article_sd.to_script_tag %>Or in a helper:
# app/helpers/structured_data_helper.rb
module StructuredDataHelper
def article_structured_data(article)
author = BetterSeo::StructuredData::Generator.person do |p|
p.name(article.author.name)
p.url(author_url(article.author))
end
article_sd = BetterSeo::StructuredData::Generator.article do |a|
a.headline(article.title)
a.description(article.excerpt)
a.author(author)
a.date_published(article.published_at.iso8601)
a.url(article_url(article))
end
article_sd.to_script_tag.html_safe
end
def organization_structured_data
org = BetterSeo::StructuredData::Generator.organization do |o|
o.name(Rails.application.config.site_name)
o.url(root_url)
o.logo(image_url('logo.png'))
o.same_as([
"https://twitter.com/yourcompany",
"https://facebook.com/yourcompany"
])
end
org.to_script_tag.html_safe
end
endThen in your layout:
<%# app/views/layouts/application.html.erb %>
<head>
...
<%= organization_structured_data %>
</head>And in article views:
<%# app/views/articles/show.html.erb %>
<%= article_structured_data(@article) %>Rails View Helpers (v0.8.0)
BetterSeo includes built-in Rails view helpers for easy structured data integration:
Generic Helper - structured_data_tag
<%# Create from type symbol with hash %>
<%= structured_data_tag(:organization,
name: "Acme Corp",
url: "https://acme.com",
logo: "https://acme.com/logo.png"
) %>
<%# Create from type symbol with block %>
<%= structured_data_tag(:article) do |article|
article.headline("My Article")
article.author("John Doe")
article.date_published("2024-01-15")
end %>
<%# Pass an existing object %>
<% org = BetterSeo::StructuredData::Organization.new(name: "Acme") %>
<%= structured_data_tag(org) %>Type-Specific Helpers
Convenience methods for each structured data type:
<%# Organization %>
<%= organization_sd(
name: "Acme Corp",
url: "https://acme.com"
) %>
<%# Or with block %>
<%= organization_sd do |org|
org.name("Acme Corp")
org.url("https://acme.com")
org.logo("https://acme.com/logo.png")
end %>
<%# Article %>
<%= article_sd(
headline: @article.title,
author: @article.author.name,
date_published: @article.published_at.iso8601
) %>
<%# Person %>
<%= person_sd do |person|
person.name("John Doe")
person.email("john@example.com")
person.job_title("Software Engineer")
end %>
<%# Product %>
<%= product_sd do |product|
product.name(@product.name)
product.brand(@product.brand)
product.offers(
price: @product.price,
price_currency: "USD",
availability: "InStock"
)
product.aggregate_rating(
rating_value: @product.average_rating,
review_count: @product.reviews_count
)
end %>
<%# Breadcrumb List %>
<%= breadcrumb_list_sd do |breadcrumb|
breadcrumb.add_item(name: "Home", url: root_url)
breadcrumb.add_item(name: "Products", url: products_url)
breadcrumb.add_item(name: @product.name, url: product_url(@product))
end %>
<%# Or from array %>
<% items = [
{ name: "Home", url: root_url },
{ name: "Products", url: products_url }
] %>
<%= breadcrumb_list_sd(items: items) %>Multiple Tags Helper - structured_data_tags
Generate multiple script tags at once:
<% org = BetterSeo::StructuredData::Organization.new(name: "Acme") %>
<% person = BetterSeo::StructuredData::Person.new(name: "John") %>
<% article = BetterSeo::StructuredData::Article.new(headline: "Title") %>
<%= structured_data_tags([org, person, article]) %>Complete Rails Example
<%# app/views/products/show.html.erb %>
<head>
<%# Page SEO tags %>
<%= seo_tags do |seo|
seo.meta do |meta|
meta.title @product.name
meta.description @product.description
end
seo.og do |og|
og.type "product"
og.title @product.name
og.image @product.image_url
end
end %>
<%# Structured data %>
<%= organization_sd do |org|
org.name("My Shop")
org.url(root_url)
end %>
<%= breadcrumb_list_sd do |bc|
bc.add_item(name: "Home", url: root_url)
bc.add_item(name: "Products", url: products_url)
bc.add_item(name: @product.category, url: category_url(@product.category))
bc.add_item(name: @product.name, url: product_url(@product))
end %>
<%= product_sd do |product|
product.name(@product.name)
product.description(@product.description)
product.image(@product.image_url)
product.brand(@product.brand)
product.sku(@product.sku)
product.offers(
price: @product.price,
price_currency: "USD",
availability: @product.in_stock? ? "InStock" : "OutOfStock",
url: product_url(@product)
)
if @product.reviews.any?
product.aggregate_rating(
rating_value: @product.average_rating,
review_count: @product.reviews_count,
best_rating: 5
)
end
end %>
</head>Helper Method Reference
| Helper | Description |
|---|---|
structured_data_tag(type, **props, &block) |
Generic helper for any type |
organization_sd(**props, &block) |
Organization structured data |
article_sd(**props, &block) |
Article structured data |
person_sd(**props, &block) |
Person structured data |
product_sd(**props, &block) |
Product structured data |
breadcrumb_list_sd(items:, &block) |
Breadcrumb list structured data |
structured_data_tags(array) |
Multiple structured data tags |
All helpers support both hash configuration and block-based DSL for maximum flexibility.
Complete Example
# Create complete structured data for a blog article
publisher = BetterSeo::StructuredData::Generator.organization do |o|
o.name("Tech Blog Publishing")
o.url("https://techblog.com")
o.logo("https://techblog.com/logo.png")
end
author = BetterSeo::StructuredData::Generator.person do |p|
p.name("Dr. Sarah Johnson")
p.email("sarah@techblog.com")
p.url("https://sarahjohnson.dev")
p.job_title("Senior Technology Writer")
end
article = BetterSeo::StructuredData::Generator.article do |a|
a.headline("The Complete Guide to Ruby on Rails in 2024")
a.description("Everything you need to know about Rails")
a.image([
"https://techblog.com/images/rails-2024-1.jpg",
"https://techblog.com/images/rails-2024-2.jpg"
])
a.author(author)
a.publisher(publisher)
a.date_published("2024-01-15T09:00:00Z")
a.date_modified("2024-01-20T15:30:00Z")
a.url("https://techblog.com/rails-complete-guide-2024")
a.word_count(3500)
a.keywords(["Ruby on Rails", "Web Development", "2024"])
a.article_section("Programming")
end
# Get JSON-LD
json_ld = article.to_json
# Get script tag for HTML
script_tag = article.to_script_tag
# Or generate all at once
all_tags = BetterSeo::StructuredData::Generator.generate_script_tags([
publisher,
author,
article
])Benefits
- SEO Enhancement: Help search engines understand your content better
- Rich Snippets: Enable rich results in search results (ratings, images, etc.)
- Type Safety: Fluent API with method chaining
- Nested Data: Automatic handling of complex relationships
- Standards Compliant: Follows Schema.org specifications
- Easy Integration: Works seamlessly with Rails views and helpers
π οΈ Advanced Tools
π Breadcrumbs Generator
Generate HTML breadcrumb navigation with Schema.org structured data support.
π‘ Basic Usage
generator = BetterSeo::Generators::BreadcrumbsGenerator.new
generator.add_item("Home", "/")
generator.add_item("Products", "/products")
generator.add_item("Laptops", "/products/laptops")
generator.add_item("MacBook Pro", nil) # Current page (no link)
# Generate HTML breadcrumbs
html = generator.to_html
# <nav class="breadcrumb" aria-label="breadcrumb">
# <ol class="breadcrumb">
# <li class="breadcrumb-item">
# <a href="/">Home</a>
# </li>
# ...
# </ol>
# </nav>
# Generate JSON-LD structured data
json_ld = generator.to_json_ld
script_tag = generator.to_script_tagWith Schema.org Markup
# Generate breadcrumbs with microdata
html = generator.to_html(schema: true)
# Includes itemscope, itemtype, itemprop attributes for rich snippetsCustom Styling
html = generator.to_html(
nav_class: "my-breadcrumb-nav",
list_class: "my-breadcrumb-list"
)Rails Integration
<!-- app/views/layouts/application.html.erb -->
<%
breadcrumbs = BetterSeo::Generators::BreadcrumbsGenerator.new
breadcrumbs.add_item("Home", root_path)
breadcrumbs.add_item("Blog", blog_path)
breadcrumbs.add_item(@post.title, nil)
%>
<%= raw breadcrumbs.to_html(schema: true) %>
<%= raw breadcrumbs.to_script_tag %>Multiple Items at Once
generator.add_items([
{ name: "Home", url: "/" },
{ name: "Products", url: "/products" },
{ name: "Laptops", url: "/products/laptops" }
])β‘ AMP Generator
Generate Accelerated Mobile Pages (AMP) HTML components.
π‘ Basic Usage
amp = BetterSeo::Generators::AmpGenerator.new(
canonical_url: "https://example.com/article",
title: "My Article Title",
description: "Article description",
image: "https://example.com/image.jpg"
)
# AMP boilerplate CSS
boilerplate = amp.to_boilerplate
# AMP runtime script
amp_script = amp.to_amp_script_tag
# <script async src="https://cdn.ampproject.org/v0.js"></script>
# Meta tags
meta_tags = amp.to_meta_tags
# Includes canonical, og:title, og:description, og:imageComplete AMP Page
<!doctype html>
<html β‘>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<%= raw amp.to_meta_tags %>
<%= raw amp.to_amp_script_tag %>
<%= raw amp.to_boilerplate %>
<% custom_css = "body { font-family: Arial; } h1 { color: #333; }" %>
<%= raw amp.to_custom_css(custom_css) %>
</head>
<body>
<!-- Your AMP content -->
</body>
</html>With Structured Data
article_data = {
"@context" => "https://schema.org",
"@type" => "Article",
"headline" => "My Article",
"author" => { "@type" => "Person", "name" => "John Doe" }
}
amp.structured_data = article_data
script = amp.to_structured_data
# <script type="application/ld+json">
# { "@context": "https://schema.org", "@type": "Article", ... }
# </script>Rails Controller Integration
class ArticlesController < ApplicationController
def amp
@article = Article.find(params[:id])
@amp = BetterSeo::Generators::AmpGenerator.new(
canonical_url: article_url(@article),
title: @article.title,
description: @article.excerpt,
image: @article.featured_image_url
)
render layout: 'amp'
end
endπ Canonical URL Manager
Manage and normalize canonical URLs with validation.
π‘ Basic Usage
manager = BetterSeo::Generators::CanonicalUrlManager.new("https://example.com/page")
# Generate HTML link tag
html = manager.to_html
# <link rel="canonical" href="https://example.com/page">
# Generate HTTP header
header = manager.to_http_header
# <https://example.com/page>; rel="canonical"
# Validate URL
manager.validate! # Raises ValidationError if invalidURL Normalization
manager = BetterSeo::Generators::CanonicalUrlManager.new
# Removes trailing slashes
manager.url = "https://example.com/page/"
manager.url # => "https://example.com/page"
# Removes fragment identifiers
manager.url = "https://example.com/page#section"
manager.url # => "https://example.com/page"
# Optional: Remove query parameters
manager.remove_query_params = true
manager.url = "https://example.com/page?utm_source=twitter&ref=123"
manager.url # => "https://example.com/page"
# Optional: Lowercase URL
manager.lowercase = true
manager.url = "https://Example.com/Page"
manager.url # => "https://example.com/page"Rails Integration
# Controller
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
canonical = BetterSeo::Generators::CanonicalUrlManager.new
canonical.remove_query_params = true
canonical.url = article_url(@article)
response.headers['Link'] = canonical.to_http_header
@canonical_tag = canonical.to_html
end
end
# View (app/views/articles/show.html.erb)
<head>
<%= raw @canonical_tag %>
</head>Advanced Normalization
manager = BetterSeo::Generators::CanonicalUrlManager.new
manager.remove_query_params = true
manager.lowercase = true
manager.url = "https://Example.com/Page/?utm_source=google#section"
manager.url # => "https://example.com/page"
manager.validate! # => trueError Handling
manager = BetterSeo::Generators::CanonicalUrlManager.new
# Relative URLs not allowed
manager.url = "/page"
manager.validate! # Raises: "Canonical URL must be absolute: /page"
# Invalid URL format
manager.url = "not a url"
manager.validate! # Raises: "Invalid URL format: not a url"
# URL is required
manager.url = nil
manager.validate! # Raises: "URL is required"π€ Robots.txt Generator
Generate robots.txt files to control search engine crawler access.
π‘ Basic Usage
robots = BetterSeo::Generators::RobotsTxtGenerator.new
# Allow all bots except specific paths
robots.add_rule("*", disallow: ["/admin", "/api/private", "/tmp"])
# Add sitemap
robots.add_sitemap("https://example.com/sitemap.xml")
# Generate robots.txt content
text = robots.to_text
# User-agent: *
# Disallow: /admin
# Disallow: /api/private
# Disallow: /tmp
#
# Sitemap: https://example.com/sitemap.xmlMultiple User Agents
robots = BetterSeo::Generators::RobotsTxtGenerator.new
# Rules for all bots
robots.add_rule("*", allow: "/", disallow: "/admin")
# Specific rules for Googlebot
robots.add_rule("Googlebot", allow: "/api/public", disallow: "/temp")
# Crawl delay for aggressive bots
robots.add_rule("Bingbot", disallow: "/admin")
robots.set_crawl_delay("Bingbot", 2)
text = robots.to_textWrite to File
robots = BetterSeo::Generators::RobotsTxtGenerator.new
robots.add_rule("*", disallow: ["/admin", "/private"])
robots.add_sitemap("https://example.com/sitemap.xml")
# Write to public directory
robots.write_to_file(Rails.root.join('public', 'robots.txt'))Rails Controller Integration
# app/controllers/robots_controller.rb
class RobotsController < ApplicationController
def index
robots = BetterSeo::Generators::RobotsTxtGenerator.new
if Rails.env.production?
robots.add_rule("*", disallow: ["/admin", "/api/private"])
robots.add_sitemap("#{request.base_url}/sitemap.xml")
else
# Block all bots in non-production
robots.add_rule("*", disallow: "/")
end
render plain: robots.to_text, content_type: "text/plain"
end
end
# config/routes.rb
get '/robots.txt', to: 'robots#index'β SEO Validator & Recommendations
Validate and score your pages for SEO best practices with AI-powered recommendations.
π‘ Basic Usage
validator = BetterSeo::Validators::SeoValidator.new
# Check individual elements
title_result = validator.check_title("My Awesome SEO-Friendly Page Title")
# => { valid: true, score: 100, message: "Title length is optimal", length: 35 }
desc_result = validator.check_description("This is a comprehensive meta description...")
# => { valid: true, score: 100, message: "Description length is optimal", length: 145 }Validate Complete Page
validator = BetterSeo::Validators::SeoValidator.new
html = <<~HTML
<!DOCTYPE html>
<html>
<head>
<title>Best Ruby SEO Gem for Rails Applications</title>
<meta name="description" content="BetterSeo provides comprehensive SEO tools for Ruby on Rails including meta tags, structured data, sitemaps, and validators.">
</head>
<body>
<h1>BetterSeo: Complete SEO Solution</h1>
<img src="logo.png" alt="BetterSeo Logo">
<h2>Features</h2>
<h2>Getting Started</h2>
</body>
</html>
HTML
result = validator.validate_page(html)
# => {
# overall_score: 95,
# title: { valid: true, score: 100, ... },
# description: { valid: true, score: 100, ... },
# headings: { valid: true, score: 100, h1_count: 1, ... },
# images: { valid: true, score: 100, ... },
# issues: []
# }Generate SEO Report
validator = BetterSeo::Validators::SeoValidator.new
result = validator.validate_page(html)
report = validator.generate_report(result)
puts report
# ============================================================
# SEO Validation Report
# ============================================================
#
# Overall Score: 95/100
#
# Title:
# Status: β
# Score: 100/100
# Length: 45 chars
# Message: Title length is optimal
#
# Description:
# Status: β
# Score: 100/100
# ...Rails Integration
# app/services/seo_validator_service.rb
class SeoValidatorService
def self.validate_page_seo(url)
html = fetch_page_html(url)
validator = BetterSeo::Validators::SeoValidator.new
validator.validate_page(html)
end
def self.audit_site
pages = Page.published.pluck(:url)
validator = BetterSeo::Validators::SeoValidator.new
results = pages.map do |url|
html = fetch_page_html(url)
validation = validator.validate_page(html)
{ url: url, score: validation[:overall_score], issues: validation[:issues] }
end
results.sort_by { |r| r[:score] }
end
endValidation Rules
- Title: 30-60 characters optimal
- Description: 120-160 characters optimal
- Headings: Exactly one H1 tag required
- Images: All images must have alt text
- Overall Score: Weighted average (Title: 30%, Description: 30%, Headings: 20%, Images: 20%)
βοΈ Configuration
π§ Global Configuration
BetterSeo.configure do |config|
# Site-wide settings
config.site_name = "My Site"
config.default_locale = :en
config.available_locales = [:en, :it, :fr, :de, :es]
# Meta tags configuration
config.meta_tags.default_title = "Default Title"
config.meta_tags.title_separator = " | "
config.meta_tags.append_site_name = true
config.meta_tags.default_description = "Default description"
config.meta_tags.default_keywords = ["keyword1", "keyword2"]
config.meta_tags.default_author = "Your Name"
# Open Graph configuration
config.open_graph.enabled = true
config.open_graph.site_name = "My Site"
config.open_graph.default_type = "website"
config.open_graph.default_locale = "en_US"
config.open_graph.default_image.url = "https://example.com/default-og.jpg"
config.open_graph.default_image.width = 1200
config.open_graph.default_image.height = 630
# Twitter Cards configuration
config.twitter.enabled = true
config.twitter.site = "@mysite"
config.twitter.creator = "@myhandle"
config.twitter.card_type = "summary_large_image"
# Structured Data configuration
config.structured_data.enabled = true
config.structured_data.organization = {
name: "My Organization",
url: "https://example.com"
}
# Sitemap configuration (planned)
config.sitemap.enabled = false
config.sitemap.host = "https://example.com"
config.sitemap.output_path = "public/sitemap.xml"
# Robots.txt configuration (planned)
config.robots.enabled = false
config.robots.output_path = "public/robots.txt"
# Image optimization configuration (planned)
config.images.enabled = false
config.images.webp.enabled = true
config.images.webp.quality = 80
endβ Checking Configuration
# Access configuration
BetterSeo.configuration.site_name
# => "My Site"
# Check if features are enabled
BetterSeo.enabled?(:open_graph)
# => true
BetterSeo.enabled?(:sitemap)
# => false
# Reset configuration (useful for testing)
BetterSeo.reset_configuration!π» Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.
# Install dependencies
bundle install
# Run tests
bundle exec rspec
# Run tests with coverage
bundle exec rspec --format documentation
# Check code coverage
open coverage/index.htmlπ§ͺ Running Tests
The gem uses RSpec with SimpleCov for test coverage.
# Run all tests
bundle exec rspec
# Run specific test file
bundle exec rspec spec/dsl/meta_tags_spec.rb
# Run with documentation format
bundle exec rspec --format documentationπ Test Statistics:
- β 899 tests passing
- π 94.3% code coverage
- ποΈ Complete test suite for all modules
- π Production-ready quality
ποΈ Architecture
lib/better_seo/
βββ version.rb # Gem version
βββ errors.rb # Custom error classes
βββ configuration.rb # Main configuration class
βββ dsl/
β βββ base.rb # Base DSL builder class
β βββ meta_tags.rb # Meta tags DSL
β βββ open_graph.rb # Open Graph DSL
β βββ twitter_cards.rb # Twitter Cards DSL
βββ generators/
β βββ meta_tags_generator.rb # HTML meta tags generator
β βββ open_graph_generator.rb # Open Graph tags generator
β βββ twitter_cards_generator.rb # Twitter Cards generator
βββ rails/
β βββ helpers/
β βββ seo_helper.rb # Rails view helpers
βββ (planned)
βββ validators/ # SEO validators
βββ sitemap/ # Sitemap generation
π€ Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/better_seo.
- π΄ Fork it
- πΏ Create your feature branch (
git checkout -b feature/my-new-feature) - β Write tests (we maintain high test coverage)
- πΎ Commit your changes (
git commit -am 'Add some feature') - π€ Push to the branch (
git push origin feature/my-new-feature) - π Create new Pull Request
π License
The gem is available as open source under the terms of the MIT License.
π Code of Conduct
Everyone interacting in the BetterSeo project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
π Why BetterSeo?
- β Production-Ready: 899 tests, 94.3% coverage
- π Complete Solution: Everything you need for SEO in one gem
- π― Rails-First: Designed specifically for Rails applications
- π§ Flexible: Use as much or as little as you need
- π Well-Documented: Comprehensive examples and guides
- π i18n Support: Built-in internationalization
- π Secure: Automatic XSS protection
- π Modern Ruby: Built with Ruby 3.0+ and Rails 6.1+
Made with β€οΈ by Alessio Bussolari