loading...
Cover image for Create powerful fast pre-rendered Angular Apps using Scully static site generator

Create powerful fast pre-rendered Angular Apps using Scully static site generator

dkoppenhagen profile image Danny Koppenhagen Updated on ・10 min read

Create powerful fast pre-rendered Angular Apps using Scully static site generator

You probably heard of the JAMStack. It's a new way of building websites and apps via static site generators that deliver better performance and higher security. There have been tools for many platforms, but surprisingly not yet for Angular. These times are finally over. With this blog post, I want to show you how you can easily create an Angular blogging app by to pre-render your complete app.


Table of contents:

On Dec 16, 2019 the static site generator Scully for Angular was presented.
Scully automatically detects all app routes and creates static sites out of it that are ready to ship for production. Scully is currently just available within an early version.
This blog post is based on versions:

@scullyio/ng-lib: 0.0.22
@scullyio/init: 0.0.28
@scullyio/scully: 0.0.92

However some of the commands or API calls used here may change in the future.
It’s my goal to keep this blog post as up-to-date as possible.

About Scully

Scully is a static site generator (SSG) for Angular apps.
It analyses a compiled Angular app and detects all the routes of the app.
It will then call every route it found, visit the page in the browser, renders the page and finally put the static rendered page to the file system.
This process is also known as pre-rendering – but with a new approach.
The result compiled and pre-rendered app ready for shipping to your web server.

Good to know: Scully does not use Angular Universal for the pre-rendering.
It uses a Chromium browser to visit and check all routes it found.

All pre-rendered pages contain just plain HTML and CSS.
In fact, when deploying it, a user will be able to instantly access all routes and see the content with almost no delay.
The resulting sites are very small static sites (just a few KBs) so that even the access from a mobile device with a very low bandwidth is pretty fast.
It's significantly faster compared to the hundreds of KBs that you are downloading when calling a “normal” Angular app on initial load.

But that’s not all: Once the pre-rendered page is shipped to the user, Scully loads and bootstraps the “real” Angular app in the background on top of the existing view.
In fact Scully will unite two great things:
The power of pre-rendering and very fast access to sites and the power of a fully functional Single Page Application (SPA) written in Angular.

Get started

The first thing we have to do is to setup our Angular app.
As Scully detects the content from the routes, we need to configure the Angular router as well.
Therefore, we add the appropriate flag --routing (we can also choose this option when the CLI prompts us).

npx -p @angular/cli ng new scully-blog --routing
cd scully-blog  # navigate into the project

The next step is to setup our static site generator Scully.
Therefore, we are using the provided Angular schematic:

ng add @scullyio/init  # add _Scully_ to the project

Et voilà here it is: We now have a very minimalistic Angular app that uses the power of Scully to automatically find all app routes, visit them and generate static pages out of them.
It's ready for us to preview.
Let's try it out by building our site and running Scully.

npm run build   # build our Angular app
npm run scully  # let _Scully_ run over our app build and serve it

Scully will run in watch mode by default and serves the result. To let Scully just run once without serving the result, just add the --nw option (npm run scully -- --nw).

After Scully has checked our app, it will add the generated static assets to our dist/static directory by default.
Let's quickly compare the result generated from Scully with the result from the initial Angular build (dist/scully-blog):

dist/
┣ scully-blog/
┃ ┣ favicon.ico
┃ ┣ ...
┃ ┗ vendor-es5.js.map
┗ static/
  ┣ assets/
  ┃ ┗ scully-routes.json
  ┣ favicon.ico
  ┣ ...
  ┗ vendor-es5.js.map

If we take a look at it, except of the file scully-routes.json, that contains just an empty array, we don't see any differences between the two builds.
This is because currently we only have the root route configured and no more further content was created.

Nonetheless we can checkout the result by visiting the following URL: localhost:1668.

This server serves the static generated pages from the dist/static directory like a normal webserver (e.g. nginx or apache).

The ScullyLibModule

You may have realized, that after running the Scully schematic, the ScullyLibModule has been added to your AppComponent:

// ...
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  // ...
  imports: [
    // ...
    ScullyLibModule
  ]
})
export class AppModule { }

This module is used by Scully to hook into the angular router and to determine once the page Scully tries to enter is fully loaded and ready to be rendered by using the IdleMonitorService from Scully internally.
If we will remove the import of the module, Scully will still work but it takes much longer to render your site as it will use a timeout for accessing the pages. So in that case even if the a page has been fully loaded, Scully would wait until the timer is expired.

Turn it into a blog

Let’s go a bit further and turn our site into a simple blog that will render our blog posts from separate markdown documents.
Scully brings this feature out of the box and it’s very easy to set it up:

ng g @scullyio/init:blog                      # setup up the `BlogModule` and related sources
ng g @scullyio/init:post --name="First post"  # create a new blog post

After these two steps we can see that Scully has now added the blog directory to our project root.
Here we can find the markdown files for creating the blog posts — one file for each post.
We now have two files there: The initially created example file from Scully and this one we created with ng g @scullyio/init:post.

Let's go further

Now that we've got Scully installed and working, let's modify our Angular app to look more like an actual blog, and not just like the default Angular app.
Therefore, we want to get rid of the Angular auto generated content in the AppComponent first.
We can simply delete all the content of app.component.html except of the RouterOutlet.
So in the end the content of our file app.component.html should look like this:

<router-outlet></router-outlet>

Let’s run the build again and have a look at the results:

npm run build   # Angular build
npm run scully  # let _Scully_ run over our app build and serve it

When checking out our dist/static directory we can see that there are new sub-directories for the routes of our static blogging sites.
The files index.html in dist/static/blog/<post-name>/ contain our static pages ready to be served.
When we are visiting the route path /blog/first-post we can see the content of our markdown source file blog/first-post.md is rendered as HTML.

If you want to prove that the page is actually really pre-rendered, just disable JavaScript by using your Chrome Developer Tools.
You can reload the page and see that the content is still displayed.
Awesome isn't it?

a simple blog created with scully

When JavaScript is enabled, Scully configures your static sites in that way, that you will see initially the static content.
In the background it will bootstrap your Angular app, and refresh the content with it.
You won't see anything flickering.

Hold on a minute! 😳

You may have realized: We haven’t written one line of code manually yet and we have already a fully functional blogging site that’s server site rendered. Isn’t that cool?
Setting up an Angular based blog has never been easier.

Good to know: Scully also detects new routes we are adding manually to our app and it will create static sites for all those pages.

Use the ScullyRoutesService

We want to take the next step.
Now we want to list an overview of all existing blog posts we have and link to their sites in our AppComponent.
Therefore, we can easily inject the ScullyRoutesService.
It will return us a list of all routes Scully found with the parsed information as a ScullyRoute array within the available$ observable.
We can easily inject the service and display the information as a list in our AppComponent.

import { Component, OnInit } from '@angular/core';
import { ScullyRoute, ScullyRoutesService } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  posts$: Observable<ScullyRoute[]>;

  constructor(private srs: ScullyRoutesService) {}

  ngOnInit() {
    this.posts$ = this.srs.available$;
  }
}

If you like, you can write this even a little shorter:

export class AppComponent implements OnInit {
  posts$ = this.srs.available$;
  constructor(private srs: ScullyRoutesService) {}
}

To display the results, we can simply use ngFor with the async pipe and list the results.
A ScullyRoute will give us the routing path inside the route key and all other markdown meta data inside their appropriate key names.
So we can extend for example our markdown metadata block with more keys (e.g. thumbnail: assets/thumb.jpg) and we can access them via those (blog.thumbnail in our case).
We can extend app.component.html like this:

<ul>
  <li *ngFor="let post of posts$ | async">
    <a [routerLink]="post.route">{{ post.title }}</a>
  </li>
</ul>

<hr />

<router-outlet></router-outlet>

This will give us a fully routed blog page:

a simple blog created with scully

The ScullyRoutesService contains all of the available routes in your app.
In fact, any route that we add to our Angular app will be detected by Scully and made available via the ScullyRoutesService.available$ observable.
To list only blog posts from the blog route and directory we can just filter the result:

/* ... */
import { map } from 'rxjs/operators';
/* ... */
export class AppComponent implements OnInit {
  /* ... */
  ngOnInit() {
    this.posts$ = this.srs.available$.pipe(
      map(routeList => {
        return routeList.filter((route: ScullyRoute) =>
          route.route.startsWith(`/blog/`),
        );
      })
    );
  }
}

Wow! That was easy, wasn’t it? Now you just need to add a bit of styling and content and your blog is ready for getting visited.

Fetch dynamic information from an API

As you may have realized: Scully needs a data source to fetch all dynamic routes in an app.
In case of our blog example Scully uses the :slug router param as a placeholder.
Scully will fill this placeholder with appropriate content to visit and pre-render the site.
The content for the placeholder comes in our blog example from the files in the /blog directory.
This has been configured from the schematics we ran before in the file scully.scully-blog.config.ts:

import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  projectRoot: "./src/app",
  projectName: "scully-blog",
  outDir: './dist/static',
  defaultPostRenderers: [],
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: "./blog"
      }
    },
  }
};

I would like to show a second example.
Imagine we want to display information about books from an external API.
So our app needs another route called /books/:isbn.
To visit this route and pre-render it, we need a way to fill the isbn parameter.
Luckily Scully helps us with this too.
We can configure Router Plugin that will call an API, fetch the data from it and pluck the isbn from the array of results to fill it in the router parameter.

In the following example we will use the public service BookMonkey API (we provide this service for the readers of our German Angular book) as an API to fetch a list of books:

import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  projectRoot: "./src/app",
  projectName: "scully-blog",
  outDir: './dist/static',
  defaultPostRenderers: [],
  routes: {
    ...
    '/books/:isbn': {
      'type': 'json',
      'isbn': {
        'url': 'https://api3.angular-buch.com/books',
        'property': 'isbn'
      }
    }
  }
};

The result from the API will have this shape:

[
  {
    "title": "Angular",
    "subtitle": "Grundlagen, fortgeschrittene Themen und Best Practices – mit NativeScript und NgRx",
    "isbn": "9783864906466",
    ...
  },
  {
    "title": "Angular",
    "subtitle": "Grundlagen, fortgeschrittene Techniken und Best Practices mit TypeScript - ab Angular 4, inklusive NativeScript und Redux",
    "isbn": "9783864903571",
    ...
  },
  ...
]

After Scully plucks the ISBN, it will just iterate over the final array: ['9783864906466', '9783864903571'].
In fact, when running Scully using npm run scully, it will visit the following routes, after we have configured the route /books/:isbn in the Angular router (otherwise non used routes will be skipped).

/books/9783864906466
/books/9783864903571

We can see the result in the log:

enable reload on port 2667
 ☺   new Angular build imported
 ☺   Started servers in background
--------------------------------------------------
Watching blog for change.
--------------------------------------------------
 ☺   new Angular build imported
Finding all routes in application.
Using stored unhandled routes
Pull in data to create additional routes.
Finding files in folder "/<path>/blog"
Route list created in files:
  "/<path>/src/assets/scully-routes.json",
  "/<path>/dist/static/assets/scully-routes.json",
  "/<path>/dist/scully-blog/assets/scully-routes.json"

Route "/books/9783864903571" rendered into file: "/<path>/dist/static/books/9783864903571/index.html"
Route "/books/9783864906466" rendered into file: "/<path>/dist/static/books/9783864906466/index.html"
Route "/blog/12-27-2019-blog" rendered into file: "/<path>/dist/static/blog/12-27-2019-blog/index.html"
Route "/blog/first-post" rendered into file: "/<path>/dist/static/blog/first-post/index.html"
Route "/" rendered into file: "/<path>/dist/static/index.html"

Generating took 3.3 seconds for 7 pages:
  That is 2.12 pages per second,
  or 473 milliseconds for each page.

  Finding routes in the angular app took 0 milliseconds
  Pulling in route-data took 26 milliseconds
  Rendering the pages took 2.58 seconds

This is great. We have efficiently pre-rendered normal dynamic content!
And that was it for today.
With the shown examples, it's possible create a full-fledged website with Scully.

Did you know that my personal blog and the overall website also been created using Scully?
Feel free to check out the sources at:
github.com/d-koppenhagen/d-koppenhagen.de

If you you want to follow all the development steps in detail, check out my provided github repository
scully-blog-example.

Conclusion

Scully is an awesome tool if you need a pre-rendered Angular SPA where all routes can be accessed immediately without loading the whole app at once.
This is a great benefit for users as they don’t need to wait until the whole bunch of JavaScript has been downloaded to their devices.
Visitors and search engines have instantly access to the sites information.
Furthermore, Scully offers a way to create very easily a blog and renders all posts written in markdown.
It will handle and pre-render dynamic routes by fetching API data from placeholders and visiting every route filled by this placeholder.

Compared to "classic" pre-rending by using Angular Universal, Scully is much easier to use and it doesn't require you to write a specific flavor of Angular.
Also Scully can easily pre-render hybrid Angular apps or Angular apps with plugins like jQuery in comparison to Angular Universal.

If you want to dig a bit deeper into the features Scully offers, check out my second article.

Thank you

Special thanks go to Aaron Frost (Frosty ⛄️) from the Scully core team, Ferdinand Malcher and Johannes Hoppe for revising this article.

Posted on Jan 1 by:

dkoppenhagen profile

Danny Koppenhagen

@dkoppenhagen

📦#nodejs 🅰️#Angular,💡#TypeScript,⚡️#RxJS, ♻️#NgRx and 📲 #NativeScript 👨🏻‍💻 Developer @dbsystel #dxhouse, 📕Author @angular_buch https://angular-buch.com

Discussion

markdown guide
 

Hi Danny! Great post, thanks 👍 Scully looks awesome!
I have a question about fetching dynamic data from JSON API: how can we populate the resulting <path>/scully-blog/dist/static/books/9783864903571/index.html file with data from the API? That is, actually, create a page from this data, not only a route with the slug.

 

Hey, you will handle this like in a common Angular app: create a service that requests your api and fill the template.
Scully will only request the api to identify all placeholders to find all routes. When running scully it will visit all the route by using the Chrome browser and your angular app built with ng build. The result is stored from scully as static html which can be served and accessed immediately with angular bootstrapping in the background.

 

Great! Thanks, I’m gonna try that 👍