dot CMS

Building Content Listing Pages with Search in Angular

Building Content Listing Pages with Search in Angular
Nicolas

Nicolas Molina

Senior Software Developer

Share this article on:

Creating dynamic content-listing pages with robust search and filtering capabilities is a common requirement for modern websites. This is especially true for blogs, e-commerce sites, or knowledge bases. By leveraging the dotCMS Angular SDK, you can efficiently build a powerful blog listing page that fetches, filters, and displays content from your dotCMS instance.

In this article, we'll walk through the process of building a dynamic blog listing page, similar to the one shown in the provided screenshot. We'll explore how to use the dotCMS Angular SDK to manage content queries, implement search functionality, and update the URL to reflect the user's search query.

Before you begin, ensure that you have version latest of angular installed, such as via :

npm install -g @angular/cli@latest

1. Project Setup

Create a New Angular Project

# Create a new Angular project
ng new my-app --style=css --ssr=false --zoneless=true

# Enter in your folder app
cd my-app

# Install dotCMS dependencies
npm i @dotcms/angular @dotcms/client @dotcms/uve @dotcms/types

# Install Tailwind CSS
npm install tailwindcss @tailwindcss/postcss postcss --force

For proper Tailwind CSS setup, please refer to the official Tailwind CSS documentation.

Now run the development server:

ng serve

The application will be running at http://localhost:4200.

screely-1758121434875.png

2. Configure dotCMS

Now we configure the connection between Angular and the dotCMS instance. To do this, add provideDotCMSClient to your application configuration:

// src/app/app.config.ts

import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

// 👇 dotcms imports
import { provideDotCMSClient } from '@dotcms/angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZonelessChangeDetection(),
    provideRouter(routes),
    provideClientHydration(withEventReplay()),
    // 👇 Here provideDotCMSClient
    provideDotCMSClient({
      dotcmsUrl: 'http://localhost:8080',
      authToken: 'your-api-key'
    }),
  ]
};

To get an API token in dotCMS, refer to the REST API Authentication documentation.

3. Create Basic Page Structure

To create the home page, use the Angular CLI command ng g c pages/homePage. This will generate the necessary files.

$ ng g c pages/homePage

CREATE src/app/pages/home-page/home-page.css
CREATE src/app/pages/home-page/home-page.spec.ts
CREATE src/app/pages/home-page/home-page.ts
CREATE src/app/pages/home-page/home-page.html

Efficient image management is vital for web performance, particularly in applications utilizing dynamic content. Therefore, on the HomePage, we will use the Angular NgOptimizedImage directive for proper image display.

For more details on image optimization, refer to Why is Image Optimization Important?.

// src/app/pages/home-page/home-page.ts
import { Component } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-home-page',
  imports: [NgOptimizedImage],
  templateUrl: './home-page.html',
  styleUrl: './home-page.css'
})
export class HomePage {

}

Next, we'll use Tailwind CSS to create a basic layout for your home-page.html.

// src/app/pages/home-page/home-page.html

<div class="flex flex-col gap-6 min-h-screen bg-slate-50">
  <main class="container mx-auto px-4 py-8">
    <div class="flex flex-col gap-4 mb-8">
      <h1 class="text-4xl font-bold text-center">Travel Blog</h1>
      <p class="text-gray-600 text-center">
        Get inspired to experience the world. Our writers will give you their
        first-hand stories and recommendations that will inspire, excite you,
        and help you make the best decisions for planning your next adventure.
      </p>
    </div>

    <!-- Grid -->
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      <!-- Article Card -->
      <div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 relative flex flex-col h-full">
        <div class="w-full h-48 relative">
          <img
            fill="true"
            class="w-full h-full object-cover"
            ngSrc="https://placehold.co/600x400"
            alt="Blog Image"
          />
        </div>
        <div class="p-4">
          <h3 class="text-lg font-semibold">Example Title</h3>
          <p class="text-gray-600">Example Teaser</p>
        </div>
      </div>
    </div>
  </main>
</div>

Afterward, add the home page to your routes in /src/app/app.routes.ts.

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomePage } from './pages/home-page/home-page';

export const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomePage },
];

To display the home page and other routes correctly, we'll quickly edit /src/app/app.ts to include the <router-outlet />.

// src/app/app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `<router-outlet />`,
  styleUrl: './app.css'
})
export class App {}

With these changes, go to http://localhost:4200/home to render the home page.

localhost_4200_home(MacBook Pro 14_).png

4. Create your content type

Next, we will create a new content type in your dotCMS instance to organize this new content.

localhost_8080_dotAdmin_(MacBook Pro 14_).png

The content type will be named "My Content."

localhost_8080_dotAdmin_(MacBook Pro 14_) (1).png

Within this content type, we will add four fields:

  • A text field named 'Title'

  • A textarea field named ‘Teaser’

  • A block editor field named 'Content'

  • An image field named 'Cover'

Make sure the “System Indexed” box is checked on any fields you want to be searchable.

localhost_8080_dotAdmin_(MacBook Pro 14_) (5).png

Now we’ll create a single contentlet according to the schema.

localhost_8080_dotAdmin_(MacBook Pro 14_) (4).png

You can now use the GraphQL tool to create a query based on your content. For example:

{
  search(query: "+contenttype:MyContent +live+true") {
    ... on MyContent {
      inode
      title
      teaser
      cover {
        fileName
        sortOrder
        description
      }
    }
  }
}
localhost_8080_dotAdmin_(MacBook Pro 14_) (7).png

5. Fetch and Display Content

To properly define the type for your contentlet in your headless app, navigate to your home page folder and create a new file named model.ts. In this file, you will define the necessary properties.

// src/app/pages/home-page/model.ts

import { DotCMSBasicContentlet, BlockEditorContent } from '@dotcms/types';

export interface MyContent extends DotCMSBasicContentlet {
  inode: string;
  title: string;
  teaser: string;
  cover: {
    fileName: string;
    sortOrder: number;
    description: string;
  };
  content: BlockEditorContent;
}

To retrieve content, inject the DotCMSClient into your home-page.ts file. You can then use this client to create queries for your content. Next, create a method called fetchContent that utilizes the client to retrieve all content and save this data in an Angular signal.

// src/app/pages/home-page/home-page.ts

import { Component, inject, signal, OnInit } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
import { DotCMSClient } from '@dotcms/angular';

import { MyContent } from './model';

@Component({
  selector: 'app-home-page',
  imports: [NgOptimizedImage],
  templateUrl: './home-page.html',
  styleUrl: './home-page.css'
})
export class HomePage implements OnInit{
  #client = inject(DotCMSClient);
  items = signal<MyContent[]>([]);

  ngOnInit() {
    this.fetchContent();
  }

  async fetchContent() {
    const response = await this.#client.content
      .getCollection('MyContent')
      .limit(10);
    this.items.set(response.contentlets as MyContent[]);
  }

}

You can now iterate through this list within your template to render the content.

<!-- // src/app/pages/home-page/home-page.html -->

<div class="flex flex-col gap-6 min-h-screen bg-slate-50">
  <main class="container mx-auto px-4 py-8">
    <div class="flex flex-col gap-4 mb-8">
      <h1 class="text-4xl font-bold text-center">Travel Blog</h1>
      <p class="text-gray-600 text-center">
        Get inspired to experience the world. Our writers will give you their
        first-hand stories and recommendations that will inspire, excite you,
        and help you make the best decisions for planning your next adventure.
      </p>
    </div>

  <!-- Grid --><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <!-- Article Card -->
  @for (item of items(); track item.identifier) {
        <div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 relative flex flex-col h-full">
          <div class="w-full h-48 relative">
            <img
              fill="true"
              class="w-full h-full object-cover"
              [ngSrc]="item.inode"
              alt="Blog Image"
            />
          </div>
          <div class="p-4">
            <h3 class="text-lg font-semibold">{{ item.title }}</h3>
            <p class="text-gray-600">{{ item.teaser }}</p>
          </div>
        </div>
  }
</div>

Currently, navigating to http://localhost:4200/home displays a single content item. However, the associated image is not rendering correctly.

localhost_4200_home(MacBook Pro 14_) (1).png

To address this, an additional provider needs to be included in your Angular application's configuration. This will enable the proper display of your assets using provideDotCMSImageLoader.

// src/app/app.config.ts

import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

// 👇 dotcms imports
import { provideDotCMSClient, provideDotCMSImageLoader } from '@dotcms/angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZonelessChangeDetection(),
    provideRouter(routes),
    provideClientHydration(withEventReplay()),
    provideDotCMSClient({
      dotcmsUrl: 'http://localhost:8080',
      authToken: 'your-api-key'
    }),
    // 👇 Here provideDotCMSImageLoader
    provideDotCMSImageLoader('http://localhost:8080'),
  ]
};

The image now displays correctly after this change, viewable at http://localhost:4200/home.

localhost_4200_home(MacBook Pro 14_) (2).png

If you create more content in dotCMS, similar to this:

localhost_8080_dotAdmin_(MacBook Pro 14_) (8).png

This creates an excellent content grid.

localhost_4200_home(MacBook Pro 14_) (3).png

6. Creating a Reusable Search Component

To handle user input for the search, we'll build a separate, reusable component called SearchComponent. This component will use a reactive form and RxJS to efficiently manage the input and emit the search query only when the user has stopped typing. To create a search component use the following command:

$ ng g c components/search
CREATE src/app/components/search/search.css
CREATE src/app/components/search/search.spec.ts
CREATE src/app/components/search/search.ts
CREATE src/app/components/search/search.html

This component will manage input logic, receiving data and emitting an output to signal search actions.

// src/app/components/search/search.ts

import { Component, effect, input, output } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-search',
  templateUrl: './search.html',
  imports: [ReactiveFormsModule],
})
export class Search {
  query = input<string>('');
  searchQueryChange = output<string>();

  inputControl = new FormControl<string>('', { nonNullable: true });

  constructor() {
    this.handleSearchQueryChange();
    effect(() => {
      const query = this.query();
      if (query) {
        this.inputControl.setValue(query);
      }
    });
  }

  handleSearchQueryChange(): void {
    this.inputControl.valueChanges
      .pipe(debounceTime(500), distinctUntilChanged(), takeUntilDestroyed())
      .subscribe((value) => {
        this.searchQueryChange.emit(value);
      });
  }
}

The template should be structured as follows:

//src/app/components/search/search.html

<div class="mb-8">
  <div class="relative">
    <div
      class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"
    >
      <svg
        class="w-4 h-4 text-gray-500"
        xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="2"
          d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
        />
      </svg>
    </div>
    <input
      type="search"
      class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-white focus:ring-blue-500 focus:border-blue-500 outline-hidden"
      placeholder="Search blogs..."
      [formControl]="inputControl"
    />
  </div>
</div>

Now we can Integrate Search with Home Page in your home page import the Search and create a method called onSearch. Change to handle the search logic.

//src/app/pages/home-page/home-page.ts

import { Component, inject, signal, OnInit } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
import { DotCMSClient } from '@dotcms/angular';
import { MyContent } from './model';
import { Search } from '../../components/search/search';

@Component({
  selector: 'app-home-page',
  imports: [NgOptimizedImage, Search],
  templateUrl: './home-page.html',
  styleUrl: './home-page.css'
})
export class HomePage implements OnInit{
  #client = inject(DotCMSClient);
  items = signal<MyContent[]>([]);
  ngOnInit() {
    this.fetchContent();
  }
  async fetchContent() {
    const response = await this.#client.content
      .getCollection('MyContent')
      .limit(10);
    this.items.set(response.contentlets as MyContent[]);
  }
  onSearch(searchTerm: string): void {
    // Implement search logic here
    console.log('Search term:', searchTerm);
  }
}

Include the search component in your home template before the Blog Grid:

// src/app/pages/home/home.component.html

<!-- Search Component -->
<app-search (searchQueryChange)="onSearch($event)" />

<!-- Grid -->
...

The search component is now visible on the Home Page, accessible at http://localhost:4200/home.

localhost_4200_home(MacBook Pro 14_) (3).png

7. Implementing the Search Logic in the Home Component

Now, let's add the search logic to the HomePage. This method will handle the search term received from the Search component and perform the content filtering.

When a user types in a search query, our component will first attempt to perform a server-side search using the dotCMS client. This is the most efficient method as it leverages the platform's native search capabilities. The query will look for blog titles that start with the user's search term.

However, a robust application also needs a fallback method. If the server-side search fails for any reason (e.g., a network error or a misconfigured API), our catch block will execute. In this case, we'll perform a client-side filter on the blogs we've already fetched. This ensures the user still sees a filtered list of blogs without a complete failure, providing a better and more resilient user experience.

// src/app/pages/home/home-page.ts


@Component(...)
export class HomePage implements OnInit {
  // ... existing code ...
  
  async onSearch(searchTerm: string) {
    try {
      if (searchTerm === '') {
        await this.fetchContent();
        return;
      }

      const response = await this.#client.content
        .getCollection('MyContent')
        .limit(10)
        .query((qb) => qb.field('title').equals(`${searchTerm}*`));

        this.items.set(response.contentlets as MyContent[]);
    } catch (error) {
      const items = this.items();
      const filteredResults = items.filter((item) =>
        item.title.toLowerCase().startsWith(searchTerm.toLowerCase()),
      );
      this.items.set(filteredResults);
    }
  }
}

Now if you try the search component you can see the search working! (Remember: You’ll need to have set the searchable fields to “System Indexed” for this to succeed. If you configure the fields as indexed after the content has already been created, you may have to reindex before they will be searchable.)

Summary

In this comprehensive guide, we've successfully built a dynamic blog listing page with search functionality using the dotCMS Angular SDK. Here's what we accomplished:

  1. Project Setup: Created a new Angular project and installed necessary dependencies

  2. dotCMS Configuration: Set up the dotCMS client and image loader providers

  3. Component Structure: Built a home component with proper routing

  4. Content Models: Defined TypeScript interfaces for blog content

  5. Data Fetching: Implemented page content retrieval with GraphQL queries

  6. Search Component: Created a reusable search component with debounced input

  7. Search Logic: Implemented server-side and client-side search functionality

  8. Responsive UI: Built a beautiful, responsive blog grid layout

The application now provides a seamless user experience with:

  • Dynamic content loading from dotCMS

  • Real-time search with debounced input

  • Responsive grid layout for blog posts

  • Error handling and loading states

  • Clean, maintainable code structure

This foundation can be extended with additional features such as pagination, advanced filtering, sorting options, and more sophisticated search algorithms.