Angular SSG using Scully (tutorial).
Tags: angular, scully, ssg, jamstack
Published: 2020-05-31 21:41:43


## Build a blog or markdown docs SSG within your Angular application using Scully.

[Scully](https://scully.io/) is a fairly recent SSG to join the JAMStack landscape.
It's biggest differentiator is that it is built for Angular projects.

# [`Demo with Netlify`](https://peaceful-minsky-29a1a1.netlify.app)

### [Original Blog Post](https://www.frntndr.com/2020/05/angular-ssg-using-scully./)

{% github https://github.com/sri-ni/ng-app-scully-blog-docs no-readme %}

{% stackblitz angular-ivy-scully-blog %}

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8hsor9ggtpaxk11sju3v.gif)

```zsh
ng add @scullyio/init
```

# Usage

This is based on the type of Angular project.

## Feature-driven app

Scully can be useful to add docs or even a blog to it.
Maybe even pre-rendered pieces of the app can provide the speed, improving the User Experience.

## Website

We'll, your Angular built website gets the blazing speed of SSG pre-rendered HTML and CSS.

# System Tooling

This is not specific to Angular or Scully.
It is tooling that you would need for modern web development.

## Install NPX

We need to install npm package runner for binaries.

```zsh
npm install -g npx
```

## Install NVM

nvm is a version manager for node. It enables switching between various versions per terminal shell.

[Github installation instructions](https://github.com/nvm-sh/nvm#installing-and-updating)

## Ensure Node version

At the time of this writing, I recommend `node` version `12.16.3` and it's latest `npm`.

```zsh
nvm install 12.16.3

node -v #12.16.3

nvm install --latest-npm
```

## Install the Angular CLI

Install it in the global scope.

```zsh
npm install -g @angular/cli
```

## Create a new Angular app

```zsh
ng new my-scully-app
```

Add routing during the interactive CLI prompts.

Add routing for existing apps if there isn't one in place, using the command below.

```zsh
ng generate module app-routing --flat --module=app
```

### Alternative method

Single line command to use the cli and create the app.

```zsh
npx -p @angular/cli@next ng new blogpostdemo
```

## Add Scully

Add the scully package to your app.

```zsh
ng add @scullyio/init
```

## Initialize a blog module

Add a blog module to the app.
It will provide some defaults along with creating a `blog` folder.

```zsh
ng g @scullyio/init:blog
```

## Initialize any custom markdown module

Alternatively, in order to control the folder, module name, route etc.
you can use the following command and respond to the interactive prompts.

```zsh
ng g @scullyio/init:markdown
```

In this case, I added a `docs` module. It will create a `docs` folder as a sibling to the `blog` folder.

## Add Angular Material

Let's add the Angular material library for a more compelling visual experience.

```zsh
ng add @angular/material
```

## Add a new blog post

Add a new blog post and provide the name of the file as a command line option.

```zsh
ng g @scullyio/init:post --name="<post-title>"
```

You can also use the following command to create new posts.
There will be couple prompts for title and target folder for the post.

```zsh
ng g @scullyio/init:post
```

In this case, two posts were created for the `blog` and `docs` each.

Add the content to your blog or docs posts.

## Setup the rendering layout for the app

Using the material library added, generate a `main-nav` component for the app.

```zsh
ng generate @angular/material:navigation main-nav
```

Setup the markup and typescript as below for the `main-nav` component.

```js
import { Component } from "@angular/core";
import { BreakpointObserver, Breakpoints } from "@angular/cdk/layout";
import { Observable } from "rxjs";
import { map, shareReplay } from "rxjs/operators";
import { ScullyRoutesService } from "@scullyio/ng-lib";
@Component({
  selector: "app-main-nav",
  templateUrl: "./main-nav.component.html",
  styleUrls: ["./main-nav.component.scss"],
})
export class MainNavComponent {
  isHandset$: Observable<boolean> = this.breakpointObserver
    .observe(Breakpoints.Handset)
    .pipe(
      map((result) => result.matches),
      shareReplay()
    );
  constructor(private breakpointObserver: BreakpointObserver) {}
}
```

```html
<mat-sidenav-container class="sidenav-container">
  <mat-sidenav
    #drawer
    class="sidenav"
    fixedInViewport
    [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'"
    [opened]="(isHandset$ | async) === false"
  >
    <mat-toolbar>Menu</mat-toolbar>
    <mat-nav-list>
      <a mat-list-item [routerLink]="'blog'">Blog</a>
      <a mat-list-item [routerLink]="'docs'">Docs</a>
    </mat-nav-list>
  </mat-sidenav>
  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="drawer.toggle()"
        *ngIf="isHandset$ | async"
      >
        <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
      </button>
      <span>App Blog Docs</span>
    </mat-toolbar>
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>
```

## Setup the Blog component

Let's setup the component to enable rendering of the `blog` posts.

We need the `ScullyRoutesService` to be injected into the component.

```js
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-blog',
  templateUrl: './blog.component.html',
  styleUrls: ['./blog.component.css'],
  preserveWhitespaces: true,
  encapsulation: ViewEncapsulation.Emulated
})
export class BlogComponent implements OnInit {
  ngOnInit() {}

  constructor(
    public routerService: ScullyRoutesService,
  ) {}
}
```

To render the listing of the available posts use the injected `ScullyRoutesService`. Check the `.available$` and iterate them. The `route` has multiple properties that can be used.

The `<scully-content>` is needed to render the markdown content when the route of the blog is activated.

```html
<h1>Blog</h1>

<h2 *ngFor="let route of routerService.available$ | async ">
  <a *ngIf="route.route.indexOf('blog') !== -1" [routerLink]="route.route"
    >{{route.title}}</a
  >
</h2>

<scully-content></scully-content>
```

Ensure the routing module `blog-routing.module.ts` looks similar to the below.

```js
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";

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

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

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

## Setup the Docs component

Let's setup the component to enable rendering of the `docs` posts.

This would be similar to the setup of the `blog` module above.

```js
import {Component, OnInit, ViewEncapsulation} from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-docs',
  templateUrl: './docs.component.html',
  styleUrls: ['./docs.component.css'],
  preserveWhitespaces: true,
  encapsulation: ViewEncapsulation.Emulated
})
export class DocsComponent implements OnInit {
  ngOnInit() {}

  constructor(
    public routerService: ScullyRoutesService,
  ) {
  }
}
```

```html
<h1>Docs</h1>

<h2 *ngFor="let route of routerService.available$ | async ">
  <a *ngIf="route.route.indexOf('docs') !== -1" [routerLink]="route.route"
    >{{route.title}}</a
  >
</h2>

<scully-content></scully-content>
```

Ensure the routing module `docs-routing.module.ts` looks similar to the below.

```js
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";

import { DocsComponent } from "./docs.component";

const routes: Routes = [
  {
    path: ":doc",
    component: DocsComponent,
  },
  {
    path: "**",
    component: DocsComponent,
  },
];

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

## Build and Serve

Build the app for development or production.

```zsh
ng build
# or
ng build --prod
```

Build the static file assets using the scully script.

```zsh
npm run scully
```

Serve using a web server like `http-server`.

```zsh
cd dist/static

http-server
```

Alternatively, use the scully serve script.

```zsh
npm run scully serve
```

We can simplify the above with a consolidated `npm` script in `package.json`.

```json
"scully:all": "ng build && npm run scully && npm run scully serve",
"scully:all:prod": "ng build --prod && npm run scully && npm run scully serve",
"scully:build:prod": "ng build --prod && npm run scully",
```

## Additional Notes

As an alternative to interactive prompts, you can use command line options to add a new markdown module.

```zsh
ng g @scullyio/init:markdown --name=articles --slug=article  --source-dir="article" --route="article"
```

## Shortcomings...

1. The biggest one is I haven't been able to find a way to render the post listing on one route / component, with a drill down method to view the post in separate route / component.
2. On the listing, until the post route is triggered, the following content is rendered. This experience could be improved.

```
Sorry, could not parse static page content
This might happen if you are not using the static generated pages.
```

## References

- [Scully](https://scully.io/)
- [Building an Angular Jamstack App with Scully](https://www.netlify.com/blog/2019/12/17/building-an-angular-jamstack-app-with-scully/)
- [Exploring Blogging With Scully - the Angular Static Site Generator](https://owlypixel.com/exploring-blog-feature-with-scully-angular-static-site-generator/)
- [Scully Tutorial: A Static Site Generator for Angular Websites](https://snipcart.com/blog/angular-static-site-generator-scully)
- [Scully, the First Static Site Generator for Angular](https://indepth.dev/scully-the-first-static-site-generator-for-angular/)
- [Building an Angular Blog With Scully](https://unicorn-utterances.com/posts/making-an-angular-blog-with-scully/)