Post contents
If you've ever used something like Gatsby or NuxtJS, you may already be familiar with Static Site Generation (SSG). If not, here's a quick rundown: You're able to export a React application to simple HTML and CSS during a build-step. This export means that (in some cases), you can disable JavaScript and still navigate your website as if you'd had it enabled. It also often means much faster time-to-interactive times, as you no longer have to run your JavaScript to render your HTML and CSS.
For a long time, React and Vue have had all of the SSG fun... Until now.
Recently, a group of extremely knowledgeable developers has created Scully, a static site generator for Angular projects. If you prefer Angular for your stack, you too can join in the fun! You can even trivially migrate existing Angular projects to use Scully!
In this article, we'll outline how to set up a new blog post site using Scully. If you have an existing blog site that you'd like to migrate to use Scully, the blog post should help you understand some of the steps you'll need to take as well.
Without further ado, let's jump in, shall we?
Initial Setup
First, we have some requirements:
- Node 12
- Angular CLI installed globally
You're able to do this using npm i -g @angular/cli
. You'll want to make sure you're using the latest version if you already have it pre-installed.
Now that we have that covered let's generate our project!
ng new my-scully-blog
We'll want to choose y
when it asks us to add routing. The second question that will be raised is regarding what flavor of CSS you'd like. I like SCSS
, so I chose that, but you're free to select any of the options that you deem fit for your blog.
If we pause here and run ng serve
, we'll find ourselves greeted with the default generated app screen from the Angular core team upon visiting the localhost:4200
URI in our browser.
The file that this code lives under is the app.component.html
file. We'll be modifying that code later on, as we don't want that UI to display on our blog site.
Adding Scully
After that, open the my-scully-blog
directory and run the following command to install and add Scully to the project:
ng add @scullyio/init
This will yield us some changed files. You'll see a new scully.my-scully-blog.config.js
file that will help us configure Scully. You'll also notice that your package.json
file has been updated to include two new commands:
"scully": "scully",
"scully:serve": "scully serve"
Here's where the "SSG" portion of Scully comes into play. You see, once you run ng build
to build your application, you should be running npm run scully
to run the static generation. That way, it will generate the HTML and CSS that your Angular code will generate on the client ahead-of-time. This means that you have one more build step, but it can be incredibly beneficial for your site's speed and usability.
We'll need to run the npm run scully
command later on, but for now, let's focus on adding Markdown support to our blog:
Adding Markdown Support
While Scully does have a generator to add in blog support, we're going to add it in manually. Not only will this force us to understand our actions a bit more to familiarize ourselves with how Scully works, but it means this article is not reliant on the whims of a changing generator.
This isn't a stab at Scully by any means, if anything I mean it as a compliment. The team consistently improves Scully and I had some suggestions for the blog generator at the time of writing. While I'm unsure of these suggestions making it into future versions, it'd sure stink to throw away an article if they were implemented.
Angular Routes
Before we get into adding in the Scully configs, let's first set up the page that we'll want our blog to show up within. We want a /blog
sub route, allowing us to have a /blog
for the list of all posts and a /blog/:postId
for the individual posts.
We'll start by generating the blog
module that will hold our routes and components.
ng g module blog --route=blog --routing=true --module=App
This will create a route called blog
and generate or modify the following files:
CREATE src/app/blog/blog-routing.module.ts (341 bytes)
CREATE src/app/blog/blog.module.ts (344 bytes)
CREATE src/app/blog/blog.component.scss (0 bytes)
CREATE src/app/blog/blog.component.html (21 bytes)
CREATE src/app/blog/blog.component.spec.ts (622 bytes)
CREATE src/app/blog/blog.component.ts (275 bytes)
UPDATE src/app/app-routing.module.ts (433 bytes)
If you look under your app-routing.module.ts
file, you'll see that we have a new route defined:
const routes: Routes = [ { path: "blog", loadChildren: () => import("./blog/blog.module").then(m => m.BlogModule) }]
This imports the blog.module
file to use the further children routes defined there. If we now start serving the site and go to localhost:4200/blog
, we should see the message "blog works!" at the bottom of the page.
Routing Fixes
That said, you'll still be seeing the rest of the page. That's far from ideal, so let's remove the additional code in app.component.html
to be only the following:
<router-outlet></router-outlet>
Now, on the /blog
route, we should only see the "blog works" message!
However, if you go to localhost:4200/
, you'll see nothing there. Let's add a new component to fix that.
ng g component homepage -m App
This will create a new homepage
component under src/app/homepage
. It's only got a basic HTML file with homepage works!
present, but it'll suffice for now. Now we just need to update the app-routing.module.ts
file to tell it that we want this to be our new home route:
import { HomepageComponent } from "./homepage/homepage.component";const routes: Routes = [ { path: "blog", loadChildren: () => import("./blog/blog.module").then(m => m.BlogModule) }, { path: "", component: HomepageComponent }];
Now, we have both /blog
and /
working as-expected!
Adding Blog Post Route
Just as we added a new route to the existing /
route, we're going to do the same thing now, but with /blog
paths. Let's add a blog-post
route to match an ID passed to blog
. While we won't hookup any logic to grab the blog post by ID yet, it'll help to have that route configured.
ng g component blog/blog-post -m blog
Then, we'll need to add that path to the blog list:
const routes: Routes = [ { path: ":postId", component: BlogPostComponent }, { path: "", component: BlogComponent }];
That's it! Now, if you go to localhost:4200/blog
, you should see the blog works!
message and on the /blog/asdf
route, you should see blog-post works!
. With this, we should be able to move onto the next steps!
The Markdown Files
To start, let's create a new folder at the root of your project called blog
. It's in this root folder that we'll add our markdown files that our blog posts will live in. Let's create a new markdown file under /blog/test-post.md
.
---title: Test postdescription: This is a post descriptionpublish: true---# Hello, WorldHow are you doing?
Keep in mind that the file name will be the URL for the blog post later on. In this case, the URL for this post will be
/blog/test-post
.
The top of the file ---
block is called the "frontmatter"_. You're able to put metadata in this block with a key/value pair. We're then able to use that metadata in the Angular code to generate specific UI based on this information in the markdown file. Knowing that we can store arbitrary metadata in this frontmatter allows us to expand the current frontmatter with some useful information:
---title: Test postdescription: This is a post descriptionpublish: trueauthorName: Corbin CrutchleyauthorTwitter: crutchcorn---
It's worth mentioning that the publish
property has some built-in functionality with Scully that we'll see later on. We'll likely want to leave that field in and keep it true
for now.
Scully Routes
Now we'll tell Scully to generate one route for each markdown file inside of our blog
folder. As such, we'll update our scully.my-scully-blog.config.js
file to generate a new /blog/:postId
route for each of the markdown files:
exports.config = { // This was generated by the `ng add @scullyio/init` projectRoot: "./src", projectName: "my-scully-blog", outDir: './dist/static', // This is new routes: { '/blog/:postId': { type: 'contentFolder', postId: { folder: "./blog" } }, }};
Before we start the build process and run Scully, let's add one more change to our blog-post.component.html
file:
<h1>My Blog Post</h1><hr><!-- This is where Scully will inject the static HTML --><scully-content></scully-content><hr><h2>End of content</h2>
Adding in the scully-content
tags will allow Scully to inject the HTML that's generated from the related Markdown post into that tag location. To register this component in Angular, we also need to update our blog.module.ts
file to add an import:
import {ScullyLibModule} from '@scullyio/ng-lib';@NgModule({ declarations: [BlogComponent, BlogPostComponent], imports: [CommonModule, BlogRoutingModule, ScullyLibModule]})export class BlogModule {}
You'll notice that if you run ng serve
at this stage and try to access localhost:4200/blog/test-post
, you'll see... Not the blog post. You'll see something like:
<h1>Sorry, could not parse static page content</h1><p>This might happen if you are not using the static generated pages.</p>
This message is showing because we're not able to get the HTML of the markdown; we haven't statically generated the site to do so. Scully injects the markdown's HTML at build time, so we're unable to get the contents of the markdown file during the development mode. We can get the route metadata from the frontmatter on the blog post, however. If you want to learn more about that, you'll have to read the next section. 😉
Running the Build
Even if you're familiar with Angular's build process, you should read this section! Scully does some non-standard behavior that will prevent some of the steps in the next sections if not understood properly.
Now that we have our code configured to generate routes based on our Markdown files let's run ng build
. The build should go off without a hitch if the code was updated alongside the post.
If you hit an error at this step, make sure to read through the steps again and pay attention to the error messages. Angular does a decent job of indicating what you need to change to get the build working again.
Now, let's run npm run scully
; Doing so should give us some message like this:
Route "" rendered into file: "/Users/ccrutchley/git/my-scully-blog/dist/static/index.html"
Route "/blog" rendered into file: "/Users/ccrutchley/git/my-scully-blog/dist/static/blog/index.html"
Route "/blog/2020-03-12-blog" rendered into file: "/Users/ccrutchley/git/my-scully-blog/dist/static/blog/2020-03-12-blog/index.html"
send reload
"ScullyIO not generating markdown blog post route" is something I've attempted to Google multiple times.
If you happen to see an error like
Pull in data to create additional routes. missing config for parameters (postId) in route: /blog/:postId. Skipping Route list created in files
you've misconfigured yourscully.config.js
file.For example, at one point I had the following code in my config file when I was getting that error:
'/blog/:postId': { type: 'contentFolder', slug: { folder: "./blog" }},
The problem is that the route and the config are mismatched. You need to configure it to look like this:
'/blog/:postId': { type: 'contentFolder', postId: { folder: "./blog" }},
Making sure that your params match like this should generate the pages as-expected.
Now, we can access the server at the bottom of the build output:
The server is available on "http://localhost:1668/"
Finally, if we go to http://localhost:1668/blog/test-post, we can see the post contents alongside our header and footer.
Scully Build Additions
You'll notice that if you open your dist
folder, you'll find two folders:
my-scully-blog
static
The reason for the two separate folders is because Scully has it's own build folder. When you ran ng build
, you generated the my-scully-blog
folder, then when you later ran npm run scully
, it generated the static
folder. As such, if you want to host your app, you should use the static
folder.
Asset Routes
If you open the /src/assets
folder, you'll notice another file you didn't have before npm run scully
. This file is generated any time you run Scully and provides you the routing metadata during an ng serve
session. Remember how I mentioned that there was a way to access the Markdown frontmatter data? Well, this is how! After running a Scully build, you'll be provided metadata at your disposal. In the next section, we'll walk through how to access that metadata!
Listing Posts
To get a list of posts, we're going to utilize Scully's route information service. To start, let's add that service to the blog.component.ts
file:
import { Component, OnInit } from '@angular/core';import { ScullyRoutesService } from '@scullyio/ng-lib';@Component({ selector: 'app-blog', templateUrl: './blog.component.html', styleUrls: ['./blog.component.scss']})export class BlogComponent implements OnInit { constructor(private scully: ScullyRoutesService) {} ngOnInit(): void { }}
Now that we have access to said service, we can add some calls inside of our ngOnInit
lifecycle method to list out the routes:
ngOnInit(): void { this.scully.available$.subscribe(routes => console.log(routes));}
If you now start your server (ng serve
) and load up your /blog
route, you should see the following printed out to your log:
0: {route: "/"}1: {route: "/blog/test-post", title: "Test post", description: "This is a post description", publish: true, authorName: "Corbin Crutchley", …}2: {route: "/blog"}
See? We're able to see all of the routes that Scully generated during the last npm run scully
post-build step. Additionally, any of the routes that were generated from a markdown file contains it's frontmatter!
Remember how I said earlier that the frontmatter fields impacted Scully? Well, that
publish
field will toggle if a route shows up in this list or not. If you change that field tofalse
, then rebuild and re-run thescully
command, it will hide it from this list.Want to list all of the routes, including the ones with
publish: false
? Well, changethis.scully.available$
tothis.scully.allRoutes$
, and you'll even have those in the fray!
We now have the list of routes, but surely we don't want to list the /blog
or the /
routes, do we? Simple enough, let's add a filter:
routes.filter(route => route.route.startsWith('/blog/') && route.sourceFile.endsWith('.md'));
And that'll give us what we're looking for:
0: {route: "/blog/test-post", title: "Test post", description: "This is a post description", publish: true, authorName: "Corbin Crutchley", …}
Final Blog List
We can cleanup the code a bit by using the Angular async
pipe:
// blog.component.tsimport { Component } from '@angular/core';import { ScullyRoutesService } from '@scullyio/ng-lib';import { map } from 'rxjs/operators';@Component({ selector: 'app-blog', templateUrl: './blog.component.html', styleUrls: ['./blog.component.scss']})export class BlogComponent { constructor(private scully: ScullyRoutesService) {} $blogPosts = this.scully.available$.pipe( map(routes => routes.filter( route => route.route.startsWith('/blog/') && route.sourceFile.endsWith('.md') ) ) );}
<!-- blog.component.html --><ul aria-label="Blog posts"> <li *ngFor="let blog of $blogPosts | async"> <a [routerLink]="blog.route"> {{blog.title}} by {{blog.authorName}} </a> </li></ul>
This code should give us a straight list of blog posts and turn them into links for us to access our posts with!
While this isn't a pretty blog, it is a functional one! Now you're able to list routes; we can even get the metadata for a post
Final Blog Post Page
But what happens if you want to display metadata about a post on the post page itself? Surely being able to list the author metadata in the post would be useful as well, right?
Right you are! Using RxJS' combineLatest
function and the ActivatedRoute
's params
property (alongside the RxJS pluck
opperator to make things a bit easier for ourselves), we're able to quickly grab a post's metadata from the post page itself.
// blog-post.component.tsimport { Component } from '@angular/core';import { ActivatedRoute } from '@angular/router';import { ScullyRoutesService } from '@scullyio/ng-lib';import { combineLatest } from 'rxjs';import { map, pluck } from 'rxjs/operators';@Component({ selector: 'app-blog-post', templateUrl: './blog-post.component.html', styleUrls: ['./blog-post.component.scss']})export class BlogPostComponent { constructor( private activatedRoute: ActivatedRoute, private scully: ScullyRoutesService ) {} $blogPostMetadata = combineLatest([ this.activatedRoute.params.pipe(pluck('postId')), this.scully.available$ ]).pipe( map(([postId, routes]) => routes.find(route => route.route === `/blog/${postId}`) ) );}
<!-- blog-post.component.html --><h1 *ngIf="$blogPostMetadata | async as blogPost">Blog Post by {{blogPost.authorName}}</h1><hr><!-- This is where Scully will inject the static HTML --><scully-content></scully-content><hr><h2>End of content</h2>
Conclusion
While this blog site is far from ready from release, it's functional. It's missing some core SEO functionality as well as general aesthetics, but that could be easily remedied. Using a package like ngx-meta
should allow you to make short work of the SEO meta tags that you're missing where areas adding some CSS should go a long way with the visuals of the site.
All in all, Scully proves to be a powerful tool in any Angular developer's toolkit, and knowing how to make a blog with it is just one use case for such a tool.
As always, I'd love to hear from you down below in our comments or even in our community Discord. Also, don't forget to subscribe to our newsletter so you don't miss more content like this in the future!