This article is a guest post by Alejandro Soto. He wrote this blog post through the Write for the Community program.
As you might know, Jamstack stands for JS/APIs/Markup, but behind this approach, there are multiple vendors outside like NextJS, Gatsby, maybe Remix and the one we are going to cover Scully.io . Some of the alternatives might include static site generation, and others even provide a hybrid mechanism. Scully.io provides the static site generation mechanism during the build phase, but later on, when the static page arrives at the browser on runtime, it gets hydrated with regular Angular strategy like any other SPA, so if you access a page after it is rendered, it will call the APIs from the frontend.
We will cover different steps, for example:
Our app is going to be structured like this, and these pages will be statically generated on build time:
/p/:postId
: containing the full content of each post, these pages will be generated on build time. For example, if we created only one ship, only one page for this route will be created, or we if we created five contents, the output will be five pages.Moreover, we’ll learn to generate our static site from Strapi API, deploy and deliver from Netlify. Also, we are going to create a dynamic build mechanism so every time a post is published or unpublished, the app is rebuilt, and the site gets updated. That's nice, isn’t it?
For this tutorial, we’ll need:
npm install -g @angular/cli
, more information here. Here we will learn the basics of creating content types, publishing, setting up user roles, and getting ready a GraphQL API.
Step 1: Content Type
Going ahead to Strapi, create a new content type called Blog Sample
Step 2: Create fields
Title
Content
Image
, select type as Single. We will need only one image.Credits
Step 3: Create first content using our content type
Go to collection types and select Blog Samples
, and hit Add New Blog Samples
.
Alright, it is time to create our first content. I’m a big fan of Sci-fi space ships, so this will be my first post for this tutorial:
When you are ready, click Save
. This will put the content on draft. Click this time Publish
. Now it should be accessible through APIs.
Step 4: GraphQL API
At this point, we need to set up GraphQL. Strapi comes with multiple plugins that can be integrated into the CMS. One of these plugins is for GraphQL.
Let’s go-ahead to the marketplace and find the one named GraphQL
and click Download
After installation, we are ready to start using endpoint https://your-strapi-instance-url/graphql .
For this tutorial, please remember that your Strapi needs to be published on the internet and public. For that, you can try your preferred platform, plenty of options such as AWS, Heroku, Digital Ocean, and many more, and consider using strong passwords.
GraphQL on Strapi authenticates using JWT passed through request header. We’ll generate a token using the super admin, but it is recommended to use dedicated user for queries with only that permission
Proceed to create a new user, from Collection Types > Users, this is required because GraphQL on Strapi does not allow us to use an admin panel’s user. I assigned Authenticated permission.
Now have to go to Settings, and then from Users & Permissions Plugin from Roles select the Authenticated role. For this, we are going to give all access to the Content Type > Blog Sample and hit Save
. On this part, we have added permissions to role Authenticated to make queries to our content type.
Let’s create token by going to https://your-strapi-instance-url/graphql and execute:
1mutation {
2 login(input: { identifier: "username", password: "password" }) {
3 jwt
4 }
5}
Now, copy the token and keep it safe as it will be used later on the code, or you can use it from the GraphQL explorer to try queries by configuring it through HTTP Headers tab. Then we can use this endpoint from any app or static site generator or even inspect the GraphQL schemas generated for our current Content Types.
We won’t do a step by step on this, because the app involves many things such as styling, templates, and so on. But let’s cover the main aspects of adding Scully.io to an existing Angular application, please fork source code here and pull from your fork.
Step 1: Angular app creation
We created a regular angular application, Scully comes as a schematic, so it can be attached to any existing Angular app. Our new app will be called scully-strapi-tutorial
.
ng new scully-strapi-tutorial
I have picked SCSS as the styling choice, but feel free to choose any of the stylings you like, and we can go inside the app’s folder.
Step 2: Scully initialization Add Scully to our project using schematic:
1ng add @scullyio/init
After running this, here are the important changes made were:
package.json
got updated, scully scripts added (we’ll use them later), and also its dependenciesscully.scully-strapi-tutorial.config.ts
configuration file addedscully
folder created, we’ll come back later hereapp.module.ts
is using now, ScullyLibModule
Step 3: GraphQL code auto-generation GraphQL code generator helps us to generate queries and types code, services for angular case, but it has other libraries integrations such for React as well. Let’s install dependencies:
1npm install apollo-angular @apollo/client graphql node-fetch
And development dependencies:
1npm install --save-dev dotenv @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-apollo-angular @graphql-codegen/typescript-operations
We need to define which queries the code generator will look at, so we have to create **types.graphql**
in the root of the project:
/types.graphql
1query BlogSamples {
2 blogSamples {
3 id
4 Title
5 created_at
6 }
7}
8
9query BlogSample($id: ID!) {
10 blogSample(id: $id) {
11 id
12 Title
13 created_at
14 Content
15 Image {
16 url
17 }
18 Credits
19 }
20}
It is a good practice to parametrize your application with the environment variables so that you can control urls, options, and other stuff, directly from the platform where your app is deployed. In our case from Netlify, we have two variables: GRAPHQL_API_URL
and GRAPHQL_API__TOKEN
, for this, let’s add an .env
file to the root of our app. This is possible because we added a dependency called dotenv
. You can take a look at the source code.
/.env
1CMS_BASE_URL=https://your-strapi-instance-url
2GRAPHQL_API_URL=https://your-strapi-instance-url/graphql
3GRAPHQL_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjEzOTY4MzQxLCJleHAiOjE2MTY1NjAzNDF9.iaxOBDCdXoBc7ZWhTVfdGdSereIGozTKphTUYhNVki0
The first variable is required for resolving image paths from your pages. The second is where the graphql endpoint is located, and the third one is the authentication token we already retrieved from Strapi. One more time, please keep this parameter safe.
To make dotenv
work with Angular we are going to pass and generate these variables on built-time into angular’s file environment.ts
using a script, which will run before any other script, this way, from Angular we can import the environment.ts
and use the configured parameters:
/env-task.js
1require("dotenv").config();
2const fs = require("fs");
3
4fs.writeFileSync(
5 "env.ts",
6 `
7export const ENV = {
8 CMS_BASE_URL: '${process.env.CMS_BASE_URL}',
9 GRAPHQL_API_URL: '${process.env.GRAPHQL_API_URL}',
10 GRAPHQL_API_TOKEN: '${process.env.GRAPHQL_API_TOKEN}'
11};
12`
13);
Have to tell code generator, where the server is located in order to read the schemas, create **codegen.yml**
in the project root, it is pulling the schema url as env variable:
/codegen.yml
1schema: ${GRAPHQL_API_URL}
2documents: "types.graphql"
3generates:
4 ./src/app/shared/graphql.gen.ts:
5 plugins:
6 - typescript
7 - typescript-operations
8 - typescript-apollo-angular
Finally, we have to add a new script to package.json
for generating graphql code
1 "generate": "graphql-codegen -r dotenv/config"
Let’s try to run it
1npm run generate
Now, file src/app/shared/graphql.gen.ts
should have been generated, containing query services, types, and definitions that we can use from our app’s code.
Step 4: Single posts route
We have to create that scully plugin postsPagesPlugin
, which will generate our ships pages on build time.
Go to scully/plugins
and create a file called postsPagesPlugin.ts
. So here is where the magic of generating pages will happen. However, this part is not responsible for retrieving specific content of the page. We’ll see that on the next steps, you might notice we are passing the token and the endpoint from the env variables. You can check in detail this configuration on the repo attached.
/scully/plugins/postsPagesPlugin.ts
1import {
2 ApolloClient,
3 ApolloLink,
4 concat,
5 gql,
6 HttpLink,
7 InMemoryCache,
8} from '@apollo/client/core';
9import fetch from 'node-fetch';
10import { HandledRoute, registerPlugin } from '@scullyio/scully';
11import { ENV } from '../../env';
12const authMiddleware = new ApolloLink((operation, forward) => {
13 // add the authorization to the headers
14 operation.setContext({
15 headers: {
16 authorization: `Bearer ${ENV.GRAPHQL_API_TOKEN}`,
17 },
18 });
19 return forward(operation);
20});
21const client = new ApolloClient({
22 link: concat(
23 authMiddleware,
24 new HttpLink({
25 uri: ENV.GRAPHQL_API_URL,
26 fetch,
27 })
28 ),
29 cache: new InMemoryCache(),
30});
31registerPlugin(
32 'router',
33 'postsPagesPlugin',
34 async (route: string, config = {}): Promise<HandledRoute[]> => {
35 const {
36 data: { blogSamples },
37 } = await client.query({
38 query: gql`
39 query BlogSamplesQuery {
40 blogSamples {
41 id
42 title
43 }
44 }
45 `,
46 });
47 return Promise.resolve(
48 blogSamples.map((blog) => ({ route: `/p/${blog.id}` }))
49 );
50 }
51);
From scully.scully-strapi-tutorial.config.ts
(this file is automatically created when you run the scully schematic on your angular project, explained before), import our plugin for pages creation:
/scully.scully-strapi-tutorial.config.ts
1import './scully/plugins/postsPagesPlugin';
And add the following to the routes configuration:
/scully.scully-strapi-tutorial.config.ts
1'/p/:postId': {
2 type: 'postsPagesPlugin'
3}
Step 5: GraphQL configuration module
Let’s add module src/app/graphql.module.ts
dedicated to the graphql configuration our services are going to use. In this module, we have to tell the endpoint and token our generated services going to use. As you know, Strapi.io needs a valid token, so it can fetch data from the GraphQL endpoint, this configuration will be injected into the application:
/src/app/graphql.module.ts
1import { NgModule } from '@angular/core';
2import { APOLLO_OPTIONS } from 'apollo-angular';
3import {
4 ApolloClientOptions,
5 ApolloLink,
6 concat,
7 InMemoryCache,
8} from '@apollo/client/core';
9import { HttpLink } from 'apollo-angular/http';
10import { environment } from 'src/environments/environment';
11
12const uri = environment.GRAPHQL_API_URL;
13
14const authMiddleware = new ApolloLink((operation, forward) => {
15 // add the authorization to the headers
16 operation.setContext({
17 headers: {
18 authorization: `Bearer ${environment.GRAPHQL_API_TOKEN}`,
19 },
20 });
21
22 return forward(operation);
23});
24
25// <-- add the URL of the GraphQL server here
26export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> {
27 return {
28 link: concat(authMiddleware, httpLink.create({ uri })),
29 cache: new InMemoryCache(),
30 };
31}
32
33@NgModule({
34 providers: [
35 {
36 provide: APOLLO_OPTIONS,
37 useFactory: createApollo,
38 deps: [HttpLink],
39 },
40 ],
41})
42export class GraphQLModule {}
Step 6: Wrapping previously generated code
As good practice, we can put services as part of the module, we need a shared module src/app/shared/``shared.module.ts
and on that exact location create a service called src/app/shared/posts.service.ts
, which is going to be provided and exposed through that module, then we can inject the module into the main app module. Also, the shared module contains reusable stuff like navigation components. PostsService.ts
located in the previously mentioned path, will wrap the generated code we created before. It will have two methods, one for getting all the posts and another to retrieve post by id:
/src/app/shared/posts.service.ts
1import { Injectable } from '@angular/core';
2import { ApolloQueryResult } from '@apollo/client/core';
3import { isScullyGenerated, TransferStateService } from '@scullyio/ng-lib';
4import { tap } from 'rxjs/operators';
5import {
6 BlogSamplesGQL,
7 BlogSamplesQuery,
8 BlogSampleGQL,
9 BlogSampleQuery,
10} from './graphql.gen';
11
12@Injectable({
13 providedIn: 'root',
14})
15export class PostsService {
16 constructor(
17 private blogSamplesGQL: BlogSamplesGQL,
18 private blogSampleGQL: BlogSampleGQL,
19 private transferStateService: TransferStateService
20 ) {}
21
22 public getPosts() {
23 if (isScullyGenerated()) {
24 return this.transferStateService.getState<
25 ApolloQueryResult<BlogSamplesQuery>
26 >(`/posts`);
27 }
28 return this.blogSamplesGQL
29 .watch()
30 .valueChanges.pipe(
31 tap((data) =>
32 this.transferStateService.setState<
33 ApolloQueryResult<BlogSamplesQuery>
34 >(`/posts`, data)
35 )
36 );
37 }
38
39 public getPost(postId: string) {
40 if (isScullyGenerated()) {
41 return this.transferStateService.getState<
42 ApolloQueryResult<BlogSampleQuery>
43 >(`/p/${postId}`);
44 }
45 return this.blogSampleGQL
46 .watch({ id: postId })
47 .valueChanges.pipe(
48 tap((data) =>
49 this.transferStateService.setState<
50 ApolloQueryResult<BlogSampleQuery>
51 >(`/p/${postId}`, data)
52 )
53 );
54 }
55}
You might notice that we are checking isScullyGenerated()
, to avoid calling the API twice, and then we fetch the cached data via transferStateService
. This transferStateService
lets you store data with a key, and that’s why it makes retrieving post by id look so simple. The data persistence happens during build time, and can be passed to the templates to generate static markup for page http response, essential for SEO. Later on, during client-side navigation, it is not needed to call API again.
Step 7: First run and static site generation locally
Ok, almost there let’s verify we have scripts in place located on package.json
/package.json
1"ng": "ng",
2"start": "npm run env && ng serve",
3"build": "npm run env && ng build",
4"test": "ng test",
5"lint": "ng lint",
6"e2e": "ng e2e",
7"scully": "scully",
8"scully:serve": "scully serve",
9"env": "node env-task.js",
10"generate": "graphql-codegen -r dotenv/config",
11"ci": "npm run build && npm run scully"
Added env
in order to generate env.ts
file based on environment variables and be able to pass them from Netlify dashboard. But also ci
script for Netlify for first build angular and then build pages with Scully.
At the end, if you run for example, npm run ci
, static pages will be generated under dist/static
folder. If you set, for example, the page title or any metadata, you can see the static page containing this data already. And you should see an output like this at the very bottom if everything worked fine:
For local development purposes, it is fine to run as a regular Angular app, with npm start
, which allows us the hot reloading.
Netlify provides an excellent way to deploy and run a static site, that’s why I picked this one, also is quite flexible and easy to use, of course, provides a free tier for personal sites or small business.
Step 1: Account Creation and log in In this step, I will show you how to deploy your site on a high-performance platform such as Netlify. First, we need to create an account on Netlify and log in, which is really simple. You can authenticate using your GitHub account or regular method such as email/password. There is a starter plan, which is a free tier plan, but this should work as expected for our purpose. ****
Step 2: Create Site from Git
I will proceed to create a new site from git
, and select github
, the place where the code is located, as the provider.
We have to click give access to Netlify from Github, to allow it to access and pull the repo, later to be built. If your repo does not appear in the list, we can click
Configure the Netlify app on Github. It will open Github permissions and allow to give access to all repos to Netlify or assign specific repo permission. I recommend the latter and hit
Save`, on Netlify now repo should appear. We can select and proceed.
Step 3: Adjust settings
Almost there, Netlify will ask for some information that we have to fill correctly, such as:
default: main
)npm run ci
dist/static
where static generated pages are placed after build.CMS_BASE_URL
, contains the base URL of Strapi, e.g https://your-strapi-instance-url
, we are using this one to resolve images paths, the graphql endpoint GRAPHQL_API_URL
which is similar to base but containing the full path, in my case was https://your-strapi-instance-url/graphql
and finally the token GRAPHQL_API_TOKEN
.Step 4: First deploy and try your brand new site
Click continue, and Netlify should start triggering the first build. At this point, we could see the build logs and monitor how the build is going on.
When the build finishes, something similar to the previous image will show up on logs. After it ends, we can look at the deployed site by clicking the Production
link or Preview Deploy
, only to clarify, the second one is more useful when you are building other than main
branch, e.g. a pull request from another branch, that is useful, the PR reviews can see what changes the contributor is doing and how it looks before going straight to production deployment.
And here we are, our site is ready deployed on production ( homework for you: set a domain for this, piece of cake ah ?? 😅 ).
This step is optional, but I encourage you to try because it is beneficial and saves time, especially for those content editors pushing content quite often.
For this, I will show how to use Strapi webhooks, so every time a post is published or unpublished, Netlify auto builds pages and reflect this to the production site.
Step 1: Build hook from Netlify
First, we need to create a build hook from Netlify, go to Site Settings
then to Build & deploy
and scroll down to Build hook
, give a name, select a proper branch. Once saved, it will create a URL that we can set from Strapi webhooks, so please keep this URL for later.
Step 2: Set the webhook from Strapi
Let’s login to Strapi as an admin user, go to Settings > Webhooks
and click Add new webhook
, give it a name, assign proper events, and of course, put in the url that Netlify generated for the build hook Strapi is going to trigger.
When we save, Strapi gives the option to click Trigger
manually and verify from Netlify, that it runs correctly. And yes, it does, awesome !!
Step 3: Create and publish new posts from Strapi and verify automatic build from Netlify Now is the real test, let’s proceed to create a couple of new Posts from Strapi and publish them to verify the hook triggers automatically based on published events.
And here were are, our two new posts generated:
I hope you liked this, and I tried to keep the App
part summarized to focus your attention on Strapi.io features, which is a great CMS, with plenty of features compared to competitors,
You have seen that we installed GraphQL as a plugin, meaning that it is very extensible and configurable. However, we had to configure additional users and roles, so the data can be consumed from GraphQL. I loved this feature, which means Strapi.io has a massive wall of security in front, not exposing and compromising your content OOTB.
If you looked at the code further, we made the app configurable through environment variables, this is possible with dotenv, you can check further the package.json
that contains a script to provide these values on build time, allowing you to avoid pushing tokens into the repo, and passing only from the platform to the app in build time (env variables through Vercel, Netlify, etc).
Remember, please fork source code here and pull your fork.
Thanks, please comment if you any questions or issues you want to discuss.
Get started with Strapi by creating a project using a starter or trying our live demo. Also, consult our forum if you have any questions. We will be there to help you.
See you soon!Alejandro is a UI Engineer from Costa Rica, and proud father of 2 that loves music, UX, food and cooking! He talks about React and JavaScript.