How to Improve Your Angular E-Commerce Application with Scully
Tags: angular, scully, seo, webvitals
Published: 2022-06-21 19:29:00

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](https://scully.io/docs/concepts/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](https://scully.io/docs/Reference/config/).

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.

```typescript
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
        },
      },
    },
  },
};
```

## 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.

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

## 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.

```typescript
// ./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();
}
```

## 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.

```typescript
// ./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"]
}
```

## 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](https://scully.io/docs/concepts/plugins/) and [Plugin Reference](https://scully.io/docs/Reference/plugins/overview/).

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.

```typescript
// ./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);
```

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

```typescript
// ./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',
    },
  },
};
```

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](https://e-commerce-scully-example.vercel.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](https://e-commerce-no-scully-example.vercel.app/) (deployed with `ng build` only), where the Scully pre-render step is not used. By the way, here's the [GitHub repo](https://github.com/fabioemoutinho/e-commerce-scully-example).

If you need any assistance or you just want to chat with us, you can reach us through our [Bitovi Community Slack](https://www.bitovi.com/community/slack).