In this lesson we will add Server Side Rendering to our application using Angular Universal. We'll build a simple Express application and see how Angular Universal builds and renders our app on the server.
Since Angular CLI v6 we can simple run ng generate universal --clientProject store
, where store
is the name of our app.
In angular.json
in the architect.server
object we define a configurations
object, to make sure the correct environment gets copied on a production build.
To make Angular Universal with lazy loaded modules we need to add the ModuleMapLoaderModule
to AppServerModule
, which we get after installing @nguniversal/module-map-ngfactory-loader
from npm.
npm install --save @nguniversal/module-map-ngfactory-loader @nguniversal/express-engine
In the project root we implement a server.ts
and define our simple express server.
When we run the new server we see that the app gets rendered before it hits the browser.
Instructor: [00:00] The default index.html file that is generated with our project is a simple file with a few tags inside the head, and only one tag in the body -- the selector of our app component, where we run ngBuildStore --prod in our project folder, and serve the output with NPX serve -s dist/store.
[00:19] We can check the source and see that it only consists of the content of our index.html, plus some additional scripts. This is also how most search engines and social media sites will see the page, which is far from ideal.
[00:33] Let's have Angular Universal to make our application SEO-friendly. Since Angular CLI version six, this process has been simplified a lot. We can simply run the command ngGenerateUniversal, and pass in the flag, --client project store, which is the project name found in angular.json.
[00:51] We see that this command generated a few files for us, updated a few others, and it ran npm install. Let's take a look at angular.json first. In our project, in the architect object, we see that there is a new object called server.
[01:04] In the options we see that the output path is set to dist/store/server, and that it uses a different main file and tsConfig. Here, we're going to add a new object called configuration, and inside that, an object called production.
[01:16] We add a file replacements array, where we replace source/environments/environments.ts with source/environments/environments/prod.ts. This makes sure that our production apply settings get applied where we build the server-side version of our app.
[01:33] The tsConfig for the server extends the default tsConfig. The main difference here is that the output is set to common JS to make sure that Node JS can handle it. We also see that the Angular compiler options has an entry module, which is our new app server module.
[01:47] The file main.server.ts is used to bootstrap our app on the server. Because our Angular app uses Zone JS, this is a great place to import Zone JS for Node. The app server module is a simple module that imports app module, server module from Angular/platform-server, and it bootstraps app component, just like app module does for the browser.
[02:06] Because we're using lazy loading, we need to add an extra module to app server module. From NPM, we install the module @ngUniversal/module-map-ngfactory-loader, and also at ngUniversal/Express [inaudible] that we need in server.ts in a bit.
[02:23] Once installed, we can add module map loader module to the imports array, and make sure to import it. Now, in package.json, let's update the build script, set it to ngBuildStore -- prod, and n ngRunStore:server:production.
[02:39] We run npm run build, and we see that both apps got built. Angular Universal requires a web server to host a generated app. Let's implement that using Node and Express. We create a new file, server.ts, in the project root, and first, add some imports.
[02:54] We import Express. We import joinFromPath. We important ngExpressEngine from ngUniversal/ExpressEngine, and we import provideModuleMap from ngUniversal module map ngFactory loader.
[03:04] We add a const port that we take from the environment variable, or set it to a default 8080. We add a const static root that returns the join method with the current working directory, and then dist and store, so it points to the browser build.
[03:20] Next, we destructure app server module, ngFactory, and lazy module map that we will require from a file called dist/store-server-main, which is our server build. The last const is app.invokesExpress. We use the app.engine method to define the view engine, and we pass in two parameters.
[03:38] The first is HTML. The second parameter is a method that gets invoked for this type of file. We pass in the ngExpressEngine function, we add a property, bootstrap, and set it to app server module ngFactory. The second property, providers, which is an array.
[03:54] Inside that array, we invoke the provideModuleMaps function, and pass in lazy module map as a parameter. Now that the view engine is defined, we can use it using app.set, and we set views to static root.
[04:06] We then use app.get to listen to start of star, and pass in express.static, with static root as a parameter, and listen to star. Pass in a method with the standard express signature request and result. This method implicitly returns rest.render method, takes a string index, and an object with one value, the request.
[04:26] Now that the server is configured, we tell it to listen to the port, and log a friendly message, so we know it's listening. We open package.json, update the start script, and set it to value ts-node./server.ts. We start the server with npm start, and when we open the listening port in the browser, we should see the server side rendered version of our page.
[04:47] We can view the source and see that the content gets rendered and added inside the HTML body by the server.
I'm getting this error - "You must pass in a NgModule or NgModuleFactory to be bootstrapped"
I can only get this running if I pass the module option via the start script in package.json:
"start": "ts-node -O '{\"module\": \"commonjs\"}' ./server",
Otherwise I get a "SyntaxError: Unexpected token *" regarding the "import * as express from 'express'" statement in server.ts.
I copied my "tsconfig.server.json" from the git repository for this lesson so it has the following line in the compilerOptions:
"module": "commonjs"
but it doesn't seem to do anything.
I'm running Node version v10.9.0.
I'm wondering why I have to pass the module option the way I'm doing it?
I've followed along with this tutorial and for some reason it does not render the server side version of the app - it remains the same as before, I've cloned and ran the version in github and it also does not render the server side version. Is there some step that I am missing?
It runs perfectly on my side by running these commands:
$ git clone --single-branch -b lesson-10 https://github.com/eggheadio-projects/egghead-course-SEO-friendly-PWA-with-angular-universal angular-seo
$ cd angular-seo
$ npm install
$ npm run build
$ npm start
Then, the application runs at http://localhost:8080 same content as on the demo.
Hi Kristy,
so it does, I must have taken a mis-step somewhere.
Thanks for your response!
I did not get lesson-10 tag running. It seems like there is already code dependent on now.sh embedded in that version in environments/environment.prod.ts
I did not get lesson-10 tag running. It seems like there is already code dependent on now.sh embedded in that version in environments/environment.prod.ts
Not sure if I follow, the value of apiUrl
in environments/environment.prod.ts
should be unrelated, it's just where the production API is found.
Sorry. When you checkout the tag of lesson-10, there is code in environments/environment.prod.ts points to the now.sh urls. Running 'npm run build' and then npm start will use external APIs on now.sh
Sorry. When you checkout the tag of lesson-10, there is code in environments/environment.prod.ts points to the now.sh urls. Running 'npm run build' and then npm start will use external APIs on now.sh
That's intentional. When running a production build we generally want to connect it to an API that is hosted online, and not on our local machines.
However, you can replace that value with any other URL that works best for you!
I added to the package.json
"build:local": "ng build store && ng run store:server",
and ran npm audit fix which changed the package.json to use "protractor": "^5.4.2"
Works now as changes to the lesson-10 tag without using now.sh
If you try to run final version of your code example there is no ssr-html in it. is it expected behavior? and it looks like SW is the cause of the problem.