Outputting JSON-LD with Angular Universal
Tags: angular, seo, universal, jsonld
Published: 2019-06-07 18:39:16

*Originally published at [https://samvloeberghs.be](https://samvloeberghs.be/posts/outputting-json-ld-with-angular-universal) on March 12, 2019*

## 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](https://en.wikipedia.org/wiki/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](https://json-ld.org/), like for example the [playground](https://json-ld.org/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](https://schema.org/LocalBusiness) 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.

```html
<!-- json-ld-example.html -->

<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](https://schema.org/docs/gs.html)
- 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 usecase. As a basic example we will use this website, and more specifically the [about page](https://samvloeberghs.be/about).

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](https://samvloeberghs.be/posts/better-sharing-on-social-media-platforms-with-angular-universal).

```ts
// 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.

```ts
// 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.

```ts
// 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
{
    "@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

### 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`(https://github.com/angular/angular/blob/master/packages/platform-browser/src/browser/transfer_state.ts) exposed via the `@angular/platform-browser` module.

Essentialy this module, and more specifically, the `TransferState`(https://github.com/angular/angular/blob/master/packages/platform-browser/src/browser/transfer_state.ts) 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`(https://github.com/angular/angular/blob/master/packages/platform-server/src/transfer_state.ts) 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.

```ts
// server: transfer_state.ts

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.

```ts
// browser: transfer_state.ts

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`.

```ts
// 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](https://expressjs.com/en/guide/using-template-engines.html).

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.

```ts
// app.module.ts
`
@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule.withServerTransition({appId: 'samvloeberghs'}),
    BrowserTransferStateModule,
    BrowserJsonLdModule,
    CommonModule,
    AppRoutingModule
  ],
  exports: [
    AppComponent,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {
}
```

```ts
// 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](https://moz.com/blog/json-ld-for-beginners) 
- [Why Is JSON-LD Important To Businesses?](https://www.forbes.com/sites/forbestechcouncil/2019/02/25/why-is-json-ld-important-to-businesses/amp/) 
- [JSON for Linking Data - Official website](https://json-ld.org/) 
- [Getting started with schema.org using Microdata ](https://schema.org/docs/gs.html)
- [Google’s Structured Data Testing Tool](https://search.google.com/structured-data/testing-tool/u/0/) 
- [Steal Our JSON-LD - Structured Data. Made Simple.](https://jsonld.com/)
- [Angular SEO with schema and JSON-LD - a different approach](https://twitter.com/0594CF)

## Special thanks to

- [Ksenia Karelskaya](https://twitter.com/0594CF)

*Originally published at [https://samvloeberghs.be](https://samvloeberghs.be/posts/outputting-json-ld-with-angular-universal) on March 12, 2019*