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)