Rendering Static Pages from Dynamic Routes in Angular with Scully

Learn how to server side render an Angular application with routes using Scully.

May 13, 2020

JAMStack 101

As JavaScript applications grow in complexity and offer users robust features, performance becomes an increasing growing concern. JAMstack is an approach to building websites with performance in mind, regardless of the JS framework under the hood. The JAM in JAMstack stands for JavaScript, APIs, and Markup. Conceptually, this means the applications’ functionality is handled by JavaScript, server side interactions utilize reusable APIs and the application pages are served as static HTML files, versus content that is injected into the page dynamically by JavaScript as is usual with single page applications. 

What is Scully?

Scully was created with the intent of being the piece of the JAMstack puzzle Angular developers have been craving. Scully makes it easy to statically generate pages to be served for modern (v8 or v9) Angular applications. 

Server Side Rendering

In order to serve content quickly, static application pages must be generated, served to the browser, and then be bootstrapped by the client. This allows applications to be painted in the browser faster, because we’re not waiting on all of our JavaScript to load and then execute, with interactivity not far behind. When this happens on the server, this approach of pre-rendering and serving HTML pages is called server side rendering. Scully takes care of the generating static pages for us.

Getting Started with Scully

Let’s take a look at implementing generating static pages with Scully on this example Restaurant Ordering application, Place My Order. This is a demo application where a user can search for restaurants based on city and state, choose a restaurant, and place an order from that restaurant. You can view the demo code on github here: https://github.com/tehfedaykin/pmo-starter

Add Scully to Your Project

To get started with Scully run in the root directory of your Angular application:

ng add @scullyio/init

This will install Scully, import the ScullyLibModule to your root app module, and generate the Scully config file (scully.{{yourApp}}.config.js) in your root directory.

Scully starts with the routing module of your Angular application to determine the pages to pre-render, then offers ways to specify other routes to render in the config file. This means if you don’t have a routing module, or if you have no routes written, Scully will not render anything. 

In the example Place My Order app, the router contains the following routes: 

const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'restaurants',
    component: RestaurantComponent,
  },
  {
    path: 'restaurants/:slug',
    component: DetailComponent,
  },
  {
    path: 'restaurants/:slug/:order',
    component: OrderComponent,
  },
  {
    path: 'order-history',
    component: HistoryComponent,
  }
];

Prepare Application Code for Scully

To start using Scully, you must first run:

ng build

This will build your Angular application files and put them in the dist directory, unless you have a custom build directory output. Be aware that Scully will look for the compiled app files in the dist directory to do its magic.

Generate Pre-rendered Pages

Once you’ve built your latest code to the dist directory, run:

npm run scully

This will render the static pages of your app based on your app router and the Scully config file.

Let’s take a look at the output and files generated:

No configuration for route "/restaurants/:slug" found. Skipping
No configuration for route "/restaurants/:slug/:order" found. Skipping
Route list created in files:
  ".../place-my-order/src/assets/scully-routes.json",
  ".../place-my-order/dist/static/assets/scully-routes.json",
  ".../place-my-order/dist/place-my-order/assets/scully-routes.json"

Route "" rendered into file: ".../place-my-order/dist/static/index.html"
Route "/restaurants" rendered into file: ".../place-my-order/dist/static/restaurants/index.html"
Route "/order-history" rendered into file: ".../place-my-order/dist/static/order-history/index.html"

Inside the scully-routes.json file we see a list of application routes:

[{"route":"/"},{"route":"/restaurants"},{"route":"/order-history"}]

Notice that the restaurants:slug routes aren’t included, and we received a message telling us no configuration for these routes was found. Because these routes are dynamic, we need to give Scully a little help with how to render a page like restaurants/jennifers-tasty-brunch-cafe.

If our application had SEO goals of making our restaurant pages searchable so potential customers could find their new favorite brunch spot, we’d absolutely want to make sure we were statically generating them for search engines including meta information used by sites like Twitter, Facebook, and Linked In.

One way we could take this is my manually listing all the possible routes in the Scully config file.

exports.config = {
  projectRoot: "./src",
  projectName: "place-my-order",
  outDir: './dist/static',
  extraRoutes: [
    '/restaurants/jennifers-tasty-brunch-cafe',
    '/restaurants/Q39',
    '/restaurants/novel',
    '/restaurants/local-pig',
    '/restaurants/shio-ramen'
    ...

But this would get tedious in a hurry, and what if restaurant owners were allowed to submit their own restaurant to be included in our app? We wouldn’t want to have to update our config file every time. Fortunately Scully provides great ways to handle custom situations.

Scully Plugins

The Scully authors created a plugin system that allows us to have more control over how Scully renders our applications, and even provided a few built in for us. <3

Using plugins will allow us to create dynamic routes and render those dynamic static pages instead of waiting for the page to load when the user navigates.

The built-in JSON plugin will help us do exactly this. To work the JSON plugin requires a route and a configuration obj with an API url and key to be used for the route, and will return a list of generated routes for the data returned by the url. This means we can use our API to create the list of routes.

In the Scully config file there is a routes prop that accepts the following routeConfig interface(*** I abstracted this a bit from the Scully source code so we can see what we need for using the JSON plugin):

interface RouteConfig {
  '[route: string]': {
    type: 'json';
    postRenderers?: string[];
    [paramName: string]: {
      url: string;
      property: string;
      headers?: HeadersObject;
      resultsHandler?: (raw: any) => any[];
    };
  };
}

To dynamically create our restaurant routes, we need to pass the route with the dynamic param, our API url, and the key we want from our data. The JSON plugin will map over the data response from the API url we pass it and return the key of each object, in this case ‘slug’.

exports.config = {
  projectRoot: "./src",
  projectName: "place-my-order",
  outDir: './dist/static',
  routes: {
    '/restaurants/:slug': {
      type: 'json',
      slug: {
        url: 'http://www.place-my-order.com/api/restaurants',
        property: 'slug',
      },
    },
  },
};

However, my endpoint response looks like this, with my array of restaurant data nested inside a ‘data’ key:

{
  data: [
      ...restaurants
  ]
}

Scully provides an optional resultsHandler method we can use to map the response of our API to an array to be iterated over:

exports.config = {
  projectRoot: "./src",
  projectName: "place-my-order",
  outDir: './dist/static',
  routes: {
    '/restaurants/:slug': {
      type: 'json',
      slug: {
        url: 'http://www.place-my-order.com/api/restaurants',
        resultsHandler: (response) => response.data,
        property: 'slug',
      },
    },
  },
};

Now when npm run scully is run all of the /restaurants/:slug routes have been added to the scully-routes.json file and the files have been dynamically created in the dist/static dir! Yay!

Serving

To serve the statically generated assets run:

npm run scully serve

You’ll be able to view your speedy app at http://localhost:1668/. Cheers!