CrackedRuby CrackedRuby

Overview

Schema.org defines a collection of schemas - structured data models - that webmasters embed in their HTML pages to describe content in a machine-readable format. Search engines including Google, Microsoft, Yahoo, and Yandex created Schema.org in 2011 as a collaborative project to standardize semantic markup across the web.

The vocabulary consists of types (like Person, Product, Event, Organization) and properties (like name, description, price, startDate) that describe entities and their attributes. Web developers add this markup to HTML using formats like JSON-LD, Microdata, or RDFa, allowing search engines to extract structured information about page content.

Schema.org markup produces rich results in search engine listings - product ratings, event dates, recipe cooking times, article authors, and business hours appear directly in search results. Social media platforms use Schema.org data for Open Graph previews. Email clients recognize event and reservation schemas for automatic calendar integration. Voice assistants parse Schema.org markup to answer spoken queries.

The vocabulary extends beyond basic types through hierarchical inheritance. Article inherits from CreativeWork, which inherits from Thing. This hierarchy allows specific types to inherit properties from parent types while adding specialized attributes. A BlogPosting inherits all Article properties while adding blog-specific fields.

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Wireless Headphones",
  "description": "High-fidelity wireless headphones",
  "brand": {
    "@type": "Brand",
    "name": "AudioTech"
  },
  "offers": {
    "@type": "Offer",
    "price": "149.99",
    "priceCurrency": "USD"
  }
}

This markup tells search engines exactly what the page describes - a product with specific attributes - rather than forcing them to infer meaning from unstructured text.

Key Principles

Schema.org operates on several fundamental principles that shape how developers implement structured data markup.

Type System and Hierarchy

The vocabulary organizes types in a hierarchical tree rooted at Thing. Every schema type inherits from Thing directly or through intermediate types. This inheritance means all types share common properties like name, description, url, and image while specialized types add domain-specific properties.

Type specificity matters. A Restaurant inherits from FoodEstablishment, which inherits from LocalBusiness, which inherits from Organization, which inherits from Thing. Each level adds relevant properties - Restaurant includes menu and servesCuisine, LocalBusiness adds openingHours and address, Organization includes founder and employee.

Property Constraints and Expected Types

Properties define expected types that specify which values the property accepts. The author property expects Person or Organization. The location property expects Place or PostalAddress or Text. Developers must provide values matching expected types or search engines may ignore the markup.

Properties can accept multiple types, but not all values are equivalent. A text string URL differs from a structured URL object. A text address differs from a PostalAddress object with structured components. Search engines prefer structured data over plain text when both options exist.

JSON-LD as Primary Format

Schema.org supports three formats for embedding markup: JSON-LD, Microdata, and RDFa. JSON-LD (JavaScript Object Notation for Linked Data) has emerged as the preferred format because it separates markup from HTML content, making maintenance simpler and reducing errors.

JSON-LD appears in script tags within the HTML head or body. Multiple script tags can contain different Schema.org objects for complex pages. The @context property specifies the Schema.org vocabulary, and @type identifies the schema type.

Graph Structure and Entity References

Complex pages often describe multiple related entities. Schema.org handles relationships through entity references using @id properties. An author property can reference a Person entity defined elsewhere on the page rather than duplicating person information.

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Person",
      "@id": "#author",
      "name": "Jane Smith",
      "jobTitle": "Senior Developer"
    },
    {
      "@type": "BlogPosting",
      "headline": "Understanding Schema.org",
      "author": {"@id": "#author"}
    }
  ]
}

The @graph property contains an array of related entities. References using @id create connections between entities without duplication.

Validation and Required Properties

Schema.org defines properties as optional, recommended, or required depending on the type. Product markup should include name, image, and offers for search engines to display rich results. Event markup requires name, startDate, and location. Missing required properties causes validation errors and prevents rich results.

Search engines validate markup against their guidelines, which sometimes exceed base Schema.org requirements. Google's requirements for JobPosting differ from general Schema.org specifications. Developers must consult platform-specific documentation alongside Schema.org vocabulary.

Ruby Implementation

Ruby web applications implement Schema.org markup through several approaches depending on the framework and application architecture.

Rails View Helpers

Rails applications typically generate Schema.org JSON-LD in view templates using helper methods that construct structured data objects.

# app/helpers/schema_helper.rb
module SchemaHelper
  def schema_org_product(product)
    {
      '@context': 'https://schema.org',
      '@type': 'Product',
      name: product.name,
      description: product.description,
      image: image_url(product.primary_image),
      brand: {
        '@type': 'Brand',
        name: product.brand.name
      },
      offers: {
        '@type': 'Offer',
        price: product.price.to_s,
        priceCurrency: 'USD',
        availability: product.in_stock? ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock'
      }
    }.to_json
  end
end

Views embed this JSON-LD in script tags:

<script type="application/ld+json">
  <%= raw schema_org_product(@product) %>
</script>

The raw helper prevents Rails from HTML-escaping the JSON, which would break the markup. The to_json method serializes the Ruby hash to valid JSON-LD.

Object-Oriented Schema Builders

More complex applications benefit from dedicated classes that encapsulate Schema.org generation logic.

# app/services/schema_org/product_builder.rb
module SchemaOrg
  class ProductBuilder
    def initialize(product)
      @product = product
    end

    def to_hash
      {
        '@context': 'https://schema.org',
        '@type': 'Product',
        name: @product.name,
        description: @product.description,
        sku: @product.sku,
        image: image_urls,
        brand: brand_schema,
        offers: offers_schema,
        aggregateRating: rating_schema,
        review: review_schemas
      }.compact
    end

    def to_json(*args)
      to_hash.to_json(*args)
    end

    private

    def image_urls
      @product.images.map { |img| Rails.application.routes.url_helpers.image_url(img) }
    end

    def brand_schema
      return nil unless @product.brand
      {
        '@type': 'Brand',
        name: @product.brand.name
      }
    end

    def offers_schema
      {
        '@type': 'Offer',
        url: Rails.application.routes.url_helpers.product_url(@product),
        price: @product.price.to_s,
        priceCurrency: @product.currency,
        availability: availability_url,
        priceValidUntil: (@product.price_valid_until || 1.year.from_now).iso8601
      }
    end

    def availability_url
      if @product.in_stock?
        'https://schema.org/InStock'
      elsif @product.discontinued?
        'https://schema.org/Discontinued'
      else
        'https://schema.org/OutOfStock'
      end
    end

    def rating_schema
      return nil unless @product.reviews.any?
      {
        '@type': 'AggregateRating',
        ratingValue: @product.average_rating.to_s,
        reviewCount: @product.reviews.count
      }
    end

    def review_schemas
      @product.reviews.recent(5).map do |review|
        {
          '@type': 'Review',
          author: {
            '@type': 'Person',
            name: review.author_name
          },
          datePublished: review.created_at.iso8601,
          reviewBody: review.content,
          reviewRating: {
            '@type': 'Rating',
            ratingValue: review.rating.to_s
          }
        }
      end
    end
  end
end

This builder pattern separates Schema.org logic from models and views, making it testable and reusable. The compact method removes nil values, preventing empty properties in the output.

Gem-Based Solutions

The schema_dot_org gem provides a DSL for building Schema.org markup declaratively.

# Using schema_dot_org gem
require 'schema_dot_org'

schema = SchemaDotOrg.new do
  product do
    name 'Wireless Headphones'
    description 'Premium wireless headphones'
    image 'https://example.com/headphones.jpg'
    
    brand organization do
      name 'AudioTech'
    end
    
    offers offer do
      price '149.99'
      price_currency 'USD'
      availability 'https://schema.org/InStock'
    end
  end
end

schema.to_json_ld

The gem handles type names, property names, and nesting, reducing boilerplate code. However, gems add dependencies and may lag behind Schema.org vocabulary updates.

Dynamic Schema Selection

Applications serving multiple content types need dynamic schema selection based on the content being displayed.

# app/services/schema_org/builder_factory.rb
module SchemaOrg
  class BuilderFactory
    def self.for(record)
      case record
      when Product
        ProductBuilder.new(record)
      when Article
        ArticleBuilder.new(record)
      when Event
        EventBuilder.new(record)
      when Person
        PersonBuilder.new(record)
      else
        GenericBuilder.new(record)
      end
    end
  end
end

# In controller or view
schema_builder = SchemaOrg::BuilderFactory.for(@resource)
@schema_markup = schema_builder.to_json

This factory pattern centralizes builder selection, making it easy to add new types and maintain consistency across the application.

Practical Examples

Article with Author and Publisher

Blog posts and articles require structured data for article search features and AMP compatibility.

# app/services/schema_org/article_builder.rb
class SchemaOrg::ArticleBuilder
  def initialize(article)
    @article = article
  end

  def to_hash
    {
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: @article.title,
      alternativeHeadline: @article.subtitle,
      description: @article.excerpt,
      image: image_object,
      datePublished: @article.published_at.iso8601,
      dateModified: @article.updated_at.iso8601,
      author: author_schema,
      publisher: publisher_schema,
      mainEntityOfPage: {
        '@type': 'WebPage',
        '@id': Rails.application.routes.url_helpers.article_url(@article)
      }
    }
  end

  private

  def image_object
    return [] unless @article.featured_image

    [{
      '@type': 'ImageObject',
      url: image_url(@article.featured_image),
      width: @article.featured_image.width,
      height: @article.featured_image.height
    }]
  end

  def author_schema
    {
      '@type': 'Person',
      name: @article.author.full_name,
      url: Rails.application.routes.url_helpers.author_url(@article.author)
    }
  end

  def publisher_schema
    {
      '@type': 'Organization',
      name: 'Tech Blog',
      logo: {
        '@type': 'ImageObject',
        url: 'https://example.com/logo.png',
        width: 600,
        height: 60
      }
    }
  end
end

Article schema requires specific image dimensions for AMP validity. The publisher logo must meet Google's size requirements (600x60 pixels or proportional).

Event with Location and Performer

Events display rich cards in search results with dates, locations, and ticket information.

# app/services/schema_org/event_builder.rb
class SchemaOrg::EventBuilder
  def initialize(event)
    @event = event
  end

  def to_hash
    {
      '@context': 'https://schema.org',
      '@type': 'Event',
      name: @event.name,
      description: @event.description,
      startDate: @event.starts_at.iso8601,
      endDate: @event.ends_at.iso8601,
      eventStatus: event_status,
      eventAttendanceMode: attendance_mode,
      location: location_schema,
      performer: performer_schemas,
      organizer: organizer_schema,
      offers: offers_schemas
    }.compact
  end

  private

  def event_status
    case @event.status
    when 'scheduled' then 'https://schema.org/EventScheduled'
    when 'cancelled' then 'https://schema.org/EventCancelled'
    when 'postponed' then 'https://schema.org/EventPostponed'
    else 'https://schema.org/EventScheduled'
    end
  end

  def attendance_mode
    case @event.attendance_mode
    when 'offline' then 'https://schema.org/OfflineEventAttendanceMode'
    when 'online' then 'https://schema.org/OnlineEventAttendanceMode'
    when 'mixed' then 'https://schema.org/MixedEventAttendanceMode'
    end
  end

  def location_schema
    if @event.online?
      {
        '@type': 'VirtualLocation',
        url: @event.virtual_url
      }
    else
      {
        '@type': 'Place',
        name: @event.venue.name,
        address: {
          '@type': 'PostalAddress',
          streetAddress: @event.venue.street_address,
          addressLocality: @event.venue.city,
          addressRegion: @event.venue.state,
          postalCode: @event.venue.zip_code,
          addressCountry: @event.venue.country_code
        }
      }
    end
  end

  def performer_schemas
    @event.performers.map do |performer|
      {
        '@type': 'Person',
        name: performer.name
      }
    end
  end

  def organizer_schema
    {
      '@type': 'Organization',
      name: @event.organizer.name,
      url: @event.organizer.website
    }
  end

  def offers_schemas
    @event.ticket_types.map do |ticket_type|
      {
        '@type': 'Offer',
        name: ticket_type.name,
        price: ticket_type.price.to_s,
        priceCurrency: 'USD',
        availability: ticket_availability(ticket_type),
        validFrom: @event.sales_start_at.iso8601,
        url: ticket_purchase_url(ticket_type)
      }
    end
  end

  def ticket_availability(ticket_type)
    ticket_type.sold_out? ? 'https://schema.org/SoldOut' : 'https://schema.org/InStock'
  end
end

Event schemas support both physical and virtual events. The eventAttendanceMode property distinguishes between offline, online, and hybrid events. Location uses VirtualLocation for online events and Place with PostalAddress for physical events.

Recipe with Nutrition Information

Recipe markup displays cooking times, ingredient lists, and nutritional information in search results.

# app/services/schema_org/recipe_builder.rb
class SchemaOrg::RecipeBuilder
  def initialize(recipe)
    @recipe = recipe
  end

  def to_hash
    {
      '@context': 'https://schema.org',
      '@type': 'Recipe',
      name: @recipe.name,
      description: @recipe.description,
      image: @recipe.images.map(&:url),
      author: {
        '@type': 'Person',
        name: @recipe.author.name
      },
      datePublished: @recipe.published_at.iso8601,
      prepTime: duration_iso8601(@recipe.prep_time_minutes),
      cookTime: duration_iso8601(@recipe.cook_time_minutes),
      totalTime: duration_iso8601(@recipe.total_time_minutes),
      recipeYield: @recipe.servings.to_s,
      recipeCategory: @recipe.category,
      recipeCuisine: @recipe.cuisine,
      keywords: @recipe.tags.join(', '),
      recipeIngredient: @recipe.ingredients.map(&:description),
      recipeInstructions: instruction_schemas,
      nutrition: nutrition_schema,
      aggregateRating: rating_schema
    }.compact
  end

  private

  def duration_iso8601(minutes)
    return nil if minutes.nil?
    "PT#{minutes}M"
  end

  def instruction_schemas
    @recipe.steps.map.with_index do |step, index|
      {
        '@type': 'HowToStep',
        position: index + 1,
        text: step.instruction,
        image: step.image&.url
      }.compact
    end
  end

  def nutrition_schema
    return nil unless @recipe.nutrition_info

    info = @recipe.nutrition_info
    {
      '@type': 'NutritionInformation',
      calories: "#{info.calories} calories",
      proteinContent: "#{info.protein_grams}g",
      fatContent: "#{info.fat_grams}g",
      carbohydrateContent: "#{info.carbohydrate_grams}g",
      fiberContent: "#{info.fiber_grams}g",
      sodiumContent: "#{info.sodium_mg}mg"
    }.compact
  end

  def rating_schema
    return nil unless @recipe.reviews.any?
    
    {
      '@type': 'AggregateRating',
      ratingValue: @recipe.average_rating.to_s,
      ratingCount: @recipe.reviews.count
    }
  end
end

Recipe schemas use ISO 8601 duration format for times. PT30M represents 30 minutes, PT1H30M represents 1 hour 30 minutes. Nutrition values include units as part of the text string.

Integration & Interoperability

Search Engine Integration

Each major search engine interprets Schema.org markup differently and supports different subsets of the vocabulary.

Google Search Console validates Schema.org markup through the Rich Results Test tool. Google supports Product, Recipe, Event, Article, FAQ, HowTo, JobPosting, LocalBusiness, and Video schemas for rich results. Other schema types get indexed but don't produce enhanced search results.

Bing Webmaster Tools includes a markup validator similar to Google's. Bing emphasizes LocalBusiness markup and supports fewer types than Google. Testing markup in both platforms prevents compatibility issues.

Yandex supports Schema.org through its Webmaster Tools platform. Yandex has stronger support for Organization and Person schemas than Google, particularly for Russian-language sites.

Social Media Open Graph Integration

Social media platforms use Open Graph protocol rather than Schema.org, but applications often need both. Rails applications can generate both simultaneously.

# app/helpers/meta_tags_helper.rb
module MetaTagsHelper
  def social_and_schema_tags(resource)
    tags = []
    
    # Open Graph tags
    tags << tag.meta(property: 'og:title', content: resource.title)
    tags << tag.meta(property: 'og:description', content: resource.description)
    tags << tag.meta(property: 'og:image', content: resource.image_url)
    tags << tag.meta(property: 'og:url', content: resource_url(resource))
    
    # Twitter Card tags
    tags << tag.meta(name: 'twitter:card', content: 'summary_large_image')
    tags << tag.meta(name: 'twitter:title', content: resource.title)
    
    # Schema.org JSON-LD
    schema_builder = SchemaOrg::BuilderFactory.for(resource)
    tags << tag.script(schema_builder.to_json.html_safe, type: 'application/ld+json')
    
    safe_join(tags, "\n")
  end
end

This helper combines Open Graph, Twitter Card, and Schema.org markup in a single method. The html_safe method allows the JSON-LD to render without escaping.

Email Client Integration

Gmail, Outlook, and other email clients parse Schema.org markup in HTML emails to create interactive features. Flight reservations, package tracking, event invitations, and restaurant reservations get special treatment when properly marked up.

# app/mailers/reservation_mailer.rb
class ReservationMailer < ApplicationMailer
  def confirmation(reservation)
    @reservation = reservation
    @schema = reservation_schema
    
    mail(to: @reservation.email, subject: 'Reservation Confirmed')
  end

  private

  def reservation_schema
    {
      '@context': 'https://schema.org',
      '@type': 'LodgingReservation',
      reservationNumber: @reservation.confirmation_code,
      reservationStatus: 'https://schema.org/ReservationConfirmed',
      underName: {
        '@type': 'Person',
        name: @reservation.guest_name
      },
      reservationFor: {
        '@type': 'LodgingBusiness',
        name: @reservation.property.name,
        address: {
          '@type': 'PostalAddress',
          streetAddress: @reservation.property.address,
          addressLocality: @reservation.property.city,
          addressRegion: @reservation.property.state,
          postalCode: @reservation.property.zip
        }
      },
      checkinTime: @reservation.checkin_date.iso8601,
      checkoutTime: @reservation.checkout_date.iso8601
    }.to_json
  end
end

The email view embeds this schema in a script tag. Gmail displays a card with check-in dates, location, and confirmation number when it detects LodgingReservation markup.

Knowledge Graph Integration

Google Knowledge Graph uses Schema.org markup to populate knowledge panels - the information boxes that appear for entities like organizations, people, and places. Consistent markup across multiple pages increases the likelihood of Knowledge Graph inclusion.

# app/services/schema_org/organization_builder.rb
class SchemaOrg::OrganizationBuilder
  def initialize(organization)
    @org = organization
  end

  def to_hash
    {
      '@context': 'https://schema.org',
      '@type': 'Organization',
      '@id': Rails.application.routes.url_helpers.root_url,
      name: @org.name,
      alternateName: @org.alternate_names,
      url: Rails.application.routes.url_helpers.root_url,
      logo: logo_schema,
      contactPoint: contact_points,
      sameAs: social_profiles,
      address: address_schema,
      founder: founder_schemas
    }.compact
  end

  private

  def logo_schema
    {
      '@type': 'ImageObject',
      url: @org.logo_url,
      width: 600,
      height: 60
    }
  end

  def contact_points
    @org.contact_methods.map do |contact|
      {
        '@type': 'ContactPoint',
        telephone: contact.phone,
        contactType: contact.contact_type,
        availableLanguage: contact.languages
      }
    end
  end

  def social_profiles
    [
      @org.twitter_url,
      @org.facebook_url,
      @org.linkedin_url,
      @org.instagram_url
    ].compact
  end

  def address_schema
    {
      '@type': 'PostalAddress',
      streetAddress: @org.street_address,
      addressLocality: @org.city,
      addressRegion: @org.state,
      postalCode: @org.zip_code,
      addressCountry: @org.country
    }
  end
end

The @id property establishes the canonical identifier for the organization. The sameAs property links social media profiles, helping Google connect different online presences to the same entity.

Tools & Ecosystem

Validation Tools

Google Rich Results Test validates Schema.org markup and shows how Google interprets the data. The tool accepts URLs or raw HTML snippets. Results indicate which schema types Google recognizes and any errors preventing rich results.

Schema Markup Validator from Schema.org provides official validation against the Schema.org specification. This validator checks technical correctness regardless of search engine support.

Structured Data Testing Tool from Bing validates markup specifically for Bing search features. Testing in multiple validators ensures cross-platform compatibility.

Ruby Gems

The schema_dot_org gem provides a DSL for building Schema.org markup programmatically. The gem handles vocabulary updates and provides validation against Schema.org specifications.

# Gemfile
gem 'schema_dot_org'

# Usage
schema = SchemaDotOrg.new do
  local_business do
    name 'Coffee Shop'
    telephone '555-1234'
    address postal_address do
      street_address '123 Main St'
      address_locality 'Portland'
      address_region 'OR'
      postal_code '97201'
    end
  end
end

The structured-data gem offers similar functionality with a different API design. Both gems reduce boilerplate but add dependencies to maintain.

The json-ld gem handles JSON-LD processing and validation. While not Schema.org-specific, it helps when working with complex JSON-LD graphs.

Browser Extensions

Schema Markup Validator browser extension highlights Schema.org markup on any webpage. The extension shows parsed markup in a sidebar, making debugging easier during development.

SEO META in 1 CLICK extension displays all meta tags including Schema.org markup for the current page. The extension aggregates Open Graph, Twitter Card, and Schema.org data in a single view.

Development Tools

The markup generator at Schema.org provides interactive forms for creating common schema types. Generated markup serves as a starting point for custom implementations.

JSON-LD Playground allows testing and debugging JSON-LD syntax. The tool validates JSON-LD structure and shows the expanded graph representation.

Rails console testing helps validate Schema.org output during development:

# In Rails console
product = Product.first
builder = SchemaOrg::ProductBuilder.new(product)
puts JSON.pretty_generate(JSON.parse(builder.to_json))

# Validate required properties
schema = builder.to_hash
required = [:name, :image, :offers]
missing = required.reject { |prop| schema[prop].present? }
puts "Missing required properties: #{missing.join(', ')}" if missing.any?

This validation catches missing required properties before deployment.

Monitoring and Analytics

Google Search Console reports on structured data through the Enhancements section. The report shows which pages have valid markup, which have errors, and which don't appear in rich results.

Performance reports filter by rich result type, showing clicks and impressions for Product, Recipe, Event, and other enhanced results. This data helps measure Schema.org impact on traffic.

Schema.org analytics tracking involves custom Google Analytics events when users interact with rich results:

// Track rich result clicks
document.querySelectorAll('.rich-result-link').forEach(function(link) {
  link.addEventListener('click', function() {
    gtag('event', 'rich_result_click', {
      'event_category': 'schema_org',
      'event_label': link.dataset.schemaType
    });
  });
});

This tracking connects Schema.org implementation to user behavior metrics.

Common Pitfalls

Missing Required Properties

Schema.org types have required properties that must be present for rich results. Product requires name, image, and offers. Event requires name, startDate, and location. Omitting required properties causes validation failures.

# Incorrect - missing required properties
{
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: 'Widget'
  # Missing: image, offers
}

# Correct - includes required properties
{
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: 'Widget',
  image: 'https://example.com/widget.jpg',
  offers: {
    '@type': 'Offer',
    price: '29.99',
    priceCurrency: 'USD'
  }
}

Validation errors don't prevent indexing but prevent rich results from appearing. Testing markup in Google Rich Results Test catches missing properties before deployment.

Incorrect Property Types

Properties expect specific value types. Providing a string when the property expects an object causes parsing errors.

# Incorrect - author as string
{
  '@type': 'Article',
  author: 'Jane Smith'  # String instead of Person object
}

# Correct - author as Person object
{
  '@type': 'Article',
  author: {
    '@type': 'Person',
    name: 'Jane Smith'
  }
}

Some properties accept multiple types. The identifier property accepts Text or PropertyValue or URL. When multiple types are valid, choose the most specific type available.

Invalid Date Formats

Schema.org requires ISO 8601 format for dates and times. Common date format mistakes break validation.

# Incorrect date formats
startDate: '01/15/2025'        # US format
startDate: '15-01-2025'        # European format
startDate: 'January 15, 2025'  # Human-readable format

# Correct ISO 8601 formats
startDate: '2025-01-15'                    # Date only
startDate: '2025-01-15T14:30:00Z'         # Date with UTC time
startDate: '2025-01-15T14:30:00-05:00'    # Date with timezone offset

Rails provides iso8601 method for ActiveSupport::TimeWithZone objects:

event.starts_at.iso8601  # => "2025-01-15T14:30:00Z"

Duration formats also use ISO 8601. PT30M represents 30 minutes, PT2H represents 2 hours, PT1H30M represents 1 hour 30 minutes.

HTML Escaping in JSON-LD

Rails automatically HTML-escapes output for security. JSON-LD requires unescaped output to remain valid JSON.

# Incorrect - escaped JSON breaks parsing
<script type="application/ld+json">
  <%= @schema.to_json %>
</script>

# Correct - raw prevents escaping
<script type="application/ld+json">
  <%= raw @schema.to_json %>
</script>

The raw helper prevents HTML escaping. The html_safe method achieves the same result. Never use raw with user-provided content - only with internally generated Schema.org markup.

Duplicate Properties

Schema.org markup should not include duplicate properties with different values. Search engines may ignore one value or reject the entire markup.

# Incorrect - duplicate name properties
{
  '@type': 'Product',
  name: 'Widget Pro',
  name: 'Professional Widget'  # Duplicate property
}

# Correct - single name property
{
  '@type': 'Product',
  name: 'Widget Pro'
}

Ruby hashes naturally prevent duplicate keys, but building markup from multiple sources can create duplicates. Using merge or compact methods helps avoid this issue.

Wrong Availability URLs

Product availability must use Schema.org enumeration URLs, not strings.

# Incorrect - string values
availability: 'in stock'
availability: 'InStock'

# Correct - Schema.org URLs
availability: 'https://schema.org/InStock'
availability: 'https://schema.org/OutOfStock'
availability: 'https://schema.org/PreOrder'

Many Schema.org properties accept only enumerated values from specific vocabularies. The specification documentation lists valid values for each enumeration type.

Inconsistent Currency Handling

Price properties require string values, not numbers. Currency codes must follow ISO 4217.

# Incorrect - numeric price
{
  price: 29.99,           # Number instead of string
  priceCurrency: 'USD'
}

# Correct - string price
{
  price: '29.99',         # String representation
  priceCurrency: 'USD'    # ISO 4217 code
}

Rails number formatting can introduce commas or currency symbols. Convert prices to plain decimal strings for Schema.org markup:

price: product.price.to_s  # => "29.99"

Missing Context or Type

Every Schema.org object requires @context and @type properties. Omitting these makes the markup invalid.

# Incorrect - missing context
{
  '@type': 'Product',
  name: 'Widget'
}

# Incorrect - missing type
{
  '@context': 'https://schema.org',
  name: 'Widget'
}

# Correct - includes both
{
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: 'Widget'
}

The @context property identifies the vocabulary (almost always https://schema.org). The @type property specifies the schema type.

Reference

Common Schema Types

Type Use Case Required Properties
Product E-commerce items name, image, offers
Article Blog posts, news headline, datePublished, author
Event Concerts, conferences name, startDate, location
Recipe Cooking instructions name, image, recipeIngredient
LocalBusiness Physical stores name, address, telephone
Person Author profiles name
Organization Companies, nonprofits name, url
WebPage General pages name, url
FAQPage FAQ sections mainEntity
JobPosting Job listings title, description, datePosted
Review Product reviews itemReviewed, reviewRating
Video Video content name, description, thumbnailUrl

Property Value Types

Property Expected Type Example
name Text Wireless Headphones
description Text High-quality wireless headphones
url URL https://example.com/product
image URL or ImageObject https://example.com/image.jpg
price Text 149.99
priceCurrency Text USD
datePublished Date or DateTime 2025-01-15T14:30:00Z
author Person or Organization Person object
address PostalAddress or Text PostalAddress object
telephone Text +1-555-123-4567
email Text contact@example.com

Date and Time Formats

Format Pattern Example
Date YYYY-MM-DD 2025-01-15
DateTime UTC YYYY-MM-DDTHH:MM:SSZ 2025-01-15T14:30:00Z
DateTime Offset YYYY-MM-DDTHH:MM:SS±HH:MM 2025-01-15T14:30:00-05:00
Duration PTnHnM PT2H30M
Time HH:MM:SS 14:30:00

Availability Enumerations

Value URL Use Case
In Stock https://schema.org/InStock Available for purchase
Out of Stock https://schema.org/OutOfStock Temporarily unavailable
Discontinued https://schema.org/Discontinued No longer produced
Pre-Order https://schema.org/PreOrder Available for pre-order
Pre-Sale https://schema.org/PreSale Pre-release sales
Sold Out https://schema.org/SoldOut Limited quantity exhausted

Event Status Enumerations

Value URL Use Case
Scheduled https://schema.org/EventScheduled Event will occur
Cancelled https://schema.org/EventCancelled Event cancelled
Postponed https://schema.org/EventPostponed Event delayed
Rescheduled https://schema.org/EventRescheduled New date scheduled

Ruby Helper Methods

Method Purpose Example
to_json Convert hash to JSON hash.to_json
iso8601 Format datetime time.iso8601
raw Prevent HTML escaping raw json_string
html_safe Mark string as safe string.html_safe
compact Remove nil values hash.compact
to_s Convert to string number.to_s

Validation Checklist

Check Validation
Context present @context key exists
Type specified @type key exists
Required properties All required fields present
Date format ISO 8601 compliance
Price format String representation
Currency code ISO 4217 compliance
URLs absolute Full URLs with protocol
Images accessible URLs return 200 status
Enumerations Schema.org URLs used
JSON validity Valid JSON syntax