Nevin's BLOG

article list

How I implemented SSR in my MEAN.js blog to update meta tags

Generic placeholder image
NevinPosted on Thursday, November 14, 2019 Ā· reading time : 15 min

In this article I demonstrate how to enable SSR in a single page website using the MEAN.js stack. The goal here is to update meta tags before rendering my blog article to facilitate web crawlers to index and share my content.

Repository

Working exemple

Today we see a lot of single page applications using a popular framework or library like Angular and React. Single page means you don’t have a lot of control on your html source because the html code is generated from your bundle and you navigate from page to page using routers directly with JavaScript. This can be a problem if, for example, you want to edit your meta tags on the fly, before the page is rendered (this can be very useful for SEO, web crawlers).

I faced this problem when I was developing my MEAN.js stack blog with angular, I needed to update my meta tags for each article, so I could share it through social medias like Facebook, with the correct title, image preview. I discovered that Server-Side Rendering was the right solution to implement, and here is how we can get the job done.

1, Setting up the project šŸ’»

We will start with my old version of my blog, without SSR, you can clone this repository and checkout to the ā€œbeforeSSRā€ branch. We will also install the packages for the backend and frontend :

git clone https://github.com/NevinDry/mean-stack-blog.git
git checkout beforeSSR
cd server
npm i
cd ../frontend/mean-stack-blog
npm i

As you can see, this app is build around the MEAN.js stack, we got an node/express backend (server), an Angular frontend (frontend/mean-stack-blog) and a MongoDb database. If you don’t have mongoDB on your machine, go ahead and install it, when it’s done, you can use the MongoDB Compass GUI to connect to your localhost mongo instance. Here we will need to create a new database called ā€œmean-blogā€. In this database, create a new collection called users and insert the following document inside :

{ 
   name: user,
   hash: $2y$12$J.bspXq9G1nFZWpVANRSJ.Xkr/pgBK066gzwHC.HJkafxWeOAwaMe
}

// the hash is generated with bcrypt, the encryption tool I use in the backend. The string to check against is ā€œadminā€.

We are all set !

We can go to the frontend/mean-stack-blog folder and run :

ng build

This will build the client app that our express server will serve. When the build is done, go to the server folder and run :

npm run start:server

This will launch our app, serve our client and connect to the mongoDB database, now if you go http://localhost:3000 you can access the blog and all it’s features. At the moment we don’t have any articles, let’s create one, go to http://localhost:3000/admin and login using the credentials we just inserted in the database (user, admin). You will access the backoffice to create an article, let’s do it !

Once you added the article, you can go back to the home page to see the list of articles on the blog. Click ā€œRead moreā€ on the article. Now we can see the problem I faced, if you display the page source (ctrl-u), you will notice that no custom meta tags from the article are displayed in the code, however we got the following block of code in BlogArticle component, that should update my meta :

//updating meta (ssr on)
this.meta.updateTag({ name: 'author', content: this.article.author });
this.meta.updateTag({ name: 'description', content: this.article.title });
this.meta.updateTag({ property: 'og:url', content: "http://localhost:3000/" + this.location.path() });
this.meta.updateTag({ property: 'og:type', content: 'article' });
this.meta.updateTag({ property: 'og:title', content: this.article.title });
this.meta.updateTag({ name: 'author', content: 'website' }); this.meta.updateTag({ property: 'og:description', content: this.article.preview });
this.meta.updateTag({ name: 'description', content: 'website' }); this.meta.updateTag({ property: 'og:image', content: this.config.getPublicImageUrl() + '/blog/' + this.article.imageLink });

This happens because this code is executed after the page is loaded, and our meta tags are coming from a HTTP request that is too late to update the html source. In fact the html source is from your index.html file, and you wont be able to change it through your angular app before it is loaded.

2, SSR comes into play šŸš€

So now we know that we have to update those meta tags before the Angular app render, and server-side rendering is exactly what we need to achieve it. We will use Angular Universal, a technology that renders Angular applications on the server.

https://angular.io/guide/universal

As we can read from the website :

A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions. Angular Universal executes on the server, generating static application pages that later get bootstrapped on the client.

This allows us to do some changes before the page is rendered, like updating our meta tags :

Google, Bing, Facebook, Twitter, and other social media sites rely on web crawlers to index your application content and make that content searchable on the web. These web crawlers may be unable to navigate and index your highly interactive Angular application as a human user could do. Angular Universal can generate a static version of your app that is easily searchable, linkable, and navigable without JavaScript. Universal also makes a site preview available since each URL returns a fully rendered page.

Let’s implement that !

We start by following the angular universal tutorial, let’s go to our angular folder (frontend/mean-stack-blog) and run the following command :

ng add @nguniversal/express-engine --clientProject mean-blog

This command will create files and modules that enable SSR in your website, check the angular universal tutorial for more information. We need to do one more thing because at the moment our express backend is serving our client app, but we don’t need this anymore because angular universal will run his own server to host the angular app. Remove the following lines from the ā€œapp.jsā€ file in server/backend :

app.use('/', express.static(path.join(__dirname, '/../webAppBuild')));
app.get(new RegExp('^(?!\/api).*$'), function(req, res){
  res.sendFile(path.resolve(__dirname + '/../webAppBuild/index.html'));
});

We are all set to build and run our SSR client :

npm run build:ssr 
npm run serve:ssr

Don’t forget to launch the backend that will now act as a independent api :

npm run start:server

Now the client is hosted on http://localhost:4000 and the backend on http://localhost:3000 If you go to the website now, surprisingly it will load indefinitely and not render, this is because some web features are not supported by SSR, let me explain that.

You can’t access browser types that doesn’t exist on the server, this means is that you can’t use window, document or localStorage and other browser types or any library that uses them. If you use a third party library found on npm that uses one of those elements above, your app won’t work.

Actually in our code, we are using localStorage to store the login token, so SSR can’t work. However there is a way to avoid this code to be run on the server, and only make it work when the app is rendered, let’s go to the AuthService to fix it : First we need to __inject the PLATFORM_ID token__ to check whether the current platform is browser or server.

import { isPlatformBrowser } from '@angular/common';
import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
  constructor(private http: HttpClient, @Inject(PLATFORM_ID) private platformId: Object) {
    this.init();
}

Now every time we use the localStorage, we need to wrap it around a platformId check like this :

  login(user: any) {
    return this.http.post<{}>(`${this.config.getUserUrl()}` + 'login', user)
      .pipe(
        catchError(this.handleError),
        map((response: any) => {
          if (response.data && response.data.token && isPlatformBrowser(this.platformId)) {
            // store user details and jwt token in local storage to keep user logged in between page refreshes
            localStorage.setItem('currentUser', JSON.stringify(response.data));
            this.init();
          }

          return response;
        })
      );
  }

  logout() {
    if (isPlatformBrowser(this.platformId)) {
      // remove user from local storage to log user out
      localStorage.removeItem('currentUser');
      this.user = null;
    }
  }

  getToken = function () {
    if (isPlatformBrowser(this.platformId)) {
      return localStorage['currentUser'];
    }
  };

You will have to track every use of window, document or localStorage in your application but also in the third party modules you added to your app. Once it’s done your app will no longer load indefinitely and you can see SSR in action :

Go to your article page and display the page source (ctrl-u), now we can see that the page has been rendered from the server, before the app loads in the browser and our meta tags are updated:

<meta name="description" content="How I implemented SSR in my MEAN.js blog to update meta tags  ">
  <meta name="keywords" content="ssr, angular, mean, universal, meta, tags">
<meta name="author" content="Nevin">
<meta property="og:url" content="https://mean-stack-blog.etiennepuissant.eu/blogArticle/5dcddbd2636e7156ec75a9d3">
<meta property="og:type" content="article">
<meta property="og:title" content="How I implemented SSR in my MEAN.js blog to update meta tags  ">
<meta property="og:description" content="In this article I demonstrate how to enable SSR in a single page website using the MEAN.js stack. The goal here is to update meta tags before rendering my blog article to facilitate web crawlers to index and share my content.">
<meta property="og:image" content="https://mean-stack-blog-api.etiennepuissant.eu/media/uploads/blog/9b20a3f6-3b9d-4b87-b538-9316729d4595.jpg">

.

One more thing for HTTP request

We get our tags from a HTTP request, this means it has been executed from the server, but I noticed the HTTP call will also be executed when the app is rendered. The request and data are exactly the same, so we have a duplicate http call, this is anoying.

Hopefully, there is a way to avoid those duplicate calls, we can reuse the request data when the app is rendered. We will use Angular Universal’s TransferHttpCacheModule on our app.module and ServerTransferStateModule on our app.server.module.

// app.module.ts
import { TransferHttpCacheModule } from '@nguniversal/common';


@NgModule({
  declarations: [
    AppComponent,
    BlogListComponent,
    LoadingComponent,
    BlogArticleComponent,
    AuthComponent,
    HomeBackOfficeComponent,
    BlogListBackofficeComponent,
    AddEditBlogComponent,
    BlogCommentsComponent,
    BlogArticleTagsComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    TransferHttpCacheModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule,
    MarkdownModule.forRoot(),
    ModalModule.forRoot()
  ],
  providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthIntercepter, multi: true },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
// app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ModuleMapLoaderModule,
    ServerTransferStateModule
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

Unfortunately, this will working only if you use the HTTPClient from Angular, but in our use-case, request calls wont be executed twice.

Conclusion

We just saw that implementing Server-Side rendering is very easy with the recent version of angular, there is not much to do because Angular Universal can set-up everything for us. In this article I demonstrate how it is useful for changing your meta on the fly, but there is also a lot of other benefit of Server-Side rendering like performance and loading improvement. Give it a try !!!

Thanks for reading !!! šŸ»

Repository

Working exemple


  • ssr
  • angular
  • mean
  • universal
  • meta
  • tags

WRITTEN BY