angularexpressserver-side-renderingangular-universalcontentful

Angular Universal doesn't load my component in view-source when using Route Resolver


I'm using SSR application together with Contentful CMS with Route Resolver to fetch the data before loading the component. When I build and serve the application there's no error and I can see the content in the client side, but when I look the view-source, everything but the initial component with the Route Resolver is being rendered. When I remove the resolver and place some static elements inside of the component, then I see it in the view-source.

I have already implemented http interceptor for absolute urls and configured my server.ts properly, but still couldn't find the reason why it's not being rendered.

routes:

import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
import { PageComponent } from './page/page.component';
import { PageResolver } from './page/page.resolver.service';


const routes: Routes = [
  { path: ':slug', component: PageComponent, resolve: { page: PageResolver } },
  { path: '', component: PageComponent, resolve: { page: PageResolver } },
  { path: '**', component: PageComponent, resolve: { page: PageResolver } }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules,
      scrollPositionRestoration: 'top',
      enableTracing: false,
      anchorScrolling: 'enabled'
    })
  ],
  exports: [RouterModule],
  providers: [PageResolver]
})
export class AppRoutingModule { }

resolver:

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { ContentfulService} from '../core/contentful.service';
import { PageModel } from '../models/page.model';

@Injectable()
export class PageResolver implements Resolve<PageModel> {

  constructor(private contentService: ContentfulService) { }

  resolve(route: ActivatedRouteSnapshot): Promise<PageModel> {
    const slug = this.getSlug(route);
    const page = this.contentService.getContentBySlug<PageModel>(slug, 'page', 10);
    return page;
  }

  private getSlug(route: ActivatedRouteSnapshot): string {
    const routeLength = route.url.length;
    if (routeLength === 0) {
      return 'home';
    }

    if (route.data.slug === 'error') {
      return route.data.slug;
    }

    return route.url.map((urlFragment) => urlFragment.path).join('/');
  }
}

page.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';

import { ContentfulService } from '../core/contentful.service';
import { PageModel } from '../models/cms/page.model';
import { ActivatedRoute } from '@angular/router';
import { untilDestroyed } from 'ngx-take-until-destroy';

@Component({
  selector: 'app-page',
  templateUrl: './page.component.html',
  styleUrls: ['./page.component.scss']
})
export class PageComponent implements OnInit, OnDestroy {
  page: PageModel;

  constructor(private route: ActivatedRoute,
              public contentful: ContentfulService) { }

  ngOnInit() {
    console.log('HIT 1');
    this.route.data.pipe(untilDestroyed(this)).subscribe(({ page }) => {
      this.page = page;
    });
  }

  ngOnDestroy(): void {
  }
}

server.ts

import 'zone.js/dist/zone-node';
import 'reflect-metadata';

require('source-map-support').install();

import express from 'express';
import compression from 'compression';
import {join} from 'path';
import domino from 'domino';
import fs from 'fs';
import path from 'path';

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

const template = fs.readFileSync(path.join(process.cwd(), 'dist/my-site', 'index.html')).toString();
console.log(template);
const win = domino.createWindow(template);
global['window'] = win;
// not implemented property and functions
Object.defineProperty(win.document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true,
    };
  },
});
global['document'] = win.document;
// othres mock
global['CSS'] = null;

import {enableProdMode} from '@angular/core';

import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();
app.use(compression());

// redirects!
const redirectowww = false;
const redirectohttps = false;
const wwwredirecto = true;
app.use((req, res, next) => {
  // for domain/index.html
  if (req.url === '/index.html') {
    res.redirect(301, 'https://' + req.hostname);
  }

  // check if it is a secure (https) request
  // if not redirect to the equivalent https url
  if (
    redirectohttps &&
    req.headers['x-forwarded-proto'] !== 'https' &&
    req.hostname !== 'localhost'
  ) {
    // special for robots.txt
    if (req.url === '/robots.txt') {
      next();
      return;
    }
    res.redirect(301, 'https://' + req.hostname + req.url);
  }

  // www or not
  if (redirectowww && !req.hostname.startsWith('www.')) {
    res.redirect(301, 'https://www.' + req.hostname + req.url);
  }

  // www or not
  if (wwwredirecto && req.hostname.startsWith('www.')) {
    const host = req.hostname.slice(4, req.hostname.length);
    res.redirect(301, 'https://' + host + req.url);
  }

  next();
});

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/my-site');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap} = require('./dist/server/main');

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
app.get('*.*', express.static(DIST_FOLDER, {
  maxAge: '1y'
}));

// All regular routes use the Universal engine
// dynamic render
app.get('*', (req, res) => {
  // mock navigator from req.
  global['navigator'] = req['headers']['user-agent'];
  const http =
    req.headers['x-forwarded-proto'] === undefined ? 'http' : req.headers['x-forwarded-proto'];

  const url = req.originalUrl;
  // tslint:disable-next-line:no-console
  console.time(`GET: ${url}`);
  res.render(
    '../my-site/index',
    {
      req: req,
      res: res,
      // provers from server
      providers: [
        // for http and cookies
        {
          provide: REQUEST,
          useValue: req,
        },
        {
          provide: RESPONSE,
          useValue: res,
        },
        // for absolute path
        {
          provide: 'ORIGIN_URL',
          useValue: `${http}://${req.headers.host}`,
        },
      ],
    },
    (err, html) => {
      if (!!err) {
        throw err;
      }

      // tslint:disable-next-line:no-console
      console.timeEnd(`GET: ${url}`);
      res.send(html);
    },
  );
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
});

Appreciate any help or advice.

EDIT:

contentful.service.ts:

import { Injectable } from '@angular/core';
import { createClient, ContentfulClientApi, EntryCollection, Entry, Asset } from 'contentful';
import { AppConfig } from './config/app-config.service';

@Injectable()
export class ContentfulService {

  private readonly contentfulClient: ContentfulClientApi;

  constructor() {
    this.contentfulClient = createClient({
      host: AppConfig.settings.contentfulHost,
      space: AppConfig.settings.contentfulSpace,
      accessToken: AppConfig.settings.contentfulAccessToken
    });
  }

  public async getContent<T>(contentId: string, include: number = 1, localeCode = AppConfig.settings.locale): Promise<T> {
    return this.contentfulClient.getEntries({ 'sys.id': contentId, include, locale: localeCode })
      .then(res => {
        return this.parseModel(res.items[0], res) as T;
      });
  }

  public async getContentBySlug<T>(slug: string, contentType: string,
                                   include: number = 1, localeCode = AppConfig.settings.locale): Promise<T> {
    console.log('CONTENTFUL STARTS.');
    return this.contentfulClient.getEntries({ content_type: contentType, 'fields.slug': slug, include, locale: localeCode })
      .then(res => {
        console.log('CONTENTFUL ENDING.');
        return this.parseModel(res.items[0], res) as T;
      });
  }

  public parseModel(model: Entry<any> | Asset, collection: EntryCollection<any>): any {
    if (!model) {
      return model;
    }
    console.log('PARSING STARTS.');
    const parsedModel = { sys: model.sys };

    for (const property in model.fields) {
      if (model.fields.hasOwnProperty(property)) {
        let value = model.fields[property];
        if (value instanceof Array) {
          const arrayValue: any[] = [];
          for (const item of value) {
            arrayValue.push(this.parseValue(item, collection));
          }
          value = arrayValue;
        } else {
          value = this.parseValue(value, collection);
        }
        parsedModel[property] = value;
      }
    }
    console.log('PARSING ENDING.');
    return parsedModel;
  }

  private parseValue(value: any, collection: EntryCollection<any>) {
    if (value && value.sys) {
      switch (value.sys.type) {
        case 'Entry':
          value = this.parseModel(value, collection);
          break;
        case 'Asset':
          value = this.parseModel(value, collection);
          break;
      }
    }
    return value;
  }

  public isContentOfType(contentItem: any, contentId: string): boolean {
    if (!contentItem || !contentItem.sys.contentType) {
      return false;
    }
    return contentItem.sys.contentType.sys.id === contentId;
  }
}


Solution

  • So here's how I solved this issue after spending days trying different approaches. Somehow contentful library for angular wasn't working properly on SSR and using route resolver. So I moved the logic and calls to contentful to our proxy API (which is already taking care of other services calls). In this way I was able to use SSR and route resolver calling contentful through the proxy API. The proxy API is built in .NET core and deployed on Azure.

    I don't know why this happens, but I hope this brings light for anyone going through similar issues with this or other similar libraries.