DEV Community

Cover image for Angular SSG using Scully (tutorial).
Sri-Ni, Thirumalaa Srinivas
Sri-Ni, Thirumalaa Srinivas

Posted on • Updated on

Angular SSG using Scully (tutorial).

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

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

Original Blog Post

GitHub logo sri-ni / ng-app-scully-blog-docs

Angular app using Scully to make docs and blog.

Alt Text

ng add @scullyio/init
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install the Angular CLI

Install it in the global scope.

npm install -g @angular/cli
Enter fullscreen mode Exit fullscreen mode

Create a new Angular app

ng new my-scully-app
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Alternative method

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

npx -p @angular/cli@next ng new blogpostdemo
Enter fullscreen mode Exit fullscreen mode

Add Scully

Add the scully package to your app.

ng add @scullyio/init
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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,
  ) {
  }
}
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

Build and Serve

Build the app for development or production.

ng build
# or
ng build --prod
Enter fullscreen mode Exit fullscreen mode

Build the static file assets using the scully script.

npm run scully
Enter fullscreen mode Exit fullscreen mode

Serve using a web server like http-server.

cd dist/static

http-server
Enter fullscreen mode Exit fullscreen mode

Alternatively, use the scully serve script.

npm run scully serve
Enter fullscreen mode Exit fullscreen mode

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",
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)