Statische Seiten in Angular mit Scully

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

Was ist Scully?

Im Grunde genommen ist Scully ein Static-Site-Generator, kurz SSG. Obwohl es bereits einige anderen Implementationen von Static-Site-Generatoren wie Gatsby, Hugo, Jekyll oder Zola gibt, ist Scully die Erste für Angular-Applikationen.

Im Vergleich zum konventionellen Jamstack hat dieses Tool jedoch einige weitere Ziele:

  • für Angular Projekte (und dies ohne grossen Aufwand)
  • Support für Markdown
  • Plugin System & Ecosystem

Scully wird also dafür verwendet, um jede Seite Ihrer Angular-Applikation zu pre-rendern und eine reine statische HTML- & CSS-Seite zu generieren. Dabei wurde Scully von HeroDevs Ende 2019 in einer Alpha-Version released. Durch das Verwenden von Scully ist es nicht mehr notwendig, dass irgendwelche JavaScript-Dateien heruntergeladen werden müssen. Aus diesem Grund ist die Download-Grösse im Vergleich zu einer normalen Angular Applikation um einiges geringer.

Jamstack

Wir bei b-nova haben uns bereits des Öfteren mit dem Thema Jamstack auseinandergesetzt. Hierzu hat Raffi bereits
auch einen Blog-Artikel geschrieben. Trotz allem möchte ich Ihnen an dieser Stelle noch einmal die wichtigsten Punkte aufzeigen. Das Ziel von Jamstack ist es zunächst einmal allen Inhalt zuvor schon zu rendern. Ebenfalls ist es so möglich den Vorteil eines CDNs zu nutzen, in dem die Daten quasi sehr nah beim Endnutzer sind und so Ladezeiten verkürzt werden können. Dadurch lassen sich auch enorme Kosten sparen. Ebenfalls wird der Sicherheitsaspekt erhöht, da es sich nur noch um statische Seite handelt.

Der Scully Prozess

Im Grunde genommen ist Scully im ersten Schritt dafür zuständig, dass die App analysiert wird und die gesammelten Informationen in statische Fils schreibt, um die notwendigen States, Seiten und Routen darstellen zu können.

Plugins

Plugins werden an den Stellen verwendet, an dem Scully Hilfe benötigt. Zum Beispiel beim Finden von allen oder bestimmen Routen. Ein Plugin gibt dabei immer ein async-Funktion oder eine Promise als Rückgabewert zurück. Dabei gibt es unterschiedliche Standard Plugins:

allDone

Diese werden aufgerufen, sobald Scully alle Prozesse ausgeführt hat. Dies ist somit nützlich, wenn Sie noch etwas machen müssen, nach dem die Seite erstellt wurde.

routeDiscoveryDone

Diese werden erst aufgerufen, wenn Scully alle Routen gesammelt und auch alle Router-Plugins fertig sind. Dies kann zum Beispiel nützlich sein, wenn Sie einen eigenen RSS-Feed erstellen möchten. Da das Plugin erst aufgerufen werde, wenn alle zuvor genannten Aufgaben durch sind.

fileHandler

Dieses Plugin wird von dem contentFolder-Plugin während des render-Prozesses verwendet. Dieses wiederum ist dafür zuständig, dass alle Ordner mit Markdown-Dateien oder aber auch andere Formate, die Sie wünschen, verarbeitet. Standardmässig bietet Scully hier ein Markdown-Plugin sowie ein asciidoc-Plugin. Wenn Sie beispielsweise csv-Dateien verarbeiten möchten, ist dies Ihr passenden Typ.

router

Sobald Ihre Route-Parameter verwendet, müssen Sie ein router-Plugin verwenden. Somit kann Scully wissen, welcher Inhalt dargestellt werden muss. Anhand eines kleinen Beispiels lässt sich dies am besten erklären.

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

Bei den ersten drei Beispielen hat Scully kein Problem die Routes zu erkennen und diese entsprechend zu rendern. Jedoch bei dem letzten hat Scully ein Problem mit dem Parameter blogId. In diesem Fall wird nun ein Plugin benötigt, welches eine Liste mit vorhanden blogIds bereitstellt und diese anschliessend dann auch gerendert werden können.

Anforderungen

Je nachdem, welche Angular Version Sie verwenden, müssen Sie darauf achten, welche Scully Version Sie verwenden.

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

  • Angular 12 → ab Scully V 2.0.0

  • Node.js 14 oder neuer

Damit auch Scully auch verwendet werden kann, ist mindestens eine Route innerhalb der App notwendig, deshalb muss die App das router-Modul importiert haben.

Scully in Aktion

Zunächst einmal müssen Sie eine App erstellen. Hier ist es bei den Einstellungen wichtig, daran zu denken, dass Scully Routing benötigt. Deshalb macht es Sinn, dies gleich in diesem Schritt hinzuzufügen.

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

Im Anschluss wechseln Sie in das App-Verzeichnis. Von Haus aus bietet Scully ein Package mit verschiedenen Angular Schematics. Dieses können Sie installieren, damit ihre App initial automatisch gebootstrapped und konfiguriert wird.

 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)

Dadurch erhalten Sie einige neue Dateien und haben diese Ordner Struktur. Hierzu gehört der scully-Ordner, die Konfigurationsdatei scully.scully.config.ts. Alleine schon mit diesen Einstellungen sind Sie bereit Scully zu verwenden.

 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

An dieser Stelle können Sie nun die Applikation Builden, mit Scully die statischen Seiten erstellen lassen und anschliessend den Server starten.

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

Im Anschlusskönnen Sie die Seite einer Angular App unter der Adresse http://localhost:1668/ aufrufen, diese ist die Variante, welches das Ergebnis von ng build bereitstellt. Unter der Adresse http://localhost:1864/ befindet sich die Variante aus dem von Scully erstellten dist-Ordner.

Um nun auch wirklich zu beweisen, dass es sich um eine statische Seite handelt, können Sie innerhalb des Browsers JavaScript einfach deaktivieren und prüfen, ob die Seite nach einem Refresh immer noch gleich aussieht. Falls Sie Chrome verwenden, können Sie innerhalb der Entwickler Tools über das command-Feld (Control+Shift+P oder Command+Shift+P) Javascript deaktivieren und andersrum.

Nun wollen wir noch ein wenig Inhalt zu unserer Seite hinzufügen. Dafür sind zunächst ein Mal neue Komponenten notwendig, die Sie für eine App benötigen.

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

Nun muss innerhalb von app.module.ts Ihre zuvor erstellten Komponenten für die Angular App verfügbar gemacht werden.

 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 {
}

Damit Ihre App weis, welche Routen sie zur Verfügung stellen soll, müssen nun neue Routes angelegt werden. Dies ist innerhalb von app-routing.module.ts.

 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 {
}

Damit Sie nun auch Inhalt sehen und auch zwischen Seiten hin und her wechseln können, müssen Sie unter app. component.html eine Liste mit Links erstellen, sowie das router-outlet-Tag für den Inhalt Ihrer verschiedenen Routen bereitstellen.

 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>

Um den Inhalt innerhalb der verschiedenen Seiten können Sie für Test-Zwecke beispielsweise einfach die Namen als Überschrift ausgeben. Dies ist in diesem Beispiel in den Dateien: not-found.component.html, bnova.component.html und blog.component.html.

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

Selbstverständlich können Sie während dem entwickeln auch den Server starten, um zu überprüfen, ob ihre Änderungen auch erfolgreich waren

1
npm start

Wie vorhin bereits beschrieben können Sie nun Scully das erste Mal mit eigenem Inhalt ausprobieren.

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

Nach dem scully Befehl erhalten sie eine neue Struktur innerhalb dem dist-Ordner. Hier ist zu beachten, dass innerhalb des static-Ordners nun 3 index.html-Daten vorhanden sind, welche den statischen Inhalt beinhalten.

 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 als Blog

Zuvor haben wir Ihnen gezeigt wie Sie eine normale Angular App nutzen können. Static-Seite-Genaratoren kommen jedoch oft in Verbindung mit Blogs in Verbindung. Deshalb bietet Scully von Haus aus hier eine Möglichkeit, die Sie auf jeden Fall kennenlernen sollten.

Auch hierfür erstellen Sie ein neues Projekt.

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

Out-off-the-box gibt es ein Scully-Command, welches die Blog Funktion hinzufügt:

1
ng generate @scullyio/init:blog 

Dadurch erhalten Sie direkt alle notwendigen Routen und einen Ordner mit einer Blog-Komponente.

 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

Falls Sie nicht die Standardeinstellungen verwenden möchten, können Sie diese auch über einen anderen Befehl hinzufügen, bei dem Sie nach einigen Einstellungen gefragt werden:

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

Praktischerweise gibt es auch einen Befehl, der Ihnen direkt einen Blogbeitrag erstellt:

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

Diesen Artikel können Sie als Markdown-Datei in dem Ordner blog finden:

 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

Innerhalb des Markdown-Files wird ein Standard-Header bestimmt, sowie eine h1-Überschrift hinzugefügt. Anschliessend können Sie Ihren Inhalt innerhalb dieses File mit Markdown-Format schreiben.

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

# Willkomen beim TechHub

Gleichzeitig ist meist eine Seite sinnvoll, welche eine Übersicht über alle verfügbaren Blog-Artikel bereitstellt. Dafür müssen Sie eine weitere Komponente erstellen.

1
ng g c bnova

Im Anschluss muss die Logik implementiert werden, die nur Blog-Artikel anzeigt, welche public = true sind. Dies wird innerhalb von bnova.component.ts gemacht. Über ScullyRoutesService bietet Scully für diesen Fall ein Observable available$ um danach zu filtern.

 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/`)
                );
            })
        );
    }

}

Selbstverständlich kann man auch den gewünschten Inhalt für die Übersicht bestimmen. Dies passiert innerhalb 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>

Wenn Sie auf den Link “Hier entlang“ klicken, wollen Sie zu der entsprechenden Blogartikel-Seite weitergeleitet werden. Dafür müssen Sie noch eine weitere Anpassung innerhalb von blog-routing.module.ts vornehmen für den “leeren” Pfad.

 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 {
}

Der Inhalt Ihrer Markdown-Dateien wird innerhalb der blog.component.html Datei gerendert. Möchten Sie dabei Style-Anpassungen vornehmen, können Sie dies innerhalb von blog.component.css machen.

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>

Für beide Beispiele finden Sie den Code auf Github: Standard Variante und Blog Variante.

Fazit

In diesem TechUp-Beitrag habe Sie nun zwei Varianten gesehen, wie Sie Scully verwenden können. Dabei kann für viele Interessierte die Blog-Funktion sehr interessant sein, da man ziemlich schnell und ohne grossen Aufwand einen eigenen Blog erstellen kann. Selbstverständlich muss Ihnen in beiden Fällen klar sein, dass dafür natürlich auch das technische Wissen notwendig sein muss, um eine Angular Applikation bereitzustellen. Sobald Sie dies aber geschafft haben oder sich genau deswegen dafür entscheiden, können Sie alle Vorteile von Jamstack nutzen.

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.