Hey Angular developers! 🅰️ Most of the time, we find ourselves in an endless cycle of updating hard-coded content, making code changes, rebuilding our apps, and redeploying just to fix a typo or update a small description, which is not efficient because there's a smarter way to go about it. In this guide, I'll walk you through how to integrate Sanity CMS, with Angular.

Pro tip: Sanity content platform lets your marketing team edit content directly on the application while you focus on building great features.

Why Sanity And Angular?

  • Real-time content updates without app redeploys
  • Structured content modeling for consistent data
  • TypeScript-first developer experience
  • Image optimization built-in
  • Free tier perfect for getting started

Prerequisites

Before we dive in, make sure you've got:

  • An Angular project
  • Node.js v16+ installed
  • A Sanity.io account (free tier works)

Section 1: Setting Up Sanity Studio

Installation

Run the command npm install -g @sanity/cli to install Sanity CLI globally. After successful installation, run the command sanity init to initialise a new sanity project

Setting Up Your Content Structure

When running sanity init, you'll encounter an important choice:

? Select project template
❯ Clean project with no predefined schemas
  Blog (schema)
  E-commerce (schema)
  Portfolio (schema)

For this tutorial, select Blog (schema) because:

  • Provides ready-to-use content models
  • Demonstrates Sanity's core concepts
  • Lets us focus on Angular integration

The blog schema includes:

  • Posts with titles, slugs, and rich text
  • Author management
  • Category system

This is perfect for learning how Sanity structures content before creating custom schemas.

For Experienced Users
Choose Clean project with no predefined schemas if you:

  • Need complete control
  • Have existing content models
  • Want to build from scratch

If you’re in the sanity project directory, run:

sanity dev

Then, open your browser and go to:

http://localhost:3333

Expected Output:

If everything goes as planned, the http://localhost:3333 will show the page below:

Sanity Success Page

The view shows a page for you to log in to your sanity studio

After a successful login based on the provider chosen, if you selected the Blog (Schema) as the project template, below is how the structure is going to look:

Sanity Studio Structure

The view shows the structure for the blog schema

Section 2: Configuring Sanity In Angular

Now that our Sanity Studio is running, it’s time to configure Sanity Client in the Angular app.

Step 1: Install Required Packages

We’ll need a few npm packages to fetch and work with data from Sanity:

npm install @sanity/client

Click on the user profile section on the http://localhost:3333

Sanity profile Section

From the displayed dropdown, select the Manage Project to navigate to the Sanity Dashboard

Sanity-Manage-Project

In the Sanity Dashboard below, we can find the projectId and can manage datasets, tokens, and collaborators. We'll need this info to configure the Angular client.

Sanity Dashboard

The image above is a general overview of the Sanity Dashboard

Step 2: Set Up Sanity Client in Angular

Create a new directory, e.g., config and a new file inside the config app directory, to handle the setup for sanity in Angular.

Here is a simplified structure:

src/app/config/sanity-client.ts

Then, add the following code details:

// src/app/config/sanity-client.ts
import sanityClient from '@sanity/client';
export const client = sanityClient({
  projectId: 'your_project_id',
  dataset: 'your-dataset-title',     
  useCdn: true,                 // `false` if you want fresh data
  apiVersion: '2023-01-01',     // Use a UTC date string
});

Placing Sanity config in its file keeps things modular and reusable, especially if we need to call the client from different parts of the Angular app (services, components, etc)

Section 3: Creating Sanity GROQ Queries In Angular

Now that we’ve set up Sanity Studio and configured the Angular app to connect to it, it’s time to define the actual queries we'll use to fetch content.

With GROQ, we can:

  • Filter documents (*[_type == "post"])
  • Fetch nested and referenced fields (author->name)
  • Sort, limit, and slice data
  • Shape your responses exactly how you want

To keep our project clean and maintainable, let’s create a dedicated directory for all our GROQ queries.
Here's a simplified structure

src/app/queries/posts.groq.ts

Create the file and paste in the following query to get all posts:

// src/app/queries/posts.groq.ts

export const getAllPostsQuery = `
*[_type == "post"]{
  _id,
  title,
  slug,
  mainImage {
    asset->{
      url
    }
  },
  author->{
    name,
    image{
    asset->{
       url
      }
    }
  },
    categories[]->{
      title,
      description
    },
  publishedAt,
  body[0]
} | order(publishedAt desc)
`;

In the query above,

  • [_type == "post"] – Fetch all documents of type post
  • { ... } – Project only the fields we want: title, slug, mainImage, etc.
  • mainImage.asset->url – Traverse the image reference to get its url
  • author->name – Follow the reference to the author and grab their name
  • body[0] – Just grab the first block of the body (optional for previews)
  • | order(publishedAt desc) – Sort posts from newest to oldest

Let's add another query in the src/app/queries/posts.groq.ts file to get full post details by slug:

export const getPostBySlugQuery = (slug: string) => `
*[_type == "post" && slug.current == "${slug}"][0]{
  _id,
  title,
  slug,
  publishedAt,
  mainImage {
    asset->{
      url
    }
  },
  author->{
    name,
    bio,
    image {
      asset->{
        url
      }
    }
  },
  categories[]->{
    title
  },
  body
}
`;

In the slug query above,

  • [_type == "post" && slug.current == "${slug}"] – Find a post where the slug matches the one passed in
  • [0] – Get just the first (and hopefully only) match
  • mainImage.asset->url – Resolve the image reference to its URL
  • author-> – Expand author details to get both name and profile image
  • categories[]-> – Get titles for all linked categories
  • body – Get the full post content (use this for rendering detail view)

Add Sample Data to Sanity

Before jumping into building the Angular service, let’s add some sample data to our Sanity Studio so we actually have something to display.

  1. Go to http://localhost:3333 to access the Sanity Studio.
  2. Click the "Category" tab and add a few categories:

    • Programming concepts
    • CMS & Headless
    • TypeScript
    • Angular
  3. Then move to the "Author" tab and add authors:

    • Kingsley Amankwah – Tech Writer with a passion for clean code
    • Angelina Jolie – Full Stack Dev & Coffee Enthusiast
    • Iddris Alba – Dev Advocate @ AngularVerse
    • Jason Stathan – GDE Dev Advocate @ Google
    • Tommy Shelby – NgRx Co-Founder
  4. Finally, go to the "Post" tab and add posts:

“Building a Blog with Sanity and Angular from Scratch”

  • Author: Iddris Alba
  • Categories: Angular, CMS & Headless

“Creating SEO-Friendly Angular Apps with SSR and Sanity”

  • Author: Iddris Alba
  • Categories: Angular, Programming Concepts, CMS & Headless

“Why Angular + Sanity CMS is a Power Combo”

  • Author: Jason Stathan
  • Categories: Programming Concepts, Angular, CMS & Headless

“TypeScript Tips for Clean Angular Code”

  • Author: Tommy Shelby
  • Categories: Angular, TypeScript

“How to Handle Rich Text from Sanity in Angular Without Packages”

  • Author: Kingsley Amankwah
  • Categories: TypeScript, Angular, CMS & Headless

“Top 5 CMS Choices for Angular Developers in 2025”

  • Author: Angelina Jolie
  • Categories: CMS & Headless

Here's a simplified image of how to add a Category:

Sanity-Blog-Category-Addition

Now that we have data in our Sanity Studio, we’re ready to create a service in Angular that pulls this content and renders it in the UI.

Section 4: Creating a Sanity Service in Angular

Now that our Sanity client is configured, let’s create an Angular service to fetch content from Sanity. This service will handle communication with the Sanity API using the GROQ queries we defined earlier.

Step 1: Generate the Sanity Service

From your terminal, generate a new service inside the sanity directory:

ng generate service services/sanity

This will create two files: sanity.service.ts and sanity.service.spec.ts in the src/app/services directory.

Step 2: Set Up the Service to Use the Sanity Client

Open the newly created sanity.service.ts file and update it as follows:

//src/app/services/sanity.service.ts
import { Injectable } from '@angular/core';
import { client } from '../config/sanity-client';
import { getAllPostsQuery, getPostBySlugQuery } from '../queries/posts.groq';
import { Post } from '../models/post.model';

@Injectable({
  providedIn: 'root',
})
export class SanityService {
  async getPost() {
    try {
      return await client.fetch(getAllPostsQuery);
    } catch (error) {
      console.log(error);
      throw error;
    }
  }

  async getPostBySlug(slug: string): Promise {
    try {
      const query = getPostBySlugQuery(slug);
      return await client.fetch(query);
    } catch (error) {
      console.error(`Error fetching post with slug "${slug}":`, error);
      throw error;
    }
  }
}

Section 5: Displaying Content from Sanity

Now that we’ve created our Angular service, let’s proceed to generate components that will display the content fetched from Sanity.

Step 1: Generate the Post Component

From your terminal, generate a new component inside the directory:

ng generate component post

This will create:

src/app/post

Which will contain the component class, its template, and its styles.

Inside src/app/post/post.component.ts, update the component to fetch data from Sanity:

export class PostsComponent implements OnInit {
  private readonly sanityService = inject(SanityService);
  private readonly router = inject(Router);

  protected posts = signal([]);


  ngOnInit() {
    this.loadPosts();
  }

  protected async loadPosts() {
    const fetchPosts = await this.sanityService.getPost();
    this.posts.set(fetchPosts);
      }

  protected navigateToPostBySlug(slug: string) {
    this.router.navigate(['post', slug]);
  }
}

The code above contains:

  • A method to fetch all posts from Sanity.
  • A method to navigate to a post's detail page using the slug.

Add the Component Template
Here’s a simple UI template to display the posts using TailwindCSS:

💡 Note: Sanity returns rich text content in a block format. To cleanly display this in the Angular templates (e.g., for the post body),
we can use a custom pipe like blockContentToText.

You can access the pipe implementation in the codebase here.

class="text-3xl font-bold mb-6 text-gray-800">Latest Posts
  class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
    @for (post of posts(); track post._id) {
    class="bg-white rounded-2xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
    >
      [src]="post.mainImage?.asset?.url"
        alt="{{ post.title }}"
        class="h-48 w-full object-cover"
      />

      class="p-5">
        class="text-xl font-semibold text-gray-800 mb-2">
          {{ post.title }}
        

        class="text-gray-600 text-sm mb-3">
          {{ post.body | blockContentToText : 100 }}
        

        class="flex flex-wrap gap-2 mt-4">
          @for (category of post.categories; track $index) {
          class="bg-gray-200 text-gray-800 text-xs px-3 py-1 rounded-full"
          >
            {{ category.title }}
          
          }
        

        class="flex items-center gap-2 mt-4">
          [src]="post.author.image?.asset?.url"
            alt="{{ post.author.name }}"
            class="w-8 h-8 rounded-full object-cover"
          />
          class="text-gray-700 text-sm">By {{ post.author.name }}

          (click)="navigateToPostBySlug(post.slug.current)"
            class="bg-blue-400 text-white text-sm ml-auto px-3 py-1 rounded-full cursor-pointer"
            >Read more...>
        
      
    
    }

With this setup the first error we will encounter is CORS ERROR
Below is an image showing how it will be displayed:

Sanity CORS ERROR

To fix that, go to the Sanity Dashboard and follow the instructions in the image below to fix the CORS ERROR:

Sanity CORS ERROR SOLUTION

After adding the development URL e.g (http://localhost:4200) to the allowed CORS origins, re-run the application. You should now see the content fetched from Sanity.

Here’s what the UI will look like after successful integration:
Sanity+Angular Integration

Note: This UI is styled with TailwindCSS , which helps achieve a clean, responsive layout.

Step 2: Generate the View Post Component

Now, let's proceed to fetch the content using the getPostBySlugQuery.

Run the following in your terminal:

ng generate component view-post

This will create:

src/app/view-post

Which will contain the component class, its template, and its styles.

In src/app/view-post/view-post.component.ts, use the code below to fetch the post by it's slug:

export class ViewPostComponent implements OnInit {
  private readonly sanityService = inject(SanityService);
  private readonly route = inject(ActivatedRoute);

  protected post = signal(null);

  ngOnInit() {
    this.fetchPostDetails();
  }

  protected async fetchPostDetails() {
    const slug = this.route.snapshot.paramMap.get('slug') ?? '';
    const response = await this.sanityService.getPostBySlug(slug);
    this.post.set(response);
  }
}

The code above contains a method to fetch post details by utilizing the activate route to get content slug

Inside the src/app/view-post/view-post.component.html, add this UI to display full post:

class="max-w-4xl mx-auto px-4 py-12">
  [src]="post()?.mainImage?.asset?.url"
    alt="{{ post()?.title }}"
    class="w-full h-96 object-cover rounded-xl shadow-md mb-8"
  />

  <!-- Title -->
  class="text-4xl font-bold text-gray-900 mb-4">
    {{ post()?.title }}
  


  class="flex items-center justify-between text-sm text-gray-500 mb-6">
    class="flex items-center gap-2">
      [src]="post()?.author?.image?.asset?.url"
        alt="{{ post()?.author?.name }}"
        class="w-8 h-8 rounded-full object-cover"
      />
      class="font-medium text-gray-700"
        >By {{ post()?.author?.name }}>
    
    {{ post()?.publishedAt | date : "longDate" }}
  

    class="flex flex-wrap gap-2 mt-4">
    @for (category of post()?.categories; track $index) {
    class="bg-gray-200 text-gray-800 text-xs px-3 py-1 rounded-full">
      {{ category.title }}
    
    }
  

  class="prose prose-lg max-w-none text-gray-800 leading-relaxed">
    class="text-gray-600 text-sm mb-3">
      {{ post()?.body | blockContentToText }}
    
  

  class="mt-12 border-t-gray-400 border-t-[0.2px] pt-6">
    class="text-lg font-semibold text-gray-800 mb-2">About the author
    class="flex items-center gap-4">
      [src]="post()?.author?.image?.asset?.url"
        alt="{{ post()?.author?.name }}"
        class="w-12 h-12 rounded-full object-cover"
      />
      
        class="font-medium">{{ post()?.author?.name }}
        class="text-sm text-gray-600">
          {{ post()?.author?.bio | blockContentToText }}

Below is an image of the content fetched using the getPostBySlugQuery:

Sanity+Angular+fetchbyslug

PS: As stated ealier, the UI has been styled using TailwindCSS hence the reason for the nice design

Tips When Writing Queries

  • Always start with [_type == "yourType"] (e.g., "post", "author")
  • Use {} to specify the fields you want back
  • Use -> to follow references
  • Use [0] to get the first result, or slice/limit ([0...5])

Conclusion

In this tutorial, we walked through the full process of integrating Sanity CMS with Angular, covering everything from setting up the Sanity Studio to displaying dynamic blog content in an Angular application. Here’s a quick recap of what we achieved:

  • Set up and configured Sanity CMS
  • Defined GROQ queries for fetching posts and post details
  • Created Angular services to interact with Sanity
  • Displayed content using Angular components
  • Styled everything beautifully using TailwindCSS
  • Handled CORS configuration for local development
  • Built a full blog-like experience with navigation to post detail pages

Whether you're building a blog, portfolio, or a content-driven application, this Sanity + Angular combo gives you the flexibility of a headless CMS with the power of Angular’s frontend ecosystem.

Access the Full Code on GitHub

The entire source code for this project is available on GitHub:

Access the Full Code Here
Don't forget to leave a star if you found it helpful!

Let's Connect!

If you have questions, feedback, or want to see more tutorials like this, feel free to reach out or follow me on my socials:

Twitter/X: @IAmKingsley001
LinkedIn: KingsleyAmankwah
GitHub: KingsleyAmankwah