Welcome to a two-part series where we will build out an Angular site that grabs data from Sanity.io, a headless CMS, using Netlify Functions and Build Hooks. Let me break it down a little more. In part 1 we will:
- Start with a pre-rendered Angular site template
- Setup up a headless CMS with Sanity.io
- Grab the data from the CMS using Netlify serverless function
In Part 2 of this series, we will:
- Add an Angular Service to handle the incoming data
- Create Angular components to display the data
- Setup a build hook to update the page when new data is added
This is what the page will look like:
⏩ Want to skip the reading and just “make it work”? Here’s is the project repo, or you can click the button below to deploy the site now!
Let’s get it started in here!
Pre-rendered Template Using Angular Universal
📓 There is a whole blog post on creating this template too!
In order to jump into grabbing data as soon as we can, we’ll use a template project that uses Angular Universal to pre-render a few pages for us. The project can either be cloned locally from the command line:
git clone https://github.com/tzmanics/angular-sanity
Or we can click the ‘Use this template’ button from the repo’s homepage.
Whichever approach we take we’ll need to install all the dependencies locally with npm. Change into the project directory and run:
npm install
The Name Game
With Angular projects, the name of the project is used all over the place. So we don’t have any confusion or any remnants of the project we will replace angular-universal-pre-render
with the new project name (in this case it’s angular-sanity
).
Instances of the project need changed in the following files:
angular.json
netlify.toml
package.json
server.ts
server.ts
src/app/app.component.spec.ts
src/app/app.component.ts
e2e/src/app.e2e-spec.ts
karma.conf.js
🐙 Here is the git commit where the project name has been changed.
The easiest approach is to find all instances of angular-universal-pre-render
and replace all with your text editor. There will be no bad side effects using this specific search & replace. Here is a link that shows you how to do it with VS Code.
Just to make sure everything is in order it’s good to serve the project locally by running the command
ng serve
Head over to http://localhost:4200/
to see the working project locally.
Deploying the Project to Netlify
We can now hook this site up to Netlify so whenever we push a new commit it will trigger a new deploy (CI/CD). Having it live we can see what our users will experience when they request the site. The template project already has a Netlify configuration file, netlify.toml
. We can just run a few commands using the Netlify CLI.
npm install netlify-cli -g
netlify login
netlify init
First, we install the Netlify CLI globally (if we don’t already have it installed) and log in (unless we’re already logged in). The netlify init
command will ask a few questions and set the command and publish settings with the project’s netlify.toml
file. This command will also trigger a deploy of the project. We can run netlify open
to get to the project dashboard, to see when the build has been published.
Once this is set up, we can commit the changes we made when we changed this project name.
git add .
git commit -m 'changes project name'
git push --set-upstream main
This push will also trigger a new build, so we’ll have the new information up on GitHub and live, yay!
Setting Up Sanity
Now it’s time to set up the Sanity.io instance locally. We can do this with the Sanity.io CLI first by installing the CLI tool then initializing a Sanity.io project:
npm install -g @sanity/cli
sanity init
When we run sanity init
it will make sure we’re logged in (and have an account) then it will step through prompts to set up a new project.
We’ll create a new project and name it backend
, say yes to the defaults, but use the Clean project with no redefined schemas
. Using the clean product will allow us to write custom schemas without too much overhead that may be confusing.
Change into the Sanity.io instance backend
folder and run sanity start
to start the UI up locally.
cd backend
sanity start
Then head to http://localhost:3333/
to see what we have to work with. It’s nothing. This is right because we haven’t added any schemas yet!
Deploying the Sanity.io Desk
Although we can host our Sanity.io instance on Netlify, I want to show the built-in way. From the terminal, we can run sanity deploy
and it will prompt for a name then set the live instance at https://.sanity.desk. Once made, this information can be found at the top of the project page at sanity.io.
Anytime we want to update the deployed version of the Sanity.io instance, we’ll need to run the sanity deploy
command again.
🐙 This is a good place to push the new code up, BUT make sure to add
backend/node_modules
&backend/dist
to the project’s.gitignore
file in the root directory!
Coding Out the Schemas
It’s time to make the custom schemas and update the main schema file. Sanity.io takes this information and uses it to model the information and also make the UI for the CMS.
📚 Learn more about Sanity.io schemas from their guides and their documentation.
For now, we will look at the code we need to add and why we’re adding each different schema.
Block Content
We want to be able to have a section for each product where we can write more about the product. The block content will allow for the person entering information to edit text, add bulleted lists, headers, and some format.
backend/schemas/blockContent.js
export default {
title: 'Block Content',
name: 'blockContent',
type: 'array',
of: [
{
title: 'Block',
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'H1', value: 'h1' },
{ title: 'H2', value: 'h2' },
{ title: 'H3', value: 'h3' },
{ title: 'H4', value: 'h4' },
{ title: 'Quote', value: 'blockQuote' },
],
lists: [{ title: 'Bullet', value: 'bullet' }],
marks: {
decorators: [
{ title: 'Strong', value: 'strong' },
{ title: 'Emphasis', value: 'em' },
],
annotations: [
{
title: 'URL',
name: 'link',
type: 'object',
fields: [
{
title: 'URL',
name: 'href',
type: 'url',
},
],
},
],
},
},
{
type: 'image',
options: { hotspot: true },
},
],
};
Category
Each product will have a category and also a parent category. This means that we can have a parent category like ‘Furniture’ but be able to drill in further with a category under ‘Furniture’ like ‘Chair’.
export default {
name: 'category',
title: 'Category',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
},
{
name: 'description',
title: 'Description',
type: 'text',
},
{
name: 'parents',
title: 'Parents',
type: 'array',
of: [
{
type: 'reference',
to: [{ type: 'category' }],
},
],
},
],
};
Product Variants
The product variant will allow us to add more detailed information to each product.
backend/schemas/productVariant.js
export default {
name: 'productVariant',
title: 'Product Variant',
type: 'object',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'price',
title: 'Price',
type: 'number',
},
{
name: 'sku',
title: 'SKU',
type: 'string',
},
{
name: 'images',
title: 'Images',
type: 'array',
of: [{ type: 'image' }],
},
],
};
Product
This is the main schema for the products we’re adding and it will reference other schemas we’ve made like category and product variant. Knowing that we can have this type of schema inception adds for a lot of possibilities for customizing how we enter data.
export default {
name: 'product',
title: 'Product',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
},
{
name: 'defaultProductVariant',
title: 'Default variant',
type: 'productVariant',
},
{
name: 'tags',
title: 'Tags',
type: 'array',
of: [{ type: 'string' }],
},
{
name: 'blurb',
title: 'Blurb',
type: 'string',
},
{
name: 'categories',
title: 'Categories',
type: 'array',
of: [
{
type: 'reference',
to: { type: 'category' },
},
],
},
{
name: 'body',
title: 'Body',
type: 'array',
of: [{ type: 'block' }],
},
],
preview: {
select: {
title: 'title',
},
},
};
One other thing to note about this schema is the preview
section at the bottom. This is what we will see in the Sanity.io UI when it showcases the list of products. Right now, we have the title but we can add an image, price, or any other information from `product` that would be handy to see.
Schema
This is where it all comes together. We’ll want to import all the new schemas we created and add them to the schema types.
// First, we must import the schema creator
import createSchema from 'part:@sanity/base/schema-creator'
// Then import schema types from any plugins that might expose them
import schemaTypes from 'all:part:@sanity/base/schema-type'
+ import blockContent from "./blockContent";
+ import category from "./category";
+ import product from "./product";
+ import productVariant from "./productVariant";
// Then we give our schema to the builder and provide the result to Sanity
export default createSchema({
// We name our schema
name: 'default',
// Then proceed to concatenate our document type
// to the ones provided by any plugins that are installed
+ types: schemaTypes.concat([blockContent, category, product, productVariant]),
})
We have all the schemas we need now! If we run sanity start
now we can see the UI that has been created and even enter some products of our own.
Importing Existing Data (optional)
We can add products now through the UI but we can also import data. I have data that I have entered, the only catch is the images are a part of the dataset in Sanity, so, unfortunately, they don’t import well. Please, feel free to add images to replace these. Image troubles aside, here is the command we can use to bring in exported Sanity.io data that matches the schemas.
In the terminal, we can run the command sanity dataset import
, pass in the URL to where the exported data lives and the name of the dataset we want to add it to. Because of the image importing situation, we’ll need to add --allow-assets-in-different-dataset
to the end of the command so it ignores that we’re adding images from another dataset.
sanity dataset import https://4cj83dm6.api.sanity.io/v1/data/export/production production --allow-assets-in-different-dataset
We can run sanity start
again to see the data added.
Adding Sanity.io Environment Variables
To connect to the CMS, we’ll need to use some project credentials: the project id, dataset, and a token. We can grab this information from the project’s dashboard. First, the project id is at the top of the page and we know we are using the ‘production’ data set. To get the Sanity.io token we’ll go to Settings/API/Tokens and click the ‘Add New Token’ button.
Then, we’ll name the token ‘functions’ and give it rights to write (which includes read, write, and delete data). When we click the ‘Add New Token’ button, we’ll get a token to copy.
Next, we’ll head back over to the project’s Netlify dashboard to enter these values. Go to Site settings/Build & deploy/Environment and click the Edit variables
button. We will add the following variables:
SANITY_TOKEN
= (the token we just created and copied)SANITY_DATASET
= productionSANITY_PROJECT_ID
= <your project id (e.g. kgu5d2ud)>
Finally, the Function!
We have everything in place to actually make a serverless function to grab the CMS data. Woohoo! Let’s get coding.
Function Setup & Test
Let’s start with a test function and see how we can test it locally. We will create a folder in the project’s root directory, then make a new file in there named getProducts.js
.
mkdir functions && touch getProducts.js
exports.handler = async () => {
return {
statusCode: 200.
body: 'ok',
};
};
Testing Locally
Through the Netlify CLI we can start up a local development environment by using the netlify dev
command. Before we run that, let’s set local build settings for command
and publish
for the dev environment in the netlify.toml
configuration file.
[build]
command = "npm run prerender"
functions = "./functions"
publish = "dist/angular-sanity/browser"
[dev]
command = "npm run start"
functions = "./functions"
publish = "src"
Now when we run netlify dev
, we can head to http://localhost:8888/.netlify/functions/getProduct
to see the output from the test function: ‘ok’. Riveting, I know.
📓 Here’s a blog post on getting started with Netlify Functions if you want to learn more about what’s going on here.
Fetching Sanity.io Data
We need a few libraries from Sanity.io to make sure the data is configured correctly.
npm install @sanity/client @sanity/image-url @sanity/block-content-to-html
Now we can add all the logic we need to the Netlify Function.
const sanityClient = require('@sanity/client');
const imageUrlBuilder = require('@sanity/image-url');
const blocksToHtml = require('@sanity/block-content-to-html');
// passing the env vars to Sanity.io
const sanity = sanityClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: process.env.SANITY_DATASET,
useCdn: true,
});
exports.handler = async () => {
// this query asks for all products in order of title ascending
const query = '*[_type=="product"] | order(title asc)';
const products = await sanity.fetch(query).then((results) => {
// then it iterates over each product
const allProducts = results.map((product) => {
// & assigns its properties to output
const output = {
id: product.slug.current,
name: product.title,
url: `${process.env.URL}/.netlify/functions/getProducts`,
price: product.defaultProductVariant.price,
description: product.blurb,
// this is where we use the Sanity.io library to make the text HTML
body: blocksToHtml({ blocks: product.body }),
};
// we want to make sure an image exists before we assign it
const image =
product.defaultProductVariant.images &&
product.defaultProductVariant.images.length > 0
? product.defaultProductVariant.images[0].asset._ref
: null;
if (image) {
// this is where we use the library to make a URL from the image records
output.image = imageUrlBuilder(sanity).image(image).url();
}
return output;
});
// this log lets us see that we're getting the projects
// we can delete this once we know it works
console.log(allProducts);
// now it will return all of the products and the properties requested
return allProducts;
});
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(products),
};
};
Live Logs & Results
Once we have our function live, where do we find the logs? Netlify has a dedicated section for the functions to make them easy to keep track of and manage. From the project dashboard, there is a ‘Functions’ tab where we can see all the functions listed that the project uses.
To deploy the updated function we can git add, commit, and push the changes. The new build will be triggered and if we go to https://angular-sanity.netlify.app/.netlify/functions/getProducts
we can see the output on the page and in our logs.
Functional Function Finita!
We have a template site set up, a customized instance of Sanity.io, and a function that’s grabbing our CMS data! We are so skilled! In part two of this tutorial, we’ll use add an Angular service and component to integrate this data into the site. Then we’ll set up a Build Hook so that we re-deploy the site with the newest data as soon as it’s entered. I hope to see you there and until then, happy coding 👩🏻💻
Resources for the Road
- [Part 2 of this series](coming soon!)
- Project Repo
- Sanity.io Documentation
- Netlify Functions
- Netlify Build Hooks
- Pre-rendering with Angular Universal