How to Integrate Your Headless App with the dotAI APIs

How to Integrate Your Headless App with the dotAI APIs
Author image

Arcadio Quintero

Frontend Developer

Share this article on:

dotAI integrates powerful AI capabilities directly into your dotCMS instance, enabling semantic searches, content generation, and more. It leverages OpenAI services and offers various endpoints for different AI functionalities.

In this post, we'll focus on two specific endpoints:

  • /api/v1/ai/search - For a semantic search that returns matching dotCMS content

  • /api/v1/ai/completions - a POST endpoint that returns both relevant dotCMS content (dotCMSResults) and an AI-generated response (openAiResponse) based on your query.

We'll build a travel information search component that lets users search content in two ways: a direct keyword-based semantic search or an enhanced search that generates an AI summary of the relevant content. Both methods draw from your existing dotCMS content, making your content more discoverable and useful in your headless application.

Here you can see a demonstration of the TravelBot component in action, showcasing both search types we'll implement in this tutorial:

80

Prerequisites

Before starting this integration, ensure you have:

  • A dotCMS instance with dotAI enabled

  • Access to dotCMS authentication token

  • A headless application (React/Next.js)

Introduction

Enhancing your headless application with AI capabilities can significantly improve user experience and content discoverability. In this guide, we'll explore how to implement a Travel Bot component that leverages dotCMS AI APIs to provide both conversational (AI Chat) and keyword-based search functionality. This dual-mode search interface demonstrates the power and flexibility of dotAI within your headless sites.

Step-by-Step Guide

1. Understanding the Example Project

Let's examine our official example that demonstrates how to build a headless application using Next.js integrated with dotCMS. This example showcases the best practices for implementing experiments in a real-world headless architecture:

# Clone the dotCMS repository 
git clone https://github.com/dotCMS/core.git 
cd core/examples/nextjs 
# Install dependencies 
npm install 
# Start the development 
server npm run dev

This example provides a foundation for our implementation. In this guide, we'll focus on explaining the key components and concepts for integrating dotAI in your headless application. The complete final implementation can be found in our GitHub repository [Compare], which includes all the components referenced in this tutorial.

2. Component Mapping Setup

A crucial step in integrating components with DotCMS is setting up the component mapping. This connects DotCMS content types to your React components:

// my-page.js - Simplified for clarity
"use client";

// Import components
import TravelBotComponent from "./content-types/TravelBotComponent";
// Other component imports...

import { DotcmsLayout } from "@dotcms/react";
// Other imports...

// Mapping of components to dotCMS content types
const componentsMap = {
  // Other components...
  DotaiHeadless: TravelBotComponent, // Our TravelBot is mapped as a DotaiHeadless ContentType
};

export function MyPage({ pageAsset, nav }) {
  // Implementation details omitted for brevity
  
  return (
    <div>
      <main>
        <DotLayoutComponent
          pageContext={{
            pageAsset,
            components: componentsMap, // This is where our component map is used
          }}
          // Other config options...
        />
      </main>
    </div>
  );
}

This mapping is essential because it tells the DotCMS layout engine which React component should render each content type. In our case, we've mapped our TravelBotComponent to be used as a DotaiHeadless content type in DotCMS.

Note on Configuration

The component mapping requires a matching ContentType in dotCMS. We've created a video demonstration showing how to set up the "DotaiHeadless" ContentType and parameterize your component. This allows you to configure aspects of your component directly from dotCMS without hardcoding values.

80

3.  Creating the Component Structure

Our implementation follows a clean, organized structure:

  1. TravelBotComponent.js - The main container component

  2. SearchInput.js - Handles user input

  3. SearchTypeSelector.js - Lets users switch between AI Chat and Keyword search modes

  4. SearchResults.js - Displays search results

Let's start by setting up the folder structure:

app/
components/
  content-types/
    travelBotComponent.js  # Main component
  travel-bot/              # Supporting components
    SearchInput.js
    SearchTypeSelector.js
    SearchResults.js

This structure separates the main component (travelBotComponent.js) from its supporting components in the travel-bot folder. This organization makes the code easier to maintain and understand.

4. Implementing the Main Component

The travelBotComponent.js serves as the core of our application, managing state and API calls. Let's focus on the most important parts:

import { useState } from "react";
import SearchInput from "../travel-bot/SearchInput";
import SearchTypeSelector from "../travel-bot/SearchTypeSelector";
import SearchResults from "../travel-bot/SearchResults";

/**
 * TravelBotComponent - A search interface that combines AI-powered and keyword search
 * for travel-related content.
 */
export default function TravelBotComponent({
  indexName,
  placeholderAiSearch,
  placeholderInputSearch,
}) {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [searchType, setSearchType] = useState("aiChat");
  const [answer, setAnswer] = useState("");

  /**
   * Makes requests to DotCMS AI endpoints
   */
  const makeDotCMSAIRequest = async (endpoint, customConfig = {}) => {
    // Base configuration common to both endpoints
    const baseConfig = {
      prompt: searchQuery,      // Search text
      threshold: 0.25,          // Relevance threshold (0-1)
      responseLengthTokens: 500, // Maximum response length
      indexName: indexName,    // DotCMS search index from props
    };

    const response = await fetch(
      `${process.env.NEXT_PUBLIC_DOTCMS_HOST}${endpoint}`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.NEXT_PUBLIC_DOTCMS_AUTH_TOKEN}`,
        },
        body: JSON.stringify({ ...baseConfig, ...customConfig }),
      }
    );
    return response;
  };

  const handleSearch = async () => {
    if (!searchQuery.trim()) return;

    setIsLoading(true);
    setAnswer("");
    setSearchResults([]);

    try {
      if (searchType === "keyword") {
        // Use the search endpoint for keyword search
        const response = await makeDotCMSAIRequest("api/v1/ai/search");
        const data = await response.json();
        setSearchResults(data.dotCMSResults || []);
      } else {
        // Use the completions endpoint for AI-enhanced search
        const response = await makeDotCMSAIRequest("api/v1/ai/completions");
        const data = await response.json();
        setSearchResults(data.dotCMSResults || []);

        if (data.openAiResponse?.choices?.[0]?.message?.content) {
          setAnswer(data.openAiResponse.choices[0].message.content);
        }
      }
    } catch (error) {
      console.error("Error searching:", error);
    } finally {
      setIsLoading(false);
    }
  };

  // ... other utility functions omitted for clarity

  return (
    <div className="max-w-3xl mx-auto p-6">
      <div className="min-h-[200px] flex flex-col">
        <SearchInput
          placeholderAiSearch={placeholderAiSearch}
          placeholderInputSearch={placeholderInputSearch}
          searchQuery={searchQuery}
          setSearchQuery={setSearchQuery}
          searchType={searchType}
          handleSearch={handleSearch}
          isLoading={isLoading}
        />

        <SearchTypeSelector
          searchType={searchType}
          setSearchType={setSearchType}
        />

        {/* UI rendering logic omitted for brevity */}
        {/* Displays loading indicator, answer summary, and search results */}
      </div>
    </div>
  );
}

5. Creating the Search Input Component

The SearchInput.js component handles user input and triggers the search:

export default function SearchInput({
  searchQuery,
  setSearchQuery,
  searchType,
  handleSearch,
  isLoading,
  placeholderAiSearch,
  placeholderInputSearch,
}) {
  const handleKeyDown = (e) => {
    if (e.key === "Enter") {
      handleSearch();
    }
  };

  return (
    <div className="flex gap-3">
      <input
        type="text"
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder={
          searchType === "aiChat" ? placeholderAiSearch : placeholderInputSearch
        }
        className="flex-1 p-3 border border-gray-300 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-200 outline-none transition-all"
      />
      <button
        onClick={handleSearch}
        disabled={isLoading}
        className="px-6 py-2 bg-blue-500 text-white font-medium rounded-lg hover:bg-blue-600 transition-colors disabled:bg-blue-400 disabled:cursor-not-allowed"
      >
        {searchType === "aiChat" ? "Ask AI" : "Search"}
      </button>
    </div>
  );
}

6. Implementing the Search Type Selector

The SearchTypeSelector.js component allows users to switch between search modes:

export default function SearchTypeSelector({ searchType, setSearchType }) {
  return (
    <div className="mt-4 flex items-center space-x-6">
      <span className="text-gray-600">Type:</span>
      <label className="flex items-center cursor-pointer">
        <input
          type="radio"
          name="searchType"
          value="aiChat"
          checked={searchType === "aiChat"}
          onChange={(e) => setSearchType(e.target.value)}
          className="w-4 h-4 text-blue-500 border-gray-300 focus:ring-blue-400"
        />
        <span className="ml-2 text-gray-600">AI Chat</span>
      </label>
      <label className="flex items-center cursor-pointer">
        <input
          type="radio"
          name="searchType"
          value="keyword"
          checked={searchType === "keyword"}
          onChange={(e) => setSearchType(e.target.value)}
          className="w-4 h-4 text-blue-500 border-gray-300 focus:ring-blue-400"
        />
        <span className="ml-2 text-gray-600">Keyword</span>
      </label>
    </div>
  );
}

7. Creating the Results Component

The SearchResults.js component displays the search results:

export default function SearchResults({ results }) {
  return (
    <div className="mt-8">
      <h2 className="text-lg font-semibold text-gray-800 mb-4">References</h2>
      <div className="space-y-4">
        {results.map((result, index) => (
          <div
            key={index}
            className="border border-gray-200 rounded-lg p-4 bg-white"
          >
            <a
              href={result.urlMap}
              className="text-orange-500 font-bold hover:underline"
            >
              {result.title}
            </a>
            <p className="text-gray-600 mt-2">{result.teaser}</p>
            <div className="mt-3 flex gap-2">
              <span className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-sm">
                {result.contentType}
              </span>
              {result.matches?.map((match, i) => (
                <span
                  key={i}
                  className="px-2 py-1 bg-blue-50 text-blue-600 rounded text-sm"
                >
                  Score: {Math.round((1 - match.distance) * 100)}%
                </span>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

8. Configuring dotAI in dotCMS

Before your Travel Bot can work properly, you need to configure dotAI in your dotCMS instance:

  1. Enable dotAI:

    • Go to Apps in your dotCMS admin panel

    • Find dotAI and make sure it's activated

  2. Create an AI Index:

    • Navigate to Dev Tools → dotAI → Manage Embeddings/Indexes

    • Create a new index named "travelBot" (matching the indexName in your code)

    • Configure which content types should be included in this index (e.g., "+contentType:blog")

    • Click "Build Index" to generate the index

  3. Generate an Authentication Token:

    • Go to Settings → Users →  {User} → API Access Tokens

      • Be sure the AI user account is appropriately (i.e., minimally) permissioned to accomplish its duties.

    • Create a new token with the appropriate permissions for AI endpoints

    • Use this token in your environment variables

How It Works Behind the Scenes

Understanding the Two dotAI Endpoints

Our application uses two different dotAI endpoints, each serving a specific purpose:

1. /api/v1/ai/search (Keyword Search)

The first endpoint provides traditional semantic search capabilities. It processes the user's query as a set of keywords and returns relevant content from your dotCMS instance.

Key features:

  • Fast, direct content retrieval

  • Ranks results by relevance

  • Returns matching content items with metadata

  • No conversational response generation

Example request to this endpoint:

{
  "prompt": "beach resorts Mediterranean",
  "threshold": 0.25,
  "responseLengthTokens": 500,
  "indexName": "travelBot"
}

This endpoint is ideal for scenarios where users know exactly what they're looking for and want to browse multiple matching content items.

2. /api/v1/ai/completions (AI Chat)

The second endpoint provides a conversational AI experience. It not only searches for relevant content but also generates a natural language response based on the matched content.

Key features:

  • Conversational approach to content discovery

  • Processes natural language queries

  • Returns relevant content AND an AI-generated response

  • Provides summaries and answers in conversational format

Example request to this endpoint:

{
  "prompt": "What are the best beach destinations in Europe for families?",
  "threshold": 0.25,
  "responseLengthTokens": 500,
  "indexName": "travelBot"
}

This endpoint is perfect for users who prefer a conversational interaction and may have complex or open-ended questions.

The Data Flow

The process flow for either search type follows these steps:

  1. User enters a query and selects search type (AI Chat or Keyword)

  2. Application sends a request to the appropriate dotAI endpoint

  3. dotCMS processes the request:

    • Analyzes the query

    • Searches the specified index ("travelBot")

    • Finds matching content based on relevance

  4. dotCMS returns results to the application

  5. Application displays the results:

    • For keyword search: Just the content results

    • For AI chat: Content results plus an AI-generated summary

The key difference between the two modes is that the completions endpoint (AI Chat) leverages large language models to generate a conversational response in addition to finding relevant content.

Conclusion

Integrating dotAI into your headless application transforms how users discover and interact with your content. By implementing both the keyword search and AI chat functionalities, you provide a flexible, powerful content discovery experience that meets diverse user needs.

The React components we've built demonstrate how easily these capabilities can be integrated into a modern frontend application, while keeping the implementation clean and maintainable.

As AI technology continues to evolve, dotCMS's dotAI will likely gain even more capabilities, which you can incorporate into your application with minimal changes to your existing integration. This future-proofs your investment in AI-powered content discovery.

Additional Resources

By following this guide, you've taken a significant step toward enhancing your headless application with powerful AI capabilities, making your content more accessible and valuable to your users.