How to Improve Your Angular E-Commerce Application with Scully

fabioemoutinho

Fábio Englert Moutinho

Posted on June 21, 2022

How to Improve Your Angular E-Commerce Application with Scully

Scully is a “Static Site Generator for Angular apps” that enables Angular apps to pre-render pages with dynamic content to improve performance metrics like First Contentful Paint (FCP), Time to Interactive (TTI), and others which are used by Search Engines to rank your website.

But is Scully the right tool for your Angular E-Commerce Application?

Let’s find out if it suits your needs. SPOILER ALERT: yes, it probably does.

How does Scully work?

Scully provides an additional step after Angular’s build step, which will identify your application’s routes to be rendered, then serve your application and launch a browser instance to navigate through selected routes. When the browser finishes rendering each route, Scully copies its rendered content and saves everything in HTML files inside dist folder.

If you want to know how Scully works behind the curtains in more detail, take a look at The Scully Process page in the official documentation.

How Does Scully Improve An E-Commerce Application?

Search Engine Optimization (SEO) is a must for any website nowadays, especially for E-Commerce Apps.

Scully will help your E-Commerce Application rank higher on search results by statically rendering each product page, making the application load faster for users and search engines alike. Performance metrics used by search engines will also improve as a result of Scully’s pre-rendering process.

The performance improvements will also lead to lower bounce rates and higher conversion rates, as users will have a better experience while navigating.

In other words, Scully essentially caches the application with statically served files, improving load time, and making processing your application easier on browsers and search engines, since there will be no javascript functions to run and no need to make external HTTP calls to fetch data.

Installation

To install Scully, run ng add @scullyio/init.

Scully will then ask which route renderer you would like to use. As of 2022, we recommend picking Puppeteer, since other options are currently in beta.

Once installation is done, you’ll notice there’s a new Scully config file, scully.[project].config.ts, and app.module.ts now imports ScullyLibModule.

To match the usability of ng serve, when developing you’ll need to run two terminals with the following commands:

  • ng build --watch - whenever there’s a file change, it will trigger the build step

  • npx scully --scanRoutes --watch - whenever the build files generated by ng build --watch change, it will trigger Scully's build step

If you need to run both the static and regular builds at the same time, you can use npx scully serve, but you won’t have automatic updates when there are changes in builds.

Dynamic Routes

Scully provides a pre-build step where it can fetch information and decide which routes your application will render based on any logic you see fit.

In order to do that, you need to go to the ./scully.[project].config.ts file and edit the routes property inside the exported config object.

The routes property is an object that can configure multiple routes, and each route can have different logic to decide which children routes will be rendered.

In the example below, we have a /product/:slug route, and Scully will fetch the url and run the resultsHandler function with the response data. resultsHandler must return a list of objects with the property defined in the property property, which in this example has the slug value. More information at the official documentation.

Scully will then find out which routes should be rendered by replacing :slug in the route /product/:slug with the slug property value for each object in resultsHandler returned array.

export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'PROJECT-NAME-HERE',
  outDir: './dist/static',
  routes: {
    '/product/:slug': {
      type: 'json',
      slug: {
        url: 'https://PRODUCT-API-HERE/products',
        property: 'slug',
        resultsHandler: (data) => {
          // you can process anything here,
          // but you must return a list of objects
          // that have a 'slug' property, as defined above in line 8
        },
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Making API Data Static

Caching API calls will make your application faster and less reliant on servers availability. With useScullyTransferState method you are able to cache the results of Observables. This means, however, that to update the data statically served by useScullyTransferState you will need to trigger a new Scully build and deploy pipeline.

The first argument of the useScullyTransferState method is the index that will be used to get the data at runtime.

The second argument expects an Observable, which means you are not limited to caching API calls, you can cache heavy operation Observables too.

// some service method
getCatalog(): Observable<Catalog> {
  return this.transferState.useScullyTransferState(
    'catalog',
    this.http.get<Catalog>('https://CATALOG-URL')
  );
}
Enter fullscreen mode Exit fullscreen mode

Not All API Requests Should Be Cached

Should you wrap every Observable with useScullyTransferState? Definitely not. There might be cases where observables should only run at runtime and don’t need to be cached.

Good examples include observables that rely on login state, like cart or user data, or when you need to hide or show specific parts of your application in the static generated version only.

// ./src/app/app.component.ts example
@Component({
  selector: 'app-root',
  template: '
    <nav>
      <app-menu></app-menu>
      <app-cart *ngIf="!isScullyRunning"></app-cart>
    </nav>
    <router-outlet></router-outlet>
  ',
})
export class AppComponent {
  readonly isScullyRunning: boolean = isScullyRunning();
}
Enter fullscreen mode Exit fullscreen mode

Relying on Environment Files

When writing plugins or dealing with the Scully config file, you may need to import environment.prod.ts or some other file.

By default, Scully only transpiles .ts files inside the ./scully folder. Fortunately, it’s rather simple to tell Scully that more files need to be transpiled to JavaScript.

In ./scully/tsconfig.json file, add an include property with the patterns/files you need. The ./**/** pattern will match all files inside ./scully folder, which is the default behavior if an include property is not defined. Then, you only need to add specific files that exist outside of ./scully folder.

// ./scully/tsconfig.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "esModuleInterop": true,
    "importHelpers": false,
    "lib": ["ES2019", "dom"],
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es2018",
    "types": ["node"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "typeRoots": ["../node_modules/@types"],
    "allowSyntheticDefaultImports": true
  },
  "include": ["./**/*", "../src/environments/environment.prod.ts"],
  "exclude": ["./**/*spec.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Creating a Custom Plugin for Incremental Builds

Rendering all the pages each time a single product changes is a waste of time and resources. It might not have a huge impact when you have a small set of products, but what if your E-Commerce application has thousands of product pages to render?

Implementing incremental builds is an optimized solution where only changed content is re-rendered and deployed, saving you and your organization time and money in CI pipeline and deployments.

Scully has a very powerful plugin system that enables you to control the pre-render process. There’s more information about Scully plugins in Plugin Concept and Plugin Reference.

Below is an example of a plugin approach to render specific products by passing a list of IDs with the scully command. Command example: npx scully --productIds=1,2,3. This approach expects that the build and deploy pipeline are triggered by a system (like a CMS) with an awareness of what content has changed.

In ./scully/plugins/plugin.ts, a product plugin is registered as a router plugin, then used in ./scully.[project].config.ts file to configure how the routes in /product/:slug are handled.

// ./scully/plugins/plugin.ts
import { HandledRoute, registerPlugin, httpGetJson } from '@scullyio/scully';
import { Product } from '../../src/app/product/product.model';
import { environment } from '../../src/environments/environment.prod';

import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

export const product = 'product';

const productRoutes = async (route?: string): Promise<HandledRoute[]> => {
  const yarg = yargs(hideBin(process.argv));
  const argv = await yarg.option('productIds', { type: 'string' }).argv;

  // if there are --productIds being passed in npx scully --productIds=1,2,3
  // then only generate static files for those products
  // else fetch all products from API and map the returned IDs.
  const productIds: string[] = argv.productIds
    ? argv.productIds.split(',')
    : ((await httpGetJson(`${environment.api.url}/products`)) as Product[]).map(
        (product) => product.id.toString()
      );
  const productRoutes: HandledRoute[] = productIds.map((id) => ({
    route: `/product/${id}`,
  }));

  return Promise.resolve(productRoutes);
};

const validator = async () => [];

registerPlugin('router', product, productRoutes, validator);
Enter fullscreen mode Exit fullscreen mode

You’ll also need to update the config file to use the product plugin:

// ./scully.[project].config.ts
import './scully/plugins/plugin';

export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'PROJECT-NAME-HERE',
  outDir: './dist/static',
  routes: {
    '/product/:slug': {
      type: 'product',
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Sweet! Now when we run the Scully build with this plugin, Scully will select to pre-render either the product IDs passed as arguments - if there are any - or the returned product IDs of an API call to ${environment.api.url}/products, meaning you have great control over which routes to render.

Scully is AWESOME!

We’ve seen how Scully can solve Angular E-Commerce applications' problems in a handful of ways.

There is little downside to using Scully. The main one is the need to run an extra step between build and deploy, but as shown in this article, you can lower the impact with incremental builds. Needless to say, if you care about lower bounce rates and increasing the likelihood your website will be a top result on search engines, Scully is your friend.

As a bonus, I’ve setup an E-Commerce example app (deployed with ng build && scully) where I used Scully to pre-render the content so you can see how the output looks using your browser’s DevTools. And you can compare it with a second E-Commerce no-Scully example app (deployed with ng build only), where the Scully pre-render step is not used. By the way, here's the GitHub repo.

If you need any assistance or you just want to chat with us, you can reach us through our Bitovi Community Slack.

💖 💪 🙅 🚩
fabioemoutinho
Fábio Englert Moutinho

Posted on June 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related