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.
Files changed (62) hide show
  1. package/.claude-plugin/plugin.json +8 -3
  2. package/CHANGELOG.md +94 -2
  3. package/agents/wp-accessibility-auditor.md +1 -1
  4. package/agents/wp-content-strategist.md +2 -2
  5. package/agents/wp-deployment-engineer.md +1 -1
  6. package/agents/wp-distribution-manager.md +1 -1
  7. package/agents/wp-monitoring-agent.md +1 -1
  8. package/agents/wp-performance-optimizer.md +1 -1
  9. package/agents/wp-security-auditor.md +1 -1
  10. package/agents/wp-site-manager.md +3 -3
  11. package/commands/wp-setup.md +2 -2
  12. package/docs/GUIDE.md +260 -21
  13. package/docs/VALIDATION.md +341 -0
  14. package/docs/guides/wp-ecommerce.md +4 -4
  15. package/docs/plans/2026-03-01-tier3-wcop-implementation.md +1 -1
  16. package/docs/plans/2026-03-01-tier4-5-implementation.md +1 -1
  17. package/docs/plans/2026-03-02-content-framework-architecture.md +612 -0
  18. package/docs/plans/2026-03-02-content-framework-strategic-reflections.md +228 -0
  19. package/docs/plans/2026-03-02-content-intelligence-phase2.md +560 -0
  20. package/docs/plans/2026-03-02-content-pipeline-phase1.md +456 -0
  21. package/docs/plans/2026-03-02-dashboard-kanban-design.md +761 -0
  22. package/docs/plans/2026-03-02-dashboard-kanban-implementation.md +598 -0
  23. package/docs/plans/2026-03-02-dashboard-strategy.md +363 -0
  24. package/docs/plans/2026-03-02-editorial-calendar-phase3.md +490 -0
  25. package/docs/validation/.gitkeep +0 -0
  26. package/docs/validation/dashboard.html +286 -0
  27. package/docs/validation/results.json +1705 -0
  28. package/package.json +16 -3
  29. package/scripts/context-scanner.mjs +446 -0
  30. package/scripts/dashboard-renderer.mjs +553 -0
  31. package/scripts/run-validation.mjs +1132 -0
  32. package/servers/wp-rest-bridge/build/server.js +17 -6
  33. package/servers/wp-rest-bridge/build/tools/index.js +0 -9
  34. package/servers/wp-rest-bridge/build/tools/plugin-repository.js +23 -31
  35. package/servers/wp-rest-bridge/build/tools/schema.js +10 -2
  36. package/servers/wp-rest-bridge/build/tools/unified-content.js +10 -2
  37. package/servers/wp-rest-bridge/build/wordpress.d.ts +0 -3
  38. package/servers/wp-rest-bridge/build/wordpress.js +16 -98
  39. package/servers/wp-rest-bridge/package.json +1 -0
  40. package/skills/wp-analytics/SKILL.md +153 -0
  41. package/skills/wp-analytics/references/signals-feed-schema.md +417 -0
  42. package/skills/wp-content/references/content-templates.md +1 -1
  43. package/skills/wp-content/references/seo-optimization.md +8 -8
  44. package/skills/wp-content-attribution/references/roi-calculation.md +1 -1
  45. package/skills/wp-content-attribution/references/utm-tracking-setup.md +5 -5
  46. package/skills/wp-content-generation/references/generation-workflow.md +2 -2
  47. package/skills/wp-content-pipeline/SKILL.md +461 -0
  48. package/skills/wp-content-pipeline/references/content-brief-schema.md +377 -0
  49. package/skills/wp-content-pipeline/references/site-config-schema.md +431 -0
  50. package/skills/wp-content-repurposing/references/auto-transform-pipeline.md +1 -1
  51. package/skills/wp-content-repurposing/references/email-newsletter.md +1 -1
  52. package/skills/wp-content-repurposing/references/platform-specs.md +2 -2
  53. package/skills/wp-content-repurposing/references/transform-templates.md +27 -27
  54. package/skills/wp-dashboard/SKILL.md +121 -0
  55. package/skills/wp-deploy/references/ssh-deploy.md +2 -2
  56. package/skills/wp-editorial-planner/SKILL.md +262 -0
  57. package/skills/wp-editorial-planner/references/editorial-schema.md +268 -0
  58. package/skills/wp-multilang-network/references/content-sync.md +3 -3
  59. package/skills/wp-multilang-network/references/network-architecture.md +1 -1
  60. package/skills/wp-multilang-network/references/seo-international.md +7 -7
  61. package/skills/wp-structured-data/references/schema-types.md +4 -4
  62. 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., "opencactus", "bioinagro")') }, async (args) => {
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': zodType = z.string(); break;
40
- case 'number': case 'integer': zodType = z.number(); break;
41
- case 'boolean': zodType = z.boolean(); break;
42
- case 'array': zodType = z.array(z.any()); break;
43
- case 'object': zodType = z.record(z.any()); break;
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
- const requestData = {
76
- action: 'plugin_information',
77
- request: {
78
- slug: params.slug,
79
- fields: {
80
- description: true,
81
- sections: true,
82
- tested: true,
83
- requires: true,
84
- rating: true,
85
- ratings: true,
86
- downloaded: true,
87
- downloadlink: true,
88
- last_updated: true,
89
- homepage: true,
90
- tags: true,
91
- compatibility: true,
92
- author: true,
93
- contributors: true,
94
- banners: true,
95
- icons: true
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
- const response = await axios.get(url, { timeout: 15000 });
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
- const urlObj = new URL(url);
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 requestData = {
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
  }
@@ -21,6 +21,7 @@
21
21
  "zod": "^3.24.2"
22
22
  },
23
23
  "devDependencies": {
24
+ "@clack/prompts": "^1.0.1",
24
25
  "@types/node": "^22.10.0",
25
26
  "tsx": "^4.19.0",
26
27
  "typescript": "^5.7.0"
@@ -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