SEO Implementation Guide
This document explains the SEO improvements implemented for the Immersive app, with a focus on verb and conjugation pages.
Overview
The SEO implementation includes:
- Meta tags (OpenGraph, Twitter Cards, description)
- JSON-LD structured data (Schema.org)
- Sitemap generation for verbs and conjugations
- Canonical tags and proper heading hierarchy
Components
1. Meta Tags Helper (app/helpers/meta_tags_helper.rb)
Provides methods for setting and rendering SEO meta tags:
Methods
page_title(text = nil)- Set/get page titlepage_description(text = nil)- Set/get page descriptionpage_image(url = nil)- Set/get page image for social sharingopen_graph_and_twitter_tags- Renders all OpenGraph and Twitter Card tagsjson_ld_tag(schema_hash)- Renders JSON-LD structured datacanonical_tag(url = nil)- Renders canonical URL tag
Default Values
- Title: “Immersive – Learn verbs and conjugations in multiple languages”
- Description: “Immersive helps you master verbs and conjugations with audio, examples and decks across multiple languages.”
- Image:
icon.pngfrom assets
Usage Example
<% page_title "Custom Page Title" %>
<% page_description "Custom page description for SEO" %>
<% page_image "https://example.com/custom-image.png" %>
2. Verbs Schema Helper (app/helpers/verbs_schema_helper.rb)
Provides JSON-LD structured data generation for verbs and conjugations following Schema.org standards.
Methods
json_ld_for_verb(verb_lemma)
Generates Schema.org LearningResource structured data for a verb page.
Includes:
- Verb name and language
- Educational metadata (type, use, accessibility)
- Translation as
alternateNameif available - URL to the verb page
Example output:
{
"@context": "https://schema.org",
"@type": "LearningResource",
"name": "avoir – French conjugation",
"inLanguage": "fr",
"url": "https://example.com/verbs/1",
"alternateName": "to have",
"educationalUse": "practice",
"learningResourceType": "conjugation table",
"isAccessibleForFree": true
}
json_ld_for_conjugation(conjugation, verb_lemma)
Generates Schema.org LearningResource structured data for a specific conjugation.
Includes:
- Conjugation form and metadata (mood, tense)
- Relationship to parent verb (
isPartOf) - Educational metadata
Example output:
{
"@context": "https://schema.org",
"@type": "LearningResource",
"name": "ai – avoir (indicatif present)",
"inLanguage": "fr",
"url": "https://example.com/verbs/1/conjugations/1",
"educationalUse": "practice",
"learningResourceType": "example sentence",
"isAccessibleForFree": true,
"isPartOf": {
"@type": "LearningResource",
"name": "avoir – French conjugation",
"url": "https://example.com/verbs/1"
}
}
json_ld_breadcrumbs(items)
Generates Schema.org BreadcrumbList for navigation hierarchy.
Parameters:
items: Array of{name: "Name", url: "URL"}hashes
Example:
json_ld_breadcrumbs([
{ name: "Home", url: root_url },
{ name: "French Verbs", url: verbs_url },
{ name: "avoir", url: verb_url(@verb_lemma) }
])
3. Application Layout Updates
The main layout (app/views/layouts/application.html.erb) now includes:
<title><%= page_title %></title>
<%= open_graph_and_twitter_tags %>
<%= canonical_tag %>
<%= yield :head %>
This ensures every page has proper meta tags and can add page-specific JSON-LD in the :head content block.
4. View Template Updates
Verbs Index (app/views/resources/verbs/index.html.erb)
SEO Improvements:
- Dynamic page title including language name
- SEO-optimized description
- H1 tag (changed from H2) for proper hierarchy
- CollectionPage JSON-LD schema
Example:
<% page_title "#{language_name(target_language)} Verbs – Learn conjugations with audio and examples | Immersive" %>
<% page_description "Browse and learn #{language_name(target_language)} verbs..." %>
<% content_for :head do %>
<%= json_ld_tag({
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": "#{language_name(target_language)} Verbs",
...
}) %>
<% end %>
Verbs Show (app/views/resources/verbs/show.html.erb)
SEO Improvements:
- Verb-specific title with language
- Description including translation if available
- LearningResource JSON-LD schema
- Breadcrumb navigation schema
Example:
<%
title = "#{@verb_lemma.lemma} – #{language_name(@verb_lemma.language)} conjugation..."
page_title title
%>
<% content_for :head do %>
<%= json_ld_tag(json_ld_for_verb(@verb_lemma)) %>
<%= json_ld_tag(json_ld_breadcrumbs([...])) %>
<% end %>
Conjugations Show (app/views/resources/conjugations/show.html.erb)
SEO Improvements:
- Conjugation-specific title
- Educational description
- LearningResource JSON-LD with
isPartOfrelationship - Full breadcrumb path
5. Sitemap Configuration (config/sitemap.rb)
Generates XML sitemaps for common verbs across all target languages. Focuses on high-value pages for SEO.
Strategy:
- Only includes common verbs (
common: true) - the most valuable for SEO - Only includes verb lemma pages (conjugation tables are on the verb page)
- Excludes individual conjugation URLs to keep sitemap manageable
- This keeps sitemap manageable (hundreds of URLs instead of millions)
Configuration:
SitemapGenerator::Sitemap.default_host = "https://immersive-app.com"
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
SitemapGenerator::Sitemap.ping_search_engines = false
SitemapGenerator::Sitemap.create do
add root_path, priority: 1.0, changefreq: "weekly"
TARGET_LANGUAGES.each do |language_code|
add verbs_path, priority: 0.9, changefreq: "weekly"
# Only include common verbs - these are the most valuable for SEO
VerbLemma.where(language: language_code).common_verbs.find_each do |verb_lemma|
add verb_path(verb_lemma),
priority: 0.8,
changefreq: "monthly",
lastmod: verb_lemma.updated_at
end
end
end
Priority Hierarchy:
- Homepage: 1.0
- Verbs index: 0.9
- Individual verbs (common only): 0.8
Generating Sitemap:
rake sitemap:refresh
Sitemap Location: public/sitemaps/sitemap.xml.gz (gzipped, in sitemaps subdirectory)
6. Robots.txt
Updated public/robots.txt to include sitemap reference:
User-agent: *
Allow: /
Sitemap: https://immersive-app.com/sitemaps/sitemap.xml.gz
Best Practices Implemented
1. Heading Hierarchy
- Each page has exactly one H1 tag
- Verbs index uses H1 for main heading
- Verb show pages use H1 for the verb title; conjugation show pages use H1 for the conjugated form
2. Meta Description Guidelines
- Unique for each page
- 150-160 characters recommended
- Includes target keywords naturally
- Descriptive and actionable
3. Title Tag Guidelines
- Unique for each page
- 50-60 characters recommended
- Primary keyword near the beginning
- Brand name at the end (separated by –)
4. Structured Data
- Uses Schema.org vocabulary
- Implements LearningResource type (appropriate for educational content)
- Includes language metadata (
inLanguage) - Shows content hierarchy with
isPartOf - Uses BreadcrumbList for navigation
5. Internal Linking
- Breadcrumbs provide clear navigation paths: a visible trail (
app/views/shared/_breadcrumbs.html.erb+app/assets/stylesheets/breadcrumbs.css) is rendered on verb and conjugation pages from the same items array that feedsjson_ld_breadcrumbs, so the visible nav and the BreadcrumbList structured data cannot drift apart - Related content linked appropriately: each verb page renders a “More verbs” section (
@related_verb_lemmasinResources::VerbsController#show) linking to common verbs of the same language nearest by frequency, so crawlers can walk the whole catalogue verb-to-verb instead of relying on the paginated index - Anchor text is descriptive
6. Accessibility = SEO
- Semantic HTML improves both
- Alt text for images (already implemented in views)
- Proper heading hierarchy aids screen readers and search engines
Monitoring: Lighthouse SEO check
.github/workflows/seo-check.yml runs Lighthouse CI in SEO-only mode against
the live site (vars.SEO_BASE_URL, defaulting to https://immersive-app.com),
using .lighthouserc.js for the audited URLs and assertions. It fails when a
page’s SEO score drops below 0.9. The audited pages default to / and
/verbs; set the SEO_URL_PATHS repository variable to a comma-separated
path list (e.g. /,/verbs,/verbs/123) to also audit a representative verb
show page without hardcoding an unstable numeric id in the repo. Deploys are tag-based, so the workflow runs
on a weekly schedule and via workflow_dispatch (run it after a deploy) rather
than on pull requests, which cannot change production.
Testing
Unit Tests
Tests are located in test/helpers/:
meta_tags_helper_test.rb- Tests meta tag generationverbs_schema_helper_test.rb- Tests JSON-LD schema generation
Run tests:
bin/rails test test/helpers/
Maintenance
When Adding New Languages
- Add language code to
TARGET_LANGUAGESconstant - Add translation to
config/locales/*.ymlfiles - Regenerate sitemap:
rake sitemap:refresh
When Adding New Verb Pages
Sitemap automatically includes new common verbs on next regeneration. Only verbs marked with common: true are included in the sitemap.
Monitoring SEO Performance
- Monitor Google Search Console
- Track organic traffic in analytics
- Review meta tag rendering with browser dev tools
Scheduled Sitemap Updates
Important: Sitemaps should be generated in production (not development) because:
- Production has the actual data
- URLs must use the production domain
- Generated files are not committed to git
Heroku Setup (Recommended)
- Add to Heroku Scheduler:
- Go to Heroku Dashboard → Your App → Scheduler
- Add new job:
- Schedule: Daily at 3:00 AM UTC (or your preferred time)
- Run Command:
rake sitemap:refresh
- Manual generation (for testing or immediate updates):
heroku run rake sitemap:refresh
Alternative: Using recurring.yml (if using Solid Queue)
production:
sitemap_refresh:
command: "rake sitemap:refresh"
schedule: at 3am every day
First-Time Setup
- Generate initial sitemap in production:
heroku run rake sitemap:refresh - Verify it was created:
heroku run ls -la public/sitemaps/ - Test the sitemap URL:
curl https://immersive-app.com/sitemaps/sitemap.xml.gz
Additional Recommendations
Future Enhancements
- Hreflang Tags - For multi-language support
<link rel="alternate" hreflang="en" href="..." /> <link rel="alternate" hreflang="fr" href="..." /> -
Rich Results Testing - Use Google’s Rich Results Test tool to validate JSON-LD
- Performance Optimization - Core Web Vitals impact SEO
- Image optimization
- Lazy loading
- Caching strategies
- Content Updates - Fresh content ranks better
- User-generated example sentences
- Community contributions
- Regular verb database updates
- Mobile Optimization - Mobile-first indexing
- Responsive design (already implemented)
- Touch-friendly UI
- Fast mobile load times
Resources
Troubleshooting
JSON-LD Not Appearing
- Check browser dev tools console for JavaScript errors
- Validate with Google Rich Results Test
- Ensure
.to_json.html_safeis used
Meta Tags Not Rendering
- Check if helpers are included in
ApplicationHelper - Verify
page_title,page_descriptionare called before layout renders - Use browser “View Source” to verify tags in HTML
Sitemap Issues
- Ensure
public/directory is writable - Verify routes exist for all sitemap URLs
- Check sitemap.xml syntax with validators
- Verify the URL in
config/sitemap.rbmatches your production domain