Outputting JSON-LD with Angular Universal
Posted on
Last update on
Angular Universal - Advanced techniques series
This article is part of a growing series around advanced techniques in Angular with Universal. If you get excited about this article be sure to check the others!
- Better sharing on social media platforms with Angular Universal
- Outputting JSON-LD with Angular Universal
- Creating a simple memory cache for your Angular Universal website or application
- Angular v9 & Universal: SSR and prerendering out of the box!
Target audience
This article and guide on generating JSON-LD with Angular Universal is targeted to developers that want to generate the JSON that represents the Linked Data object and inject it correctly in the HTML that gets generated on the server. We will re-use and explain some techniques that the TransferState
key-value cache service uses to transfer server-state to the application on the client side.
First of all; What is JSON-LD?
JSON-LD is a lightweight Linked Data format. It is easy for humans and machines to read and write. It is based on the already successful JSON format and provides a way to help JSON data interoperate at Web-scale.
Linked Data empowers people that publish and use information on the Web. It is a way to create a network of standards-based, machine-readable data across Web sites. It allows an application to start at one piece of Linked Data, and follow embedded links to other pieces of Linked Data that are hosted on different sites across the Web.
These 2 definitions of JSON-LD and Linked Data were simply taken from the website of JSON-LD. This article does not go deep into the full specification of JSON-LD and it does not teach you how to properly generate your Linked Data structures. The tools of the JSON-ld website, like for example the playground, give you all you need to validate your Linked Data structures.
A basic example
What you can see below is a typical example of a JSON-LD data object, injected into the HTML. It describes a LocalBusiness
by following its definition and providing the properties that identifiy the entity. For example, the address, it's geolocation, opening hours etc are given. As you can see the address has it's own type PostalAddress
. Most of the types can specificy an external link and this is how data gets interweaved on the same website or resource and even on other websites.
json-ld-example.html (source)
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "LocalBusiness",
"address": {
"@type": "PostalAddress",
"addressLocality": "Manhattan",
"addressRegion": "NY",
"postalCode":"10036",
"streetAddress": "400 Broadway"
},
"description": "This is your business description.",
"name": "Craig's Car Repair",
"telephone": "555-111-2345",
"openingHours": "Mo,Tu,We,Th,Fr 09:00-17:00",
"geo": {
"@type": "GeoCoordinates",
"latitude": "20.75",
"longitude": "13.98"
},
"sameAs" : [ "http://www.facebook.com/example",
"http://www.twitter.com/example",
"http://plus.google.com/example"]
}
</script>
Why should you care about JSON-LD?
You should definitely care about JSON-LD for several reasons:
- It's simpler to generate, more structured and easier to read than Microdata
- It links or allows your data to be linked with other entities / data subjects on the world-wide web
In general; it ads a lot more semantic value to your HTML markup by providing context to what you show on the page and makes this context machine-readable, allowing it to be parsed more easily by search engines and other crawlers on the web.
Generating your JSON Linked Data structure
In the simple case of a blog or not too complex website, generating your JSON Linked Data can be done using the same base data you use to set your meta tags for social sharing and SEO. Your most important concern is building the correct structure. How your structure looks is completely dependent on your use case. As a basic example we will use this website, and more specifically the about page.
Using the router we first define the correct SEO data associated with our route. Just like we did before, setting the correct social share meta tags.
about-routing.module.ts
RouterModule.forChild([{
path: '',
component: AboutComponent,
data: {
seo: {
title: `About Sam - ${environment.seo.title}`,
description: `I'm a 30 year old software engineer living in Belgium.`,
shareImg: '/assets/share/about.png',
}
}
}])
Using a service we can subscribe to route changes to extract this data and pass the data to a service to parse the JSON-LD object.
route-helper.service.ts
@Injectable({
providedIn: 'root',
})
export class RouteHelper {
constructor(
private readonly router: Router,
private readonly activatedRoute: ActivatedRoute,
private readonly jsonLdService: JsonLdService
) {
this.setupRouting();
}
private setupRouting() {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => this.activatedRoute),
map(route => {
while (route.firstChild) {
route = route.firstChild;
}
return route;
}),
filter(route => route.outlet === 'primary')
).subscribe((route: ActivatedRoute) => {
const seo = route.snapshot.data['seo'];
// generate your JSON-LD object here
const jsonLd = {
name: seo.title,
url: environment.url + this.router.routerState.snapshot.url,
};
this.jsonLdService.setData('Website', jsonLd);
});
}
}
The JsonLdService
injected above is a simple service that caches and updates the data structure we need to output. A basic implementation of this service can be as follows, where you as a developer are still in charge of correctly structuring your object.
ngx-seo/json-ld.service.ts
@Injectable({
providedIn: 'root',
})
export class JsonLdService {
private jsonLd: any = {};
setData(type: string, rawData: any) {
this.jsonLd = this.getObject(type, rawData);
}
getObject(type: string, rawData?: any) {
let object = {
'@context': 'http://schema.org',
'@type': type,
};
if (rawData) {
object = Object.assign({}, object, rawData);
}
return object;
}
toJson() {
return JSON.stringify(this.jsonLd);
}
}
Using the approach explained above our current JSON-LD data-object in memory will look like this:
json output
{
"@context": "http://schema.org",
"@type": "Website",
"name": "About Sam - Sam Vloeberghs - Freelance Webdeveloper & Software Engineer",
"url": "https://samvloeberghs.be/about"
}
This basic example is what we wanted to achieve. The next step is getting this object outputted in our static HTML.
Injecting the JSON in the static HTML
Update (July 2020): The details that follow are not 100% correct anymore. We can use the JsonLdService
without a separate Browser and Server module. All you need to do is inject the simplified JsonLdModule
in your normal root component. For more details I refer to the ngx-seo
module.
Getting inspiration in the Angular source code
Injecting the JSON-LD data object in the DOM is only really required when we generate the HTML on the server. To get to this result I looked into the BrowserTransferStateModule
exposed via the @angular/platform-browser
module.
Essentialy this module, and more specifically, the TransferState
service it provides, does the same thing we want to achieve. It caches data, the result from for example HTTP calls, in an object and holds it in memory during the application lifecycle. Apart from the HTTP calls, that's exactly what our JsonLdService
does.
The server counter part, ServerTransferStateModule
exposed via the @angular/platform-server
, serializes the data and injects it in the DOM before generating the HTML on the server and sending it back over the wire to the browser. Again, that is exactly what we want to achieve. This is achieved by providing an extra factory function to the BEFORE_APP_SERIALIZED
token. Right after the application becomes stable all factories attached to the BEFORE_APP_SERIALIZED
are executed.
server: transfer_state.ts (full source code here)
export function serializeTransferStateFactory(
doc: Document, appId: string, transferStore: TransferState
) {
return () => {
const script = doc.createElement('script');
script.id = appId + '-state';
script.setAttribute('type', 'application/json');
script.textContent = escapeHtml(transferStore.toJson());
doc.body.appendChild(script);
};
}
@NgModule({
providers: [
TransferState,
{
provide: BEFORE_APP_SERIALIZED,
useFactory: serializeTransferStateFactory,
deps: [DOCUMENT, APP_ID, TransferState],
multi: true,
}
]
})
export class ServerTransferStateModule {
}
When the Angular application bootstraps in the browser, the TransferState
service picks up the serialized state in the static HTML and unserializes it, making it available as a direct cache. This way we avoid that the application makes a similar request for the same data to the server, for which the server-side-rendered version already did the call. We don't need this part, but it's good to know.
browser: transfer_state.ts (full source code here)
export function initTransferState(
doc: Document, appId: string
) {
const script = doc.getElementById(appId + '-state');
let initialState = {};
if (script && script.textContent) {
try {
initialState = JSON.parse(unescapeHtml(script.textContent));
} catch (e) {
console.warn('Exception while restoring TransferState for app ' + appId, e);
}
}
return TransferState.init(initialState);
}
@NgModule({
providers: [{
provide: TransferState,
useFactory: initTransferState,
deps: [DOCUMENT, APP_ID]
}s],
})
export class BrowserTransferStateModule {
}
Our simple BrowserJsonLdModule and ServerJsonLdModule
Following the concepts and ideas we learned from the ServerTransferStateModule
and the BrowserTransferStateModule
we create our own ServerJsonLdModule
and BrowserJsonLdModule
. The only small differences are limited to changing the type of the <script>
element and injecting it in the <head>
instead of right before the </body>
tag. We also don't need to pickup any state from the static HTML, as is done in the BrowserTransferStateModule
.
ngx-seo/json-ld.module.ts
import { NgModule } from '@angular/core';
import { JsonLdService } from 'ngx-seo/json-ld.service';
@NgModule({
providers: [
JsonLdService,
]
})
export class BrowserJsonLdModule {
}
ngx-seo/json-ld.server.module.ts
import { NgModule } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { BEFORE_APP_SERIALIZED } from '@angular/platform-server';
import { JsonLdService } from 'ngx-seo/json-ld.service';
export function serializeJsonLdFactory(doc: Document, jsonLdService: JsonLdService) {
const serializeAndInject = function () {
const script = doc.createElement('script');
script.setAttribute('type', 'application/ld+json');
script.textContent = jsonLdService.toJson();
doc.head.appendChild(script);
};
return serializeAndInject;
}
@NgModule({
providers: [
JsonLdService, {
provide: BEFORE_APP_SERIALIZED,
useFactory: serializeJsonLdFactory,
deps: [DOCUMENT, JsonLdService],
multi: true,
},
],
})
export class ServerJsonLdModule {
}
Differentiate execution on the server
To differentiate the execution of our application on the server versus on the browser, in Angular Universal we typically create a new server module app.server.module.ts
next to app.module.ts
that will directly import the AppModule
exported by app.module.ts
. This version of the application is used in the HTML renderer configured at the server side, in most cases as a rendering engine.
If we go back to the example of the TransferState
service, we see that the app.server.module.ts
is importing the ServerTransferStateModule
. This will overwrite the provider of the JsonLdService
imported in the app.module.ts
and provide the extra serialize functionality, next to also providing the TransferState
service.
app.module.ts
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule.withServerTransition({appId: 'samvloeberghs'}),
BrowserTransferStateModule,
BrowserJsonLdModule,
CommonModule,
AppRoutingModule
],
exports: [
AppComponent,
],
bootstrap: [AppComponent],
})
export class AppModule {
}
app.server.module.ts
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule,
ServerTransferStateModule,
ServerJsonLdModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {
}
We do the same thing with our BrowserJsonLdModule
and ServerJsonLdModule
. The browser module has to be imported in app.module.ts
while the server module has to be imported in app.server.module.ts
.
Conclusion
Generating JSON-LD using Angular Universal is pretty straight-forward. By borrowing concepts from the Angular source code we can create our own JsonLd
modules and services that enable us to output JSON-LD in the statically generated HTML on the server. The result of this code can be found live on this website, just look at the source and do a hard refresh. Why a hard refresh you might ask? Because of the service worker caching the default index.html
as the app shell.
Further reading
- A Guide to JSON-LD for Beginners
- Why Is JSON-LD Important To Businesses?
- JSON for Linking Data - Official website
- Getting started with schema.org using Microdata
- Google’s Structured Data Testing Tool
- Steal Our JSON-LD - Structured Data. Made Simple.
- Angular SEO with schema and JSON-LD - a different approach
Special thanks to
for reviewing this post and providing valuable and much-appreciated feedback!
Contents
- Introduction
- Target audience
- First of all; What is JSON-LD?
- A basic example
- Why should you care about JSON-LD?
- Generating your JSON Linked Data structure
- Injecting the JSON in the static HTML
- Getting inspiration in the Angular source code
- Our simple BrowserJsonLdModule and ServerJsonLdModule
- Differentiate execution on the server
- Conclusion
- Further reading
- Special thanks to
By reading this article I hope you can find a solution for your problem. If it still seems a little bit unclear, you can hire me for helping you solve your specific problem or use case. Sometimes even just a quick code review or second opinion can make a great difference.