Creating SSG with Scully

6 분 소요

Task: Scully Integration and SEO Optimization

This document outlines a series of tests conducted to evaluate Scully’s capabilities, focusing on its impact on SEO (Search Engine Optimization) through dynamic title and meta tag management. We aim to determine whether Scully effectively generates static HTML files with optimized meta information for different routes within an Angular application.

  1. Does Scully generate index.html files for all routes during the build process?
  2. Can global meta and title settings be configured?
  3. If so, is it possible to configure distinct, dynamic meta and title settings for each route?

Testing Methodology

The following tests were designed to evaluate title and meta settings:

  • First Page: Employ lazy loading. Configure the title and meta within the routing module and apply these settings within the component.
  • Second Page: Implement fast loading. Directly apply title and meta settings within the component itself.
  • Third Page: Predefine values in a JSON configuration file. Load these values when the route changes, applying the title and meta accordingly. Test the behavior when component and routing configurations overlap.

Implementation

First, we initialize an Angular project and establish baseline meta and title configurations.

An Angular project was generated, and the routing module was configured as follows:

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { SecondPageComponent } from './secondpage/secondpage.component';

export const routes: Routes = [
  {
    path: 'firstpage',
    loadComponent: () => import('./firstpage/firstpage.component').then(c => c.FirstpageComponent),
    data: { title: 'first' } // firstpage 
  },
  {
    path: 'secondpage',
    component: SecondPageComponent // secondpage 
  },
  {
    path: 'thirdpage',
    loadComponent: () => import('./thirdpage/thirdpage.component').then(c => c.ThirdpageComponent) // thirdpage 
  },
];
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes)
  ]
};

Scully Installation

The Scully library was then integrated into the Angular project:

ng add @scullyio/init

Upon successful installation, provideScully is added to app.config.ts.

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideScully } from '@scullyio/ng-lib';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideScully() // Scully 
  ]
};

Additionally, Scully-related scripts are appended to package.json.

  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "scully": "scully",
    "scully:serve": "scully serve"
  },

Finally, a scully.[project].config.ts file is generated. This file contains various configurations and can be used when query data needs to be reflected. Refer to the references below for more details.

import { ScullyConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
  projectRoot: "./src",
  projectName: "scully-sample",
  outDir: './dist/static',
  routes: {}
};

Scully Build Process

Scully generates index files based on routing at build time. Therefore, the project must be built before Scully is applied.

ng build --prod && npm run scully

Executing this command will trigger Scully to find all routes and generate corresponding index.html files. The logs will indicate this process:

...
Route "/firstpage" rendered into file: "./dist/static/firstpage/index.html"
Route "/secondpage" rendered into file: "./dist/static/secondpage/index.html"
Route "/thirdpage" rendered into file: "./dist/static/thirdpage/index.html"
Route "/" rendered into file: "./dist/static/index.html"

Generating took 3.74 seconds for 4 pages:
  That is 2.67 pages per second,
  or 375 milliseconds for each page.
  
  Finding routes in the angular app took 3 milliseconds
  Pulling in route-data took 0 milliseconds
  Rendering the pages took 2.91 seconds

By default, Scully generates build files in the dist/static directory. Examining this directory confirms that an index.html file has been successfully created for each route.

If no specific title and meta values are configured, Scully defaults to applying the settings defined in src/index.html to all generated index.html files.

Verification involves opening each generated index.html file to ensure the title and meta tags are correctly applied.

Implementing Dynamic Title and Meta Tags

We leverage Angular’s @angular/platform-browser module, specifically the Meta and Title services, to implement dynamic title and meta tags.

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

const titleService = inject(Title);
const metaService = inject(Meta);

titleService.setTitle('');
metaService.addTag({ name: 'description', content: '...' }); 

To avoid repetitive code in each component, a service is created to handle title and meta tag updates.

// src/app/seo.service.ts
import { Injectable, inject } from '@angular/core';
import { Meta, Title, MetaDefinition } from '@angular/platform-browser';

@Injectable({ providedIn: 'root' })
export class SEOService {
  private title = inject(Title);
  private meta = inject(Meta);

  updateTitle(title?: string): void {
    if (title) {
      this.title.setTitle(title);
    }
  }

  updateOgDescription(desc: string): void {
    this.meta.updateTag({ property: 'og:description', content: desc });
  }

  updateDescription(desc: string): void {
    this.meta.updateTag({ name: 'description', content: desc });
  }
}

For the firstpage.component, the title is dynamically set based on the route data defined in app.routing.module.

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

@Component({
  selector: 'app-firstpage',
  standalone: true,
  template: `<p>firstpage works!</p>`,
})
export class FirstpageComponent implements OnInit {
  private seoService = inject(SEOService);
  private activatedRoute = inject(ActivatedRoute);

  ngOnInit(): void {
    const routeData = this.activatedRoute.snapshot.data;
    this.seoService.updateTitle(routeData['title']);
  }
}

For secondpage.component, the title and meta information are set directly within the component.

// src/app/secondpage/secondpage.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { SEOService } from '../seo.service';

@Component({
  selector: 'app-secondpage',
  standalone: true,
  template: `<p>secondpage works!</p>`,
})
export class SecondpageComponent implements OnInit {
  private seoService = inject(SEOService);

  ngOnInit(): void {
    this.seoService.updateTitle('2pageTitle');
    this.seoService.updateOgDescription('second description');
  }
}

A JSON file is created to store meta data for the third page. The JSON keys match the route configuration.

// <metaData.json>
{
	"thirdpage": {
		"property": "og:description",
      	"content": "second description",
      	"name": "description"
	}
}

The SEOService is modified to retrieve the appropriate JSON data based on the current route.

// src/app/seo.service.ts
// ... (omitted)
import metaInfo from '../assets/metaData.json';

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

  getMeta(url: string): MetaDefinition | undefined {
    return (metaInfo as any)[url];
  }

  setMeta(data?: MetaDefinition): void {
    if (data) {
      this.meta.updateTag(data);
    }
  }
}

In app.component.ts, the routing changes are detected, and the corresponding URL is passed to the seoService.

Routing changes are typically detected using Router and ActivedRoute, but here we utilize the ScullyRoutesService provided by Scully.

(To view all routes detected by Scully, use available$. To view the current URL, use getCurrent().)

// src/app/app.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { SEOService } from './seo.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: `<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit {
  title = 'scully-sample';
  
  private scully = inject(ScullyRoutesService);
  private seoService = inject(SEOService);
  
  currentLink$ = this.scully.getCurrent();

  ngOnInit() {
    this.currentLink$.subscribe(link => {
      if (link.route && link.route !== '/') {
        const metaData = this.seoService.getMeta(link.route);
        this.seoService.updateTitle(link.title);
        this.seoService.setMeta(metaData);
      }
    });
  }
}

For the thirdpage.component, code is added to onInit to forcefully set the Meta, similar to secondpage.component. This allows us to determine whether the title and meta tags set by routing are applied, or if those set within the component take precedence.

// src/app/thirdpage/thirdpage.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { SEOService } from '../seo.service';

@Component({
  selector: 'app-thirdpage',
  standalone: true,
  template: `<p>thirdpage works!</p>`,
})
export class ThirdpageComponent implements OnInit {
  private seoService = inject(SEOService);

  ngOnInit(): void {
    this.seoService.updateTitle('3pageTitle from Component');
    this.seoService.updateOgDescription('third description from Component');
  }
}

The test reveals that the values set in the thirdpage component’s ngOnInit are applied, indicating that Scully’s getCurrent() is invoked later, overwriting the previously set values.

Conclusion

The test results are summarized as follows:

  1. First Page: Applying values set in the routing module from within the component (Success).
  2. Second Page: Applying values from a component that is already loaded (Success).
  3. Third Page: Applying values loaded from a JSON configuration file when the route changes (Success).
  4. Third Page: Overlapping settings between the component and routing (Component settings are applied).

Further Considerations

Further research is required to ascertain whether these configurations are correctly interpreted by domestic search portals like Naver and Daum.

References

댓글남기기