How to build Jamstack site with Angular and Scully
The Gatsby and Next.js feature development race is nice to watch if you’re a React developer, but what about our Angular friends? Scully is the first and only static site generator for Angular, although it works completely differently than any other generator.
In this article, I will explain how to implement a website in Angular, install Scully, and configure it to generate all pages as static files. I will also touch on deployment options and comparison with other generators.
What is Angular, and why developers use it
Angular is one of the most used JavaScript frameworks together with React and Vue. Not many beginners choose Angular due to its objectively steepest learning curve. For the same reason, it took me a few years to try and implement my first Angular site, and I did not enjoy the process at all. However, over time you begin to see the benefits of Angular such as strictly defined project structure, great support of TypeScript, modular scopes, and others. If you’re used to Angular, you’ll be able to quickly get up to speed in any project that is presented to you, which is probably why Angular is so popular in large projects and enterprise apps.
What is Scully
Scully is a static site generator for Angular. If you’ve implemented a Jamstack site before and think you know how SSGs work, let me stop you right there. Scully works entirely differently. Typically, static site generators give you a framework, a project structure, and you build your app on top of that. Not Scully. Scully takes your compiled Angular app as input and asks you to provide a list of URLs. Paths you want to generate as static files. It then runs your app, hits every path, and stores the output in an HTML file.
Building a Jamstack app with Angular
So how do we build a Jamstack app with Angular? I’ll first explain some of the Angular basics you will definitely stumble upon and then show how to configure Scully. Every described component in this article contains a link to its implementation on GitHub.
Modules and components
Angular comes with a concept of modules and components. A component can be a menu, header, footer, any scoped piece of code and logic. The component always needs to belong to a module. Modules typically contain components related to each other and to some specific functionality. Like ComponentsModule, AppRoutingModule, ScullyLibModule, or AppModule, which is the main module of every Angular app.
By default, all the components can only be used in the scope of their parent module. If you want to use a component outside of its module, the module needs to export it.
So if we want to use the MenuComponent in our app, we first need the ComponentsModule to export MenuComponent, then import the ComponentsModule in AppModule, and after that import the MenuComponent in a place where we want to render it.
Every route needs to define a URL path and a component that will be responsible for rendering output. For example, if you want to create a blog post page under URL https://your.domain/articles/url-of-the-article, the route will look like this:
The FullArticleComponent needs to be imported into the AppRoutingModule. This way, all requests to URLs starting with /articles/ will be forwarded to the FullArticleComponent.
Components and templating basics
Now that we have modules and routing in place, we can move forward to components and rendering markup. Components are split into multiple files depending on their purpose. They typically feature logic (code) and markup:
./article.component.ts
./article.component.html
But there can also be other files. For example, .sass file for style definitions.
./article.component.sass
You can generate components using the following command:
ng generate component Article --module ComponentsModule
The code part of the component is responsible for preparing data for rendering. It contains functions for data gathering and transforming. In my case, most of the time, I used a service that gathered data from Kontent.ai, and a Moment.js library to transform dates.
All public members of the component class are available to the markup.
When it comes to the markup part, every platform handles rendering data differently. However, we’re almost always trying to achieve the same goals. Take a look at the table below for Angular reference:
The entry component is typically the HomeComponent as it’s defined in the default empty route in AppRoutingModule. The HTML base frame is in the index.html file located in the src folder under the project root.
Adding DeliveryClient and environment files
Now, how do we add some real content to our project?
I already touched on the data gathering a bit above. For any outside communication, you will need an environmental file. It’s used to store URLs of API endpoints, the access keys, and generally any sensitive data that can vary among environments. Most commonly, it’s called .env and is placed in the root of a project. In Angular, the file is called environment.ts and is stored in a separate folder /environments.
I used the file to store the project ID of a Kontent.ai project that contains all content I want to use on the site:
To facilitate outside communication, you can create injectable helper classes—services. I created the KontentService which instantiates the Kontent.ai SDK’s DeliveryClient:
@Injectable({
providedIn: 'root'
})
export class KontentService {
public deliveryClient: IDeliveryClient;
constructor(angularHttpService: AngularHttpService) {
this.deliveryClient = new DeliveryClient({
projectId: environment.kontent.deliveryProjectId,
httpService: angularHttpService,
typeResolvers: [
new TypeResolver('article', () => new Article()),
new TypeResolver('author', () => new Author()),
new TypeResolver('category', () => new Category()),
new TypeResolver('content_page', () => new ContentPage()),
new TypeResolver('menu_item', () => new MenuItem()),
new TypeResolver('site_metadata', () => new SiteMetadata()),
new TypeResolver('tag', () => new Tag())
]
})
}
}
Note that I’m using AngularHttpService instead of standard HttpService. Otherwise Angular would not wait until all data is fetched when pre-rendering the site.
Routing to the same component with a different URL
In your components, it’s best to get data in the ngOnInit function. However, if the component processes multiple URLs, you need to subscribe to the navigation events and trigger the data gathering manually to ensure the component renders the right data. Look at these two URLs:
They both get processed by the component FullArticleComponent. The first article will be rendered correctly, but when you click a link to see the other, Angular won’t trigger the ngOnInit function, and you won’t get any update. To mitigate this, make sure you’re subscribed to navigation event:
ngOnInit(): void {
this.loadData(); // load data in extra function
this.router.events.pipe(
filter((event: RouterEvent) => event instanceof NavigationEnd)
).subscribe(() => {
this.loadData();
});
}
Note: This is an Angular feature. If you plan to use only the static site without the Angular bundle, you can skip this as Scully hits every path separately.
Configuring Scully to pre-render the site
All the steps above describe how to create an Angular site. Unlike other static site generators, Scully runs on top of the built site, hits every path, and stores the generated source code in an HTML file. You only need to tell Scully what are the paths it should hit.
The routes you defined in AppRoutingModule are dynamic. For example, path '/articles/:slug' can match one, two, but also ten thousand articles depending on how many you’ve added to the headless CMS. Scully is not crawling your website trying to find all links and pages but requires you to list all the paths you want to be pre-rendered.
Let’s add Scully to the project:
ng add @scullyio/init
And open the configuration file called scully.{project name}.config.ts. The important part is the definition of routes. You need to provide a list of paths for every route defined in AppRoutingModule that you want to be pre-rendered. If the route does not exactly match its definition in AppRoutingModule, it will be ignored.
There are multiple ways to generate paths for Scully depending on where you’re getting the data from. Here we are using a headless CMS and getting the data from API. Therefore, we first need to process the API response using the resultsHandler function and then specify which property holds the URL slug of each item. Feel free to copy-paste the API URL in your browser to see the raw data.
Scully runs on top of the built Angular site, so first you’ll need to run:
ng build
And then execute Scully:
npm run scully
This will work if you’ve implemented and configured everything correctly. But that hardly ever happens on the first try. :-) So let’s have a look at the commands we use most often during development.
You can test your Angular app with hot reload using:
ng serve
Once your site is ready, don’t forget to build it. Then when you’re trying to configure the routes for Scully, you can run:
npm run scully -- -- scanRoutes
This will ask Scully to check the route config and save the generated routes in /dist/static/assets/scully-routes.json so you can check if the list matches your expectations.
And finally, you can check the pre-rendered pages by running the Scully server:
npm run scully:serve
Building a Scully site for production
You may have noticed that the pre-rendered site is also downloading the full Angular bundle and once it’s ready, your site suddenly becomes a full Angular SPA. That is intended behavior. Scully ensures that visitors see the content of your site as soon as possible while the bundle is downloaded later.
Clone the environment file and name it environment.prod.ts
Run your build with a prod flag:
npm run build – --prod && npm run scully
The output folder is defined in the scully.{project name}.config.ts file and is by default /dist/static. Don’t forget to set the publish directory on your hosting platform. If you’re deploying on Netlify, you’ll find the setting in Build & deploy section under Build settings:
Conclusion
And that’s it. Once the build finishes, your site is live and pre-generated.
If you get stuck on some part of the build process or want to get a head start, check out the Scully starter site for Kontent.ai repo on GitHub. Good luck! :)
Subscribe to the Kontent.ai newsletter
Get the hottest updates while they’re fresh! For more industry insights, follow our LinkedIn profile.