Angular SSG using Scully.

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

May 30, 2020
angular

Scully 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

Repo in Github

Edit using Stackblitz

Angular Scully demo gif
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.

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

Ensure Node version

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

nvm install 12.16.3

node -v #12.16.3

nvm install --latest-npm

Install the Angular CLI

Install it in the global scope.

npm install -g @angular/cli

Create a new Angular app

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.

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

Alternative method

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

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

Add Scully

Add the scully package to your app.

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.

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.

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.

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.

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.

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.

ng generate @angular/material:navigation main-nav

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

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

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.

<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.

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.

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,
  ) {
  }
}
<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.

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.

ng build
# or
ng build --prod

Build the static file assets using the scully script.

npm run scully

Serve using a web server like http-server.

cd dist/static

http-server

Alternatively, use the scully serve script.

npm run scully serve

We can simplify the above with a consolidated npm script in package.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.

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

comments powered by Disqus