claude-plugin-wordpress-manager 2.12.2 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -3
- package/CHANGELOG.md +94 -2
- package/agents/wp-accessibility-auditor.md +1 -1
- package/agents/wp-content-strategist.md +2 -2
- package/agents/wp-deployment-engineer.md +1 -1
- package/agents/wp-distribution-manager.md +1 -1
- package/agents/wp-monitoring-agent.md +1 -1
- package/agents/wp-performance-optimizer.md +1 -1
- package/agents/wp-security-auditor.md +1 -1
- package/agents/wp-site-manager.md +3 -3
- package/commands/wp-setup.md +2 -2
- package/docs/GUIDE.md +260 -21
- package/docs/VALIDATION.md +341 -0
- package/docs/guides/wp-ecommerce.md +4 -4
- package/docs/plans/2026-03-01-tier3-wcop-implementation.md +1 -1
- package/docs/plans/2026-03-01-tier4-5-implementation.md +1 -1
- package/docs/plans/2026-03-02-content-framework-architecture.md +612 -0
- package/docs/plans/2026-03-02-content-framework-strategic-reflections.md +228 -0
- package/docs/plans/2026-03-02-content-intelligence-phase2.md +560 -0
- package/docs/plans/2026-03-02-content-pipeline-phase1.md +456 -0
- package/docs/plans/2026-03-02-dashboard-kanban-design.md +761 -0
- package/docs/plans/2026-03-02-dashboard-kanban-implementation.md +598 -0
- package/docs/plans/2026-03-02-dashboard-strategy.md +363 -0
- package/docs/plans/2026-03-02-editorial-calendar-phase3.md +490 -0
- package/docs/validation/.gitkeep +0 -0
- package/docs/validation/dashboard.html +286 -0
- package/docs/validation/results.json +1705 -0
- package/package.json +16 -3
- package/scripts/context-scanner.mjs +446 -0
- package/scripts/dashboard-renderer.mjs +553 -0
- package/scripts/run-validation.mjs +1132 -0
- package/servers/wp-rest-bridge/build/server.js +17 -6
- package/servers/wp-rest-bridge/build/tools/index.js +0 -9
- package/servers/wp-rest-bridge/build/tools/plugin-repository.js +23 -31
- package/servers/wp-rest-bridge/build/tools/schema.js +10 -2
- package/servers/wp-rest-bridge/build/tools/unified-content.js +10 -2
- package/servers/wp-rest-bridge/build/wordpress.d.ts +0 -3
- package/servers/wp-rest-bridge/build/wordpress.js +16 -98
- package/servers/wp-rest-bridge/package.json +1 -0
- package/skills/wp-analytics/SKILL.md +153 -0
- package/skills/wp-analytics/references/signals-feed-schema.md +417 -0
- package/skills/wp-content/references/content-templates.md +1 -1
- package/skills/wp-content/references/seo-optimization.md +8 -8
- package/skills/wp-content-attribution/references/roi-calculation.md +1 -1
- package/skills/wp-content-attribution/references/utm-tracking-setup.md +5 -5
- package/skills/wp-content-generation/references/generation-workflow.md +2 -2
- package/skills/wp-content-pipeline/SKILL.md +461 -0
- package/skills/wp-content-pipeline/references/content-brief-schema.md +377 -0
- package/skills/wp-content-pipeline/references/site-config-schema.md +431 -0
- package/skills/wp-content-repurposing/references/auto-transform-pipeline.md +1 -1
- package/skills/wp-content-repurposing/references/email-newsletter.md +1 -1
- package/skills/wp-content-repurposing/references/platform-specs.md +2 -2
- package/skills/wp-content-repurposing/references/transform-templates.md +27 -27
- package/skills/wp-dashboard/SKILL.md +121 -0
- package/skills/wp-deploy/references/ssh-deploy.md +2 -2
- package/skills/wp-editorial-planner/SKILL.md +262 -0
- package/skills/wp-editorial-planner/references/editorial-schema.md +268 -0
- package/skills/wp-multilang-network/references/content-sync.md +3 -3
- package/skills/wp-multilang-network/references/network-architecture.md +1 -1
- package/skills/wp-multilang-network/references/seo-international.md +7 -7
- package/skills/wp-structured-data/references/schema-types.md +4 -4
- package/skills/wp-webhooks/references/payload-formats.md +3 -3
|
@@ -9,7 +9,7 @@ const server = new McpServer({
|
|
|
9
9
|
version: '1.1.0',
|
|
10
10
|
});
|
|
11
11
|
// Register multi-site management tools
|
|
12
|
-
server.tool('switch_site', { site_id: z.string().describe('Site ID to switch to (e.g., "
|
|
12
|
+
server.tool('switch_site', { site_id: z.string().describe('Site ID to switch to (e.g., "mysite", "othersite")') }, async (args) => {
|
|
13
13
|
const { switchSite } = await import('./wordpress.js');
|
|
14
14
|
try {
|
|
15
15
|
const newSite = switchSite(args.site_id);
|
|
@@ -36,11 +36,22 @@ function toZodType(prop, isRequired) {
|
|
|
36
36
|
return prop;
|
|
37
37
|
let zodType;
|
|
38
38
|
switch (prop.type) {
|
|
39
|
-
case 'string':
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
case '
|
|
43
|
-
case '
|
|
39
|
+
case 'string':
|
|
40
|
+
zodType = z.string();
|
|
41
|
+
break;
|
|
42
|
+
case 'number':
|
|
43
|
+
case 'integer':
|
|
44
|
+
zodType = z.number();
|
|
45
|
+
break;
|
|
46
|
+
case 'boolean':
|
|
47
|
+
zodType = z.boolean();
|
|
48
|
+
break;
|
|
49
|
+
case 'array':
|
|
50
|
+
zodType = z.array(z.any());
|
|
51
|
+
break;
|
|
52
|
+
case 'object':
|
|
53
|
+
zodType = z.record(z.any());
|
|
54
|
+
break;
|
|
44
55
|
default: zodType = z.any();
|
|
45
56
|
}
|
|
46
57
|
if (prop.description)
|
|
@@ -24,9 +24,6 @@ import { plausibleTools, plausibleHandlers } from './plausible.js';
|
|
|
24
24
|
import { cwvTools, cwvHandlers } from './cwv.js';
|
|
25
25
|
import { slackTools, slackHandlers } from './slack.js';
|
|
26
26
|
import { wcWorkflowTools, wcWorkflowHandlers } from './wc-workflows.js';
|
|
27
|
-
import { linkedinTools, linkedinHandlers } from './linkedin.js';
|
|
28
|
-
import { twitterTools, twitterHandlers } from './twitter.js';
|
|
29
|
-
import { schemaTools, schemaHandlers } from './schema.js';
|
|
30
27
|
// Combine all tools
|
|
31
28
|
export const allTools = [
|
|
32
29
|
...unifiedContentTools, // 8 tools
|
|
@@ -55,9 +52,6 @@ export const allTools = [
|
|
|
55
52
|
...cwvTools, // 4 tools
|
|
56
53
|
...slackTools, // 3 tools
|
|
57
54
|
...wcWorkflowTools, // 4 tools
|
|
58
|
-
...linkedinTools, // 5 tools
|
|
59
|
-
...twitterTools, // 5 tools
|
|
60
|
-
...schemaTools, // 3 tools
|
|
61
55
|
];
|
|
62
56
|
// Combine all handlers
|
|
63
57
|
export const toolHandlers = {
|
|
@@ -87,7 +81,4 @@ export const toolHandlers = {
|
|
|
87
81
|
...cwvHandlers,
|
|
88
82
|
...slackHandlers,
|
|
89
83
|
...wcWorkflowHandlers,
|
|
90
|
-
...linkedinHandlers,
|
|
91
|
-
...twitterHandlers,
|
|
92
|
-
...schemaHandlers,
|
|
93
84
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { searchWordPressPluginRepository } from '../wordpress.js';
|
|
2
|
+
import axios from 'axios';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
// Define the schema for plugin repository search
|
|
4
5
|
const searchPluginRepositorySchema = z.object({
|
|
@@ -70,38 +71,29 @@ export const pluginRepositoryHandlers = {
|
|
|
70
71
|
},
|
|
71
72
|
get_plugin_details: async (params) => {
|
|
72
73
|
try {
|
|
73
|
-
// For plugin details, we use a different action in the WordPress.org API
|
|
74
74
|
const apiUrl = 'https://api.wordpress.org/plugins/info/1.2/';
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
// Use axios directly for this specific request
|
|
100
|
-
const axios = (await import('axios')).default;
|
|
101
|
-
const response = await axios.post(apiUrl, requestData, {
|
|
102
|
-
headers: {
|
|
103
|
-
'Content-Type': 'application/json'
|
|
104
|
-
}
|
|
75
|
+
// WordPress.org API requires GET with PHP-style bracket notation
|
|
76
|
+
const response = await axios.get(apiUrl, {
|
|
77
|
+
params: {
|
|
78
|
+
action: 'plugin_information',
|
|
79
|
+
'request[slug]': params.slug,
|
|
80
|
+
'request[fields][description]': true,
|
|
81
|
+
'request[fields][sections]': true,
|
|
82
|
+
'request[fields][tested]': true,
|
|
83
|
+
'request[fields][requires]': true,
|
|
84
|
+
'request[fields][rating]': true,
|
|
85
|
+
'request[fields][ratings]': true,
|
|
86
|
+
'request[fields][downloaded]': true,
|
|
87
|
+
'request[fields][downloadlink]': true,
|
|
88
|
+
'request[fields][last_updated]': true,
|
|
89
|
+
'request[fields][homepage]': true,
|
|
90
|
+
'request[fields][tags]': true,
|
|
91
|
+
'request[fields][compatibility]': true,
|
|
92
|
+
'request[fields][author]': true,
|
|
93
|
+
'request[fields][contributors]': true,
|
|
94
|
+
'request[fields][banners]': true,
|
|
95
|
+
'request[fields][icons]': true,
|
|
96
|
+
},
|
|
105
97
|
});
|
|
106
98
|
// Format the plugin details
|
|
107
99
|
const plugin = response.data;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { makeWordPressRequest, getActiveSite } from '../wordpress.js';
|
|
1
|
+
import { makeWordPressRequest, getActiveSite, getSiteConfig } from '../wordpress.js';
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
@@ -73,7 +73,15 @@ export const schemaHandlers = {
|
|
|
73
73
|
}
|
|
74
74
|
} else {
|
|
75
75
|
// Fetch URL and extract ALL JSON-LD blocks
|
|
76
|
-
|
|
76
|
+
let resolvedUrl = url;
|
|
77
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
78
|
+
const config = getSiteConfig();
|
|
79
|
+
const baseUrl = config?.url?.replace(/\/wp-json.*$/, '').replace(/\/$/, '') || '';
|
|
80
|
+
if (baseUrl) {
|
|
81
|
+
resolvedUrl = baseUrl + (url.startsWith('/') ? url : '/' + url);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const response = await axios.get(resolvedUrl, { timeout: 15000 });
|
|
77
85
|
const html = response.data;
|
|
78
86
|
const jsonLdRegex = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
|
|
79
87
|
const allJsonLd = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { makeWordPressRequest, logToFile, getActiveSite } from '../wordpress.js';
|
|
1
|
+
import { makeWordPressRequest, logToFile, getActiveSite, getSiteConfig } from '../wordpress.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
// Site-aware cache for post types to reduce API calls
|
|
4
4
|
const postTypesCache = new Map();
|
|
@@ -35,7 +35,15 @@ function getContentEndpoint(contentType) {
|
|
|
35
35
|
// Helper function to parse URL and extract slug and potential post type hints
|
|
36
36
|
function parseUrl(url) {
|
|
37
37
|
try {
|
|
38
|
-
|
|
38
|
+
let fullUrl = url;
|
|
39
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
40
|
+
const config = getSiteConfig();
|
|
41
|
+
const baseUrl = config?.url?.replace(/\/wp-json.*$/, '').replace(/\/$/, '') || '';
|
|
42
|
+
if (baseUrl) {
|
|
43
|
+
fullUrl = baseUrl + (url.startsWith('/') ? url : '/' + url);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const urlObj = new URL(fullUrl);
|
|
39
47
|
const pathname = urlObj.pathname;
|
|
40
48
|
// Remove trailing slash and split path
|
|
41
49
|
const pathParts = pathname.replace(/\/$/, '').split('/').filter(Boolean);
|
|
@@ -101,8 +101,5 @@ export declare function hasSlackWebhook(siteId?: string): boolean;
|
|
|
101
101
|
export declare function getSlackWebhookUrl(siteId?: string): string;
|
|
102
102
|
export declare function hasSlackBot(siteId?: string): boolean;
|
|
103
103
|
export declare function makeSlackBotRequest(method: string, endpoint: string, data?: Record<string, any>, siteId?: string): Promise<any>;
|
|
104
|
-
/**
|
|
105
|
-
* Search the WordPress.org Plugin Repository
|
|
106
|
-
*/
|
|
107
104
|
export declare function searchWordPressPluginRepository(searchQuery: string, page?: number, perPage?: number): Promise<any>;
|
|
108
105
|
export {};
|
|
@@ -39,8 +39,6 @@ const bufSiteClients = new Map();
|
|
|
39
39
|
const sgSiteClients = new Map();
|
|
40
40
|
const plSiteClients = new Map();
|
|
41
41
|
const slackBotClients = new Map();
|
|
42
|
-
const liSiteClients = new Map();
|
|
43
|
-
const twSiteClients = new Map();
|
|
44
42
|
let activeSiteId = '';
|
|
45
43
|
const parsedSiteConfigs = new Map();
|
|
46
44
|
const MAX_CONCURRENT_PER_SITE = 5;
|
|
@@ -113,14 +111,6 @@ export async function initWordPress() {
|
|
|
113
111
|
await initSlackBotClient(site.id, site.slack_bot_token);
|
|
114
112
|
logToStderr(`Initialized Slack Bot for site: ${site.id}`);
|
|
115
113
|
}
|
|
116
|
-
if (site.linkedin_access_token) {
|
|
117
|
-
await initLinkedInClient(site.id, site.linkedin_access_token);
|
|
118
|
-
logToStderr(`Initialized LinkedIn for site: ${site.id}`);
|
|
119
|
-
}
|
|
120
|
-
if (site.twitter_bearer_token) {
|
|
121
|
-
await initTwitterClient(site.id, site.twitter_bearer_token);
|
|
122
|
-
logToStderr(`Initialized Twitter for site: ${site.id}`);
|
|
123
|
-
}
|
|
124
114
|
}
|
|
125
115
|
activeSiteId = defaultSite || sites[0].id;
|
|
126
116
|
logToStderr(`Active site: ${activeSiteId}`);
|
|
@@ -244,30 +234,6 @@ async function initSlackBotClient(id, botToken) {
|
|
|
244
234
|
});
|
|
245
235
|
slackBotClients.set(id, client);
|
|
246
236
|
}
|
|
247
|
-
async function initLinkedInClient(id, accessToken) {
|
|
248
|
-
const client = axios.create({
|
|
249
|
-
baseURL: 'https://api.linkedin.com/rest/',
|
|
250
|
-
headers: {
|
|
251
|
-
'Content-Type': 'application/json',
|
|
252
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
253
|
-
'LinkedIn-Version': '202401',
|
|
254
|
-
'X-Restli-Protocol-Version': '2.0.0',
|
|
255
|
-
},
|
|
256
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
257
|
-
});
|
|
258
|
-
liSiteClients.set(id, client);
|
|
259
|
-
}
|
|
260
|
-
async function initTwitterClient(id, bearerToken) {
|
|
261
|
-
const client = axios.create({
|
|
262
|
-
baseURL: 'https://api.twitter.com/2/',
|
|
263
|
-
headers: {
|
|
264
|
-
'Content-Type': 'application/json',
|
|
265
|
-
'Authorization': `Bearer ${bearerToken}`,
|
|
266
|
-
},
|
|
267
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
268
|
-
});
|
|
269
|
-
twSiteClients.set(id, client);
|
|
270
|
-
}
|
|
271
237
|
// ── Site Management ──────────────────────────────────────────────────
|
|
272
238
|
/**
|
|
273
239
|
* Get the active site's client, or a specific site's client
|
|
@@ -681,73 +647,27 @@ export async function makeSlackBotRequest(method, endpoint, data, siteId) {
|
|
|
681
647
|
limiter.release();
|
|
682
648
|
}
|
|
683
649
|
}
|
|
684
|
-
// ── LinkedIn Interface ──────────────────────────────────────────
|
|
685
|
-
export function hasLinkedIn(siteId) {
|
|
686
|
-
const id = siteId || activeSiteId;
|
|
687
|
-
return liSiteClients.has(id);
|
|
688
|
-
}
|
|
689
|
-
export async function makeLinkedInRequest(method, endpoint, data, siteId) {
|
|
690
|
-
const id = siteId || activeSiteId;
|
|
691
|
-
const client = liSiteClients.get(id);
|
|
692
|
-
if (!client) {
|
|
693
|
-
throw new Error(`LinkedIn not configured for site "${id}". Add linkedin_access_token to WP_SITES_CONFIG.`);
|
|
694
|
-
}
|
|
695
|
-
const limiter = getLimiter(id);
|
|
696
|
-
await limiter.acquire();
|
|
697
|
-
try {
|
|
698
|
-
const response = await client.request({ method, url: endpoint, data: method !== 'GET' ? data : undefined, params: method === 'GET' ? data : undefined });
|
|
699
|
-
return response.data;
|
|
700
|
-
}
|
|
701
|
-
finally {
|
|
702
|
-
limiter.release();
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
export function getLinkedInPersonUrn(siteId) {
|
|
706
|
-
const id = siteId || activeSiteId;
|
|
707
|
-
const sites = JSON.parse(process.env.WP_SITES_CONFIG || '[]');
|
|
708
|
-
const site = sites.find((s) => s.id === id);
|
|
709
|
-
if (!site?.linkedin_person_urn) {
|
|
710
|
-
throw new Error(`LinkedIn person URN not configured for site "${id}". Add linkedin_person_urn to WP_SITES_CONFIG.`);
|
|
711
|
-
}
|
|
712
|
-
return site.linkedin_person_urn;
|
|
713
|
-
}
|
|
714
|
-
// ── Twitter/X Interface ─────────────────────────────────────────
|
|
715
|
-
export function hasTwitter(siteId) {
|
|
716
|
-
const id = siteId || activeSiteId;
|
|
717
|
-
return twSiteClients.has(id);
|
|
718
|
-
}
|
|
719
|
-
export async function makeTwitterRequest(method, endpoint, data, siteId) {
|
|
720
|
-
const id = siteId || activeSiteId;
|
|
721
|
-
const client = twSiteClients.get(id);
|
|
722
|
-
if (!client) {
|
|
723
|
-
throw new Error(`Twitter not configured for site "${id}". Add twitter_bearer_token to WP_SITES_CONFIG.`);
|
|
724
|
-
}
|
|
725
|
-
const limiter = getLimiter(id);
|
|
726
|
-
await limiter.acquire();
|
|
727
|
-
try {
|
|
728
|
-
const response = await client.request({ method, url: endpoint, data: method !== 'GET' ? data : undefined, params: method === 'GET' ? data : undefined });
|
|
729
|
-
return response.data;
|
|
730
|
-
}
|
|
731
|
-
finally {
|
|
732
|
-
limiter.release();
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
export function getTwitterUserId(siteId) {
|
|
736
|
-
const id = siteId || activeSiteId;
|
|
737
|
-
const sites = JSON.parse(process.env.WP_SITES_CONFIG || '[]');
|
|
738
|
-
const site = sites.find((s) => s.id === id);
|
|
739
|
-
if (!site?.twitter_user_id) {
|
|
740
|
-
throw new Error(`Twitter user ID not configured for site "${id}". Add twitter_user_id to WP_SITES_CONFIG.`);
|
|
741
|
-
}
|
|
742
|
-
return site.twitter_user_id;
|
|
743
|
-
}
|
|
744
650
|
// ── Plugin Repository (External API) ────────────────────────────────
|
|
745
651
|
/**
|
|
746
652
|
* Search the WordPress.org Plugin Repository
|
|
747
653
|
*/
|
|
654
|
+
// Flatten nested objects into PHP-style bracket notation for query params
|
|
655
|
+
function flattenParams(obj, prefix = '') {
|
|
656
|
+
const result = {};
|
|
657
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
658
|
+
const fullKey = prefix ? `${prefix}[${key}]` : key;
|
|
659
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
660
|
+
Object.assign(result, flattenParams(value, fullKey));
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
result[fullKey] = String(value);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
748
668
|
export async function searchWordPressPluginRepository(searchQuery, page = 1, perPage = 10) {
|
|
749
669
|
const apiUrl = 'https://api.wordpress.org/plugins/info/1.2/';
|
|
750
|
-
const
|
|
670
|
+
const params = flattenParams({
|
|
751
671
|
action: 'query_plugins',
|
|
752
672
|
request: {
|
|
753
673
|
search: searchQuery,
|
|
@@ -766,9 +686,7 @@ export async function searchWordPressPluginRepository(searchQuery, page = 1, per
|
|
|
766
686
|
tags: true,
|
|
767
687
|
},
|
|
768
688
|
},
|
|
769
|
-
};
|
|
770
|
-
const response = await axios.post(apiUrl, requestData, {
|
|
771
|
-
headers: { 'Content-Type': 'application/json' },
|
|
772
689
|
});
|
|
690
|
+
const response = await axios.get(apiUrl, { params });
|
|
773
691
|
return response.data;
|
|
774
692
|
}
|
|
@@ -97,6 +97,15 @@ See `references/traffic-attribution.md`
|
|
|
97
97
|
- Combining analytics with WooCommerce conversion data
|
|
98
98
|
- Discrepancy analysis between platforms
|
|
99
99
|
|
|
100
|
+
### Section 7: Signal Feed Generation (Content Intelligence)
|
|
101
|
+
See `references/signals-feed-schema.md`
|
|
102
|
+
- Generating `.content-state/signals-feed.md` from analytics data
|
|
103
|
+
- NormalizedEvent format for GenSignal compatibility
|
|
104
|
+
- Delta calculation against previous period
|
|
105
|
+
- Anomaly detection with configurable threshold (default ±30%)
|
|
106
|
+
- Pattern matching: Search Intent Shift, Early-Adopter Surge, Hype→Utility Crossover
|
|
107
|
+
- Integration with wp-content-pipeline (Phase 1) and wp-editorial-planner (Phase 3)
|
|
108
|
+
|
|
100
109
|
## Reference Files
|
|
101
110
|
|
|
102
111
|
| File | Content |
|
|
@@ -106,6 +115,147 @@ See `references/traffic-attribution.md`
|
|
|
106
115
|
| `references/cwv-monitoring.md` | CWV metric definitions, thresholds, PageSpeed/CrUX APIs |
|
|
107
116
|
| `references/analytics-dashboards.md` | Dashboard patterns, report templates, KPIs, combined data |
|
|
108
117
|
| `references/traffic-attribution.md` | UTM params, source/medium, GA4 + WooCommerce conversions |
|
|
118
|
+
| `references/signals-feed-schema.md` | NormalizedEvent format, delta rules, pattern matching, anomaly detection |
|
|
119
|
+
|
|
120
|
+
## Signal Feed Generation Workflow
|
|
121
|
+
|
|
122
|
+
### When to Use
|
|
123
|
+
|
|
124
|
+
- User asks to "generate signals", "analyze performance and create signals", "run content intelligence"
|
|
125
|
+
- User wants to understand which analytics trends are actionable
|
|
126
|
+
- User mentions GenSignal integration or NormalizedEvent
|
|
127
|
+
- After running a standard analytics report (Sections 1-6), user wants structured output for strategic planning
|
|
128
|
+
|
|
129
|
+
### Prerequisites
|
|
130
|
+
|
|
131
|
+
1. At least one analytics service configured (GA4, Plausible, or GSC)
|
|
132
|
+
2. A `.content-state/{site_id}.config.md` exists for the target site
|
|
133
|
+
3. Historical data for at least 2 periods (to calculate deltas)
|
|
134
|
+
|
|
135
|
+
### Step 7: GENERATE SIGNAL FEED
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
COLLECT → BASELINE → NORMALIZE → DELTA → ANOMALY → PATTERN → WRITE
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**7.1 COLLECT — Gather current period data**
|
|
142
|
+
|
|
143
|
+
Call the following MCP tools for the requested period (default: last 30 days):
|
|
144
|
+
|
|
145
|
+
| Tool | Data Collected | Entity Type |
|
|
146
|
+
|------|---------------|-------------|
|
|
147
|
+
| `ga4_top_pages` | Top 20 pages by pageviews, sessions, engagement time | Page |
|
|
148
|
+
| `ga4_traffic_sources` | Source/medium breakdown with sessions, bounce rate | Source |
|
|
149
|
+
| `ga4_report` | Site-level aggregate: total sessions, pageviews, conversions | Site |
|
|
150
|
+
| `gsc_search_analytics` | Top 20 keywords by impressions, clicks, CTR, position | Keyword |
|
|
151
|
+
| `pl_aggregate` | If Plausible configured: visitors, pageviews, bounce_rate | Site (cross-validate) |
|
|
152
|
+
| `cwv_crux_origin` | If CrUX available: LCP, CLS, INP, FCP, TTFB | Site |
|
|
153
|
+
|
|
154
|
+
Not all tools need to succeed. Generate events only from tools that return data. Record which tools contributed in `source_tools` frontmatter.
|
|
155
|
+
|
|
156
|
+
**7.2 BASELINE — Load comparison period data**
|
|
157
|
+
|
|
158
|
+
Read the existing `.content-state/signals-feed.md` if present. Extract the `period` and events to use as baseline for delta calculation.
|
|
159
|
+
|
|
160
|
+
If no previous feed exists:
|
|
161
|
+
- Call the same tools with date range offset by the period length (e.g., if current period = Feb, baseline = Jan)
|
|
162
|
+
- If baseline tools fail: proceed without deltas (omit `delta_pct` fields)
|
|
163
|
+
|
|
164
|
+
Record the comparison period in `comparison_period` frontmatter field.
|
|
165
|
+
|
|
166
|
+
**7.3 NORMALIZE — Map to NormalizedEvent format**
|
|
167
|
+
|
|
168
|
+
For each data point from 7.1, create a NormalizedEvent:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
- entity_id: "{EntityType}:{identifier}"
|
|
172
|
+
relation: "{metric_name}"
|
|
173
|
+
value: {numeric_value}
|
|
174
|
+
unit: "{count|seconds|percentage|position}"
|
|
175
|
+
ts: "{period_end_ISO8601}"
|
|
176
|
+
provenance:
|
|
177
|
+
source_id: "{mcp_tool_name}"
|
|
178
|
+
site: "{site_id}"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Entity ID mapping:**
|
|
182
|
+
- GA4 top pages → `Page:{page_path}`
|
|
183
|
+
- GA4 traffic sources → `Source:{source_name}`
|
|
184
|
+
- GA4 site aggregates → `Site:{site_id}`
|
|
185
|
+
- GSC keywords → `Keyword:{query}`
|
|
186
|
+
- CWV metrics → `Site:{site_id}` with relation = metric name
|
|
187
|
+
|
|
188
|
+
**Relation mapping:**
|
|
189
|
+
- GA4 `screenPageViews` → `pageviews`
|
|
190
|
+
- GA4 `sessions` → `sessions` (page) or `total_sessions` (site)
|
|
191
|
+
- GA4 `averageSessionDuration` → `avg_engagement_time`
|
|
192
|
+
- GA4 `bounceRate` → `bounce_rate`
|
|
193
|
+
- GSC `impressions` → `search_impressions`
|
|
194
|
+
- GSC `clicks` → `search_clicks`
|
|
195
|
+
- GSC `ctr` → `search_ctr`
|
|
196
|
+
- GSC `position` → `search_position`
|
|
197
|
+
- CWV metrics → lowercase (e.g., `lcp`, `cls`, `inp`)
|
|
198
|
+
|
|
199
|
+
All CWV time-based metrics are normalized to seconds. API values in milliseconds should be divided by 1000.
|
|
200
|
+
|
|
201
|
+
**7.4 DELTA — Calculate percentage changes**
|
|
202
|
+
|
|
203
|
+
For each NormalizedEvent, find the matching baseline event (same `entity_id` + `relation`):
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
delta_pct = round(((current_value - baseline_value) / baseline_value) * 100)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Edge cases:
|
|
210
|
+
- Baseline = 0, current > 0 → `delta_pct: +999`
|
|
211
|
+
- Both = 0 → `delta_pct: 0`
|
|
212
|
+
- No baseline match → omit `delta_pct`
|
|
213
|
+
|
|
214
|
+
**7.5 ANOMALY — Identify significant changes**
|
|
215
|
+
|
|
216
|
+
Read `anomaly_threshold` from feed config (default: 30). Filter events where `|delta_pct| >= anomaly_threshold`.
|
|
217
|
+
|
|
218
|
+
**7.6 PATTERN — Match GenSignal patterns**
|
|
219
|
+
|
|
220
|
+
Check each anomaly against 3 detectable patterns:
|
|
221
|
+
|
|
222
|
+
**Search Intent Shift:**
|
|
223
|
+
- Entity is `Keyword:*`
|
|
224
|
+
- Conditions (any): `search_ctr` delta ≥ +20% with `search_position` delta ≤ +5%, OR `search_impressions` delta ≥ +50% on keywords with commercial modifiers, OR `search_impressions` delta ≥ +100% on any keyword
|
|
225
|
+
- Action: "Investigate: content cluster opportunity"
|
|
226
|
+
|
|
227
|
+
**Early-Adopter Surge:**
|
|
228
|
+
- Entity is `Source:*`
|
|
229
|
+
- Conditions: `referral_sessions` delta ≥ +50% AND site-level `total_sessions` delta < +20%
|
|
230
|
+
- Action: "Scale: increase posting frequency on {source}"
|
|
231
|
+
|
|
232
|
+
**Hype→Utility Crossover:**
|
|
233
|
+
- Entity is `Page:*`
|
|
234
|
+
- Conditions: `avg_engagement_time` delta ≥ +15% AND `bounce_rate` delta ≤ -10% AND `pageviews` delta between -20% and +10%
|
|
235
|
+
- Action: "Shift: add conversion touchpoints to {page}"
|
|
236
|
+
|
|
237
|
+
No pattern match → "Unclassified anomaly" / "Review: investigate cause"
|
|
238
|
+
|
|
239
|
+
**7.7 WRITE — Generate signals-feed.md**
|
|
240
|
+
|
|
241
|
+
Write `.content-state/signals-feed.md` with YAML frontmatter and organized body sections. See `references/signals-feed-schema.md` for exact format.
|
|
242
|
+
|
|
243
|
+
**After writing**, present summary to user:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
Signal Feed generato per {site_id}:
|
|
247
|
+
- Periodo: {period}
|
|
248
|
+
- Eventi normalizzati: {count}
|
|
249
|
+
- Anomalie rilevate: {anomaly_count}
|
|
250
|
+
- Pattern riconosciuti: {pattern_list}
|
|
251
|
+
|
|
252
|
+
Anomalie principali:
|
|
253
|
+
1. {entity} — {relation} {delta}% → {pattern}: {action}
|
|
254
|
+
2. ...
|
|
255
|
+
|
|
256
|
+
Per approfondire un segnale con GenSignal: "approfondisci il segnale N con GenSignal"
|
|
257
|
+
Per creare brief dai segnali: "crea brief per i segnali azionabili"
|
|
258
|
+
```
|
|
109
259
|
|
|
110
260
|
## MCP Tools
|
|
111
261
|
|
|
@@ -136,6 +286,7 @@ Use the **`wp-monitoring-agent`** for automated analytics reporting, performance
|
|
|
136
286
|
- **`wp-content-optimization`** — use analytics data to prioritize content optimization efforts
|
|
137
287
|
- **`wp-content-attribution`** — track content sources and attribute traffic to specific campaigns
|
|
138
288
|
- **`wp-monitoring`** — monitor site uptime and health alongside analytics performance
|
|
289
|
+
- **`wp-content-pipeline`** — use signal insights to create content briefs for publishing
|
|
139
290
|
|
|
140
291
|
## Cross-references
|
|
141
292
|
|
|
@@ -143,6 +294,8 @@ Use the **`wp-monitoring-agent`** for automated analytics reporting, performance
|
|
|
143
294
|
- CWV monitoring feeds into `wp-performance` for technical optimization
|
|
144
295
|
- Traffic attribution connects to `wp-social-email` for campaign tracking
|
|
145
296
|
- Dashboard patterns support `wp-monitoring` alerting workflows
|
|
297
|
+
- Signal feed generation bridges to `wp-content-pipeline` for data-driven content creation
|
|
298
|
+
- GenSignal integration: signals-feed.md is the exchange format between wp-analytics and GenSignal pattern detection
|
|
146
299
|
|
|
147
300
|
## Troubleshooting
|
|
148
301
|
|