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 title
  • page_description(text = nil) - Set/get page description
  • page_image(url = nil) - Set/get page image for social sharing
  • open_graph_and_twitter_tags - Renders all OpenGraph and Twitter Card tags
  • json_ld_tag(schema_hash) - Renders JSON-LD structured data
  • canonical_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.png from 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 alternateName if 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 isPartOf relationship
  • 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 feeds json_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_lemmas in Resources::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 generation
  • verbs_schema_helper_test.rb - Tests JSON-LD schema generation

Run tests:

bin/rails test test/helpers/

Maintenance

When Adding New Languages

  1. Add language code to TARGET_LANGUAGES constant
  2. Add translation to config/locales/*.yml files
  3. 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

  1. Monitor Google Search Console
  2. Track organic traffic in analytics
  3. 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
  1. 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
  2. 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

  1. Generate initial sitemap in production:
    heroku run rake sitemap:refresh
    
  2. Verify it was created:
    heroku run ls -la public/sitemaps/
    
  3. Test the sitemap URL:
    curl https://immersive-app.com/sitemaps/sitemap.xml.gz
    

Additional Recommendations

Future Enhancements

  1. Hreflang Tags - For multi-language support
    <link rel="alternate" hreflang="en" href="..." />
    <link rel="alternate" hreflang="fr" href="..." />
    
  2. Rich Results Testing - Use Google’s Rich Results Test tool to validate JSON-LD

  3. Performance Optimization - Core Web Vitals impact SEO
    • Image optimization
    • Lazy loading
    • Caching strategies
  4. Content Updates - Fresh content ranks better
    • User-generated example sentences
    • Community contributions
    • Regular verb database updates
  5. Mobile Optimization - Mobile-first indexing
    • Responsive design (already implemented)
    • Touch-friendly UI
    • Fast mobile load times

Resources

Troubleshooting

JSON-LD Not Appearing

  1. Check browser dev tools console for JavaScript errors
  2. Validate with Google Rich Results Test
  3. Ensure .to_json.html_safe is used

Meta Tags Not Rendering

  1. Check if helpers are included in ApplicationHelper
  2. Verify page_title, page_description are called before layout renders
  3. Use browser “View Source” to verify tags in HTML

Sitemap Issues

  1. Ensure public/ directory is writable
  2. Verify routes exist for all sitemap URLs
  3. Check sitemap.xml syntax with validators
  4. Verify the URL in config/sitemap.rb matches your production domain

This site uses Just the Docs, a documentation theme for Jekyll.