Static pages in Angular with Scully

17.11.2021Ricky Elfner
Mobile Angular Content Delivery Network Open source Hands-on How-to JavaScript Mobile App Development Tutorial TypeScript

What is Scully?

Basically, Scully is a static site generator, or SSG for short. Although there are several other implementations of the static site generators like Gatsby, Hugo, Jekyll or Zola are available, Scully is the first for Angular applications.

Compared to the conventional Jamstack, however, this tool has a few more goals:

  • for Angular projects (without a big effort)
  • Support for Markdown
  • Plugin System & Ecosystem

So Scully is used to pre-render every page of your Angular application into a purely static one with generated HTML & CSS. Scully is developed by HeroDevs, at the end of 2019 in an alpha version released. Using Scully eliminates the need to have any JavaScript files need to be downloaded on the client. For this reason, the download size compared to a normal Angular application is a lot less.

Jam stack

We at b-nova have often dealt with the topic of jam stack. Raffi has already done this and also wrote a blog article. In spite of everything at this point I would like to point out the most important points again. The goal of Jamstack is to render all content once before it is displayed. It is also possible to use the advantage of a CDN in which the data is practically very close to the end user and loading times can be shortened. This can also produce enormous cost savings. The security aspect is also increased, since it is only a static page.

The Scully Trial

Basically, in the first step Scully is responsible for analyzing the app and the collects and writes information in static files in order to be able to display the necessary states, pages and routes.

plugins

Plugins are used wherever Scully needs help. For example, when finding or determining all routes. A plugin always returns a async function or a promise as a return value. There are different standard plugins:

allDone

These are called as soon as Scully has completed all processes. This is useful when you are still doing something after the page was created.

routeDiscoveryDone

These are only called when Scully has collected all routes and all Router plugins are ready. This can for example be useful if you want to create your own RSS feed. Since the plugin is only called when all the above tasks have been completed.

fileHandler

This plugin is used by the contentFolder plugin during the render process. This is responsible for processing all folders with Markdown files or any other format you wish. Scully offers a Markdown plug-in and an asciidoc plug-in as standard. For example, if you have csv files you want to process, this is your suitable type.

router

Once your Route parameter is used, you’ll need to use a router plugin. So Scully can know which content must be presented. A small example is the best way to explain this.

1
2
3
4
/
/home
/aboutus
/home/content/:blogId

The first three examples Scully has no problem recognizing the routes and rendering them accordingly. However, with the last one Scully will have a problem with the parameter blogId. In this case, a plug-in is required that contains a list with makes blogIds available and these can then also be rendered.

Requirements

Depending on which version of Angular you are using, you have to pay attention to which version of Scully you are using.

  • Angular 9, 19, 11 → Scully V1.1.1

  • Angular 12 → from Scully V 2.0.0

  • Node.js 14 or newer

So that Scully can also be used, at least one route is required within the app, so the app will be imported into the router module.

Scully in action

First, you need to create an app. When making settings here, it is important to remember that Scully Routing needed. So it makes sense to add this right at this step.

1
2
ng new scully
? Would you like to add Angular routing? (y/N) y

Then switch to the app directory. Scully offers a package with different angulars schematics. You can install this so that your app is initially bootstrapped and configured automatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
ng add @scullyio/init

> ℹ Using package manager: npm
> ✔ Found compatible package version: @scullyio/init@2.0.1.
> ✔ Package information loaded.
 
> The package @scullyio/init@2.0.1 will be installed and executed.
> Would you like to proceed? Yes
> ✔ Package successfully installed.
>     Install ng-lib
>     ✅️ Added dependency
> UPDATE src/app/app.module.ts (466 bytes)
> UPDATE src/polyfills.ts (3013 bytes)
> UPDATE package.json (1174 bytes)
> ✔ Packages installed successfully.
>     ✅️ Update package.json
>     ✅️ Created scully configuration file in scully.b-nova-blog.config.ts
> CREATE scully.b-nova-blog.config.ts (186 bytes)
> UPDATE package.json (1248 bytes)
> CREATE scully/tsconfig.json (450 bytes)
> CREATE scully/plugins/plugin.ts (305 bytes)

This will give you some new files and give you that folder structure. This includes the scully folder, which contains the configuration file scully.scully.config.ts. With these settings alone you are ready to Scully use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.
├── README.md
├── angular.json
├── dist
├── karma.conf.js
├── node_modules
├── package-lock.json
├── package.json
├── scully
│   ├── plugins
│   └── tsconfig.json
├── scully.log
├── scully.scully.config.ts
├── src
│   ├── app
│   ├── assets
│   ├── environments
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.css
│   └── test.ts
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

At this point you can now use the build to create the static pages with Scully and then start the server.

1
2
3
npm run build
npm run scully
npm run scully:serve

You can then access the page of an Angular app at the address http://localhost:1668/, this is the variant that provides the result of ng build. Under the adress http://localhost:1864/ is the variant from the one created by the Scully dist folder.

In order to really prove that it is a static page, you can do this within the browser. Simply deactivate JavaScript and check whether the page still looks the same after a refresh. If you have Chrome you can use the command field (Control+Shift+P oder Command+Shift+P) within the developer tools to deactivate Javascript and vice versa.

Now let’s add a little more content to our site. First, there are new components necessary that you need for an app.

1
2
3
ng g c blog-content
ng g c bnova
ng g c not-found

Now your previously created components must be made available for the Angular App within app.module.ts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {BlogContentComponent} from './blog-content/blog-content.component';
import {BnovaComponent} from './bnova/bnova.component';
import {NotFoundComponent} from './not-found/not-found.component';
import {ScullyLibModule} from '@scullyio/ng-lib';

@NgModule({
    declarations: [
        AppComponent,
        BlogContentComponent,
        BnovaComponent,
        NotFoundComponent
    ],
    imports: [
        BrowserModule,
        AppRoutingModule,
        ScullyLibModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule {
}

So that your app knows which routes it should make available, new routes must now be created. This is done within the app-routing.module.ts file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {BlogContentComponent} from './blog-content/blog-content.component';
import {BnovaComponent} from './bnova/bnova.component';

const routes: Routes = [
    {path: '', redirectTo: '/blog', pathMatch: 'full'},
    {path: 'blog', component: BlogContentComponent},
    {path: 'bnova', component: BnovaComponent},
    {path: '**', redirectTo: ''}
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule {
}

So that you can now see the content and switch back and forth between pages, you have to create a list of links under app. component.html, as well as the router-outlet tag for the content of your provide different routes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

<ul>
	<li>
		<a routerLink="/blog">Blog</a>
	</li>
	<li>
		<a routerLink="/bnova">b-nova</a>
	</li>
</ul>

<div class="container text-center">
	<router-outlet></router-outlet>
</div>

For test purposes, you can simply use the names as output heading. In this example it is in the files: not-found.component.html, bnova.component.html and blog.component.html.

1
<h1>b-nova Page</h1>

Of course, you can also start the server during development to check whether your changes have also been made successful

1
npm start

As described earlier, you can now try Scully for the first time with your own content.

1
2
3
npm run build
npm run scully
npm run scully:serve

After the scully command you get a new structure within the dist folder. It should be noted here that within the static folder there are now three index.html files which contain the static content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.
├── scully
│   ├── 3rdpartylicenses.txt
│   ├── assets
│   │   └── scully-routes.json
│   ├── favicon.ico
│   ├── index.html
│   ├── main.bde481ed552807a57961.js
│   ├── polyfills.d0b83dd6ef3f6f708b41.js
│   ├── runtime.eb164646829a27e7afd5.js
│   └── styles.31d6cfe0d16ae931b73c.css
└── static
    ├── 3rdpartylicenses.txt
    ├── 404.html
    ├── assets
    │   └── scully-routes.json
    ├── blog
    │   └── index.html
    ├── bnova
    │   └── index.html
    ├── favicon.ico
    ├── index.html
    ├── main.bde481ed552807a57961.js
    ├── polyfills.d0b83dd6ef3f6f708b41.js
    ├── runtime.eb164646829a27e7afd5.js
    └── styles.31d6cfe0d16ae931b73c.css

Scully as a blog

Before that we showed you how you can use a normal Angular app. Static side genarators often come in connection with blogs. So Scully’s innate choice here is one that you definitely will get to know.

You will also create a new project for this.

1
2
ng new scully-blog
? Would you like to add Angular routing? (y/N) y

Out-off-the-box there is a Scully command that adds the blog function:

1
ng generate @scullyio/init:blog 

This will give you all the necessary routes and a folder with a blog component right away.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.
├── app
│   ├── app-routing.module.ts
│   ├── app.component.css
│   ├── app.component.html
│   ├── app.component.spec.ts
│   ├── app.component.ts
│   ├── app.module.ts
│   └── blog
│       ├── blog-routing.module.ts
│       ├── blog.component.css
│       ├── blog.component.html
│       ├── blog.component.spec.ts
│       ├── blog.component.ts
│       └── blog.module.ts
├── assets
├── environments
│   ├── environment.prod.ts
│   └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
└── test.ts

If you don’t want to use the default settings, you can add them using another command, which asks you for some settings:

1
2
3
4
5
6
ng generate @scullyio/init:markdown

? What name do you want to use for the module? blog
? What slug do you want for the markdown file? title
? Where do you want to store your markdown files? mdblog
? Under which route do you want your files to be requested? blog

Conveniently, there is also a command that directly creates a blog post for you:

1
ng g @scullyio/init:post --name="Willkomen beim TechHub"

You can find this article as a Markdown file in the folder blog:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.
├── README.md
├── angular.json
├── blog
│   ├── 2021-11-01-blog.md
│   └── willkomen-beim-tech-hub.md
├── karma.conf.js
├── node_modules
├── package-lock.json
├── package.json
├── scully
├── scully.scully-blog.config.ts
├── src
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

A standard header is specified within the Markdown file and a h1 heading is added. Afterward you can write your content within this file using Markdown format.

1
2
3
4
5
---
title: "Willkomen beim TechHub description: blog description published: false"
---

# Willkomen beim TechHub

At the same time, it is usually useful to have a page that provides an overview of all available blog articles. Therefore, you need to create another component.

1
ng g c bnova

Then the logic has to be implemented that only shows blog articles which are public = true. This will be done within bnova.component.ts. In this case, Scully offers a ScullyRoutesService Observable available$ to filter for.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {Component, OnInit} from '@angular/core';
import {ScullyRoute, ScullyRoutesService} from '@scullyio/ng-lib';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

@Component({
    selector: 'app-bnova',
    templateUrl: './bnova.component.html',
    styleUrls: ['./bnova.component.css']
})
export class BnovaComponent implements OnInit {

    constructor(private scully: ScullyRoutesService) {
    }

    posts$: Observable<ScullyRoute[]> = this.scully.available$;

    ngOnInit(): void {
        this.posts$ = this.scully.available$.pipe(
            map(routeList => {
                return routeList.filter((route: ScullyRoute) =>
                    route.route.startsWith(`/blog/`)
                );
            })
        );
    }

}

Of course, you can also determine the content you want for the overview. This happens within bnova. component.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<h1>b-Nova TechHub</h1>
<div *ngFor="let post of posts$ | async" class="col s12 m6">
	<h2>Title: {{post.title}}</h2>
	<p>Description: {{post.description}}</p>
	<p>Slug: {{post.slug}}</p>
	<p>b-nova Tag: {{post.bnovaTag}}</p>

	<p>
		<a [routerLink]="post.route">Hier entlang</a>
	</p>
</div>

If you click on the link This way, you want to be redirected to the corresponding blog article page. For this you have to make another adjustment within blog-routing.module.ts for the empty Path.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {BnovaComponent} from '../bnova/bnova.component';

import {BlogComponent} from './blog.component';

const routes: Routes = [
    {
        path: ':slug',
        component: BlogComponent,
    },
    {
        path: '',
        component: BnovaComponent,
    },
    {
        path: '**',
        component: BlogComponent,
    }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule],
})
export class BlogRoutingModule {
}

The contents of your Markdown files will be rendered within the blog.component.html file. In case you want to make style adjustments, you can do this within blog.component.css.

1
2
3
4
5
6
7
<h3>ScullyIo content</h3>
<hr>

<!-- This is where Scully will inject the static HTML -->
<scully-content></scully-content>
<hr>
<h4>End of content</h4>

For both examples you can find the code on GitHub: Standard Variante and Blog Variante.

Conclusion

In this TechUp post, you’ve now seen two ways how you can use Scully. For many use cases the blog function will be very interesting, since you can have your own pretty blog quickly and without much effort. Of course, in both cases it must be clear that the technical knowledge is requiered to provide an Angular application. As soon as you successfuly kicked off, you can take full advantage of Jamstack.


This text was automatically translated with our golang markdown translator.

Ricky Elfner

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.