Add FAQ markup schema to your articles in Ruby on Rails
Last updated on December 20, 2021
What is FAQ structured data?
FAQ structured data is a way to markup your questions and answers for your articles. If your article answers specific questions, it's good to add this markup to your page, as this improves SEO and increases the probability for Google to find them and display them on the search result page.
The FAQ section within Google Result page looks like this
You need to generate JSON containing your questions and answers, they wrap them within a<script type="application/ld+json">as per the specification provided by Google. Look at the template below to give you an idea of what we will output at the end of this article.
1<script type="application/ld+json">2{3"@context": "https://schema.org",4"@type": "FAQPage",5"mainEntity": [{6"@type": "Question",7"name": "Question goes here",8"acceptedAnswer": {9"@type": "Answer",10"text": "Answer here"11}12}, {13"@type": "Question",14"name": "Question goes here",15"acceptedAnswer": {16"@type": "Answer",17"text": "Answer goes here"18}19}, {20"@type": "Question",21"name": "Question goes here",22"acceptedAnswer": {23"@type": "Answer",24"text": "Answer goes here"25}26}]27}28</script>
There are several Google guidelines which you should follow.
Use this FAQ markup if you answer more than one question within your article.
Avoid adding multiple answers to the same question - Google might think that you are trying to cheat the system.
Don't add answers which have been submitted by users (for example, comments).
You shouldn't use the FAQ markup as a marketing/advertising opportunity - it should answer a specific question so avoid any product-specific content.
1create_table :article_faqs do |t|2t.integer :article_id3t.string :question4t.string :answer56t.timestamps7end
Every Article can have many FAQs and the AritcleFaq belongs to the Article model.
1class ArticleFaq < ApplicationRecord2belongs_to :article3end
An article can either have zero, one or many ArticleFaqs, so we would like to make this flexible. We would addhas_manyassociation andaccepts_nested_attributes_foras we will implement a nested form. Nested forms allow us to create as many FAQs as we need.
1has_many :article_faqs, dependent: :destroy2accepts_nested_attributes_for :article_faqs, reject_if: :all_blank, allow_destroy: true
Step 2: Add a nested form within your article form
There are several approaches to implementing this, such as using a gem like cocoon. However, in this example, we will use stimulusJS. The stimulus controller is easy to understand and you can be extended if you need. You are welcome to use whichever approach you prefer.
Copy the nested_form stimulus controller and paste it in your app/javascript/controllers directory.
1// nested_form_controller.js23import { Controller } from "stimulus"4export default class extends Controller {5static targets = ["links", "template"]67connect() {8}910add_association(event){11event.preventDefault()1213var content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())14this.linksTarget.insertAdjacentHTML("beforebegin", content)15}1617remove_association(event){18event.preventDefault()1920let wrapper = event.target.closest(".nested-fields")21if (wrapper.dataset.newRecord == "true"){22wrapper.remove()23} else{24wrapper.querySelector("input[name*='_destroy']").value = 125wrapper.style.display = 'none'26}27}28}
If you don't have stimulus configured, you can follow this article to install it.
1<%# article/_form.html.erb %>234<%# ... rest of your Article form %>56<div data-controller="nested-form">7<h3>FAQs</h3>8<template data-target="nested-form.template">9<%= f.fields_for :article_faqs, ArticleFaq.new, child_index: "NEW_RECORD" do |task| %>10<%= render "faq", form: faq %>11<% end %>12</template>13<%= f.fields_for :article_faqs do |faq| %>14<%= render "faq", form: article_faqs %>15<% end %>16<div data-target="nested-form.links">17<%= link_to "Add a new FAQ", "#", data: {action: "click->nested-form#add_association"} %>18</div>19</div>
1<%# article/_faq.html.erb %>23<div class="nested-fields" data-new-record="<%= form.object.new_record? %>">4<div class="form-group">5<%= form.text_field :question, placeholder: "Your question..." %>6<%= form.text_field :answer, placeholder: "Your answer..." %>7<%= link_to "Remove FAQ", "#", data: {action: "click->nested-form#remove_association"} %>8</div>9<%= form.hidden_field :_destroy %>10</div>
The final step is to go to your article's controller and add the nested attributes to your permitted params:
1# articles_controller.rb23article_faqs_attributes: [:id, :question, :answer, :_destroy]
1class ArticleStructuredData < Patterns::Service23def initialize(article)4@article = article5end67def generate_faq_schema8return if @article.article_faqs.empty?910JSON.pretty_generate(11"@context" => "http://schema.org",12"@type" => "FAQPage",13"mainEntity" => @article.article_faqs.map { |faq|14{15"@type" => "Question",16"name" => faq.question,17"acceptedAnswer" => {18"@type" => "Answer",19"text" => faq.answer,20}21}22}23)24end25end
In your controller, initialise the service object within yourshowaction and assign the return value to an instance variable.
1@faq_structured_data = ArticleStructuredData.new(@article).generate_faq_schema
Step 4: Conditionally render in your view
Since some articles might not have FAQs, we want to conditionally render the script tag. Wrap your script tag around a content_for helper so we can render it in the <head> tag.
1<%# articles/show.html.erb %>234<%= content_for :structured_data do %>5<% if @faq_structured_data.present? %>6<script type="application/ld+json">7<%= @faq_structured_data.html_safe %>8</script>9<% end %>10<% end %>
The final step is to check forcontent_for?(:structured_data)in our layout.If present, render our script tag within our head tag.
1<head>2<% if content_for?(:structured_data) %>3<%= yield(:structured_data) %>4<% end %>5</head>
Level up your web development skills
Get articles, guides and interviews right in your inbox. Join a community of fellow developers.
No spam. Unsubscribe at any time.