2

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

Max Amorim
  • 147
  • 1
  • 13
  • Show your resolver's code (you included the routes code twice) – David Mar 04 '20 at 13:37
  • @David my bad, updated with the correct resolver. – Max Amorim Mar 05 '20 at 13:35
  • Any error in the console server side? Otherwise, add some `console.log` in your code to figure out what is called exactly. And could you provide the code for `getContentBySlug`? – David Mar 05 '20 at 13:46
  • @David I have added the contentful service code. I also added logs in the server.ts, resolver, service and component. I receive the logs from all of them but not from the component: `Node Express server listening on http://localhost:4000` `RESOLVER STARTS.` `CONTENTFUL STARTS.` `RESOLVER ENDING.` `EXPRESS GET: /: 295.366ms` `CONTENTFUL ENDING.` `PARSING STARTS.` `PARSING ENDING.` – Max Amorim Mar 05 '20 at 16:10

4 Answers4

1

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.

Max Amorim
  • 147
  • 1
  • 13
0

I fixed it on my project, i used the

@NgModule({
  imports: [RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules,
      scrollPositionRestoration: "enabled",
      useHash: true
    }
  )
  ],
  exports: [RouterModule]
})

on the imports of AppRoutingModule and works for me, see my code bellow:

import {NgModule} from '@angular/core';
import {PreloadAllModules, RouterModule, Routes} from '@angular/router';
import {HomeComponent} from "./views/home/home.component";
import {CreditTypeComponent} from "./views/credit-type/credit-type.component";
import {ForPartnersComponent} from "./views/for-partners/for-partners.component";
import {CareersComponent} from "./views/careers/careers.component";
import {HelpTabComponent} from './views/help/help-tab.component';
import {BePartnerFormComponent} from "./views/be-partner-form/be-partner-form.component";
import {EmbeddedSimulatorComponent} from "./components/simulator/embedded-simulator/embedded-simulator.component";
import {BlogListComponent} from "./views/blog/blog-list/blog-list.component";
import {BlogPostComponent} from "./views/blog/blog-post/blog-post.component";

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'home'
  },
  {
    path: 'home',
    component: HomeComponent
  },
  {
    path: 'solutions',
    children: [
      {
        path: 'credit-type',
        component: CreditTypeComponent
      },
      {
        path: 'simulator',
        component: EmbeddedSimulatorComponent
      },
      {
        path: 'for-partners',
        component: ForPartnersComponent
      },
    ]
  },
  {
    path: 'careers',
    component: CareersComponent
  },
  {
    path: 'help',
    component: HelpTabComponent
  },
  {
    path: 'be-partner',
    component: BePartnerFormComponent
  },
  {
    path: 'blog',
    children: [
      {
        path: 'post/:postId', component: BlogPostComponent
      },
      {
        path: '', component: BlogListComponent
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules,
      scrollPositionRestoration: "enabled",
      useHash: true
    }
  )
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {
}
0

So I know that this question isn't answered much. But let me put light on this scenario.

Resolver

A data provider class can be used with the router to resolve data during navigation. The interface defines a resolve() method that is invoked right after the ResolveStart router event. The router waits for the data to be resolved before the route is finally activated.

How it works

Create a resolver within your component / architecture based. In your General-Landing-Component.ts subscribe to the resolver function and invoke the API calls to view page on SSR.

General-Landing-Resolver.ts

import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { ActivatedRouteSnapshot,  Resolve,  RouterStateSnapshot,  Router,  ActivatedRoute,} from '@angular/router';

import { Observable, of } from 'rxjs';
import { ContentfulService } from '../../contentful/services/contentful.service';
import { ContentfulResponse } from '../../contentful/interfaces/contentful-response';
import { map } from 'rxjs/operators';
import { isPlatformBrowser } from '@angular/common';
@Injectable({
  providedIn: 'root',
})
export class generalLandingResolver implements Resolve<ContentfulResponse> {
  urlPath: any;
  responseData: any;
  private isBrowser: boolean = false;
  constructor(
    private contentfulService: ContentfulService,
    private router: Router,
    private activatedRoute: ActivatedRoute,
    @Inject(PLATFORM_ID) private platformId: Object
  ) {
    this.isBrowser = isPlatformBrowser(platformId);
  }
  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<any> {
    const tree = this.router.parseUrl(state.url);
    const children = tree.root.children.primary;
    const segments = children.segments[1].path;
    this.urlPath = segments;

    return this.contentfulService.generalLandingPageBySlug(this.urlPath).pipe(
      map((response) => {
        if (this.isBrowser) {
          sessionStorage.setItem('response', response);
        }
      })
    );
  }
}

General-Landing-Component.ts

import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { Router } from '@angular/router';
import { combineLatest, EMPTY, Observable } from 'rxjs';
import { ContentfulResponse } from '../../contentful/interfaces/contentful-response';
import { ContentfulService } from '../../contentful/services/contentful.service';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';

@Component({
  selector: 'app-general-landing-page',
  templateUrl: './general-landing-page.component.html',
  styleUrls: ['./general-landing-page.component.scss'],
})
export class GeneralLandingPageComponent implements OnInit {
  contentfulResponse$!: Observable<ContentfulResponse>;
  componentschoosen: any;
  componentsResults: any;
  urlPath: any;
  siteURL: any;
  siteURLpath: any;
  private isBrowser: boolean = false;

  constructor(
    private contentfulService: ContentfulService,
    private router: Router,
    @Inject(DOCUMENT) private dom: Document,
    @Inject(PLATFORM_ID) private platformId: Object
  ) {
    this.isBrowser = isPlatformBrowser(platformId);
  }

  ngOnInit(): void {
    this.siteURL = new URL(this.dom.URL);
    this.siteURLpath = this.siteURL?.pathname;
    this.urlPath = this.siteURLpath.split('/')[2];

    this.contentfulService
      .generalLandingPageBySlug(this.urlPath)
      .subscribe((_) => {
        if (this.contentfulService.generalLandingResponse == null) {
          this.contentfulService.generalLandingResponse = sessionStorage.getItem(
            'response'
          );
        }

        if (
          this.contentfulService.generalLandingResponse.includes(
            '__typename ...'
          )
        ) {
          this.contentfulResponse$ = this.contentfulService.generalLandingPageProcessedData(
            this.contentfulService.generalLandingResponse,
            this.urlPath
          );
        }
      });

    this.router.routeReuseStrategy.shouldReuseRoute = function () {
      return false;
    };
  }

  ngAfterContentChecked(): void {
    if (this.isBrowser) {
      sessionStorage.removeItem('response');
    }
  }
}

Import the resolver file

General-Landing-Component.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreModule } from '../../core/core.module';
import { GeneralLandingPageComponent } from './general-landing-page.component';
import { generalLandingResolver } from './general-landing-page.resolver';

const routes: Routes = [
  {
    path: '',
    component: GeneralLandingPageComponent,
    resolve: { generalContent: generalLandingResolver },
  },
];

@NgModule({
  declarations: [GeneralLandingPageComponent],
  imports: [CommonModule, CoreModule, [RouterModule.forChild(routes)]],
})
export class GeneralLandingPageModule {}

General-Landing-Component.html

<ng-container *ngIf="contentfulResponse$ | async as data">
  <ng-container *ngFor="let component of data?.components">
    <app-contentful
      [template]="component.pageData.__typename"
      [component]="component.pageData"
    ></app-contentful>
  </ng-container>
</ng-container>
Surya R Praveen
  • 3,393
  • 1
  • 24
  • 25
0

In my case calling api without domain like /api/content/... instead of www.example.com/api/content/... was the issue

Anil Kumar
  • 41
  • 3