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;
}
}
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.