Prerendering Dynamic SEO Friendly Angular Routes with Scully | Task

Ole Ersoy
Mar - 19  -  4 min

Scenario

We have an angular app that renders Product descriptions. It has two routes defined like this:

const routes: Routes = [
    { path: '', component: HomeComponent },
    { path: ':product', component: ProductComponent }
];

Approach

Project

Create create a new Angular project with routing, skip test generation, and open it up in VSCode:

ng new ngscully --routing --skip-tests
cd ngscully
code .

Components

Create home and product component:

ng g c components/product
ng g c components/home

Update the ProductComponent such that it shows the current :product routed to. Note that we have moved the component template inside the component.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-product',
  template: `<p>The product id is {{product}}</p>`,
  styleUrls: ['./product.component.scss']
})
export class ProductComponent implements OnInit {
  public product: string

  constructor(private route:ActivatedRoute) {
    this.product = this.route.snapshot.paramMap.get(':product');
   }

  ngOnInit(): void {
  }
}

Routing

Replace everything in src/app/app.component.html with

<router-outlet></router-outlet> .

Update src/app/app-routing.module.ts with the routes:

const routes: Routes = [
    { path: '', component: HomeComponent },
    { path: ':product', component: ProductComponent }
];

Serve the application just to make sure we are all good so far:

ng serve -o

Test the routes:

http://localhost:4200/
http://localhost:4200/someblogid

The :product route should show someblogid rendered when that route is triggered.

Stop the server as we need to compile the build with Scully added.

Adding Scully without stopping the live server causes the live server build to fail.

Add Scully

ng add @scullyio/init

And build and prerender our app.

ng build
npm run scully

Scully Rendered Routes

Look in src/assets/scully-routets.json.

We see that Scully rendered this route:

[{"route":"/"}]

Which is the HomeComponent.

The corresponding static asset is dist/static/index.html.

Looking inside it we see that is now includes home works! and that is content normally generated by Javascript after application load.

But what about the product route? We did not tell Scully the paths of any of the dynamic :product id values that this route can take and in order to render these dynamic routes we must do so.

Open scully.ngscully.config.ts (Located in the root project folder) and addextraRoutes: [‘/p1’, ‘/p2’] so that it looks like this:

import { ScullyConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
projectRoot: "./src",
projectName: "ngscully",
outDir: './dist/static',
routes: {},
extraRoutes: ['/p1', '/p2']
};

Now run npm run scully again.

Scully tells us what files it is generating:

Route "" rendered into file: "./dist/static/index.html"
Route "/p2" rendered into file: "./dist/static/p2/index.html"
Route "/p1" rendered into file: "./dist/static/p1/index.html"

And if we look in dist/static/p2/index.html we see that it contains the paragraph string product id is p2.

Thus we now have static content for our SEO needs. But what about dynamic title and meta tag updates?

Title and Meta Tag Service

Create an SEOService service.

ng g s services/SEO

This is the implementation:

import { Injectable } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';

@Injectable({
  providedIn: 'root'
})
export class SEOService {

  constructor(private titleService: Title, private metaService: Meta) { }

  public setTitle(title: string) {
    this.titleService.setTitle(title)
  }

  public updateMeta(description: string) {
    this.metaService.updateTag({ name: 'description', content: `Product Page for ${description}` });
  }
}

Refactor the Product Component

Update the product component design to such that it sets title and meta tags dynamically:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { SEOService } from 'src/app/services/seo.service';

@Component({
  selector: 'app-product',
  template: `<p>The product id is {{id}}</p>`,
  styleUrls: ['./product.component.scss']
})
export class ProductComponent implements OnInit {
  public id: string

  constructor(private route: ActivatedRoute, private seo: SEOService) {
    this.route.paramMap.subscribe(params => {
      this.id = params.get("product")
    });
  }

  ngOnInit(): void {
    this.seo.setTitle(this.id)
    this.seo.setMeta(this.id)
  }
}

Rerun the Scully Build

ng build
npm run scully

Take a look at the static pages generated for the product routes. They now include the dynamic title and meta tags we added using the SEOService.

Note that if you refactor the app it is very important to run ng build again as Scully uses the build to create the static files.

A simple way to ensure this is to update the package.json Scully script so that it looks like this:

"scully": "ng build --prod && scully",