For a long long time, Google search optimization relied on a limited set of tools like auto crawl, search console or supported schema types. The things, however, started changing in 2015 when Google finally decided to give ld+json a green signal, opening doors for a variety of rich cards which have strictly been limited till that point.

If you're not familiar with rich cards, this is what Google has to say about them,

A card is the fundamental presentation unit for Search results. A rich card is a more engaging level of presentation because it improves on the standard Search result with a more structured and visual preview of things you describe with your markup.

Google now supports an array of rich cards like BreadCrumbs, Sitelinks, Sitelink Search, Corporate Contact Info, Logos and more, and they can easily be incorporated into normal pre-rendered sites using the ld+json script. With JavaScript frameworks such as AngularJs or React, the things however tend to go south. The reason is very straightforward -- the views render after the main page loads, and thus we can include SEO data of that view in the body only after the view has loaded (or the AJAX request has successfully completed). This, however, can be taken as an advantage as well as we can dynamically change the content of the script depending on the page with a single directive.


Okay, so here's what we are going to do:

- Create a directive.

- Declare directive's element tag inside the main page

- Load SEO data into a scope variable

- Watch for scope change on that variable

- Filter the scope variable's data and put it inside a script

- Replace the directive with that script

1. Creating the directive and adding element tag

We declare a directive with name, richcard (feel free to choose any other) and within it, access $filter service. We will know more about this service in a while. A directive function is expected to return a set of properties related to the target element for the angular compiler to know of its behavior. If you look at the example below, you'll notice we are returning restrict and link properties.

A restrict property tells the compiler what kind of declaration it is -- is it an element or an attribute directive. There are two more restrictions, class  (C) and comment (M), but we will focus on the first two only. The value EA to restrict allows us to use this directive as either an element or an attribute.  This example uses element and the declaration for that looks like this,

<richcard></richcard>

The link property, on the other hand, lets us access the element and root scope. We manipulate the directive element here. We will talk about manipulation after we have our scope data ready.

var app = angular.module('home', []);
app.directive('richcard', ['$filter', function ($filter) {
return {
restrict: 'EA',
link: function (scope, element) {
scope.$watch('ld', function (value) {
var val = $sce.trustAsHtml($filter('json')(value));
element[0].outerHTML = '<script type="application/ld+json">'+ val + '</script>'
});
}
};
}]);
view raw RichCardDirective.js hosted with ❤ by GitHub

2. Loading SEO data into Scope variable

This is fairly an easy step. Just like we initialize a $scope variable, we will initialize a $scope.$root variable inside the desired controller with our ld+json object. The benefit with the latter is that we can have our directive declared on the main page, rather than partials. If you want to use it on a single partial only, go for $scope. The example below decalred a root scope variable, ld, containing schema oriented data of My Wonderful Site. If you're unaware of the schema required for linked data json, head over to Google's Structured Data guide.

app.controller('homeController', ['$scope', function ($scope) {
$scope.$root.ld = {
"@context": "http://schema.org/",
"@type": "WebSite",
"name": 'My Wonderful Site',
"image": "https://mywonderfulsite.com/logo.png",
"description": 'We have the best content in the world',
"url": 'https://mywonderfulsite.com/'
}
}]);
view raw Controller.js hosted with ❤ by GitHub

3. Watching for Scope change

We go back to the first example. Notice that we have a watch declared on our scope inside directive's link function. This watch lets us know when the scope's value has changed and we trigger our manipulation only then. The $watch function accepts the variable name to be watched and callback function holding the value of the variable. We use this value to filter our JSON and make it usable for our purpose in the next step.

4. Filter JSON Object

The value we receive from the $watch callback is a pure JSON Object, which can't be appended with a string as per our requirement. We need to change it as per our need. There are two ways to do so, the formal way goes with filtering it as JSON. We use the earlier declared $filter service. We first tell the $filter service that we are inputting a JSON object, and then pass the object itself. $filter converts the passed JSON object into a JSON string suitable for our use.

var val = $filter('json')(value);

 The informal but straightforward way simply stringifies it using JSON.stringify(...).

var val = JSON.stringify(value);

5. Replace directive element with Script

Now that we have our JSON string ready, we can append it in between opening and closing script strings and apply the whole string as the outerHTML of the element which in the true sense, replaces the element with the script.

element[0].outerHTML = '<script type="application/ld+json">'+ val + '</script>'

Okay, so we are done. Run the project and see the change happen. An example ld+json script with data looks like this one I found on Apple's website.

/* visit www.apple.com and look at
the source code to find it.
*/
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@id": "https://www.apple.com/#organization",
"@type": "Organization",
"name": "Apple",
"url": "https://www.apple.com/",
"logo": "https://www.apple.com/ac/structured-datahttps://cdn.contextneutral.com/i/knowledge_graph_logo.png?201703170823",
"contactPoint": [
{
"@type": "ContactPoint",
"telephone": "+1-800-692-7753",
"contactType": "sales",
"areaServed": [ "US" ]
}
],
"sameAs": [
"http://www.wikidata.org/entity/Q312",
"https://www.youtube.com/user/Apple",
"https://www.linkedin.com/company/apple",
"https://www.facebook.com/Apple",
"https://www.twitter.com/Apple"
]
}
</script>

Take a look at this example to know of the structure and visit schema.org for more schema types. Also, once you have correctly created and used the directive, go to Google's Structured Data Testing Tool (SDTT) to check for any errors in your data. Keep in mind that if the ld+json script's data relies on a partial's controller, you may first have to render the page before putting its url in SDTT for the tool to check its presence or you could simply put the script's code in the code snippet box of SDTT.