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:
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.
3. Creating the Component Structure
Our implementation follows a clean, organized structure:
TravelBotComponent.js - The main container component
SearchInput.js - Handles user input
SearchTypeSelector.js - Lets users switch between AI Chat and Keyword search modes
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:
Enable dotAI:
Go to Apps in your dotCMS admin panel
Find dotAI and make sure it's activated
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
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:
User enters a query and selects search type (AI Chat or Keyword)
Application sends a request to the appropriate dotAI endpoint
dotCMS processes the request:
Analyzes the query
Searches the specified index ("travelBot")
Finds matching content based on relevance
dotCMS returns results to the application
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.