Skip to content

How to Integrate SEO Analysis Workflows with CMS Platforms Like Strapi

Problem

When I tried to set up an automated SEO analysis workflow for my Strapi v5 CMS, I ran into a fundamental mismatch. The original workflow assumed static HTML files where AI agents could directly read and modify markdown files. But my content lives in a database, accessed through APIs.

I asked on Reddit about this:

“From your description, website repo implies static html pages. Any recommended workflow for data on CMS? I don’t think keeping local files for all pages is the way to go.”

I got a helpful reply pointing out that if the CMS has an API or MCP support, I could integrate directly instead of maintaining local file copies.

Environment

  • Strapi v5 CMS
  • Next.js v16 frontend
  • Model Context Protocol (MCP) for AI integration
  • Node.js 22

What happened?

I have a Strapi v5 CMS serving content to a Next.js v16 frontend. When I wanted to run automated SEO analysis using AI agents, I initially tried to export all my content to local markdown files:

Terminal window
# My first naive approach
npm run export-content -- --output ./content-backup

This created hundreds of markdown files:

Directory structure
./content-backup/
├── articles/
│ ├── article-1.mdx
│ ├── article-2.mdx
│ └── ... (200+ files)
└── pages/
├── page-1.mdx
└── ... (50+ files)

But this approach had obvious problems:

  • Synchronization nightmare - every content update required re-export
  • Version control bloat - tracking 250+ generated files
  • Stale content risk - analysis might run on outdated exports
  • Manual overhead - no automatic triggers when content changes

I needed a better approach that worked directly with the CMS API.

How to solve it?

I broke this into three layers: API access, webhook triggers, and MCP integration for AI agents.

Layer 1: Strapi v5 Content API Access

First, I needed to fetch content directly from Strapi. Here’s my initial attempt:

strapi-fetcher.ts
const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337';
const STRAPI_API_TOKEN = process.env.STRAPI_API_TOKEN;
// First attempt - using v4 patterns
const fetchContent = async (contentType: string) => {
const response = await fetch(
`${STRAPI_URL}/api/${contentType}?publicationState=live`,
{
headers: {
Authorization: `Bearer ${STRAPI_API_TOKEN}`,
},
}
);
const { data } = await response.json();
return data.attributes; // v4 pattern
};

This failed with:

Terminal window
TypeError: Cannot read properties of undefined (reading 'attributes')

The problem? Strapi v5 changed the response format. I checked the documentation and found that v5 uses:

  • status parameter instead of publicationState (‘draft’ | ‘published’)
  • Flattened response (no nested attributes object)
  • documentId instead of id for document access

Here’s the corrected version:

strapi-content-fetcher.ts
const fetchStrapiContent = async (
contentType: string,
status: 'published' | 'draft' = 'published'
) => {
const response = await fetch(
`${STRAPI_URL}/api/${contentType}?status=${status}`,
{
headers: {
Authorization: `Bearer ${STRAPI_API_TOKEN}`,
},
}
);
if (!response.ok) {
throw new Error(`Strapi API error: ${response.status}`);
}
const { data } = await response.json();
// Strapi v5: direct access, no attributes nesting
return data;
};
// Fetch single document by documentId
const fetchStrapiDocument = async (
contentType: string,
documentId: string
) => {
const response = await fetch(
`${STRAPI_URL}/api/${contentType}/${documentId}`,
{
headers: {
Authorization: `Bearer ${STRAPI_API_TOKEN}`,
},
}
);
const { data } = await response.json();
return data;
};

Now when I fetch content:

const articles = await fetchStrapiContent('articles', 'published');
console.log(articles[0].title); // Direct access in v5
Terminal window
[ { documentId: 'abc123', title: 'My First Article', content: '...' } ]

Layer 2: Webhook Integration for Real-Time Triggers

I wanted SEO analysis to run automatically when content changes. Strapi webhooks handle this:

strapi-webhook-config.json
{
"name": "SEO Analysis Trigger",
"url": "https://my-seo-service.com/webhook/strapi",
"events": ["entry.create", "entry.update", "entry.publish"],
"headers": {
"X-Strapi-Webhook-Secret": "your-webhook-secret-here"
}
}

I set up a webhook handler:

webhook-handler.ts
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhook/strapi', async (req, res) => {
const signature = req.headers['x-strapi-webhook-secret'];
// Verify webhook signature
if (signature !== process.env.STRAPI_WEBHOOK_SECRET) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, model, entry } = req.body;
// Trigger SEO analysis for the affected content
if (event === 'entry.publish' || event === 'entry.update') {
await triggerSEOAnalysis(model, entry.documentId);
}
res.json({ received: true });
});
async function triggerSEOAnalysis(contentType: string, documentId: string) {
// Call MCP server to analyze content
const response = await fetch('http://localhost:3001/mcp/analyze', {
method: 'POST',
body: JSON.stringify({ contentType, documentId }),
});
return response.json();
}

Layer 3: MCP Server for AI Integration

The final layer connects AI agents to Strapi through MCP (Model Context Protocol). Here’s my MCP server:

strapi-mcp-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "strapi-seo-connector",
version: "1.0.0",
});
// Tool: Fetch content for SEO analysis
server.tool(
"fetch-content",
{
contentType: z.string().describe("Strapi content type (e.g., 'articles')"),
documentId: z.string().optional().describe("Specific document ID"),
status: z.enum(['published', 'draft']).optional().default('published'),
},
async ({ contentType, documentId, status }) => {
const content = documentId
? await fetchStrapiDocument(contentType, documentId)
: await fetchStrapiContent(contentType, status);
return {
content: [{
type: "text",
text: JSON.stringify(content, null, 2),
}],
};
}
);
// Tool: Update SEO metadata
server.tool(
"update-seo-metadata",
{
contentType: z.string(),
documentId: z.string(),
seoTitle: z.string().optional(),
seoDescription: z.string().optional(),
keywords: z.array(z.string()).optional(),
},
async ({ contentType, documentId, seoTitle, seoDescription, keywords }) => {
const response = await fetch(
`${STRAPI_URL}/api/${contentType}/${documentId}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${STRAPI_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: {
seoTitle,
seoDescription,
keywords,
},
}),
}
);
const { data } = await response.json();
return {
content: [{
type: "text",
text: `Updated SEO metadata for ${contentType}/${documentId}`,
}],
};
}
);
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);

The Architecture

Here’s how the three layers connect:

Integration Flow
┌───────────────┐ ┌──────────────┐ ┌─────────────┐
│ Strapi CMS │ ──────→ │ Webhook │ ──────→ │ MCP Server │
│ (Content API) │ │ Handler │ │ (AI Bridge) │
└───────────────┘ └──────────────┘ └─────────────┘
│ │ │
│ │ ▼
│ │ ┌─────────────────┐
│ │ │ AI Agent │
│ │ │ (SEO Analysis) │
│ │ └─────────────────┘
│ │ │
│ │ ▼
│ │ ┌─────────────────┐
│ ←──────────────────────┼───────────── │ Recommendations │
│ API Update │ │ & Metadata │
└─────────────────────────┘ └─────────────────┘

The flow:

  1. Content created/updated in Strapi
  2. Webhook fires on entry.publish or entry.update
  3. MCP server fetches content via Strapi API
  4. AI agent analyzes content for SEO issues
  5. MCP server updates metadata back to Strapi

Common Mistakes I Made

I hit several issues during integration:

Mistake 1: Using v4 API patterns

// Wrong - v4 pattern
const title = data.attributes.title;
// Correct - v5 pattern
const title = data.title;

Mistake 2: Storing API tokens in frontend code

// Wrong - exposed in Next.js client component
const token = 'your-token-here'; // Never do this!
// Correct - server-side only
const token = process.env.STRAPI_API_TOKEN; // Backend only

Mistake 3: Ignoring rate limits

Strapi has default rate limits. I added throttling:

rate-limited-fetcher.ts
import pLimit from 'p-limit';
const limit = pLimit(5); // Max 5 concurrent requests
const fetchAllContent = async (contentTypes: string[]) => {
const promises = contentTypes.map(type =>
limit(() => fetchStrapiContent(type))
);
return Promise.all(promises);
};

Mistake 4: Missing authentication

// Wrong - no auth header
await fetch(`${STRAPI_URL}/api/articles`);
// Correct - include auth
await fetch(`${STRAPI_URL}/api/articles`, {
headers: { Authorization: `Bearer ${token}` },
});

The Benefits

With this three-layer approach:

  1. No local file overhead - Content stays in Strapi database
  2. Real-time analysis - Webhooks trigger SEO checks immediately on publish
  3. CMS-agnostic skills - MCP servers configurable for different providers (WordPress, Contentful, Sanity)
  4. Version safety - Analyze drafts before publishing

Summary

In this post, I showed how to integrate SEO analysis workflows with Strapi v5 CMS using a three-layer architecture: Content API for fetching, webhooks for real-time triggers, and MCP servers for AI agent integration. The key point is that you don’t need local file copies - the MCP server acts as a bridge between your CMS and AI agents, enabling automated SEO recommendations without synchronization overhead.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments