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

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.
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 !!! š»
WRITTEN BY

Nevin
Full-Stack Developer