These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Pendo?
Pendo is a product experience platform that developers integrate into web and mobile applications to capture user behavior analytics and deliver in-app guidance. The platform operates through client-side SDKs—primarily a JavaScript agent for web applications—that automatically track page views, clicks, and form interactions while enabling you to inject dynamic UI elements like tooltips, modals, and multi-step walkthroughs.
From a technical perspective, Pendo functions as an analytics and engagement layer embedded in your application. The JavaScript SDK loads asynchronously, initializes with visitor and account metadata, and transmits event data to Pendo's analytics backend.
Developers use the SDK's JavaScript API to identify users via pendo.initialize() and pendo.identify(), send custom events with pendo.track(), and programmatically control guide behavior. The platform provides REST APIs (Engage API, Feedback API, Agent API) for managing guides, segments, and metadata programmatically.
Why Integrate Pendo with Strapi
Integrating Pendo with Strapi addresses content-driven application analytics through dual tracking capabilities, automated content workflow measurement, admin panel feature adoption visibility, and frontend content consumption analysis.
- Track content performance across the entire lifecycle. Connect editorial activity in the Strapi admin panel—content creation, revision cycles, publication timing—with end-user engagement metrics in your frontend application. Identify which content types generate the most engagement, measure time-to-publish for different content categories, and correlate content metadata with user behavior patterns.
- Monitor CMS feature adoption and identify usability gaps. Track which features administrators actually use in your Strapi admin panel. Discover unused custom fields, neglected content types, or confusing UI elements within the Strapi admin dashboard. This data can drive UX prioritization decisions grounded in actual usage patterns rather than assumptions.
- Implement unified analytics across admin and frontend environments. Maintain separate but connected analytics streams for CMS administrators and application end-users. Administrators tracked in the admin panel provide editorial workflow insights, while end-users tracked in your frontend application provide content consumption metrics—both feeding into a holistic view of your content system's performance.
- Enable in-app guidance for content editors. Deliver contextual onboarding flows and feature announcements directly within your Strapi admin panel. When you add custom fields, introduce new content types, or modify editorial workflows, guide administrators through changes by injecting in-app guides and tooltips. This approach provides real-time support without requiring external documentation or training sessions.
How to Integrate Pendo with Strapi
This integration implementation targets frontend applications consuming Strapi's API rather than embedding Pendo within Strapi's backend. The JavaScript SDK loads in your application's client layer, initializes with user data fetched from Strapi's authentication API, and tracks interactions with content delivered through Strapi's REST or GraphQL endpoints.
Step 1: Install the Pendo JavaScript SDK
Place the Pendo installation snippet in your application's main HTML file or framework entry point. This creates a global pendo object and asynchronously loads the Pendo agent from the CDN at https://cdn.pendo.io/agent/static/.
For standard HTML applications, place the Pendo install script in your index.html before the closing </head> tag:
<script>
(function(apiKey){
(function(p,e,n,d,o){
var v,w,x,y,z;
o=p[d]=p[d]||{};
o._q=o._q||[];
v=['initialize','identify','updateOptions','pageLoad','track'];
for(w=0,x=v.length;w<x;++w)(function(m){
o[m]=o[m]||function(){o._q[m===v[0]?'unshift':'push']([m].concat([].slice.call(arguments,0)));};
})(v[w]);
y=e.createElement(n);y.async=!0;y.src='https://cdn.pendo.io/agent/static/'+apiKey+'/pendo.js';
z=e.getElementsByTagName(n)[0];z.parentNode.insertBefore(y,z);
})(window,document,'script','pendo');
})('YOUR_PENDO_API_KEY');
</script>Replace 'YOUR_PENDO_API_KEY' with your actual Pendo API key from your subscription settings. Use an environment variable (such as PENDO_API_KEY, NEXT_PUBLIC_PENDO_API_KEY for Next.js, or VITE_PENDO_API_KEY for Vue/Vite) rather than committing it directly to your codebase:
})('${process.env.NEXT_PUBLIC_PENDO_API_KEY}');For Next.js applications, load the script in _app.js:
// pages/_app.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const script = document.createElement('script');
script.innerHTML = `
(function(apiKey){
(function(p,e,n,d,o){
var v,w,x,y,z;
o=p[d]=p[d]||{};
o._q=o._q||[];
v=['initialize','identify','updateOptions','pageLoad','track'];
for(w=0,x=v.length;w<x;++w)(function(m){
o[m]=o[m]||function(){o._q[m===v[0]?'unshift':'push']([m].concat([].slice.call(arguments,0)));};
})(v[w]);
y=e.createElement(n);y.async=!0;y.src='https://cdn.pendo.io/agent/static/'+apiKey+'/pendo.js';
z=e.getElementsByTagName(n)[0];z.parentNode.insertBefore(y,z);
})(window,document,'script','pendo');
})('${process.env.NEXT_PUBLIC_PENDO_API_KEY}');
`;
document.head.appendChild(script);
}, []);
useEffect(() => {
const handleRouteChange = () => {
if (window.pendo) {
window.pendo.pageLoad();
}
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return <Component {...pageProps} />;
}
export default MyApp;Step 2: Initialize Pendo with Strapi User Data
This is where most integrations get tricky—you need user context before Pendo can track anything meaningful. After user authentication completes, fetch the authenticated user's data from Strapi's /api/users/me endpoint and pass this metadata to pendo.initialize().
A utility function streamlines this initialization:
// utils/pendo.js
import axios from 'axios';
export async function initializePendoWithStrapiUser() {
try {
const token = localStorage.getItem('jwt');
if (!token) {
console.warn('No authentication token found');
return;
}
const response = await axios.get(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/users/me`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
const user = response.data;
if (window.pendo) {
window.pendo.initialize({
visitor: {
id: user.id.toString(),
email: user.email,
full_name: user.username,
role: user.role?.name || 'user',
creationDate: user.createdAt
},
account: {
id: user.organization?.id?.toString() || 'default',
name: user.organization?.name || 'Individual'
}
});
}
} catch (error) {
console.error('Failed to initialize Pendo:', error);
}
}For single-page applications, this initialization should occur after verifying the authenticated user's credentials and fetching their metadata from your authentication context. For Strapi implementations, retrieve the user's profile via the /api/users/me endpoint using the stored JWT token, then initialize Pendo with the returned user data mapped to Pendo's visitor and account object structure.
// components/AuthProvider.js
import { createContext, useContext, useEffect, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadUser = async () => {
const token = localStorage.getItem('jwt');
if (token) {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/users/me`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
const userData = await response.json();
setUser(userData);
// Initialize Pendo after user data loads with Strapi user metadata
if (window.pendo) {
window.pendo.initialize({
visitor: {
id: userData.id.toString(),
email: userData.email,
full_name: userData.username,
role: userData.role?.name || 'user',
creationDate: userData.createdAt
},
account: {
id: userData.organization?.id.toString() || 'default',
name: userData.organization?.name || 'Individual'
}
});
}
} catch (error) {
console.error('Authentication failed:', error);
localStorage.removeItem('jwt');
}
}
setLoading(false);
};
loadUser();
}, []);
return (
<AuthContext.Provider value={{ user, loading }}>
{children}
</AuthContext.Provider>
);
}Step 3: Track Content Interactions
Raw page views don't tell you much. What matters is understanding which content actually drives user behavior. When users view articles, watch videos, or interact with content delivered from Strapi, capture these interactions using Pendo's Track Events functionality with the pendo.track() method.
Custom Track Events capture specific user actions beyond automatic interaction tracking. The basic syntax for tracking events is:
pendo.track('eventName', {
property1: 'value1',
property2: 'value2'
});For Strapi content interactions, implement a reusable tracking utility:
// utils/contentTracking.js
export const trackContentEvent = (eventName, contentMetadata) => {
if (!window.pendo) {
console.warn('Pendo not initialized');
return;
}
pendo.track(eventName, {
contentId: contentMetadata.id?.toString(),
contentType: contentMetadata.__component || contentMetadata.type,
title: contentMetadata.title,
category: contentMetadata.category?.name,
author: contentMetadata.author?.username,
publishDate: contentMetadata.publishedAt,
timestamp: Date.now()
});
};
// Specific tracking functions for content lifecycle events
export const trackContentViewed = (article) => {
trackContentEvent('ContentViewed', article);
};
export const trackContentEngagement = (contentId, engagementData) => {
pendo.track('ContentEngagement', {
contentId: contentId.toString(),
durationSeconds: engagementData.duration,
scrollPercentage: engagementData.scrollDepth,
interactionType: 'read'
});
};
export const trackContentCreated = (newArticle) => {
trackContentEvent('ContentCreated', {
...newArticle,
status: newArticle.publishedAt ? 'published' : 'draft'
});
};Important constraints: According to Pendo's Track Events documentation, event properties are limited to 512 bytes total JSON size, property names must be 32 characters or fewer, and property values should be strings or booleans for optimal reporting functionality.
These tracking functions integrate into your components:
// pages/articles/[slug].js
import { useState, useEffect } from 'react';
import { trackContentViewed, trackContentEngagement } from '../utils/contentTracking';
export default function Article({ article }) {
const [startTime] = useState(Date.now());
const [scrollDepth, setScrollDepth] = useState(0);
useEffect(() => {
// Track initial page view
trackContentViewed(article);
// Track scroll depth
const handleScroll = () => {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = window.scrollY;
const depth = Math.round((scrollTop / (documentHeight - windowHeight)) * 100);
if (depth > scrollDepth) {
setScrollDepth(depth);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
// Track engagement on component unmount
const duration = Math.round((Date.now() - startTime) / 1000);
trackContentEngagement(article.id, {
duration: duration,
scrollDepth: scrollDepth
});
};
}, [article.id, startTime, scrollDepth]);
return (
<div>
<h1>{article.attributes.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.attributes.body }} />
</div>
);
}Step 4: Implement Server-Side Event Tracking
Client-side tracking misses a critical piece—content operations that happen server-side need tracking too, or you'll have blind spots in your analytics. Backend operations like content creation, publication, and user actions happen outside the browser and need server-side tracking.
The approach involves creating a function that sends Track Event payloads to Pendo's tracking API (https://track.pendo.io/track/api/v1/track) with the visitor ID, account ID, event name, and relevant properties.
Register this function in Strapi's lifecycle hooks using strapi.db.lifecycles.subscribe() in your application's bootstrap function, passing the visitor and account IDs from the authenticated context along with metadata about the content action.
A Pendo service in your Strapi application handles this:
// src/api/article/services/pendo.js
const axios = require('axios');
module.exports = {
async trackEvent(visitorId, accountId, eventName, properties) {
try {
const trackEventPayload = {
type: "track",
event: eventName,
visitorId: visitorId.toString(),
accountId: accountId.toString(),
timestamp: Date.now(),
properties: properties,
context: {
ip: this.ctx?.request?.ip,
userAgent: this.ctx?.request?.header['user-agent'],
url: this.ctx?.request?.url
}
};
await axios.post(
'https://track.pendo.io/track/api/v1/track',
trackEventPayload,
{
headers: {
'Content-Type': 'application/json',
'x-pendo-integration-key': process.env.PENDO_SHARED_SECRET
}
}
);
} catch (error) {
strapi.log.error('Pendo tracking failed:', error);
}
}
};Register lifecycle hooks to trigger tracking:
// src/api/article/content-types/article/lifecycles.js
const axios = require('axios');
module.exports = {
async afterCreate(event) {
const { result, params } = event;
// Track content creation server-side using Pendo Track API
if (params.data.createdBy) {
const trackEventPayload = {
type: "track",
event: "ArticleCreated",
visitorId: params.data.createdBy.id.toString(),
accountId: params.data.organization?.id.toString() || 'default',
timestamp: Date.now(),
properties: {
articleId: result.id.toString(),
title: result.title,
category: result.category?.name,
status: result.publishedAt ? 'published' : 'draft'
}
};
try {
await axios.post('https://track.pendo.io/track/api/v1/track', trackEventPayload, {
headers: {
'Content-Type': 'application/json',
'x-pendo-integration-key': process.env.PENDO_SHARED_SECRET
}
});
} catch (error) {
console.error('Pendo tracking error:', error);
}
}
},
async afterUpdate(event) {
const { result, params } = event;
const pendoService = strapi.service('api::article.pendo');
// Track publication events
if (result.publishedAt && !params.data.previousPublishedAt) {
await pendoService.trackEvent(
params.data.updatedBy.id,
params.data.organization?.id || 'default',
'ArticlePublished',
{
articleId: result.id.toString(),
title: result.title,
timeToPublish: Date.now() - new Date(result.createdAt).getTime()
}
);
}
}
};Step 5: Configure Content Security Policy
Applications enforcing Content Security Policy headers need Pendo's domains whitelisted. The Pendo CDN domain https://cdn.pendo.io must be whitelisted in your CSP directives. In Next.js, update your next.config.js to configure security headers:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.pendo.io https://pendo-io-static.storage.googleapis.com",
"connect-src 'self' https://track.pendo.io https://app.pendo.io",
"img-src 'self' data: https://cdn.pendo.io https://pendo-io-static.storage.googleapis.com",
"style-src 'self' 'unsafe-inline' https://cdn.pendo.io https://pendo-io-static.storage.googleapis.com"
].join('; ')
}
]
}
];
}
};For applications deployed behind Nginx or other reverse proxies, configure CSP headers at the proxy level:
# nginx.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.pendo.io; connect-src 'self' https://track.pendo.io; img-src 'self' data: https://cdn.pendo.io; font-src 'self';";Step 6: Verify Installation
The browser console command pendo.validateEnvironment() verifies the installation:
pendo.validateEnvironment()This outputs installation status, current visitor and account metadata, and configuration warnings. Check that visitor and account IDs match your Strapi user data, the API key corresponds to your subscription, and no initialization errors appear in the console.
Project Example: Documentation Portal with User Journey Analytics
A technical documentation portal demonstrates the practical value of combining Strapi's content management with Pendo's analytics capabilities. This example implements a knowledge base where technical writers manage documentation through Strapi's admin panel while Pendo tracks reader behavior to identify content gaps and optimize documentation structure.
Architecture Overview
The portal consists of three layers:
Content Layer (Strapi): Stores documentation articles as collection types with attributes for title, content body, category, related articles, and code examples. Strapi's REST API exposes documentation content with filtering, search, and population capabilities.
Presentation Layer (Next.js): Server-side renders documentation pages for SEO while maintaining interactivity for search, navigation, and feedback collection. The application fetches content from Strapi's API and implements client-side routing.
Analytics Layer (Pendo): Implements custom event tracking through Pendo's JavaScript SDK to monitor documentation interactions including page views, scroll depth, time on page, and navigation paths. Developers configure Track Events to capture specific user interactions such as code snippet copying, external link clicks, and feedback submissions.
Custom Track Events allow measurement of business-critical documentation actions with customizable metadata properties, enabling analytics for content-specific user behavior.
Content Model Implementation
Documentation content type in Strapi with these fields:
// src/api/documentation/content-types/documentation/schema.json
{
"kind": "collectionType",
"collectionName": "documentations",
"info": {
"singularName": "documentation",
"pluralName": "documentations",
"displayName": "Documentation"
},
"options": {
"draftAndPublish": true
},
"attributes": {
"title": {
"type": "string",
"required": true
},
"slug": {
"type": "uid",
"targetField": "title"
},
"content": {
"type": "richtext",
"required": true
},
"category": {
"type": "relation",
"relation": "manyToOne",
"target": "api::category.category"
},
"relatedArticles": {
"type": "relation",
"relation": "manyToMany",
"target": "api::documentation.documentation"
},
"codeExamples": {
"type": "component",
"repeatable": true,
"component": "content.code-block"
},
"viewCount": {
"type": "integer",
"default": 0
},
"searchKeywords": {
"type": "json"
}
}
}Custom controller to increment view counts and track searches using Pendo's Track Events API:
// src/api/documentation/controllers/documentation.js
const { createCoreController } = require('@strapi/strapi').factories;
const axios = require('axios');
async function sendPendoTrackEvent(visitorId, accountId, eventName, properties) {
const trackEventPayload = {
type: "track",
event: eventName,
visitorId: visitorId.toString(),
accountId: accountId.toString(),
timestamp: Date.now(),
properties: properties
};
try {
await axios.post('https://track.pendo.io/track/api/v1/track', trackEventPayload, {
headers: {
'Content-Type': 'application/json',
'x-pendo-integration-key': process.env.PENDO_SHARED_SECRET
}
});
} catch (error) {
console.error('Pendo tracking error:', error);
}
}
module.exports = createCoreController('api::documentation.documentation', ({ strapi }) => ({
async findOne(ctx) {
const { slug } = ctx.params;
const entity = await strapi.db.query('api::documentation.documentation').findOne({
where: { slug },
populate: ['category', 'relatedArticles', 'codeExamples']
});
if (!entity) {
return ctx.notFound();
}
// Increment view count
await strapi.db.query('api::documentation.documentation').update({
where: { id: entity.id },
data: { viewCount: (entity.viewCount || 0) + 1 }
});
// Track content view in Pendo
const userId = ctx.state.user?.id;
if (userId) {
await sendPendoTrackEvent(userId, ctx.state.user.organization?.id || 'default', 'ContentViewed', {
contentId: entity.id.toString(),
contentType: 'documentation',
title: entity.title,
category: entity.category?.name,
viewCount: (entity.viewCount || 0) + 1
});
}
return this.transformResponse(entity);
},
async search(ctx) {
const { query } = ctx.request.query;
const results = await strapi.db.query('api::documentation.documentation').findMany({
where: {
$or: [
{ title: { $containsi: query } },
{ content: { $containsi: query } },
{ searchKeywords: { $contains: query } }
]
},
populate: ['category']
});
// Track search event in Pendo
const userId = ctx.state.user?.id;
if (userId) {
await sendPendoTrackEvent(userId, ctx.state.user.organization?.id || 'default', 'SearchPerformed', {
searchQuery: query,
resultCount: results.length,
timestamp: Date.now()
});
}
return results;
}
}));Frontend Implementation with Analytics
Documentation viewer with integrated tracking capabilities:
// pages/docs/[slug].js
import { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';
export default function DocumentationPage({ article }) {
const router = useRouter();
const [scrollDepth, setScrollDepth] = useState(0);
const [timeOnPage, setTimeOnPage] = useState(0);
const startTimeRef = useRef(Date.now());
useEffect(() => {
if (window.pendo && article) {
window.pendo.track('content_viewed', {
contentId: article.id.toString(),
title: article.attributes.title,
category: article.attributes.category?.name,
slug: article.attributes.slug
});
}
const handleScroll = () => {
const scrollPercentage = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
setScrollDepth(Math.round(scrollPercentage));
};
const timeInterval = setInterval(() => {
const elapsed = Math.round((Date.now() - startTimeRef.current) / 1000);
setTimeOnPage(elapsed);
if (window.pendo && elapsed > 0 && elapsed % 30 === 0) {
pendo.track('page_time_milestone', {
durationSeconds: elapsed.toString(),
pageUrl: window.location.pathname,
engagementType: 'time_based'
});
}
}, 5000);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
clearInterval(timeInterval);
if (window.pendo) {
window.pendo.track('documentation_engagement', {
article_id: article.id.toString(),
duration_seconds: Math.round((Date.now() - startTimeRef.current) / 1000),
scroll_depth: scrollDepth,
completion_status: scrollDepth > 80 ? 'complete' : 'partial'
});
}
};
}, [article, scrollDepth]);
const handleCodeCopy = (codeSnippet) => {
navigator.clipboard.writeText(codeSnippet);
if (window.pendo) {
window.pendo.track('CodeSnippetCopied', {
articleId: article.id.toString(),
language: codeSnippet.language,
snippetLength: codeSnippet.code.length
});
}
};
const handleRelatedArticleClick = (relatedArticle) => {
if (window.pendo) {
window.pendo.track('RelatedArticleClicked', {
sourceArticleId: article.id.toString(),
targetArticleId: relatedArticle.id.toString(),
targetTitle: relatedArticle.attributes.title
});
}
router.push(`/docs/${relatedArticle.attributes.slug}`);
};
return (
<div className="documentation-container">
<article>
<header>
<h1>{article.attributes.title}</h1>
{article.attributes.category && (
<span className="category">
{article.attributes.category.data.attributes.name}
</span>
)}
</header>
<div dangerouslySetInnerHTML={{ __html: article.attributes.content }} />
{article.attributes.codeExamples?.length > 0 && (
<section className="code-examples">
<h2>Code Examples</h2>
{article.attributes.codeExamples.map((example, index) => (
<div key={index} className="code-block">
<div className="code-header">
<span>{example.language}</span>
<button onClick={() => handleCodeCopy(example)}>
Copy
</button>
</div>
<pre>
<code>{example.code}</code>
</pre>
</div>
))}
</section>
)}
{article.attributes.relatedArticles?.data?.length > 0 && (
<aside className="related-articles">
<h3>Related Documentation</h3>
<ul>
{article.attributes.relatedArticles.data.map((related) => (
<li key={related.id}>
<a onClick={() => handleRelatedArticleClick(related)}>
{related.attributes.title}
</a>
</li>
))}
</ul>
</aside>
)}
</article>
</div>
);
}
export async function getServerSideProps({ params }) {
const { slug } = params;
const response = await axios.get(
`${process.env.STRAPI_URL}/api/documentations/${slug}`,
{
params: {
populate: ['category', 'relatedArticles', 'codeExamples']
}
}
);
return {
props: {
article: response.data.data
}
};
}Search Implementation with Analytics
Track custom user interactions and content engagement using Pendo's event tracking capabilities:
// components/DocumentationSearch.js
import { useState } from 'react';
import axios from 'axios';
export default function DocumentationSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const handleSearch = async (e) => {
e.preventDefault();
if (!query.trim()) return;
setLoading(true);
try {
const response = await axios.get(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/documentations/search`,
{
params: { query }
}
);
setResults(response.data);
if (window.pendo) {
window.pendo.track('DocumentationSearched', {
searchTerm: query,
resultCount: response.data.length,
hasResults: response.data.length > 0
});
}
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
};
const handleResultClick = (result, index) => {
if (window.pendo) {
window.pendo.track('SearchResultClicked', {
searchTerm: query,
resultPosition: index + 1,
articleTitle: result.title,
articleId: result.id.toString()
});
}
};
return (
<div className="search-container">
<form onSubmit={handleSearch}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search documentation..."
/>
<button type="submit" disabled={loading}>
{loading ? 'Searching...' : 'Search'}
</button>
</form>
{results.length > 0 && (
<div className="search-results">
{results.map((result, index) => (
<a
key={result.id}
href={`/docs/${result.slug}`}
onClick={() => handleResultClick(result, index)}
>
<h4>{result.title}</h4>
<p>{result.category?.name}</p>
</a>
))}
</div>
)}
</div>
);
}Analytics Dashboard Integration
This implementation provides several actionable analytics insights:
Content performance metrics: Track which documentation articles receive the most views, identify articles with high bounce rates (low scroll depth), and measure average time spent on different documentation categories.
Track custom events: Implement custom event tracking using Pendo's Track Events API to capture specific user actions such as content views, engagement metrics, and feature interactions. Developers can track events with custom properties using pendo.track('eventName', {property: 'value'}) to measure user behavior including article consumption, guide completions, and content lifecycle actions within Strapi-based applications.
User journey mapping: Understand typical navigation paths through documentation, identify articles that effectively guide users to related content, and measure documentation engagement based on scroll depth and time-on-page metrics.
Content engagement patterns: Track code snippet copy rates and code block interaction metrics to identify valuable examples, monitor related article click-through rates, and identify articles that consistently lead to successful problem resolution (measured by bounce rates, return visit frequency, and task completion indicators).
The combination of Strapi's content flexibility with Pendo's behavioral analytics creates a feedback loop where usage data directly informs content strategy and documentation improvements.
Strapi Open Office Hours
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation and Pendo documentation.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.