This is a part 2 of the series. And here is where most of the juice is.
In this part we will add dynamic content to the website. By dynamic content I mean any type of content that is not hardcoded in the HTML itself.
Specifically, here the idea is to have a set of markdown files, and use it as a source of content for the pages. In short here is what we will try to do:
/post-number-one
Let's start by creating the markdown file. That file can be anywhere, but let's have it in our repo, inside a content
folder.
Now we need to get Angular Prerender to get that file, turn it into HTML and use it as the data in the prerendering.
When you setup Angular Universal, a file server.ts
gets created. When starting this project I placed the endpoint to get the markdown content there. And I have spent hours trying to figure out why npm run prerender
doesn't have access to that endpoint, and npm run dev:ssr
does. Massive headache.
🕬 Prerender doesn't run the server.ts
. Repeat again, prerender doesn't run the server.ts
.
With this revelation I've set up a separate Node App that will serve as a micro-backend, serving only one endpoint.
Here is what we want the micro-backend to do:
For transforming the Markdown to HTML, I've used GitHub Octokit API.
Let's add it to the project.
npm i @octokit/core
We will create a file called backend.js
, with the following content.
const express = require('express');
const { readFile, existsSync } = require('fs');
const Path = require('path');
const { Octokit } = require("@octokit/core");
const app = express();
const api = express.Router();
const octokit = new Octokit();
const markdownToHtml = (file) => {
return new Promise((resolve, reject) => {
readFile(file, 'utf8', function (err, data) {
if (err) {
return reject(err);
}
octokit.request('POST /markdown', {
text: data
}).then(d => {
resolve(d.data);
}).catch(e => reject(e));
});
})
}
api.get('/post/:slug', (req, res) => {
const slug = req.params.slug;
const filePath = `./content/${slug}.md`;
const path = Path.join(__dirname, filePath);
const fileExists = existsSync(path);
if (!fileExists) {
res.status(404).send('Page not found');
return;
}
markdownToHtml(filePath)
.then((d) => {
res.json({
content: d,
});
})
.catch((e) => {
res.status(400).send(e)
});
});
app.use('/api', api);
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(
`Backend is runing on: http://localhost:${port}`
);
});
Add a script in package.json
for running the micro-backend.
"backend:start": "node backend.js"
At this point we have a fully working micro-backend that we can run with npm run backend
.
You can see this done in the following commit in the demo repository.
While developing the frontend you can start the micro-backend to run on your local machine, and you can use npm run dev:ssr
to run Angular in the server side rendering mode. That way it will refresh on any change you do on the frontend side.
The steps explained here are visible in this commit in the demo repo.
We will need some modules added in the AppModule
HttpClientModule
: we will use this to make HTTP request to our micro-backendRouterModule
: we will need to use ActivatedRoute
to be able to get the URL parameterBrowserTransferStateModule
: that one works in conjuncture with ServerTransferStateModule
And for the AppServerModule
add ServerTransferStateModule
.
Let's add two components:
HomeComponent
: we will use that for a static page for the homepage and the base routePostComponent
: we will use that to present the dynamic content fetched via HTTP during the pre-render processng g c post --module=app
ng g c home --module=app
When importing the RouterModule
in the AppModule
we will use this route setup:
RouterModule.forRoot([
{ path: '', component: HomeComponent },
{ path: '**', component: PostComponent }
]),
This wild card thing is a nifty way to tell Angular to send all routes we want to prerender to the PostComponent
.
By default Angular Universal prerender will prerender the paths that are defined in the Router by using auto discover feature. As we won't have paths defined in the router, Angular allows us to define the exact paths we want to prerender.
To do that we will add routes.txt
to the root of the project, with the following content.
/post-number-one
And we will change the prerender npm script to include the routes.txt
definitions.
"prerender": "ng run angular-prerender-markdown:prerender --no-guess-routes --routes-file routes.txt"
When you run the prerender, the prerender will search for a specified route in the routes.txt
, it will then check the router to see what component to match with that route. In our case as we have **
pointing to the PostComponent
, that means that every route (except for base route, which points to HomeComponent
) is pointed to the PostComponent
.
The PostComponent
is pretty dumb. When initialized it uses the HttpService to get the content, and it renders that content, as-is, in the template.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-post',
templateUrl: './post.component.html',
styleUrls: ['./post.component.css']
})
export class PostComponent implements OnInit {
postContent: any = '';
constructor(private route: ActivatedRoute, private http: HttpClient) { }
ngOnInit(): void {
const slug = this.route.snapshot.url[0].path;
this.http.get<{content: string}>(`http://localhost:8080/api/post/${slug}`).subscribe(data => {
this.postContent = data.content;
}, () => {
this.postContent = 'Page not found';
});
}
}
And that is pretty much it. By doing this you can add markdown files to be pre-rendered by Angular Universal and serve them as a webpage.
This post you are reading is done in exactly that manner.