TLDR;
By using Angular Scully and Prisma we have a powerful ecosystem where we can leverage full type safety and easily connect to a database that contains data that will be injected into the components by Scully. After the Scully process, we can upload our app to a CDN and have a fully working SEO-friendly Angular app.
What is Prisma?
Prisma is an open-source ORM, it consists of three parts:
- Prisma Client: Auto-generated and type-safe query builder for Node.js & TypeScript
- Prisma Migrate: Migration system
- Prisma Studio: GUI to view and edit data in your database
Prisma Client can be used in any Node.js (supported versions) environment. The power of Prisma is that they have a good and powerful type safety solution when using it with Typescript, and by default supports a set of Easy to use querIes.
What is Scully
Scully is the static site generator for Angular projects looking to embrace the Jamstack.
It will use your application and will create a static index.html
for each of your pages/routes. Every index.html
will have the content already there, and this will make your application show instantly for the user. Also, this will make your application very SEO-friendly. On top of this, your SPA will still function as it did before.
How to set up an Angular app with Scully?
In this article, we are not going very deep into the setup of Angular and Scully. For a good reference, you can have a look at the repo. Below are the steps you can follow along:
First, letβs set up a new Angular app
npx ng new ng-scully --minimal
Second, letβs add Scully to the Angular application
ng add @scullyio/init
Scully schematics will do the following:
- Add Scully dependencies to
package.json
and install it - Import
ScullyLibModule
toAppModule
- Add
'zone.js/dist/task-tracking'
topolyfills.ts
- Add
scully.<project_name>.config.ts
to the root directory. This is Scully configuration file that we will utilize to configure Scully.
Now we have a setup that works with Angular, but we need to take one step more for this demo.
ng generate @scullyio/init:blog
The above command adds the blog modules' routes to the Angular application.
In addition, it creates a ./blog
folder for the blog's markdown files.
How to use Prisma with Scully?
Iβve made the choice to use postgress as my database, in combination with docker.
Below Iβm showing you my docker-compose file:
version: '3'
services:
postgres:
image: postgres
ports:
- "5432:5432"
restart: always
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
Now we only need to run it so that Prisma can connect to it.
docker-compose up -d
Now we can continue with Prisma, first we need to install Prisma
npm install prisma --save-dev
After installation we will run the init command as shown below:
npx prisma init
This init command will setup Prisma with the required files in the directory.
After this we need to change the .env
file with our database connection:
DATABASE_URL="postgresql://prisma:prisma@localhost:5432/mydb?schema=public"
Setup Prisma configuration
To use Prisma with Scully we first need to add the setup for Prisma.
As Prisma is an ORM for a database we need to tell Prisma what tables and/ or database it is connected to. This information is placed in the schema.prisma
file:
// This is your Prisma schema file,
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
name String?
email String? @unique
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
posts Post[]
@@map(name: "users")
}
Note: You're occasionally using
@map
and@@map
to to transform some field and model names to different column and table names in the underlying database.
This Prisma schema defines two models, each of which will map to a table in the underlying database: User
and Post
. Notice that there's also a relation (one-to-many) between the two models, via the author
field on Post
and the posts
field on User
.
Now that we have our schema defined, we need to create
our tables in our database. This can be done by running the following CLI command:
npx prisma db push
You should see the following output:
Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
Datasource "db": PostgreSQL database "mydb", schema "public" at "localhost:5432"
The database is already in sync with the Prisma schema.
β Generated Prisma Client (3.8.1 | library) to .\node_modules\@prisma\client in
75ms
As our database is ready to be used, letβs add some data. We are going to use Prisma Studio, this is an easy way to explore and manipulate the data. You can open up Prisma Studio by running
npx prisma studio
Create a Prisma Scully Plugin
As we now have an operating database and an ORM library (Prisma) we now can use all these parts to receive the data and use it in Scully. Letβs start by creating the first basis for our custom plugin.
import { PrismaClient } from "@prisma/client";
import {
HandledRoute,
logError,
registerPlugin,
RouteConfig,
} from "@scullyio/scully";
import { convertAndInjectContent } from "@scullyio/scully/src/lib/renderPlugins/content-render-utils/convertAndInjectContent";
export const prismaPlugin = "prismaPlugin";
const prisma = new PrismaClient();
const routerPlugin = async (route: string, config: RouteConfig) => {
// here we are looking up all posts
const data = await prisma.post.findMany({
// where the published property is true
where: { published: true },
// and we include the author
include: {
author: {
// and we only want the author's name
select: { name: true },
},
},
});
return Promise.resolve(
// let's loop over all posts
data.map((post) => {
// and return a new route for each post
const { id, title, published, author, content } = post;
return {
...config,
route: `/blog/${id}`,
data: {
id,
title,
published,
author: author.name,
content,
},
} as HandledRoute;
})
);
};
registerPlugin("router", prismaPlugin, routerPlugin);
async function prismaDomPlugin(dom: any, route: HandledRoute | undefined) {
if (!route) return dom;
try {
try {
// here we use the power of scully and use the filehandler to convert the content to html
return convertAndInjectContent(dom, route.data.content, "md", route);
} catch (e) {
logError(`Error during contentText rendering`);
console.error(e);
}
return dom;
} catch (e) {}
}
registerPlugin("postProcessByDom", prismaPlugin, prismaDomPlugin);
Letβs break this code down from the top.
import { PrismaClient } from '@prisma/client';
import { logError, registerPlugin, RouteConfig } from '@scullyio/scully';
//define our plugin name
export const prismaPlugin = 'prismaPlugin';
// setup our PrismaClient
const prisma = new PrismaClient();
// our router plugin
const routerPlugin = async (route: string, config: RouteConfig) => {
...
};
Now we are going to retrieve the posts
with the Prisma client. When all the data is gathered we will return new routes that will be used on our post-render step.
const prisma = new PrismaClient();
const routerPlugin = async (route: string, config: RouteConfig) => {
// here we are looking up all posts
const data = await prisma.post.findMany({
// where the published property is true
where: { published: true },
// and we include the author
include: {
author: {
// and we only want the author's name
select: { name: true },
},
},
});
return Promise.resolve(
// let's loop over all posts
data.map((post) => {
// and return a new route for each post
const { id, title, published, author, content } = post;
return {
...config,
route: `/blog/${id}`,
data: {
id,
title,
published,
author: author.name,
content,
},
} as HandledRoute;
})
);
};
The post-process plugin is used to transform the render HTML. In our custom plugin, we make use of the Scully system, the converAndInjectContent
function will look at the fileHandler
plugins, and if it finds an extension of a file type. In our case, it will look for the fileHandler
for markdown files. This plugin will transform our data coming from the database from markdown to HTML.
async function prismaDomPlugin(dom: any, route: HandledRoute | undefined) {
if (!route) return dom;
try {
try {
// here we use the power of scully and use the filehandler to convert the content to html
return convertAndInjectContent(dom, route.data.content, "md", route);
} catch (e) {
logError(`Error during contentText rendering`);
console.error(e);
}
return dom;
} catch (e) {}
}
Now that we have set up our plugin, we need to make one new change to our Scully configuration. We need to change the original blog route to use our custom plugin, first, we need to import our custom plugin
import { prismaPlugin } from "./scully/plugins/plugin";
Then we need to define our router and post process plugin to being used in our blog route.
routes: {
"/blog/:slug": {
type: prismaPlugin,
}
}
Finally, we are ready to run our Scully system to scan for new routes, run npx scully --scan
$ npx scully --scan
β new Angular build files imported
β Starting servers for project "asp-example"
β Started Angular distribution server on "http://localhost:1864/"
β Started Scully static server on "http://localhost:1668/"
β Scully Development Server is up and running
β Puppeteer is being launched
β Successfully scanned Angular app for routes
β Successfully added routes created from routePlugins
β Route list created in files:
".\src\assets\scully-routes.json",
"dist\static\assets\scully-routes.json",
"dist\asp-example\assets\scully-routes.json"
β Route "/blog" rendered into ".\dist\static\blog\index.html"
β Route "/home" rendered into ".\dist\static\home\index.html"
β Route "/" rendered into ".\dist\static\index.html"
β Route "/blog/1" rendered into ".\dist\static\blog\1\index.html"
Total time used 5.74 seconds
4 pages have been created
Rendering the pages took 2.99 seconds
That is 1.34 pages per second,
or 749 milliseconds for each page.
Finding routes in the angular app took 2.68 seconds
Pulling in route-data took 47 milliseconds
We now have our first page rendered with Angular, Scully, and Prisma.
Conclusion
With Prisma we have a powerful type of safety solution to connect to a database, combine this with the power of Scully we can easily create static pages from an Angular application and upload it to a CDN.