[Estimated read time: 7 minutes]
One of the biggest takeaways from SearchFest in Portland earlier this year was the rapidly rising importance of semantic search and structured data — in particular Schema.org. And while implementing Schema used to require a lot of changes to your site's markup, the JSON-LD format has created a great alternative to adding microdata to a page with minimal code.
What was even more exciting was the idea that you could use Google Tag Manager to insert JSON-LD into a page, allowing you to add Schema markup to your site without having to touch the site's code directly (in other words, no back and forth with the IT department).
Trouble is, while it seemed like Tag Manager would let you insert a JSON-LD snippet on the page no problem, it didn't appear to be possible to use other Tag Manager features to dynamically generate that snippet. Tag Manager lets you create variables by extracting content from the page using either CSS selectors or some basic JavaScript. These variables can then be used dynamically in your tags (check out Mike's post on semantic analysis for a good example).
So if we wanted to grab that page URL and pass it dynamically to the JSON-LD snippet, we might have tried something like this:
But that doesn't work. Bummer.
Meaning that if you wanted to use GTM to add the the BlogPosting Schema type to each of your blog posts, you would have to create a different tag and trigger (based on the URL) for each post. Not exactly scalable.
But, with a bit of experimentation, I’ve figured out a little bit of JavaScript magic that makes it possible to extract data from the existing content on the page and dynamically create a valid JSON-LD snippet.
Dynamically generating JSON-LD
The reason why our first example doesn't work is because Tag Manager replaces each variable with a little piece of JavaScript that calls a function — returning the value of whatever variable is called.
We can see this error in the Google Structured Data Testing Tool:
The error is the result of Tag Manager inserting JavaScript into what should be a JSON tag — this is invalid, and so the tag fails.
However, we can use Tag Manager to insert a JavaScript tag, and have that JavaScript tag insert our JSON-LD tag.
If you're not super familiar with JavaScript, this might look pretty complicated, but it actually works the exact same way as many other tags you’re probably already using (like Google Analytics, or Tag Manager itself).
Here, our Schema data is contained within the JavaScript "data" object, which we can dynamically populate with variables from Tag Manager. The snippet then creates a script tag on the page with the right type (application/ld+json), and populates the tag with our data, which we convert to JSON using the JSON.stringify function.
The purpose of this example is simply to demonstrate how the script works (dynamically swapping out the URL for the Organization Schema type wouldn't actually make much sense). So let's see how it could be used in the real world.
Dynamically generating Schema.org tags for blog posts
Start with a valid Schema template
First, build out a complete JSON/LD Schema snippet for a single post based on the schema.org/BlogPosting specification.
Identify the necessary dynamic variables
There are a number of variables that will be the same between articles; for example, the publisher information. Likewise, the main image for each article has a specific size generated by WordPress that will always be the same between posts, so we can keep the height and width variables constant.
In our case, we've identified 7 variables that change between posts that we'll want to populate dynamically:
Create the variables within Google Tag Manager
- Main Entity ID: The page URL.
- Headline: We'll keep this simple and use the page title.
- Date Published and Modified: Our blog is on WordPress, so we already have meta tags for "article:published_time" and "article:modified_time". The modified_time isn't always included (unless the post is modified after publishing), but the Schema specification recommends including it, so we should set dateModified to the published date if it there isn't already a modified date. In some circumstances, we may need to re-format the date — fortunately, in this case, it's already in the ISO 860 format, so we're good.
- Author Name: In some cases we're going to need to extract content from the page. Our blog lists the author and published date in the byline. We'll need to extract the name, but leave out the time stamp, trailing pipe, and spaces.
- Article Image: Our blog has Yoast installed, which has specified image tags for Twitter and Open Graph. Note: I'm using the meta twitter:image instead of the og:image tag value due to a small bug that existed with the open graph image on our blog when I wrote this.
- Article Description: We'll use the meta description.
Here is our insertion script, again, that we'll use in our tag, this time with the properties swapped out for the variables we'll need to create:
I'm leaving out dateModified right now — we'll cover than in a minute.
Extracting meta values
Fortunately, Tag Manager makes extracting values from DOM elements really easy — especially because, as is the case with meta properties, the exact value we need will be in one of the element's attributes. To extract the page title, we can get the value of the <title> tag. We don't need to specify an attribute name for this one:
For meta properties, we can extract the value from the content attribute:
Tag Manager also has some useful built-in variables that we can leverage — in this case, the Page URL:
Processing page elements
For extracting the author name, the markup of our site makes it so that just a straight selector won't work, meaning we’ll need to use some custom JavaScript to grab just the text we want (the text of the span element, not the time element), and strip off the last 3 characters (" | ") to get just the author's name.
In case there's a problem with this selector, I've also put in a fallback (just our company name), to make sure that if our selector fails a value is returned.
Testing
Tag Manager has a great feature that allows you to stage and test tags before you deploy them.
Once we have our variables in place, we can enter the Preview mode and head to one of our blog posts:
Here we can check the values of all of our variables to make sure that the correct values are coming through.
Finally, we set up our tag, and configure it to fire where we want. In this case, we're just going to fire these tags on blog posts:
And here's the final version of our tag.
For our dateModified parameter, we added a few lines of code that check whether our modified variable is set, and if it’s not, sets the "dateModified" JSON-LD variable to the published date. You can find the raw code here.
Now we can save the tag, deploy the current version, and then use the Google Structured Data Testing Tool to validate our work:
Success!!
This is just a first version of this code, which is serving to test the idea that we can use Google Tag Manager to dynamically insert JSON-LD/Schema.org tags. However after just a few days we checked in with Google Search Console and it confirmed the BlogPosting Schema was successfully found on all of our blog posts with no errors, so I think this is a viable method for implementing structured data.
Structured data is becoming an increasingly important part of an SEO's job, and with techniques like this we can dramatically improve our ability to implement structured data efficiently, and with minimal technical overhead.
I'm interested in hearing the community's experience with using Tag Manager with JSON-LD, and I'd love to hear if people have success using this method!
Happy tagging!
@ChrisGoddard
Have you tried "{{Page URL}}" in "double quotation" marks?
Also, have you seen Google`s Mariya Moeva video on #JSON-LD which mentions GTM as a deployment method:
Also see please these slides from a talk, that I did on this recently:
From personal experience - I only use GTM to deploy client-side code via an all pages rule for:
For Page Specific JSON-LD - where there can be 20+ variations/tags, I recommend serverside code in the <head> just after the <title> and meta description.
Thanks
Phil.
RogerBot cut-off the link to youtube so reposting Google`s Mariya Moeva video on #JSON-LD here:
https://www.youtube.com/watch?v=L9BqE01SLeE&t=230
Hi Phil, thanks for your comment.
You don't want to put variables within quotation marks in the context I gave because this is all being run in JavaScript and the variable (which is rendered as a call to a JavaScript function) returns a string, which will be interpreted correctly. It's not possible to use dynamic variables to generate JSON-LD directly. First a JavaScript object must be generate that contains all of the values and then it can be transformed in to JSON for interpretation as JSON-LD.
This is different than rendering JSON-LD using server-side code in which you would need to encapsulate template variables in double quotes.
Obviously server-side code is an ideal solution, but not one that is always available. Using dynamic code like this means that a single snippet could generate valid Schema across multiple pages.
Please let me know if this didn't answer your question.
@Chris
Thanks for the reply.
I understand what you are saying, but... sorry, I dont think answer is correct. Here`s why...
If you see this working example:
https://bit.ly/winningthewar33You will note that Google`s structured data testing tool has correctly rendered the output correctly:
"url": "https://example.com\/_testing_script\/____json_ld\/json-ld-test.html",
"url": "{{Page URL}}",
Yes, in normal JS world... encapsulating a variable in quotes would prevent this running as a variable, as it would then become a string.
However, in GTM world... {{variables}} within customHTML are being replaced via server-side code when gtm.js is complied & minified. Thus, encapsulating {{variables}} in quotation marks does work in this instance.
I suspect the issue you encountered is on the DOM scrapping variables which might not be rendering in-time... Q: have you tried changing the trigger from "Page Initialized" to "DOM ready" or "Window loaded" instead, and then checking the output in the Google SD testing tool?
Thanks & nice tip about reading the meta tag for publish_date via a CSS selector.
Cheers.
Phil.
P.S. It might also be useful to use "url": "{{Page URL Protocol}}://{{Page Hostname}}{{Page Path}}{{Page Query}}" rather than "url": "{{Page URL}}" so that the HTTP protocol matches the page.
P.P.S. {{Page Query}} would need to be conditionally loaded if a "?" is present. Thus in reality it would need to be invoked via a JSvariable. As GTM (rather annoyingly) removes the prepended "?" from location.search when it generates {{Page Query}}.
Hi Phil,
I think I understand you. I think we were talking about two difference places in my post. In my current working example, {{Page URL}} as well as the other dynamic variables are not output directly into a JSON-LD tag but are instead part of a object declaration in JavaScript.
You are right, it would appear that for basic dynamic tags, encapsulating them in quotation marks causes GTM to render them as strings. That's good to know and will be helpful for basic JSON-LD implementations.
However using this dynamic implementation is useful when you want the fields to be dynamic as well as the values. By generating the JSON-LD snippet indirectly, it is possible to place logic about what fields you might want to include.
For instance, if your blog sometimes included posts with a contributing author, you wouldn't want the "contributor" field to always be in your mark up. By generating a JavaScript object first, we can dynamically add fields before we convert it into JSON for the JSON-LD tag.
Either way, both methods successfully generate a valid JSON-LD tag. Thanks for pointing out the issue with the initial example though.
Thanks Chris for the excellent alternative for including structured data en Websites. We have run into updating some old Websites to insert code in all pages which involved editing all these pages and a waste of time.
I would definitely give your solution a try using JSON.
Hi Baltran,
I agree that editing some old websites is time consuming, especially if you need to edit each page separately. If you will run into such problems in the future I recommend new tool, which will make it a lot easier - https://webthrust.com/. You can edit most important meta tags for all pages very easily. Soon you will be able to add structured data as well. Everything without touching tons of html files.
So helpful! This definitely makes it faster to update web pages with schema, without having to go through the dev team.
This is outstanding. Really makes a lot of things a lot easier.
Very nice trick, I love the idea of being able to edit the microdata without touching the website code. Will definetly think about using this solution.
Very good article Chris. I hadn't considered using GTM for any schema in the past, but now with your insight I may give it a go.
Thanks
Tim
Hi Chris
Thank you very much for the post. The truth is that the issue of JSON tags do not have anything clear and now I have very understandable. I will keep the post and I'll read again when I need
Question about this: if GTM tag is placed in the <body> instead of in the <head>, will this still be possible?
Thanks Chris
I'm having an issue in that the structured data code is appearing outside the </html> tag.
This means that I can not see the code in "view source" or with the "fetch and render" tool? However it is displayed in the structured data test tool. (Which is how I know it is outside of the html tag.)
Is it actually possible to change where the code is placed within a page or is it ok for it to sit outside of the html?
Remove Chris suggested method... getElementByTagName("head")[0].appendChild(script);
Just use <script>//...//</script> instead.
Read the 2nd comment in this list for details of why "{{Page URL}}" works & getElementByTagName is not necessary.
Thanks
Phil.
Thanks Chris!
We are discovering Google Tag Manager for our clients and it's an adverture!
Thanks for sharing this post!
Love this - one of the best Moz articles of recent months. Wish all posts would be as actionnable as this one. Thanks!
Great Article, it Really is a great help for all.
I had never used GTM for any schema but now after reading these insight I will definitely give it a try.
Regards
Pulkit Thakur
Thanks Chris for sharing,
I have facing same issue with my site i forgot to add og:image tag in my site as it has millions of pages and was wondering how to get time from my engineering team but after reading this article i have implemented twitter card as well.
Did anyone already try using GTM for Schema Product markup?
I did this for 4 ecommerce clients with up to 4000 products, but this should not be the go-to-solution for larger ecommerce sites - as it takes Google some time to pick up the markups for all products this way.
For smaller ecommerce sites however - the following Google Tag Manager setup will work. Might take some months to get all of them into their system, but eventually you will. :-)
The setup is for single-product pages and a minified script, just so you can get a hang of the basics.
Also you do not need to input it within a JavaScript like described in this post. Google will catch it without (but testing tool won't). Just keep an eye on Search Console (under structured data) and enjoy the graph going up.
Tag setup:
Custom HTML tag, remember to add support for document.write. Input the following JSON-LD:
<script type='application/ld+json'>
{
"@context": "https://www.schema.org",
"@type": "Product",
"url": "{{Page URL}}",
"name": "{{Productname}}",
"image": "{{Productimage}}",
"description": "{{Produktdescription}}",
"offers": {
"@type": "Offer",
"price": "{{Productprice}}",
"priceCurrency": "USD" }
}
</script>
Then add a trigger that makes the tag fire on the correct pages.
Trigger example setup:
Find a trigger that is unique to single-product-pages. If all product-pages listed under example.com/products/ are single-page products - then the trigger is easy (Page URL begins with example.com/products/). If it's however an add-to-cart function or similar, then a custom JavaScript variable that can check if the site is a single-product-site or not and return a true or false is an easy solution. There are surely also other trigger setups that will work.
Example if there is an "add-to-cart" button with class="btn-cart":
Variable-name: Productsite
function() {
return document.querySelector('.btn-cart') ? true : false;
}
Trigger setup will then be Page View, Variable "Productsite" equals "true".
Seing as the GTM tag loads asynchronously (and this is added after that loads) I never had any problems with variables not being fetched. But if you want to be on the safe side, you can chose a DOM ready instead of a Page View trigger setup, to ensure that the script won't fire before the variables are ready.
Variables:
Page URL is a built in variable, but the rest you will have to add as custom variables (as explained in the post). Small tip: GTM Sonar plugin will be of help if you are new to this.
Set up variables with the same names as in the JSON-LD script used in the tag. In this example you have Productname, Productimage, Productdescription and Productprice.
For productname, image and description, most ecommerce sites will have standard input fields that reoccur. With GTM-sonar you can tune in on these fast - if you preffer Dev.Console then document.querySelectorAll('inputhere') will ofcourse be sufficient to test your variables.
Variable setup where applicable (easy elements) is DOM element, CSS-selector and Element being whatever GTM-sonar hands you (or the ones you have discovered). Attribute might be necessary if you want the href or src rather than the innerHTML.
Lets say that the productname is always the first h1. The setup will then be:
Variable-type: DOM-element
Swap ID for CSS-selector
Input element: h1
The variable should then fetch the text in the innerHTML part of the H1 field for the script.
For productprice you will often come across the price field adding information that won't allow the markups to validate (only numerics and punctuation will validate here). So you will often need a custom JavaScript variable for this part.
Script for this if the price is behind a class called "product-price" and contains innerHTML: "USD 399,-":
function() {
var element = document.querySelector('.product-price');
var price = element.innerHTML.replace('USD', '').replace(' ', '').replace(/ /gi,'').replace(',', '').replace('-', '');
return price;
}
After you added all variables you want to include for the script, test variables, trigger and JSON-LD rendering in preview mode - and refine till it works perfectly.
Hope this helps!
Brilliant post Chris!
Are there any other creative ways you are using Google Tag Manager?
Thanks Yaniv! I'm currently working on a few other posts that describe a couple of different techniques I've been working on including scraping logged-in user data and using cookies to trigger events based on specific sequences of page views. Keep an eye out here or the SERPs blog for more coming soon!
Thank you very much for your advice. The JSON labels was something that did not control and had to delegate this activity in a Wordpress plugins called "Markup (JSON-LD) structured in schema.org". This plugins has made this all tags implemented unless the author points out that MOZ me that is disabled and unspecified. I do not know why.
This makes dynamically generated schema.org Markup with GTM so easy. Thanks for sharing this, this will safe a lot of people a lot of time in trying to figure this out. Will give it a try on a blog now and see how i go.
Thanks again !
I always used to find it hard when it comes to on-page SEO factors, but through this informative and interesting post I learnt many things. I really enjoyed it and would love to read another post regarding on page SEO. Thanks a lot for sharing.
Very nice article and sometime I uses Json Formatter or Json Viewer for analyse, Validate , Format JSON.
Thanks for helpful article, I just want only fix some small issues in code. Check this https://gist.github.com/jannavratil/4c3aa1efc44e71...
Excellent article Chris, I will be implementing this on our own website asap, looking forward to getting this sorted across the blog! Thanks again, Kevin.
That is an amazing guide Chris! I really appreciate it as I've been struggling with finding a working solution to generate those dynamically via GTM. However, wouldn't this create a JavaScript-ception and affect the page speed? Haven't tested it yet, I'm just wondering.
GTM loads in asynchronous mode, so there is minimal impact on pageSpeed. Also, GTM can be set to cache JS tags so that onload tags/script refreshes minimise the impact even further.
It must be generated using an interface or a programming language like Python , on dynamic websites in HTML page being performed .
Very well explained. Only knew him by hearsay and have to use Google Tag Manager, which we can sure make a profit
Thanks for this great article.
One question: I have created variables from all kinds of DOM elements for my product JSON LD markup but I dont know how to extract the image URL...could someone help?
Thank you!
Never thought GTM for rich snippet markup. Great read
Great article. Right now I'm using a plugin that does this job for me because my level of javascript is simply null. Anyway it is always good to keep learning.
Hi Guys, I'm looking for a schema specialist that can help me with a project, does anybody know of someone that is reasonably priced? Please let me know! :)
I would be keen to find someone too. We have some knowledge though.
David, please let me know if you find someone! Thank you! :)
I have successfully implemented Schema Markup via Google Tag Manager according to Google's Structured Data Testing Tool, but the Markup doesn't show up in Search Console under structured data. Did I miss a something?
We use GTM for other things and have started using it for Schema. It does seem a little complicated when all we want to do is make the page url dynamic. I guess I am a simpleton. I love the idea but it seems quite complicated. Also it's more for pages on site we need the changes, and particularly when we have a client with location pages, not a blog. Because it's a GTM code unless we can make the URL dynamic (so it changes with each page) we need to create a new tag for each page that only fires on a particular location page. Couldn't we just do this all in javascript?
Hey just a quickie here - I wasn't able to see any of the JSON-LD printed in "view source" mode.
I was only able to see it when I used "inspect element"
Is this something you or anyone else has run into? Do I need to use a specific trigger?
Thanks!
Hi Jeremy,
I guess it is because "view source" is the source code but with the "inspect element" you see the loaded DOM so I think you cannot see the JSON-LD that is hidden in the GTM with the "view source"
Wow!
GTM at his full potential...Thanks a lot for the post. I think with the custom JS variable you can do amazing stuff like manipulating DOM elements or sending data from the DataLayer to GA or Facebook.
Is it possible to implement canonicals, open graph social, twitter cards and schema.org via json ld ? or would one only implement schema.org
Thanks Chris, very detailed explanation of how to dynamically generate Schema using GTM. Whilst inserting a snippet on the page with GTM was a good start to cover the basics, it's really useful to have a work around that lets you utilise variables and implement more complex structured data. Need to try soon.
Many thanks!
Great article and something I want to start doing immediately! I have just one question. What if I wanted to use the information from the Open Graph in my code. Can you please provide an example of how I would specify the description? Referring page: https://www.personalinjuryjustice.com/2016/06/15/construction-supervisors-to-get-separate-trials-in-workers-death/. I tried using meta[property="og:description"] to get the SCHEMA - Article Description, but I always get 'null' in the GTM Preview. Thanks.
UPDATE: Was able to figure out these issues. Now all I have left is to work on parsing out the author name and published date from code (since we do not have those handy meta properties setup).
If you don't mind sharing, what was your solution for the meta description variable?
Hi Lohan,
Try this...
Name: Meta Description
Type: CSS selector
Element Selector: meta[name='description']
Attribute Name: content
Thanks
Phil.
Great ! Thank you very much !
So I implemented LocaBusiness schema on a few test sites without it, and so far Search Console "sees" it, but is reporting it as 0 items 0 times. Weird.
AFAIK... you can only use HealthClub & Restaurant for LocalBusiness... https://bit.ly/winningthewar33
HealthClub even has a "reserve/book" action, which is interesting when rendered on SERPs.
Adding GEO data or OpeningHours to Organization tag, triggers a validation error.
Thanks chris!. i was looking for this, was facing trouble earlier to put this on website. google tag manager has made it easier to setup. i will setup this in our site
This post is really awesome. While using GTM I always thoughts of using it differently and I found this, likewise now I can do some testing with my work. Thank you @Chris this is fabulous I am gonna implement this today.
Hello Chris! Thanks for the post . You have made it clear the issue of labels. Very useful.
Thanks Chris! Great article with steps to accomplish it and thanks for the helpful follow-up information.
"However using this dynamic implementation is useful when you want the fields to be dynamic as well as the values. By generating the JSON-LD snippet indirectly, it is possible to place logic about what fields you might want to include."
I am finding while one day all seems to test okay, a later day and I find it not so. Are you aware of a schema fluctuation or update that may be causing this?
Hi Jeannie,
I haven't run into any issue but can you give some more information about your snippet and the errors you're seeing? Maybe provide a URL? Feel free to email me at chris [at] serps.com if you don't want to publish client info.
@Jeannie Hill
Yes, Google when through a period of adding new "recommended/optional" fields into the structure data testing tool, which resulted in pages that PASSED validation suddenly FAILING validation. This was extremely annoying!
However, it appears to have stabilised now, although I am surprised the "sameAs" is not a recommended or required field - as this is a key part of the social graph linking.
Here were some of the validation field Google added...
image:A value for the image field is required.
description:The description field is recommended. Please provide a value if available.
warningendDate:The endDate field is recommended. Please provide a value if available.
warningimage:The image field is recommended. Please provide a value if available.
warningperformer:The performer field is recommended. Please provide a value if available.
warningurl:The url field is recommended. Please provide a value if available.
performer:The performer field is recommended. Please provide a value if available.
urlTemplate: ios-app://530168168/cbs/shows/bbt/vid/2356865/occupation_recal?campaign=google_kp_watch (The value provided for urlTemplate must be a valid URL.)
actionPlatform: A value for the actionPlatform field is required.
inLanguage:A value for the inLanguage field is required.
Reference:
https://plus.google.com/117298997433687198127/post...
https://lh3.googleusercontent.com/-HSxKGwi3EQY/VOCB7jmMmPI/AAAAAAAABtc/Ez3Vf49L0CQMbOMdfjjZ-5uQ8VjV9TTyQCL0B/w1341-h718-no/screenshot%2Bjson%2Berrors.png
Also the Google help documents themselves are scatter with errors, due to the validation field being added after the help files were uploaded.
Tremendous insight Chris. Quick question... There is precious little info available on doing proper schema markup and structiure for JobPosting. Wondering if that might ever be a topic you would tackle?
This is such an awesome post, Chris! Props for figuring out how to make this idea work dynamically in GTM; it's a game changer. UpBuild now has all of our blogged posts running dynamic JSON-LD using this method. Thanks!
This is perfect for websites that don't have a CMS with advanced SEO plugins. But even if they do, since you can control on what pages the tags fire with GTM, you won't have lots of different markups (some which may not be needed) on each page.
Brilliant post! *claps*
Very helpfulllllll! While using GTM I always thoughts of using it differently and I found this, likewise now I can do some testing with my work. Thank you and this definitely makes it faster to update web pages with schema, without having to go through the developer team.
I've been fortunate enough to work with WordPress websites mainly so I've always used plugins to help me add schema. This article goes a bit over my head, but makes me a little happier knowing I can use Google Tag Manager to help me out when I need something a little deeper than a plugin! Thanks for the article!
Thanks for the information, at https://www.boxchilli.com/ we have been implementing schemas to all of our client websites. This post really helps thanks!
Awesome Post, i really loved it. Thanks MOZ
Thanks chris!
Thank chris, for such an informative post. I would definitely give it a try with my blog.
This post is really very useful for us, i have read complete, i would like to thanks for sharing this information with us, have a great day
excellent article thanks
Thanks for the information..
Lead Retrieval App