title: Angular Universal with Contentful and TransferState
body: <p>I am having some real issue with <a href="https://www.contentful.com/" rel="nofollow noreferrer">contentful</a> and angular universal. I have tried so many things to try to get the content to load and be rendered in <strong>View Page Source</strong> but everything I have tried has failed.
  The closest I have got is with using the <strong>TransferState</strong> which I found with a tutorial covering API calls.</p>
  <p>I created this class:</p>
  <pre><code>import { Injectable } from '@angular/core';
  import { TransferState } from '@angular/platform-browser';
  import { Observable, from } from 'rxjs';
  import { HttpClient } from '@angular/common/http';
  import { tap } from 'rxjs/operators';
  
  @Injectable({
    providedIn: 'root',
  })
  export class TransferHttpService {
    constructor(
      private transferHttp: TransferState,
      private httpClient: HttpClient
    ) {}
    get(url, options?): Observable&lt;any&gt; {
      return this.getData(url, options, () =&gt; {
        return this.httpClient.get(url, options);
      });
    }
    post(url, body, options?): Observable&lt;any&gt; {
      return this.getData(url, options, () =&gt; {
        return this.httpClient.post(url, body, options);
      });
    }
    delete(url, options?): Observable&lt;any&gt; {
      return this.getData(url, options, () =&gt; {
        return this.httpClient.delete(url, options);
      });
    }
    put(url, body, options?): Observable&lt;any&gt; {
      return this.getData(url, options, () =&gt; {
        return this.httpClient.put(url, body, options);
      });
    }
    getData(url, options, callback: () =&gt; Observable&lt;any&gt;): Observable&lt;any&gt; {
      const optionsString = options ? JSON.stringify(options) : '';
      let key = `${url + optionsString}`;
      try {
        return this.resolveData(key);
      } catch (e) {
        console.log('In catch', key);
        return callback().pipe(
          tap((data) =&gt; {
            console.log('cache set', key);
            this.setCache(key, data);
          })
        );
      }
    }
    resolveData(key) {
      let resultData: any;
      if (this.hasKey(key)) {
        resultData = this.getFromCache(key);
        console.log('got cache', key);
      } else {
        throw new Error();
      }
      return from(Promise.resolve(resultData));
    }
    setCache(key, value) {
      this.transferHttp.set(key, value);
    }
    getFromCache(key) {
      return this.transferHttp.get(key, null); // null set as default value
    }
    hasKey(key) {
      return this.transferHttp.hasKey(key);
    }
  }
  </code></pre>
  <p>And then where I injected <code>HttpClient</code>, I replaced with my new class (<code>TransferHttpService</code>) which works fine.
  When using contentful, I updated my old service to this:</p>
  <pre><code>import { Injectable } from '@angular/core';
  import { createClient, Entry } from 'contentful';
  import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
  import { Observable, from } from 'rxjs';
  
  import { environment } from '@environments/environment';
  import { Content, Page, Menu, MenuItem, Footer, Image } from '@models';
  import { TransferHttpService } from './transfer-http.service';
  
  @Injectable({
    providedIn: 'root',
  })
  export class ContentfulService {
    private client = createClient({
      space: environment.space,
      accessToken: environment.cdaAccessToken,
    });
  
    constructor(private http: TransferHttpService) {}
  
    getMetadata(): Observable&lt;any&gt; {
      return this.http.getData('metadata', null, () =&gt; {
        return from(
          this.client
            .getEntries({
              content_type: 'metadata',
            })
            .then((response) =&gt; response.items[0])
        );
      });
    }
  
    getPages(): Observable&lt;Page[]&gt; {
      return this.http.getData('pages', null, () =&gt; {
        return from(
          this.client
            .getEntries({
              content_type: 'page',
              include: 3,
            })
            .then((response) =&gt; {
              let pages = response.items.map((item) =&gt;
                this.createPage(item, this.createContent)
              );
              return pages;
            })
        );
      });
    }
  
    getNavigation(): Observable&lt;Menu&gt; {
      return this.http.getData('navigation', null, () =&gt; {
        return from(
          this.client
            .getEntries({
              content_type: 'navigationMenu',
            })
            .then((response) =&gt; this.createMenu(response.items[0]))
        );
      });
    }
  
    getFooter(): Observable&lt;Footer&gt; {
      return this.http.getData('footer', null, () =&gt; {
        return from(
          this.client
            .getEntries({
              content_type: 'footer',
              include: 2,
            })
            .then((response) =&gt; this.createFooter(response.items[0]))
        );
      });
    }
  
    public createImage(component: any): Image {
      if (!component) return;
      return {
        title: component.fields.title,
        url: component.fields.file.url,
        alt: component.fields.file.fileName,
      };
    }
  
    private createFooter(component: any): Footer {
      return {
        title: component.fields.title,
        htmlContent: documentToHtmlString(component.fields.content),
        careersText: component.fields.careersText,
        careersLink: component.fields.careersLink,
        cookiePolicyLink: component.fields.cookiePolicyLink,
        privacyPolicyLink: component.fields.privacyPolicyLink,
        termsLink: component.fields.termsLink,
      };
    }
  
    private createMenu(menu: any): Menu {
      return {
        title: menu.fields.title,
        links: menu.fields.links.map(this.createMenuItem),
      };
    }
  
    private createMenuItem(item: any): MenuItem {
      return {
        path: item.fields.path,
        text: item.fields.linkText,
      };
    }
  
    private createPage(page: Entry&lt;any&gt;, createContent: any): Page {
      return {
        title: page.fields['title'],
        slug: page.fields['path'],
        linkText: page.fields['linkText'],
        content: page.fields['content'].map(createContent),
      };
    }
  
    private createContent(component: Entry&lt;any&gt;): Content {
      return {
        type: component.sys.contentType.sys.id,
        fields: component.fields,
      };
    }
  }
  </code></pre>
  <p>As you can see I am executing the <strong>createClient</strong> <code>getEntries</code> promise to an observable after I have handled the response, which I then pass to the <code>TransferHttpService</code> to the <code>getData</code> method which is responsible for setting/getting the cached response.</p>
  <p>I suspect this is where the issue lies but the header and footer always seem to be cached.</p>
  <hr />
  <p>I have been playing around with this and I got better results when I changed my <strong>TransferHttpService</strong> to this:</p>
  <pre><code>import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
  import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
  import {
    TransferState,
    StateKey,
    makeStateKey,
  } from '@angular/platform-browser';
  import { Observable, from } from 'rxjs';
  import { tap } from 'rxjs/operators';
  import { isPlatformBrowser, isPlatformServer } from '@angular/common';
  
  @Injectable({ providedIn: 'root' })
  export class TransferHttpService {
    constructor(
      protected transferState: TransferState,
      private httpClient: HttpClient,
      @Inject(PLATFORM_ID) private platformId: Object
    ) {}
  
    request&lt;T&gt;(
      method: string,
      uri: string | Request,
      options?: {
        body?: any;
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        reportProgress?: boolean;
        observe?: 'response';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getData&lt;T&gt;(
        method,
        uri,
        options,
        (method: string, url: string, options: any) =&gt; {
          return this.httpClient.request&lt;T&gt;(method, url, options);
        }
      );
    }
  
    /**
     * Performs a request with `get` http method.
     */
    get&lt;T&gt;(
      url: string,
      options?: {
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        observe?: 'response';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getData&lt;T&gt;(
        'get',
        url,
        options,
        (_method: string, url: string, options: any) =&gt; {
          return this.httpClient.get&lt;T&gt;(url, options);
        }
      );
    }
  
    /**
     * Performs a request with `post` http method.
     */
    post&lt;T&gt;(
      url: string,
      body: any,
      options?: {
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        observe?: 'response';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getPostData&lt;T&gt;(
        'post',
        url,
        body,
        options,
        // tslint:disable-next-line:no-shadowed-variable
        (_method: string, url: string, body: any, options: any) =&gt; {
          return this.httpClient.post&lt;T&gt;(url, body, options);
        }
      );
    }
  
    /**
     * Performs a request with `put` http method.
     */
    put&lt;T&gt;(
      url: string,
      _body: any,
      options?: {
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        observe?: 'body';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getPostData&lt;T&gt;(
        'put',
        url,
        _body,
        options,
        (_method: string, url: string, _body: any, options: any) =&gt; {
          return this.httpClient.put&lt;T&gt;(url, _body, options);
        }
      );
    }
  
    /**
     * Performs a request with `delete` http method.
     */
    delete&lt;T&gt;(
      url: string,
      options?: {
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        observe?: 'response';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getData&lt;T&gt;(
        'delete',
        url,
        options,
        (_method: string, url: string, options: any) =&gt; {
          return this.httpClient.delete&lt;T&gt;(url, options);
        }
      );
    }
  
    /**
     * Performs a request with `patch` http method.
     */
    patch&lt;T&gt;(
      url: string,
      body: any,
      options?: {
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        observe?: 'response';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getPostData&lt;T&gt;(
        'patch',
        url,
        body,
        options,
        // tslint:disable-next-line:no-shadowed-variable
        (
          _method: string,
          url: string,
          body: any,
          options: any
        ): Observable&lt;any&gt; =&gt; {
          return this.httpClient.patch&lt;T&gt;(url, body, options);
        }
      );
    }
  
    /**
     * Performs a request with `head` http method.
     */
    head&lt;T&gt;(
      url: string,
      options?: {
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        observe?: 'response';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getData&lt;T&gt;(
        'head',
        url,
        options,
        (_method: string, url: string, options: any) =&gt; {
          return this.httpClient.head&lt;T&gt;(url, options);
        }
      );
    }
  
    /**
     * Performs a request with `options` http method.
     */
    options&lt;T&gt;(
      url: string,
      options?: {
        headers?:
          | HttpHeaders
          | {
              [header: string]: string | string[];
            };
        observe?: 'response';
        params?:
          | HttpParams
          | {
              [param: string]: string | string[];
            };
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
      }
    ): Observable&lt;T&gt; {
      // tslint:disable-next-line:no-shadowed-variable
      return this.getData&lt;T&gt;(
        'options',
        url,
        options,
        // tslint:disable-next-line:no-shadowed-variable
        (_method: string, url: string, options: any) =&gt; {
          return this.httpClient.options&lt;T&gt;(url, options);
        }
      );
    }
  
    // tslint:disable-next-line:max-line-length
    getData&lt;T&gt;(
      method: string,
      uri: string | Request,
      options: any,
      callback: (
        method: string,
        uri: string | Request,
        options: any
      ) =&gt; Observable&lt;any&gt;
    ): Observable&lt;T&gt; {
      let url = uri;
  
      if (typeof uri !== 'string') {
        url = uri.url;
      }
  
      const tempKey = url + (options ? JSON.stringify(options) : '');
      const key = makeStateKey&lt;T&gt;(tempKey);
      try {
        return this.resolveData&lt;T&gt;(key);
      } catch (e) {
        console.log('in catch', key);
        return callback(method, uri, options).pipe(
          tap((data: T) =&gt; {
            if (isPlatformBrowser(this.platformId)) {
              // Client only code.
              // nothing;
            }
            if (isPlatformServer(this.platformId)) {
              console.log('set cache', key);
              this.setCache&lt;T&gt;(key, data);
            }
          })
        );
      }
    }
  
    private getPostData&lt;T&gt;(
      _method: string,
      uri: string | Request,
      body: any,
      options: any,
      callback: (
        method: string,
        uri: string | Request,
        body: any,
        options: any
      ) =&gt; Observable&lt;any&gt;
    ): Observable&lt;T&gt; {
      let url = uri;
  
      if (typeof uri !== 'string') {
        url = uri.url;
      }
  
      const tempKey =
        url +
        (body ? JSON.stringify(body) : '') +
        (options ? JSON.stringify(options) : '');
      const key = makeStateKey&lt;T&gt;(tempKey);
  
      try {
        return this.resolveData&lt;T&gt;(key);
      } catch (e) {
        return callback(_method, uri, body, options).pipe(
          tap((data: T) =&gt; {
            if (isPlatformBrowser(this.platformId)) {
              // Client only code.
              // nothing;
            }
            if (isPlatformServer(this.platformId)) {
              this.setCache&lt;T&gt;(key, data);
            }
          })
        );
      }
    }
  
    private resolveData&lt;T&gt;(key: StateKey&lt;T&gt;): Observable&lt;T&gt; {
      const data = this.getFromCache&lt;T&gt;(key);
      console.log(data);
  
      if (!data) {
        throw new Error();
      }
  
      if (isPlatformBrowser(this.platformId)) {
        console.log('get cache', key);
        // Client only code.
        this.transferState.remove(key);
      }
      if (isPlatformServer(this.platformId)) {
        console.log('we are the server');
        // Server only code.
      }
  
      return from(Promise.resolve&lt;T&gt;(data));
    }
  
    private setCache&lt;T&gt;(key: StateKey&lt;T&gt;, data: T): void {
      return this.transferState.set&lt;T&gt;(key, data);
    }
  
    private getFromCache&lt;T&gt;(key: StateKey&lt;T&gt;): T {
      return this.transferState.get&lt;T&gt;(key, null);
    }
  }
  </code></pre>
  <p>after spending a few hours on this I have found that contentful only works when there is an API call to my server, which is odd.....</p>
  <hr />
  <p>Argh! I swear, this should not be this difficult.
  If I have a component like this:</p>
  <pre><code>import { Component, OnInit } from '@angular/core';
  
  import { Page } from '@models';
  import { ContentfulService, TransferHttpService } from '@services';
  import { environment } from '@environments/environment';
  
  @Component({
    templateUrl: './pages.component.html',
    styleUrls: ['./pages.component.scss'],
  })
  export class PagesComponent implements OnInit {
    public pages: Page[];
  
    constructor(
      private brandService: TransferHttpService,
      private contentfulService: ContentfulService
    ) {}
  
    ngOnInit(): void {
      this.listPages();
      this.listBrands();
    }
  
    private listPages(): void {
      this.contentfulService
        .getPages()
        .subscribe((pages: any) =&gt; (this.pages = pages));
    }
  
    private listBrands(): void {
      this.brandService
        .get(`${environment.apiUrl}/brands/simple`)
        .subscribe((response: any) =&gt; {
          console.log(response);
        });
    }
  }
  </code></pre>
  <p>when I run my local server <code>npm run dev:ssr</code> I get this in the console:</p>
  <p><a href="https://i.stack.imgur.com/APp2O.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/APp2O.png" alt="enter image description here" /></a></p>
  <p>Which is perfect. Everything is in my <strong>View Page Source</strong>.
  BUT! If I remove the <code>listBrands</code> method (because it's not needed, I was just using it as a test). I get this:</p>
  <p><a href="https://i.stack.imgur.com/h7epB.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/h7epB.png" alt="enter image description here" /></a></p>
  <p>Which is not good; nothing is cached and my <strong>View Page Source</strong> has no header, footer, page or metadata.....</p>
  <p>I can't understand what is going on. Why does it work when I have another API call to a different server?</p>