Adding FAQ (Frequently Asked Questions) Schema to Hugo-based website

Contents

Do you write on your website a series of frequently asked questions (FAQs) and try to answer them? Do you know that putting text on a website is not enough to gain traction? Even if your questions and answers are unique and could be desired by users, they may never find their audience.

Playing with the SEO aspect may help, but if we concentrate only on the surface – visible part, we will still be missing out. What is important is what beneath, just in the background. This is how we can simply describe what is Schema.

Structured data (Schema) is presented on a website in a way that is not visible to an ordinary visitor but processed, when found, by search engines.

Schema is very important and will be even more this year (2023), as mentioned directly by many people at Google.

To get your question-answers into search engines you should put your interest into the Frequently Asked Questions (FAQ) Schema.

Here is how I did this on some of the websites that I made with Hugo.

Before we start you need to be aware that there is no single way to implement FAQ Schema into your website.

When I wrote this article I found a different (better) approach to some of the elements, so at the same time as I describe here, I improve my code.

I will present you my approaches in single page FAQ (with multiple questions and answers presented separately on a page) and questions and answers on their pages (single page containing the question and answer). I will show you also how to use the details disclosure element on a single-page FAQ.

The solutions work for me but do not necessarily may work for you, or, based on them, you can create your solution. Feel free to share your approach in the discussion under the post.


Before you start, here are a couple of websites that you may need to understand the structure of FAQ Schema as well as how to test it before putting it live.


FAQ on a single page #

My single page layout single.html contains the following part to display the content.

<section id="content">
	{{ .Content }}
</section>

We need to start, in layout, by telling, through frontmatter, when the page is a FAQ page. This FAQ page will need to contain itemscope itemtype="https://schema.org/FAQPage" information as follows:

<section id="content"{{if .Params.faq }} itemscope itemtype="https://schema.org/FAQPage"{{ end}}>
      {{ .Content }}
</section>

In such a way, when we specify faq: true in the frontmatter of our markdown file, we will display the necessary start point that will tell that the page will contain a reference to FAQ Schema.

---
faq: true
---

Content #

To create our FAQ page, let’s create our markdown file faq.md in the content folder.

---
title: FAQ
faq: true
---

Lorem ipsum dolor sit amet...

Into the content (Lorem ipsum dolor sit amet...) we will type out questions and answers. As the title in frontmatter is our <h1> tag (equal to # in markdown) we will compose our questions as <h2> elements (##).

I am using heading elements as you can link directly to each of them from different pages using the ID attribute.

## Customers asking me how to do this fancy thing?
To make this fancy thing you need to have this fancy stuff.

## What fancy stuff do I need?
You will need fancy elements and fancy liquid.

Each question above ## contains an answer underneath.

These questions and answers are just text. To allow search engines to detect which one is a question and which answer is to which one, we need to do some work.

For this purpose, we will create two shortcodes in layouts\shortcodes. One is called faq-question.html and the other faq-answer.html.

Each question we will wrap between shortcode as follow:

{{% faq-question %}}
## Customers asking me how to do this fancy thing?
To make this fancy thing you need to have this fancy stuff.
{{% /faq-question %}}

{{% faq-question %}}
## What fancy stuff do I need?
You will need fancy elements and fancy liquid.
{{% /faq-question %}}

Now we will have separated questions on our FAQ page.

Each question needs to be wrapped in itemscope itemtype="https://schema.org/Question" itemprop="mainEntity", this is what we will put into our shortcode faq-question.html.

<span itemscope itemtype="https://schema.org/Question" itemprop="mainEntity">
    {{ .Inner }}
</span>

Now we need to tell which part inside the question block is an answer. We will wrap an answer into our faq-answer.html shortcode.

{{% faq-question %}}
## Customers asking me how to do this fancy thing?
{{% faq-answer %}}
To make this fancy thing you need to have this fancy stuff.
{{% /faq-answer %}}
{{% /faq-question %}}

{{% faq-question %}}
## What fancy stuff do I need?
{{% faq-answer %}}
You will need fancy elements and fancy liquid.
{{% /faq-answer %}}
{{% /faq-question %}}

Each answer needs to be wrapped in ‌itemscope itemtype="https://schema.org/Answer" itemprop="acceptedAnswer", this is what we will put into our shortcode faq-answer.html.

<span itemscope itemtype="https://schema.org/Answer" itemprop="acceptedAnswer">
    <span itemprop="text">{{ .Inner }}</span>
</span>

We don’t need to worry about formatting, as the question and answer will be processed by the search engine in plain text form.

There is one thing missing.

We got our FAQ Page specified, Questions split between each other and Answer to them easily identified. We need to let know that <h2> that contains our question is indeed a question to our FAQ Schema.

Customising heading #

Each question shall contain the itemprop=name element. In that case our <h2> which will contain id="customers-asking-me-how-to-do-this-fancy-thing" need to have added itemprop=name as well.

The easiest way is to add a markdown attribute.

For example, this:

## Customers asking me how to do this fancy thing?

Will render as:

<h2 id="customers-asking-me-how-to-do-this-fancy-thing">Customers asking me how to do this fancy thing?</h2>

If we add {itemprop=name} after the heading (in same line) like that:

## Customers asking me how to do this fancy thing? {itemprop=name}

This will render as:

<h2 id="customers-asking-me-how-to-do-this-fancy-thing" itemprop="name">Customers asking me how to do this fancy thing?</h2>

The element {itemprop=name} will not be visible to the visitor.

In such a way, we got everything ready and working.

However, the markdown attribute is not always working.

If we got customised render-heading.html in layouts\_default\_markup, as described here, the markdown attribute will not be passed through.

<h{{ .Level }} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a href="#{{ .Anchor | safeURL }}">¶</a></h{{ .Level }}>

Let’s change it by adding .Attributes to the code:

<h{{ .Level }} {{- range $k, $v := .Attributes -}}{{- printf " %s=%q" $k $v | safeHTMLAttr -}}{{- end -}} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a href="#{{ .Anchor | safeURL }}">¶</a></h{{ .Level }}>

At this time I would want to customise one more thing. The element (on my website is the symbol #), which serves as a link to the section, will be passed with the question into the schema. We need to exclude that by telling when the page is FAQ, this shall not be displayed.

Replacing:

<a class="anchor" href="#{{ .Anchor | safeURL }}">#</a>

With:

{{ if (not .Page.Params.faq) }}<a class="anchor" href="#{{ .Anchor | safeURL }}">#</a>{{ end }}

Validation #

Run our Hugo (hugo server) on our local machine, go to the page and see the page source (In Safari on macOS right click on a page and select Show Page Source).

Copy the whole page source into your code editor and by using the option search/replace find http://localhost:1313 and replace it with https://example.com.

Then copy the whole code again and head to Rich Results Test tool and in Code tab paste out the code and click the Test Code button.

After a short analysis, you should see detected new structured data.

FAQ Schema with 1 Valid item detected

It will be 1 valid item detected, as this is only a single page. However, if you click on it, you will see that Google is correctly identifying multiple questions and their corresponding answers from this single page.

Here is how it looks on my example page yummyrecipes.uk/tips that got implemented FAQ using the above method.

FAQ Schema of the example website YummyRecipes.uk

Once you publish your page with FAQ head to Google Search Console and crawl your URL and request indexing (or re-indexing, if the page is already in Google).

After that wait patiently. Your new FAQ section in the Enhancements section shall appear in roughly 24 to 48 hours.

Google Search Console FAQ Schema with 1 Valid Item

FAQ on individual pages #

If your questions require a complex answer, which is not typically fit in 1-2 paragraphs, then it may be worth creating a page for each question.

For example, you may have the following structure:

/faq/
/faq/customers-asking-me-how-to-do-this-fancy-thing/
/faq/what-fancy-stuff-do-i-need/

Remember, do not use individual pages by only thinking that this will help to position in Google, as more pages are better for SEO. That’s a myth! Think more about this like that. If your question requires a complex answer, which is more than 1-2 paragraphs, then it may be beneficial to have it on a separate page.

Put ourselves into your reader’s (visitors) shoes. Do you prefer to read the question and short answer on the same page, or do you prefer to wondering between pages only to read a single paragraph? Think about usability first!

Here we will start with two things.

  1. We will create a page that will list all questions (this will be in /faq/). This is optional and depends on your approach and the design of your website.
  2. We will create FAQPage, Question and Answer page.

Listing all questions #

There are multiple ways how to achieve that. I will explain mine, which will work on identifying pages based on category and tag in frontmatter.

Example:

---
title: Customers asking me how to do this fancy thing?
category:
 - FAQ
tags:
 - faq
---

To make this fancy thing you need to have this fancy stuff.
{{< faq "faq" >}}

Shortcode:

<ul class="faq-list">
  {{ $tagged := .Get 0 }}
  {{ range .Site.Pages }}
    {{ if in .Params.categories "FAQ" }}
      {{ if in .Params.tags $tagged }}
       <li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
       {{ end }}
     {{ end }}
  {{ end }}
</ul>

You may ask why I am using category and tag.

This is why, as I can split FAQ into sections.

In that case, all pages that belong to the FAQ section will have category set as FAQ. Different questions may be identified by different tags and grouped under the different sections on the same page like below.

## Frequently Asked Questions for Sellers

{{< faq "Sellers FAQ" >}}

## Frequently Asked Questions for Buyers

{{< faq "Buyers FAQ" >}}
---
tags:
 - 'Sellers FAQ`
---

Page with question and answer #

Let’s start with our single page layout single.html that will contain the following elements responsible for displaying a title (taken from frontmatter) and content:

<article>
	<section id="content">
		<div class="page-header">
			<h1>{{ .Title }}</h1>
		</div>
		<div class="page-content">
			{{ .Content }}
		</div>
	</section>
</article>

We need to modify this to state that this is a FAQPage, the title is our Question and the content is our Answer.

  • Let’s start with identifying the page as FAQ by adding {{ if in .Params.categories "FAQ" }} itemscope itemtype="https://schema.org/FAQPage"{{ end}} to <article> element;
  • Let’s add {{ if in .Params.categories "FAQ" }} itemscope itemtype="https://schema.org/Question" itemprop="mainEntity"{{ end}} to <section id="content"> element;
  • Let’s add {{ if in .Params.categories "FAQ" }} itemprop="name"{{ end}} to <div class="page-header"> element;
  • Let’a add {{ if in .Params.categories "FAQ" }} itemscope itemtype="https://schema.org/Answer" itemprop="acceptedAnswer"{{ end}} to <div class="page-content"> element.
  • Let’s wrap {{ .Content }} between conditional {{ if in .Params.categories "FAQ" }}<span itemprop="text">{{ end}} and closing {{ if in .Params.categories "FAQ" }}</span>{{ end}}

This will look as follow:

<article {{ if in .Params.categories "FAQ" }}itemscope itemtype="https://schema.org/FAQPage"{{ end}}>
	<section id="content"{{ if in .Params.categories "FAQ" }} itemscope itemtype="https://schema.org/Question" itemprop="mainEntity"{{ end}}>
		<div class="page-header"{{ if in .Params.categories "FAQ" }} itemprop="name"{{ end}}>
			<h1>{{ .Title }}</h1>
		</div>
		<div class="page-content"{{ if in .Params.categories "FAQ" }} itemscope itemtype="https://schema.org/Answer" itemprop="acceptedAnswer"{{ end}}>
			{{ if in .Params.categories "FAQ" }}<span itemprop="text">{{ end}}
				{{ .Content }}
			{{ if in .Params.categories "FAQ" }}</span>{{ end}}
		</div>
	</section>
</article>

Validation #

Run our Hugo (hugo server) on our local machine, go to the single page, that contains your single question and its answer, and see the page source (In Safari on macOS right click on a page and select Show Page Source).

Similarly, like in the previous validation, copy the whole page source into your code editor and by using the option search/replace find http://localhost:1313 and replace it with https://example.com.

Then copy the whole code again and head to Rich Results Test tool and in Code tab paste out the code and click the Test Code button.

It shall once again return as follow:

FAQ Schema with 1 valid item detected

Once you publish your pages with questions and answers and you request indexing (each individual) through Google Search Console, after a few days (24 to 48 hours) you will start seeing your pages appearing in Enhancement sections.

Google Search Console FAQ Schema with 28 Valid items

Here is how it looks on my example page that contains questions. Each question is referring to each FAQ page.

FAQ on a single page with details disclosure element #

Sometimes you may have plenty of questions and answers gathered. None of them fit into separate pages approach but putting them on a single page will create a chaotic document. Sometimes it will work when it’s implemented with a Table of Content, but in other cases may not.

When it’s not working for you, you may implement details disclosure element.

Each question will be displayed on the page but the answer will only be presented (for the user) when the user will click on it. Search engines will see whole questions/answers without the need for additional interaction.

This is how I did it on one of my websites.

Repeating the main part, where in layout, by using frontmatter parameter faq: true we specify when itemscope itemtype="https://schema.org/FAQPage" will appear.

<section id="content"{{if .Params.faq }} itemscope itemtype="https://schema.org/FAQPage"{{ end}}>
      {{ .Content }}
</section>

On our page, we will be using a single shortcode (in the first method there were two shortcodes) with a parameter. The shortcode will wrap each question containing an answer. The parameter will be a question and the rest between shortcodes will be an answer.

This will look something like that:

{{% faq-details "## Customers asking me how to do this fancy thing? {itemprop=name}" %}}
To make this fancy thing you need to have this fancy stuff.
{{% /faq-details %}}

{{% faq-details "## What fancy stuff do I need? {itemprop=name}" %}}
You will need fancy elements and fancy liquid.
{{% /faq-details %}}

It will look less pretty, especially if you preview your markdown files offline, but when served with Hugo all will function as planned.

I am using, inside the shortcode parameter, markdown ## to render the heading and markdown attribute {itemprop=name} as described earlier.

The faq-detail.html shortcode looks as follows:

<details itemscope itemtype="https://schema.org/Question" itemprop="mainEntity">
  <summary>{{ .Get 0 | markdownify }}</summary>
  <span itemscope itemtype="https://schema.org/Answer" itemprop="acceptedAnswer">
    <span itemprop="text">
      {{ .Inner }}</span>
  </span>
</details>

Noticed that <span itemprop="text"> is on its own line but {{ .Inner }} and closing </span> together. This is quite essential as if we want to maintain formatting dictated by markdown, that’s how it needs to be. Also, the closing </span> after {{ .Inner }}, as for some reason it is below it’s getting displayed as text on the page.

Doing code validation in Rich Results Test we will achieve the same output as in the first approach, a single FAQ page with multiple questions and answers.

Here is how it looks on my example page paraplan.com.pl/kontakt/pytania-i-odpowiedzi/ (Polish).


As mentioned.

This is a guide on how I implement FAQ on three different websites in three different layouts and approaches. I am curious how you can do yours. Share your thoughts below if you want.

Comments