# Adding Structured Data to a Rails application

When it comes to SEO, content is king.

However, content is not exclusively what can be seen or read, metadata is also part of the content, and it helps us better communicate what the content is about and what entities are part of it.

In this article, we will learn **how to add structured data, a.k.a. schema markup, to a Rails application.**

Let's start by understanding what structured data is, feel free to skip to the application setup if you're already familiar with the topic.

## What is structured data?
Schema markup, also known as structured data, is metadata that we can add to pages to help search engines understand what our content is about.

We've learned about [meta tags in Rails apps,](https://avohq.io/blog/meta-tags-rails) which serve a similar purpose but in a different manner.

In contrast to meta tags, structured data is a convened-upon method to categorize content using a set of entities and their properties.

This allows search engines to understand the content's context and the relationships within its entities.

Structured data can tell search engines that a page is about:

- A product with price, availability, reviews, discounts, etc.
- A book with an ISBN, several pages, an abstract, etc.
- A recipe with ingredients, cooking time, calories, etc.
- A person with a name, birthdate, email, phone number, etc.

However, structured data doesn't exclusively represent what a page is about. It can also represent entities that are present within a page as a part of the whole:

- Breadcrumbs.
- Frequently Asked Questions.
- Multimedia: audio, image, or video objects.
- Comments, reviews, quotations, maps, etc.
- A logo, an organization, among others.

Using this data and its relations, search engines can better understand what our content is about and what entities are represented within it.

{{info}}
Schema markup, developed by Google, Bing, and Yahoo in 2011, is an initiative to standardize the way structured data is defined on the internet. It helps give semantic meaning and context to entities in webpages using a common language. As of 2024, over 45 million web domains markup web pages using it.
{{/info}}
## Why add structured data to my app?
There are two main reasons why adding structured data to our application or website is desirable:

1. It can help search engines and other bots better understand what our content is about and what entities it represents and relates to.
2. It can improve our site's visibility and, with it, boost the click-through rate we get on the SERPs, helping us rank better and get more traffic.

A more in-depth understanding of our content and its entities can help us search engines rank our content for queries we wouldn't otherwise rank for.

{{warning}}
Structured data presents many benefits, but **it's not considered a ranking factor.** It can indirectly help us rank better if the improved CTR results in a good user experience, which signals to search engines that our content fulfills what users were expecting. This means that we can add structured data to a webpage without any noticeable improvements in rankings or traffic. **Consider it an indirect means to an end.**
{{/warning}}

## What will we build?
For this article, we will be building upon Buk, a dummy book directory application we already used for articles like [adding a sitemap to a Rails app](https://avohq.io/blog/sitemap-for-rails-applications).

![Buk application home page](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6Mzk4LCJwdXIiOiJibG9iX2lkIn19--3606725dbcd2fc348e2ddbd9b24bafce8f10cd7f/buk-home-page.webp)


We will be adding structured data to this application to enrich the information we give to search engines.

Even though we could add the structured data using partials or even add it directly to the `Book` or `Article` models, we will be using the `schema_dot_org` gem which helps us with data validation and typing which can be crucial, especially if our site has many pages and entities.

Concretely, we will add the following entities to the appropriate pages:

- WebSite with the SearchAction to represent the ability to search for books from the homepage.
- Organization
- Breadcrumb
- Book
- Review

If you like to see how everything is implemented, you can check the [application repository](https://github.com/erozas/buk).

## Setup 
We can use many formats to represent structured data for our Rails application.

But, to make things simple and follow search engine's recommendations, we will stick to `JSON-LD` which stands for **“JavaScript Object Notation for Linked Data”**.

This means that we declare structured data as JSON and include it within the `head` of the document we're trying to add structured data to.

There are many ways to achieve this with Rails. We will focus on how to add SD with and without a gem:

### Adding structured data without a gem
In Rails, there are a couple of ways we can add SD without using a gem.

The first way is to declare the data as a hash in a given model:

```ruby
class User
  def to_json_ld
    {
      "@context": "https://schema.org",
      "@type": "Person",
      "name": username,
      "url": "https://avohq.io/accounts/#{id}"
    }
  end
end
```

Then, we can add this `Person` information to another type schema, as an author of an `Article` for example, or add it directly to the `<head>` of a page:

```erb
<%= content_for(:head) do %>
  <script type="application/ld+json">
    <%= @user.to_json_ld %>
  </script>
<% end %>
```

Another way is to use namespaced POROs to represent the entities we want to include as structured data.

Every object of this type would have to be defined within the `Schema` namespace to avoid collisions, and it would have to receive an object on the constructor and respond to the `to_json_ld` method:

```ruby
module Schema
  class ImageObject
    include Rails.application.routes.url_helpers

    def initialize(image)
      @image = image
    end

    def to_json_ld
      {
        "@context": "https://schema.org",
        "@type": "ImageObject",
        "url": rails_blob_url(@image),
        "height": @image.metadata[:height],
        "width": @image.metadata[:width]
      }
    end

    private

    def default_url_options
      Rails.application.config.action_mailer.default_url_options || { host: 'localhost:3000' }
    end
  end
end
```

```ruby
module Schema
  class Article
    def initialize(article)
      @article = article
    end

    def to_json_ld
      {
        "@context": "https://schema.org",
        "@type": "Article",
        "author": @article.user.to_json_ld,
        "name": @article.title,
        "url": "https://avohq.io/blog/#{@article.slug}",
        "image": Schema::ImageObject.new(@article.cover).to_json_ld,
        "description": @article.excerpt,
        "articleBody": @article.content,
        "publisher": {
          "@type": "Organization",
          "name": "AvoHQ",
          "url": "https://avohq.io"
        },
        "datePublished": @article.published_at,
        "dateModified": @article.updated_at,
      }
    end
  end
end
```

This will produce a result that looks like the following:

```ruby
{:@context=>"https://schema.org",
 :@type=>"Article",
 :author=>{:@context=>"https://schema.org", :@type=>"Person", :name=>"erozas", :url=>"https://avohq.io/accounts/1"},
 :name=>"Microservices: When to Adopt and When to Avoid",
 :url=>"https://avohq.io/blog/microservices-when-to-adopt-and-when-to-avoid",
 :image=>
  {:@context=>"https://schema.org",
   :@type=>"ImageObject",
   :url=>"http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjEsInB1ciI6ImJsb2JfaWQifX0=--0c8a98de44ad2cc4a753561d6e8b54a53bf7f5bc/3.webp",
   :height=>630,
   :width=>1200},
 :description=>"A pragmatic guide to evaluating whether microservices architecture is right for your organization and project needs.",
 :articleBody=>
  "Microservices architecture has become something of a buzzword in software development circles, often presented as the inevitable evolution beyond monolithic applications. However, the reality is more nuanced—microservices offer significant benefits in certain contexts while introducing unnecessary complexity in others.",
 :publisher=>{:@type=>"Organization", :name=>"AvoHQ", :url=>"https://avohq.io"},
 :datePublished=> "2025-03-21 01:46:12.979479000 UTC +00:00",
 :dateModified=> "2025-03-22 01:54:11.865287000 UTC +00:00"}
```

Notice that we're using a mix of techniques: a call to `.to_json_ld` on the `User` model for the `Person` schema and the `Schema::Image` PORO to represent the `ImageObject` schema.

We would then have to define a class for every possible schema type.

Solving the problem like this is perfectly fine but, unless we're cautious, we can create invalid schemas or provide the wrong types of values.

That's why we will use a gem that helps us with validation and typing:

{{info}}
We could also define schemas directly within partials and call it a day. However, if we foresee our application needing to define more schemas in the future, using POROs or a gem is probably a better decision to keep the code organized and more maintainable.
{{/info}}

### Adding structured data using a gem
We will use the `schema_dot_org` gem, which allows us to define and validate the structured data to make sure we're rendering the right things.

Let's start by adding and installing the gem:

```console
$ bundle add schema_dot_org && bundle install
```

At the date of writing this, the gem produces invalid results because it adds a `contextForValidation` attribute to the generated JSON.

There's an open PR that resolves the issue, but it hasn't been merged yet, so we will be using a fork of the gem:

```ruby
gem "schema_dot_org", git: "https://github.com/erozas/schema-dot-org"
```

Besides addressing the issue with the `contextForValidation`, this fork also will help us later when we start defining the schemas.

But let's not get ahead of ourselves. Let's start by seeing how the gem works:

The gem comes with some predefined entities that we can find under the `lib/schema_dot_org` directory of the gem.

Some of these entities are `Person`, `Place`, `Product`, `WebSite`, `Organization`, `SearchAction`, `ItemList`, among others.

But, unlike with the previous PORO approach, the classes that define the entities are not expecting model instances or other Ruby objects, but the individual attributes like `name` or `url`.

To define a `Person` we would do something like this:

```ruby
person = SchemaDotOrg::Person.new(name: "Exequiel Rozas", url: "https://erozas.com", same_as: ["https://github.com/erozas"])
person.to_json_ld
```

Which produces the following output:

```ruby
"<script type=\"application/ld+json\">\n{\"@context\":\"https://schema.org\",\"@type\":\"Person\",\"name\":\"Exequiel Rozas\",\"url\":\"https://erozas.com\",\"sameAs\":[\"https://github.com/erozas\"]}\n</script>"
```

We can then include that directly in the document's `<head>` or use the `content_for` helper:

```erb
<%= content_for(:head) do %>
  <%= @person %>
<% end %>
```

Like we said before, the gem only has a limited set of schema types, but we can extend them by defining new classes within the `SchemaDotOrg` namespace that inherit from `SchemaType`.

Let's start by adding an `Article` schema that also receives an `ImageObject`:

```ruby
# app/models/schema_dot_org/image_object.rb
module SchemaDotOrg
  class ImageObject < SchemaType
    validated_attr :url, type: String
    validated_attr :height, type: Integer
    validated_attr :width, type: Integer
  end
end
```

We can add many more attributes for the [ImageObject](https://schema.org/ImageObject), but we will stick to these because the process is basically the same for every type of attribute we want to add.

Now, we define the `Article` schema:

```ruby
# app/models/schema_dot_org/article.rb
module SchemaDotOrg
  class Article < SchemaType
    validated_attr :author, type: SchemaDotOrg::Person, allow_nil: true
    validated_attr :name, type: String
    validated_attr :url, type: String, allow_nil: true
    validated_attr :image, type: SchemaDotOrg::ImageObject, allow_nil: true
    validated_attr :article_body, type: String, allow_nil: true
    validated_attr :description, type: String, allow_nil: true
    validated_attr :publisher, type: SchemaDotOrg::Organization, allow_nil: true
    validated_attr :date_published, type: String, allow_nil: true
    validated_attr :date_modified, type: String, allow_nil: true
  end
end
```

Besides primitives like `String` it can also receive a `Person`, an `Organization` or an `ImageObject`.

We can define as many schemas as we like, as long as we follow the conventions.

Now that we know how to use the `schema_dot_org` gem, let's start by adding the site-related structured data:

{{info}}
Besides the JSON-LD format, we can add structured data to a page using the `Microdata` and `RDFa` formats, which use attributes to represent the desired entities and their properties. However, both of these formats have their limitations and are more error-prone. **Search engines recommend JSON-LD for structured data.**
{{/info}}

### Graph array to group entities
Occasionally, we need to define various entities that might be grouped together.

For example, a page representing an article might include the `WebSite`, `WebPage`, `Article` and `BreadcrumbList` entities.

To achieve this out of the box, we would need to define many `<script>` tags, which is redundant and clutters the page with unnecessary code.

Luckily, we can use the `@graph` property to declare a set of entities that are grouped together via the `@id` property.

Out of the box, the `schema_dot_org` gem doesn't have support for `@graph` or `@id` properties, but the fork we installed does.

The way to declare a graph is by first declaring schemas for individual properties and passing them to the `SchemaDotOrg.create_graph` class method:

```ruby
person = SchemaDotOrg::Person.new(name: "Exequiel Rozas", id: "https://avohq.io/#person")
site = SchemaDotOrg::WebSite.new(name: "Avo HQ", url: "https://avohq.io", id: "https://avohq.io/#website" )
graph = SchemaDotOrg.create_graph(person, site)
graph.to_json_ld
```

Which produces the following output:

```javascript
<script type=\"application/ld+json\">\n{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Person\",\"@id\":\"https://avohq.io/#person\",\"name\":\"Exequiel Rozas\"},{\"@type\":\"WebSite\",\"@id\":\"https://avohq.io/#website\",\"name\":\"Avo HQ\",\"url\":\"https://avohq.io\"}]}\n</script>
```

Now, we can define multiple related schemas within a single object and without the need to duplicate tags.

{{info}}
The `@id` property is used to uniquely identify an item within a webpage. It allows our properties to reference each other. For example, if we had an `Article` entity with an `author` property, we could give the `author` an `@id` that references the `Person` entity without the need to duplicate anything, thus communicating to search engines that the author of the article is actually the person defined below.
{{/info}}

## Schema types
There are currently around 800 different schema types that we can use to define entities in our applications.

Naturally, not all of them are supported by search engines, but adding the appropriate schema to our web pages can make a difference.

However, some schemas are a safe bet for almost every website.

We will start by covering those and then adding appropriate schemas for the example application shown above.

### Website and organization schemas

The first structured data we might want to add is the one that's related to the site in general.

For this, we will use four schemas: `Organization`, `WebSite`, `WebPage` and `ImageObject`.

The [organization](https://schema.org/Organization) contains information about the actual company behind the website, the [website](https://schema.org/WebSite) represents the actual site or collection of web pages, the [web page](https://schema.org/WebPage) represents the current page which is the home page or any other static page on our website, and the `ImageObject` represents the OpenGraph image.

The gem doesn't have schemas for the `WebPage` and the `ImageObject` but we've already defined the latter, so let's create the `WebPage` schema:

```ruby
# app/models/schema_dot_org/web_page.rb
module SchemaDotOrg
  class WebPage < SchemaType
    validated_attr :name, type: String
    validated_attr :url, type: String
    validated_attr :description, type: String, allow_nil: true
    validated_attr :inLanguage, type: String, allow_nil: true
  end
end

```

Now, we will define the schema in a controller action. For our example, the controller is the `StaticController` and we will use a filter to assign the schema to an instance variable:

```ruby
class StaticController < ApplicationController
  before_action :set_schema, only: [:home]

  def home
    @books = Book.all
  end

  private

  def set_schema
    organization = SchemaDotOrg::Organization.new(
      name: "Buk",
      url: "https://buk.com",
      email: "hello@buk.com",
      logo: "https://buk.com/logo.png",
      telephone: "+1234567890"
    )

    web_site = SchemaDotOrg::WebSite.new(
      url: "https://buk.com",
      name: "Buk",
      about: "Buk is a book sharing platform where users can gather and share their favorite books with each other.",
      publisher: organization
    )

    web_page = SchemaDotOrg::WebPage.new(
      url: "https://buk.com",
      name: "Buk",
      description: "Rediscover the joy of reading using Buk to explore a  world of possibilities with our carefully curated collection of books.",
      in_language: "en-US",
    )

    @schema = SchemaDotOrg::GraphContainer.new([organization, web_site, web_page])
  end
end
```

Next, we create a partial that we will use to display the schema script on the document's head whenever rendered on a view:

```erb
<%# app/views/shared/_schema.html.erb %>
<%= content_for :head do %>
  <%= schema %>
<% end %>
```

Now, we can render the partial in the `home` page and have the schema show in the document's head:

```erb
<%# app/views/static/home.html.erb %>
<%= render "shared/schema", schema: @schema %>
```

We can avoid using the `schema` partial and just declare the `content_for :head` on every page we need the schema to show up. That's ultimately up to taste.

As a side note, we can add this schema to other pages of our site by changing the `WebPage` schema and using specific descriptions for those pages.

{{info}}
Enjoying this article? You might enjoy Avo, the ultimate [Rails admin](https://avohq.io/rails-admin) and internal tooling gem for Ruby on Rails that can help you build better Rails apps faster.
{{/info}}

### Articles
After adding the schemas for the home page, let's add a schema for articles.

We could use the `Article` or the `BlogPosting` schema type. In our case, we will use the first.

Because the gem doesn't define any of those, we will add an `Article` type:

```ruby
module SchemaDotOrg
  class Article < SchemaType
    validated_attr :id, type: String, allow_nil: true
    validated_attr :author, type: SchemaDotOrg::Person, allow_nil: true
    validated_attr :name, type: String
    validated_attr :url, type: String, allow_nil: true
    validated_attr :image, type: SchemaDotOrg::ImageObject, allow_nil: true
    validated_attr :article_body, type: String, allow_nil: true
    validated_attr :description, type: String, allow_nil: true
    validated_attr :publisher, type: SchemaDotOrg::Organization, allow_nil: true
    validated_attr :date_published, type: String, allow_nil: true
    validated_attr :date_modified, type: String, allow_nil: true
  end
end
```

After defining it, we can add the schema in the controller like we did before:

```ruby
class ArticlesController < ApplicationController
  before_action :set_schema, only: [:show]

  # Rest of the code

  def set_schema
    organization = SchemaDotOrg::Organization.new(
      name: "Buk",
      url: "https://buk.com",
      email: "hello@buk.com",
      logo: "https://buk.com/logo.png",
      telephone: "+1234567890"
    )

    author = SchemaDotOrg::Person.new(
      name: @article.user.username,
      url: user_url(@article.user),
    )

    image = SchemaDotOrg::ImageObject.new(
      url: url_for(@article.cover),
      width: @article.cover.metadata["width"],
      height: @article.cover.metadata["height"]
    )

    article = SchemaDotOrg::Article.new(
      name: @article.title,
      url: article_url(@article),
      description: @article.excerpt,
      article_body: @article.content,
      date_published: @article.published_at.to_time.iso8601,
      publisher: organization,
      image: image,
      author: author
    )

    @schema = SchemaDotOrg::GraphContainer.new([organization, article])
  end
end
```

Now, we add it to the `show` partial just like we did before, and we should have a nice script that outputs what we require.

But we're missing the structured data for breadcrumbs, and we have those in our example application blog:

![Structured data for breadcrumbs](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6Mzk5LCJwdXIiOiJibG9iX2lkIn19--162a46d9c6b16c0361c385a3f84b49021037a895/structured-data-for-breadcrumbs.webp)

Let's see how we could add them:

### Breadcrumbs
We can define structured data for [breadcrumbs](https://avohq.io/blog/breadcrumbs-rails), using the `BreadcrumbList` markup to hold the list of breadcrumbs and the `ListItem` element to define the actual breadcrumbs and the `Thing` markup to represent the `item` property of the list item.

The `schema_dot_org` fork already defines those, so let's create the breadcrumbs.

Our breadcrumb structure is simple: **Home** that points to the root page, the **Blog** item that points to the blog index, and the **Article** that points to the current article.

As we will be including the schema in the `ArticlesController` everything from the `Article` schema is the same except we will also pass a `BreadcrumbList` object to the `@graph`:

```ruby
  def set_schema
    organization = SchemaDotOrg::Organization.new(
      name: "Buk",
      url: "https://buk.com",
      email: "hello@buk.com",
      logo: "https://buk.com/logo.png",
      telephone: "+1234567890"
    )

    author = SchemaDotOrg::Person.new(
      name: @article.user.username,
      url: user_url(@article.user),
    )

    image = SchemaDotOrg::ImageObject.new(
      url: url_for(@article.cover),
      width: @article.cover.metadata["width"],
      height: @article.cover.metadata["height"]
    )

    article = SchemaDotOrg::Article.new(
      name: @article.title,
      url: article_url(@article),
      description: @article.excerpt,
      article_body: @article.content,
      date_published: @article.published_at.to_time.iso8601,
      publisher: organization,
      image: image,
      author: author
    )

    home_breadcrumb = SchemaDotOrg::ListItem.new(
      position: 1,
      name: "Home",
      item: root_url
    )

    blog_breadcrumb = SchemaDotOrg::ListItem.new(
      position: 2,
      name: "Blog",
      item: blog_url,
    )

    article_breadcrumb = SchemaDotOrg::ListItem.new(
      position: 3,
      name: @article.title,
      item: article_url(@article)
    )

    breadcrumbs = SchemaDotOrg::BreadcrumbList.new(
      item_list_element: [home_breadcrumb, blog_breadcrumb, article_breadcrumb]
    )

    @schema = SchemaDotOrg::GraphContainer.new([organization, article, breadcrumbs])
  end
```

Now, we can also generate structured data for any types of breadcrumbs on our site.

### Books
Given that our example application is about book sharing, let's add markup for a `Book`.

Let's start by creating the class to generate the desired markup:

```ruby
module SchemaDotOrg
  class Book < SchemaType
    validated_attr :name, type: String
    validated_attr :author, type: SchemaDotOrg::Person, allow_nil: true
    validated_attr :description, type: String, allow_nil: true
    validated_attr :publisher, type: SchemaDotOrg::Organization, allow_nil: true
    validated_attr :isbn, type: String, allow_nil: true
    validated_attr :date_published, type: String, allow_nil: true
    validated_attr :genre, type: Array, allow_nil: true
    validated_attr :about, type: String, allow_nil: true
    validated_attr :in_language, type: String, allow_nil: true
    validated_attr :thumbnail, type: SchemaDotOrg::ImageObject, allow_nil: true
  end
end
```

Then, in the `BooksController` we define the schema just like we did before:

```ruby
# app/controllers/books_controller.rb
class BooksController < ApplicationController
  before_action :set_book, only: [:show]
  before_action :set_schema, only: [:show]

  private
  def set_schema
    author = SchemaDotOrg::Person.new(
      name: @book.author_name,
      url: author_url(@book.author),
    )
    
    publisher = SchemaDotOrg::Organization.new(
      name: @book.publisher.name,
      url: @book.publisher.url,
      logo: url_for(@book.publisher.logo),
    )

    thumbnail = SchemaDotOrg::ImageObject.new(
      url: url_for(@book.cover),
    )

    @schema = SchemaDotOrg::Book.new(
      name: @book.title,
      author: author,
      publisher: publisher,
      isbn: @book.isbn,
      date_published: @book.published_at.to_date.iso8601,
      genre: @book.genres.map(&:name),
      description: @book.description,
      thumbnail: thumbnail,
    )
  end
end
```

### Products
There are really no products in the example application, but we could add the appropriate SD with the `Product` schema type.

The markup can have many properties, but the gem includes `name`, `description`, `image`, `url` and a special `offers` property that accepts an `AggregateOffer` to communicate about offers or price reductions.

So, we will define the `set_schema` method to show how it can be done:

```ruby
# app/controllers/products_controller.rb

def set_schema
  images = []
  
  @product.images.each do |image|
    images << SchemaDotOrg::ImageObject.new(
      url: url_for(image),
      width: image.metadata["width"],
      height: image.metadata["height"]
    )
  end

  offer = SchemaDotOrg::AggregateOffer.new(
    lowPrice: @product.discounted_price,
    highPrice: @product.price,
    priceCurrency: "USD",
    offerCount: @product.count_on_hand
  )
  
  @schema = SchemaDotOrg::Product.new(
    name: @product.name,
    description: @product.description,
    image: images,
    url: product_url(@product)
  )
end
```

Please note that the `AggregateOffer` uses camel case for the keys.

Now we have structured data for our products too!

### FAQ Page
To add the Frequently Asked Questions markup, we will define a `FAQPage` schema type and then use the `Question` and `Answer` schemas to handle the actual questions.

In the example application, we defined the actual questions in a YAML file stored in the `config` directory that looks like this:

```YAML
faqs:
  - question: What is Buk?
    answer: Buk is a revolutionary book sharing platform that connects readers, allows book exchanges, and helps you discover new literary adventures.
    
  - question: How does book sharing work?
    answer: Users can list books they're willing to share, request books from others, and arrange local or digital exchanges. Our platform ensures a safe and seamless sharing experience.
```

Once we have that, we set the schema in the controller:

```ruby
# app/controllers/static_controller.rb
class StaticController < ApplicationController
  # Rest of the code

  def faq
    faqs_path = Rails.root.join("config", "faqs.yml")
    @faqs = YAML.load_file(faqs_path)
    @schema = set_faq_schema
  end

  private
  def set_faq_schema
    questions = []
    @faqs.each do |question|
      answer = SchemaDotOrg::Answer.new(
        text: question['answer']
      )
      question = SchemaDotOrg::Question.new(
        name: question['question'],
        accepted_answer: answer
      )
      questions << question
    end
    SchemaDotOrg::FAQPage.new(
      name: "Frequently Asked Questions",
      main_entity: questions
    )
  end
end
```

After passing the schema to the `schema` partial, we get an appropriate schema markup for our FAQ section

## Going further
Showcasing every possible type of schema in an article like this is not really practical.

Your application might have many other types of schemas or implement the same one we showed here a bit differently.

As long as you follow the convention of defining your new schemas within the `SchemaDotOrg` namespace and have the classes inherit from `SchemaType` you should be able to define as many as you want.

The best place to see how the schemas should look is the official [Schema.org](https://schema.org) website: don't hesitate to explore it and search for your desired markup there.

{{warning}}
Note: not every schema defined in the `schema.org` specification is supported by search engines or produces enriched search results. As a starting guide, check the [structured data schemas supported by Google](https://developers.google.com/search/docs/appearance/structured-data/search-gallery) guide.
{{/warning}}

### Schema validation
Before publishing changes to the structured data of our sites, we need to make sure that it's properly formatted and valid.

To do so, we can use the [schema.org validator,](https://validator.schema.org/) where we can test by URL or by pasting the generated code fragment from the `json+ld` script.

If we've added our site to the Google Search Console like we taught in the [sitemaps in Rails](https://avohq.io/blog/sitemap-for-rails-applications) article, we can check the **Enhancements** section to see how our structured data is indexed and if there are any errors with it.

In my experience, even if we add the appropriate markup to pages that are indexed, they won't necessarily show up in this section, but errors tend to appear, so keeping an eye on it is advisable.

## Summary
Adding structured data to our website can help us with our [Rails SEO](https://avohq.io/rails-seo) efforts by making it easier for search engines to understand what our content is about and the entities it represents.

There are around 800 different schema markups to represent entities that might be present in our websites or applications, but not all of them are picked by search engines to produce enriched search results.

To add structured data to a Rails application, we used a fork of the `schema_dot_org` gem, which adds the ability to support the `@graph` property and some extra schema types we needed.

Because structured data is essentially JSON, we can achieve the same results by using POROs or even partials, but the gem has some nice features like validations.

Before adding structured data to our application, make sure that it's valid by using a validator service so we can avoid wasting crawl budget unnecessarily.

Lastly, there are many browser extensions that allow us to inspect the JSON+LD markup for websites. Use them to learn how others are solving the issue or even find out if your competitors are using structured data and how.

I hope you enjoyed the article and that you can implement structured data in your Rails applications.

Happy coding!
