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
sri-ni / ng-app-scully-blog-docs
Angular app using Scully to make docs and blog.
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...
- 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.
- 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.

Top comments (0)