-
Contents
Generating Structured Data with JavaScript
Google is getting ever more efficient at processing content generated via JavaScript. This means generating structured data using JavaScript is now a viable option in situations where custom development is difficult or slow.
What's more, structured data generated via JavaScript can still be validated in the same way as other implementations, using the following tools:
FAQ HTML Structure
There are two code snippets to choose from depending on the structure of your FAQs. You'll need to inspect the HTML code on your website to determine which one is appropriate for your structure:
Nested: If your FAQs have a nested structure each question and answer pair will have its own wrapper element. This is common when you have your FAQs in an accordion for example.
Linear: If your questions and answers appear one after the other with no wrapping elements then your FAQs have a linear structure. This code snippet can be used for standard text content, where questions and answers are simply written into the page content under an appropriate heading.
Here's an example of an FAQ accordion using a nested structure. Note, each question and answer pair is wrapped by a <div> with the class "panel" and "panel-default".
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle collapsed" data-toggle="collapse">Question 1?</a>
</h4>
</div>
<div class="panel-collapse collapse">
<div class="panel-body">
<p>Answer 1.</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle collapsed" data-toggle="collapse">Question 2?</a>
</h4>
</div>
<div class="panel-collapse collapse">
<div class="panel-body">
<p>Answer 2.</p>
</div>
</div>
</div>
</div>
The following is an example of a linear structure where the questions and answers are simply contained within <p> tags. Note, it's important to have a way to differentiate between questions and headings. In this case, the style attribute is the only way to tell the difference, which isn't ideal but will still work.
<p style="font-size: 20px;">
<strong>Question 1?</strong>
</p>
<p>Answer 1.</p>
<p style="font-size: 20px;">
<strong>Question 2?</strong>
</p>
<p>Answer 2.</p>
Implementing Structured Data via Google Tag Manager
For ease of implementation, we recommend using Google Tag Manager. This has the added benefits of allowing you to preview your new code prior to putting it live and version history should you need to revert to a previous instance.
The following JavaScript snippets should be implemented using a Custom HTML tag in Google Tag Manager and set to trigger on All Pages.
When adding JavaScript, via a Custom HTML tag, using Google Tag Manager there are a couple of things to be aware of:
All code must be wrapped in an anonymous function.
Google Tag Manager uses an older JavaScript compiler, which means variables need to be declared using "var" instead of the more modern "const" and "let".
Using the JavaScript Code Snippets
To use these code snippets you will need to update the following values:
All CSS selector variables: selectorAccordions, selectorQuestions (not required for linear structures), selectorQuestionName and selectorAnswer.
If you want to use xPath set useXPath to true and update the xPath variable.
Here's a brief overview of what each CSS selector relates to:
selectorAccordions:This represents the element that contains the full set of FAQs, which could be an accordion block for example.
selectorQuestions: This represents that element that wraps each question and answer pair. If your FAQs use a linear structure this selector is not required/present.
selectorQuestionName: This represents the name or heading for each question.
selectorAnswer: This represents the answer for each question.
There is also an option to use XPath in cases where CSS selectors aren't enough. For example, if you have multiple accordions on your website but only some are used for FAQs. XPath can be used to target only accordions that appear below a heading containing "FAQs".
useXPath: Set this variable to false if your CSS selectors exclusively target FAQ sections on the website.
xPath: This method provides an alternative way to select elements that contain the full set of FAQs. Using an XPath expression provides a way of differentiating between elements with the same HTML structure by evaluating content that appears next to within the page.
Nested FAQs Code Snippet
This example is based on the nested HTML structure provided above.
The XPath option is being used to only target accordions that are preceded by an <h2> containing the text "FAQ". Using this approach allows new FAQ content to automatically be picked up without the need to alter the JavaScript code.
<script>(function(){
// CSS selectors
var selectorAccordions = '.panel-group';
var selectorQuestions = '.panel';
var selectorQuestionName = '.panel-title';
var selectorAnswer = '.panel-body';
// Global variables
var accordions;
var output = [];
// xPath
var useXPath = true;
var xPath = "//h2[contains(., 'FAQ')]/following-sibling::div[@class='panel-group'][1]";
if (useXPath) {
var elements = document.evaluate(xPath, document, null, XPathResult.ANY_TYPE, null );
// Convert to array
var elementsArray = [];
var element = elements.iterateNext();
while (element) {
elementsArray.push(element);
element = elements.iterateNext();
}
accordions = elementsArray;
}
else {
accordions = document.querySelectorAll(selectorAccordions);
};
// Loop through accordions
for (var i = 0; i < accordions.length; i++) {
var accordion = accordions[i];
var questions = accordion.querySelectorAll(selectorQuestions);
var questionsJson = [];
// Loop through questions
for (var j = 0; j < questions.length; j++) {
var question = questions[j];
var name = question.querySelector(selectorQuestionName).textContent.trim();
var answer = question.querySelector(selectorAnswer).innerHTML.trim();
var questionObject = {
"@type": 'Question',
"name": name,
"acceptedAnswer": {
"@type": 'Answer',
"text": answer
}
};
questionsJson.push(questionObject);
}
// Generate structured data
var structuredData = {
"@context": 'https://schema.org',
"@type": 'FAQPage',
"mainEntity": questionsJson
};
output.push(structuredData);
}
// Add script element to head
for (var k = 0; k < output.length; k++) {
var script = document.createElement('script');
script.setAttribute('type', 'application/ld+json');
script['textContent'] = JSON.stringify(output[k]);
document['head'].appendChild(script);
}
})(document);</script>
Linear FAQs Code Snippet
This example is based on the linear HTML structure provided above.
Like the last example, the XPath option is being used to only target text that follows headings containing a particular phrase.
<script>(function(){
// CSS selectors
var selectorAccordions = '.panel-group';
var selectorQuestionName = "p[style*='font-size: 20px']";
var selectorAnswer = "p:not(p[style*='font-size: 20px'])";
// Global variables
var accordions;
var output = [];
// xPath
var useXPath = true;
var xPath = "//h3[contains(., 'your questions answered')]/parent::span[contains(@class,'hs_cos_wrapper')][1]";
if (useXPath) {
var elements = document.evaluate(xPath, document, null, XPathResult.ANY_TYPE, null );
// Convert to array
var elementsArray = [];
var element = elements.iterateNext();
while (element) {
elementsArray.push(element);
element = elements.iterateNext();
}
accordions = elementsArray;
}
else {
accordions = document.querySelectorAll(selectorAccordions);
};
// Loop through accordions
for (var i = 0; i < accordions.length; i++) {
var accordion = accordions[i];
var items = accordion.querySelectorAll(selectorQuestionName + ", " + selectorAnswer);
var questionsJson = [];
var questionObject = {
"@type": 'Question',
"name": '',
"acceptedAnswer": {
"@type": 'Answer',
"text": ''
}
};
// Loop through items
for (var j = 0; j < items.length; j++) {
var item = items[j];
var nextItem = items[j + 1];
if (item.matches(selectorQuestionName)) {
questionObject['name'] = item.textContent.trim();
questionObject['acceptedAnswer']['text'] = '';
}
else if (item.matches(selectorAnswer)) {
questionObject['acceptedAnswer']['text'] += item.outerHTML.trim();
}
if (nextItem && nextItem.matches(selectorQuestionName) || j + 1 === items.length) {
questionsJson.push(JSON.parse(JSON.stringify(questionObject)));
questionObject['name'] = '';
questionObject['acceptedAnswer']['text'] = '';
}
}
// Generate structured data
var structuredData = {
"@context": 'https://schema.org',
"@type": 'FAQPage',
"mainEntity": questionsJson
};
output.push(structuredData);
}
// Add script element to head
for (var k = 0; k < output.length; k++) {
var script = document.createElement('script');
script.setAttribute('type', 'application/ld+json');
script['textContent'] = JSON.stringify(output[k]);
document['head'].appendChild(script);
}
})(document);</script>