Trendplot Connector WP Registry grade A. No security issues foundWP RegistryNo security issues foundOpen the reportA
Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/magpern/trendplotconnector/releases/download/v1.0.1/trendplot-connector-1.0.1.zipWordPress plugin that acts as a write-first content bridge between Trendplot and WordPress. Trendplot discovers content through crawling; this plugin does what crawling cannot: push drafts into WordPress, store Trendplot metadata as persistent post meta, and maintain product/article relationships across sync cycles.
1. Installation
# Clone the repository to your development machine
git clone https://github.com/magpern/TrendplotConnector.git \
/home/magpern/plugin-development/TrendplotConnector
# Deploy to WordPress (run from development machine)
rsync -av --delete --exclude='.git' \
/home/magpern/plugin-development/TrendplotConnector/ \
/home/magpern/woocommerce/wp-content/plugins/trendplot-connector/
Warning: The
--deleteflag removes any files in the deploy target that are not in the repository. Never edit the deploy directory directly.
2. Activation
# From /home/magpern/woocommerce/
docker compose run --rm wpcli wp plugin activate trendplot-connector
docker compose run --rm wpcli wp rewrite flush
3. Configuration
Navigate to Trendplot → Settings in the WordPress admin sidebar.
| Field | Description |
|---|---|
| Enable Connector | Master on/off switch for the REST API |
| Site ID | Unique identifier for this site; must match the X-Trendplot-Site-Id header sent by Trendplot |
| Shared Secret | HMAC-SHA256 signing secret; use the Generate button or enter manually |
| Allowed Origin | Base URL of the Trendplot service (reserved for future CORS enforcement) |
Generate Secret — Click to create a new cryptographically random 64-character secret. The secret is stored in wp_options and is never written to any file.
Test Connection — Opens the /health endpoint in a new browser tab.
4. Endpoint Overview
All routes use namespace trendplot/v1 under /wp-json/.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health |
Conditional¹ | Connectivity check |
| GET | /site-info |
Required | WordPress/WooCommerce version info |
| GET | /categories |
Required | List all post categories |
| GET | /tags |
Required | List all post tags |
| GET | /drafts |
Required | List Trendplot-created drafts |
| POST | /drafts |
Required | Create a draft post |
| GET | /drafts/{id} |
Required | Retrieve current status of a Trendplot-managed post |
| PATCH | /drafts/{id} |
Required | Update a Trendplot-created draft |
| GET | /posts/{id}/seo |
Required | Read current Rank Math SEO values for any Trendplot-managed post |
| PATCH | /posts/{id}/seo |
Required | Update Rank Math SEO for any Trendplot-managed post (published or draft) |
| POST | /posts/{id}/publish |
Required | Publish a Trendplot-managed draft |
| POST | /posts/{id}/unpublish |
Required | Revert a published Trendplot-managed post to draft |
| PATCH | /posts/{id}/meta |
Required | Write Trendplot metadata to any post |
¹ If no shared secret is configured, /health is public. Once a secret is configured, HMAC auth is required.
5. HMAC Signing
All authenticated requests must include three headers:
X-Trendplot-Site-Id: <your-site-id>
X-Trendplot-Timestamp: <unix-timestamp>
X-Trendplot-Signature: <hmac-hex>
The signature is HMAC-SHA256 over the following string:
METHOD\n
/wp-json/trendplot/v1/path\n
timestamp\n
body
For GET requests, body is an empty string. For POST/PATCH, body is the exact JSON string as sent (do not re-serialize).
Bash signing example (GET):
SITE_ID="YOUR_SITE_ID"
SECRET="YOUR_SECRET_HERE"
METHOD="GET"
ENDPOINT="/wp-json/trendplot/v1/site-info"
TS=$(date +%s)
BODY=""
SIG=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$ENDPOINT" "$TS" "$BODY" | \
openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -s \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
"https://example.com${ENDPOINT}"
Bash signing example (POST with body):
METHOD="POST"
ENDPOINT="/wp-json/trendplot/v1/drafts"
TS=$(date +%s)
BODY='{"title":"Test Article","content":"<p>Content here.</p>","trendplot_article_id":"art_001"}'
SIG=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$ENDPOINT" "$TS" "$BODY" | \
openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -s -X POST \
-H "Content-Type: application/json" \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
-d "$BODY" \
"https://example.com${ENDPOINT}"
The timestamp must be within 5 minutes of the server clock. Sign the body bytes exactly as sent — do not re-serialize or reformat the JSON.
6. Creating a Draft
METHOD="POST"
ENDPOINT="/wp-json/trendplot/v1/drafts"
TS=$(date +%s)
BODY='{
"title": "Why BPC-157 and TB-500 Are Often Discussed Together",
"content": "<p>BPC-157 and TB-500 are two peptides...</p>",
"excerpt": "An overview of why these two peptides are frequently discussed together.",
"slug": "bpc-157-tb-500-research-overview",
"categories": [12, 34],
"tags": [5, 8],
"trendplot_article_id": "art_abc123",
"trendplot_source": "campaign_xyz",
"trendplot_generated": "2026-06-07T12:00:00Z",
"related_products": [101, 102],
"related_articles": [],
"seo": {
"title": "BPC-157 and TB-500 Research Overview | Example Site",
"description": "A research overview of the BPC-157 and TB-500 peptides.",
"focus_keyword": "bpc-157 tb-500 research",
"canonical_url": "https://example.com/bpc-157-tb-500/",
"robots": [],
"schema_type": "Article"
}
}'
# ... compute SIG as above ...
curl -s -X POST \
-H "Content-Type: application/json" \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
-d "$BODY" \
"https://example.com${ENDPOINT}"
Response (201 Created):
{
"id": 200,
"title": "Why BPC-157 and TB-500 Are Often Discussed Together",
"slug": "",
"status": "draft",
"url": "https://example.com/?p=200",
"edit_url": "https://example.com/wp-admin/post.php?post=200&action=edit",
"created_at": "2026-06-07T12:00:00+00:00",
"trendplot_article_id": "art_abc123",
"seo_status": "written"
}
Idempotency: If a draft with the same trendplot_article_id already exists, the endpoint returns 409 Conflict with the existing post ID. Trendplot must explicitly decide to update the existing draft (Phase 2) rather than creating a duplicate.
Required fields: title, content. All other fields are optional.
slug (optional): Sets post_name (the URL slug) via sanitize_title(). If omitted, WordPress generates a slug from the title. Two draft posts may share the same slug — WordPress only enforces slug uniqueness at publish time (appending -2, -3, etc.).
SEO fields (seo object, optional — requires Rank Math SEO active):
| Field | Type | Constraint | Description |
|---|---|---|---|
title |
string | max 300 chars | SEO title (rank_math_title) |
description |
string | max 500 chars | Meta description (rank_math_description) |
focus_keyword |
string | max 300 chars | Focus keyword (rank_math_focus_keyword) |
canonical_url |
string (URL) | valid URL or "" |
Canonical URL (rank_math_canonical_url) |
robots |
string[] | see whitelist | Robots directives (rank_math_robots) |
schema_type |
string | see whitelist | Rich snippet type (rank_math_rich_snippet) |
Omit seo entirely to skip SEO writes. Pass seo: {} to no-op. An empty string for any string field deletes that meta value. An empty robots array deletes the stored value.
seo_status values:
| Value | Meaning |
|---|---|
"written" |
Rank Math active; SEO fields written |
"skipped" |
Rank Math not active; SEO fields ignored |
"none" |
No seo key in the request |
7. Updating a Draft
Use PATCH /drafts/{id} to update an existing Trendplot-created draft. All request fields are optional; supply only what needs changing. At least one updateable field must be present.
Allowed target posts:
| Condition | Result |
|---|---|
| Post does not exist | 404 not_found |
Post has no _trendplot_article_id |
403 not_trendplot_draft |
Post is publish or private |
409 published_post_rejected |
Post is draft, pending, or future |
✅ Allowed |
Request body (all fields optional; at least one required):
{
"title": "Updated article title",
"content": "<p>Updated HTML content.</p>",
"excerpt": "Updated excerpt.",
"slug": "updated-article-slug",
"categories": [12, 34],
"tags": [5, 8],
"trendplot_article_id": "art_abc123",
"trendplot_source": "campaign_xyz",
"trendplot_generated": "2026-06-07T12:00:00Z",
"trendplot_last_sync": "2026-06-07T12:00:00Z",
"related_products": [101, 102],
"related_articles": [200, 201],
"seo": {
"title": "Updated SEO title | Example Site",
"description": "Updated meta description.",
"robots": ["noindex"]
}
}
slug (optional): Updates post_name via sanitize_title(). Only applies to draft, pending, and future posts — published posts are rejected entirely by this endpoint. If the slug is already taken by a published post, WordPress appends -2, -3, etc. at publish time; two drafts may share the same slug without conflict. The updated slug is reflected in the response slug field.
Endpoint scope: PATCH /drafts/{id} is the right endpoint for updating article body, title, excerpt, and slug. Use PATCH /posts/{id}/seo exclusively for Rank Math SEO field updates on already-published posts — that endpoint never touches content, title, excerpt, or slug.
SEO field rules for PATCH: Same fields and validation as POST /drafts. Only the fields present in the seo object are written — omitted fields are left unchanged. Pass "" to delete a stored value; pass [] to delete stored robots. All validation runs before any WordPress write; on failure the post is left completely unchanged.
Identity rule: If trendplot_article_id is supplied in the body, it must match the _trendplot_article_id already on the post. A mismatch returns 409 article_id_mismatch to prevent accidentally updating the wrong draft.
_trendplot_last_sync is always written. If not supplied in the body, it is set to the current UTC timestamp automatically.
Elementor behavior: If content is updated, _elementor_data is rebuilt from the new HTML using the same boxed container structure as POST /drafts. The Full Width template assignment and all Elementor meta keys are preserved.
Bash example (PATCH with new title and content):
SITE_ID="YOUR_SITE_ID"
SECRET="YOUR_SECRET_HERE"
DRAFT_ID=200
METHOD="PATCH"
ENDPOINT="/wp-json/trendplot/v1/drafts/${DRAFT_ID}"
TS=$(date +%s)
BODY='{
"title": "Why BPC-157 and TB-500 Are Often Studied Together — Updated",
"content": "<p>Updated research overview.</p>",
"trendplot_article_id": "art_abc123"
}'
SIG=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$ENDPOINT" "$TS" "$BODY" | \
openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -s -X PATCH \
-H "Content-Type: application/json" \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
-d "$BODY" \
"https://example.com${ENDPOINT}"
Response (200 OK):
{
"id": 200,
"title": "Why BPC-157 and TB-500 Are Often Studied Together — Updated",
"slug": "why-bpc-157-and-tb-500-are-often-studied-together",
"status": "draft",
"url": "https://example.com/?p=200",
"edit_url": "https://example.com/wp-admin/post.php?post=200&action=edit",
"modified_at": "2026-06-07T12:05:00+00:00",
"trendplot_article_id": "art_abc123",
"updated": true,
"seo_status": "written"
}
Error codes:
| Code | HTTP | Meaning |
|---|---|---|
not_found |
404 | Post ID does not exist |
not_trendplot_draft |
403 | Post has no _trendplot_article_id — not created by Trendplot |
published_post_rejected |
409 | Post is published or private — cannot be updated via this endpoint |
article_id_mismatch |
409 | Supplied trendplot_article_id does not match the post's stored value |
validation_error |
400 | Invalid category/tag ID, invalid product ID, empty body, etc. |
content_too_large |
413 | Content exceeds 200,000 characters |
8. Retrieving Draft Status
Use GET /drafts/{id} to check the current WordPress status of a post that Trendplot created or manages. This is the primary way to detect whether a human editor has published, moved to pending, or otherwise changed the state of a Trendplot-created post.
Rules:
| Condition | Result |
|---|---|
| Post does not exist | 404 not_found |
Post has no _trendplot_article_id |
403 not_trendplot_post |
Post is trash, private, or other non-standard status |
404 not_found |
Post is draft, pending, future, or publish |
✅ 200 OK |
No request body is required — HMAC auth headers only.
Bash example:
SITE_ID="YOUR_SITE_ID"
SECRET="YOUR_SECRET_HERE"
DRAFT_ID=200
METHOD="GET"
ENDPOINT="/wp-json/trendplot/v1/drafts/${DRAFT_ID}"
TS=$(date +%s)
BODY=""
SIG=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$ENDPOINT" "$TS" "$BODY" | \
openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -s \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
"https://example.com${ENDPOINT}"
Response (200 OK):
{
"id": 200,
"title": "Why BPC-157 and TB-500 Are Often Studied Together",
"status": "draft",
"url": "https://example.com/?p=200",
"edit_url": "https://example.com/wp-admin/post.php?post=200&action=edit",
"modified_at": "2026-06-07T12:05:00+00:00",
"trendplot_article_id": "art_abc123",
"trendplot_last_sync": "2026-06-07T12:00:00+00:00",
"related_products": [101, 102],
"related_articles": [],
"seo": {
"title": "BPC-157 and TB-500 Research Overview | Example Site",
"description": "A research overview of the BPC-157 and TB-500 peptides.",
"focus_keyword": "bpc-157 tb-500 research",
"canonical_url": "https://example.com/bpc-157-tb-500/",
"robots": [],
"schema_type": "Article"
}
}
Notes:
- Post
contentandexcerptare never returned — this endpoint exposes status and metadata only. related_productsandrelated_articlesare returned as arrays (empty array[]if not set).trendplot_last_syncisnullif never written.- A
statusof"publish"means a human editor has published the post in WordPress admin. seocontains current Rank Math meta for the post. Fields with no stored value arenull;robotsis always an array.seoisnull(not an object) when Rank Math is not active on the site.
Error codes:
| Code | HTTP | Meaning |
|---|---|---|
not_found |
404 | Post ID does not exist or is not in a supported status |
not_trendplot_post |
403 | Post has no _trendplot_article_id — not managed by Trendplot |
Verification checklist:
- [ ]
GET /drafts/<nonexistent-id>→404 not_found - [ ]
GET /drafts/<id-without-trendplot-meta>→403 not_trendplot_post - [ ]
GET /drafts/<valid-trendplot-draft-id>→200with correct fields, nocontent - [ ]
GET /drafts/<published-trendplot-post-id>→200with"status": "publish" - [ ] Response contains
related_productsandrelated_articlesas arrays - [ ] No
content,excerpt, or customer data in response
9. Dedicated SEO Endpoint
Use PATCH /posts/{id}/seo to update Rank Math SEO metadata on any Trendplot-managed post — including published posts. Use GET /posts/{id}/seo to read the current values. Neither endpoint touches content, title, excerpt, Elementor data, categories, or tags.
Allowed posts
| Condition | Result |
|---|---|
| Post does not exist | 404 not_found |
Post is trash |
404 not_found |
Post has no _trendplot_article_id |
403 not_trendplot_post |
Post is draft, pending, future, or publish |
✅ Allowed |
Unlike PATCH /drafts/{id}, published posts are accepted.
Supported SEO fields
| Field | Type | Notes |
|---|---|---|
title |
string (max 300) | SEO title tag |
description |
string (max 500) | Meta description |
focus_keyword |
string (max 300) | Primary keyword |
canonical_url |
URL string or "" |
Empty string deletes |
robots |
string array | See robots whitelist in Section 16 |
schema_type |
string | off, Article, NewsArticle, BlogPosting |
All fields are optional per request. An explicit empty string deletes the stored value. An explicit empty robots array ([]) deletes the stored robots directive. Unknown keys return 400 validation_error.
Rank Math inactive behavior
When Rank Math is not active (rank_math_active: false):
GET /posts/{id}/seoreturns"seo": nullPATCH /posts/{id}/seoreturns"seo_status": "skipped"— the draft/post is not modified- The
rank_math_activefield in every response signals this state to the caller
PATCH /posts/{id}/seo
Request body:
{
"seo": {
"title": "Updated SEO Title | Example Site",
"description": "Updated meta description.",
"focus_keyword": "bpc-157 tb-500 research",
"canonical_url": "https://example.com/bpc-157-tb-500/",
"robots": [],
"schema_type": "Article"
}
}
Bash example:
SITE_ID="YOUR_SITE_ID"
SECRET="YOUR_SECRET_HERE"
POST_ID=200
METHOD="PATCH"
ENDPOINT="/wp-json/trendplot/v1/posts/${POST_ID}/seo"
TS=$(date +%s)
BODY='{
"seo": {
"title": "BPC-157 and TB-500 — Updated SEO Title | Example Site",
"description": "Updated research overview meta description.",
"focus_keyword": "bpc-157 tb-500",
"robots": [],
"schema_type": "Article"
}
}'
SIG=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$ENDPOINT" "$TS" "$BODY" | \
openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -s -X PATCH \
-H "Content-Type: application/json" \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
-d "$BODY" \
"https://example.com${ENDPOINT}"
Response (200 OK):
{
"id": 200,
"rank_math_active": true,
"seo_status": "written",
"seo": {
"title": "BPC-157 and TB-500 — Updated SEO Title | Example Site",
"description": "Updated research overview meta description.",
"focus_keyword": "bpc-157 tb-500",
"canonical_url": null,
"schema_type": "Article",
"robots": [],
"score": null
}
}
seo_status values: "written" (Rank Math active, fields written), "skipped" (Rank Math inactive), "none" (empty seo: {}).
GET /posts/{id}/seo
Returns the current Rank Math SEO values without modifying anything.
Bash example:
METHOD="GET"
ENDPOINT="/wp-json/trendplot/v1/posts/${POST_ID}/seo"
TS=$(date +%s)
BODY=""
SIG=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$ENDPOINT" "$TS" "$BODY" | \
openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -s \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
"https://example.com${ENDPOINT}"
Response (200 OK):
{
"id": 200,
"status": "publish",
"rank_math_active": true,
"seo": {
"title": "BPC-157 and TB-500 — Updated SEO Title | Example Site",
"description": "Updated research overview meta description.",
"focus_keyword": "bpc-157 tb-500",
"canonical_url": null,
"schema_type": "Article",
"robots": [],
"score": 72
}
}
seo is null when Rank Math is not active. score is null when the post has not been opened and saved in the Rank Math editor (score is computed by the editor's JavaScript and persisted only on save).
Rank Math score and analysis
| Field | Availability |
|---|---|
seo.score |
✅ Read from rank_math_seo_score post meta (integer 0–100, or null if not yet scored) |
| Analysis breakdown (errors / warnings / passed) | ⛔ Deferred — Rank Math computes the per-post analysis entirely in its editor JavaScript; it is never persisted to the database and cannot be read back server-side without re-implementing the analysis engine |
Trendplot-created posts have score: null until the post is opened in the WordPress Rank Math editor and saved. The score is then available on subsequent GET requests.
Verification checklist
- [x]
PATCH /posts/{id}/seoon a published Trendplot post →200,seo_status: "written",rank_math_active: true - [x]
GET /posts/{id}/seoreturnsstatus,rank_math_active, and all SEO fields includingscore - [x] Rank Math meta confirmed in DB after PATCH (
rank_math_title,rank_math_description, etc.) - [x] Post content, title, excerpt, Elementor data unchanged after PATCH
- [x]
scorereads back correctly whenrank_math_seo_scoremeta is present - [x] Non-Trendplot post →
403 not_trendplot_post - [x] Non-existent post →
404 not_found - [x] Trashed post →
404 not_found - [x] Unknown
seokey →400 validation_error
10. Tagging Existing Content
Use PATCH /posts/{id}/meta to attach Trendplot metadata to any existing WordPress post or WooCommerce product:
POST_ID=101
METHOD="PATCH"
ENDPOINT="/wp-json/trendplot/v1/posts/${POST_ID}/meta"
TS=$(date +%s)
BODY='{
"trendplot_article_id": "art_abc123",
"trendplot_source": "campaign_xyz",
"trendplot_last_sync": "2026-06-07T12:00:00Z",
"related_products": [101, 102],
"related_articles": [200, 201]
}'
# ... compute SIG as above ...
curl -s -X PATCH \
-H "Content-Type: application/json" \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
-d "$BODY" \
"https://example.com${ENDPOINT}"
Response (200 OK):
{
"id": 101,
"post_type": "product",
"trendplot_article_id": "art_abc123",
"trendplot_generated": null,
"trendplot_source": "campaign_xyz",
"trendplot_last_sync": "2026-06-07T12:00:00Z",
"related_products": [101, 102],
"related_articles": [200, 201],
"updated_at": "2026-06-07T12:00:00+00:00"
}
Only keys in the _trendplot_* whitelist are accepted. Unknown keys return 400 invalid_meta_key.
11. Deployment Workflow
# 1. Develop in the git repository
cd /home/magpern/plugin-development/TrendplotConnector
# ... make changes ...
# 2. Sync to WordPress
rsync -av --delete --exclude='.git' \
/home/magpern/plugin-development/TrendplotConnector/ \
/home/magpern/woocommerce/wp-content/plugins/trendplot-connector/
# 3. Verify plugin is active
cd /home/magpern/woocommerce
docker compose run --rm wpcli wp plugin list --name=trendplot-connector
# 4. Commit and push
cd /home/magpern/plugin-development/TrendplotConnector
git add .
git commit -m "feat: description of changes"
git push origin main
12. WP-CLI Verification
# Activate plugin
docker compose run --rm wpcli wp plugin activate trendplot-connector
# Confirm active
docker compose run --rm wpcli wp plugin list --name=trendplot-connector
# Flush rewrite rules
docker compose run --rm wpcli wp rewrite flush
# List all registered Trendplot routes
docker compose run --rm wpcli wp eval \
'foreach(rest_get_server()->get_routes() as $r => $h) { if(str_contains($r,"trendplot")) echo $r."\n"; }'
# Test /health (no auth when no secret configured)
curl -sk https://YOUR_WORDPRESS_SITE/wp-json/trendplot/v1/health | python3 -m json.tool
# Set test credentials (placeholder values only — replace with real values)
docker compose run --rm wpcli wp option update trendplot_connector_settings \
'{"enabled":"1","site_id":"YOUR_SITE_ID","shared_secret":"YOUR_SECRET_HERE","allowed_origin":"https://app.trendplot.io"}' \
--format=json
# Verify meta stored on a post
docker compose run --rm wpcli wp post meta get <POST_ID> _trendplot_article_id
docker compose run --rm wpcli wp post meta get <POST_ID> _trendplot_source
# List Trendplot-created drafts
docker compose run --rm wpcli wp post list \
--post_status=draft \
--meta_key=_trendplot_article_id \
--fields=ID,post_title,post_status
13. Metadata Namespace
All Trendplot metadata uses the _trendplot_ prefix. The leading underscore marks them as private (hidden from the default WordPress Custom Fields UI).
| Key | Type | Purpose |
|---|---|---|
_trendplot_article_id |
string | Trendplot's internal article/content ID |
_trendplot_generated |
string (ISO 8601) | When Trendplot generated or last updated the content |
_trendplot_source |
string | Source identifier (campaign ID, template ID, etc.) |
_trendplot_last_sync |
string (ISO 8601) | When Trendplot last successfully synced this item |
_trendplot_related_products |
int[] | WooCommerce product IDs linked to this post |
_trendplot_related_articles |
int[] | WordPress post IDs of related Trendplot articles |
The connector enforces a hardcoded whitelist (MetaStore::ALLOWED_KEYS). Any write request containing keys outside this list is rejected with 400 invalid_meta_key.
14. Related Research Articles Block
When a WooCommerce product has Trendplot articles linked to it, the plugin automatically renders a Related Research Articles section on that product's front-end page.
How it works
- When Trendplot creates a draft via
POST /draftsit includesrelated_products: [<product_id>]in the body. - The connector stores this as
_trendplot_related_products(serialized int array) on the article post. - On each product page request, the block queries for published posts whose
_trendplot_related_productsarray contains the current product ID. - Only published posts are returned — drafts, private, and pending posts are never shown.
- If no articles match, the block renders nothing and leaves no empty markup.
- Results are cached per product with a one-hour transient. The cache is invalidated immediately when
_trendplot_related_productsis written on any post viaPOST /draftsorPATCH /posts/{id}/meta.
Linking an article to a product
Include related_products in the POST /drafts body:
BODY='{
"title": "Why BPC-157 Matters for Recovery Research",
"content": "<p>Research on BPC-157...</p>",
"trendplot_article_id": "art_bpc_001",
"related_products": [101]
}'
Or add the relationship to an existing post via PATCH /posts/{article_id}/meta:
BODY='{"related_products": [101, 102]}'
Admin settings
Navigate to Trendplot → Settings → Related Research Articles.
| Setting | Default | Description |
|---|---|---|
| Enable Block | On | Show or hide the block site-wide |
| Block Title | (empty) | Override the heading text; leave blank for "Related Research Articles" |
| Maximum Articles | 5 | Cap on how many links to show per product (1–20) |
| Placement | After product tabs | Where the block appears on the product page |
Placement options:
| Value | Hook | Position |
|---|---|---|
| After product tabs (default) | woocommerce_product_after_tabs |
Inside .woocommerce-tabs, below the last tab panel |
| After short description | woocommerce_single_product_summary p.21 |
Below the short description, above Add to Cart |
| After product meta | woocommerce_single_product_summary p.45 |
Below the SKU/category meta line |
CSS classes
The block uses BEM class names. Override styles in your theme's CSS:
.trendplot-related-articles { }
.trendplot-related-articles__title { }
.trendplot-related-articles__list { }
.trendplot-related-articles__item { }
.trendplot-related-articles__link { }
The title text can also be changed programmatically without touching settings:
add_filter('trendplot_related_articles_title', fn() => 'Further Reading');
Fallback behaviour
- No linked articles — the block outputs nothing; no empty
<section>is rendered. - All linked articles are drafts/private — same as above; the WP_Query filters to
post_status = publishonly. - Block disabled in settings — the WooCommerce hook is never registered; zero HTML output and zero database queries.
WP-CLI verification
# Confirm related_products meta is stored on a Trendplot article
docker compose run --rm wpcli wp post meta get <ARTICLE_ID> _trendplot_related_products
# Run the same query the block uses (replace 101 with your product ID)
docker compose run --rm wpcli wp eval '
$q = new WP_Query([
"post_type" => "post",
"post_status" => "publish",
"posts_per_page" => 5,
"no_found_rows" => true,
"meta_query" => [["key"=>"_trendplot_related_products","value"=>serialize(101),"compare"=>"LIKE"]],
]);
foreach ($q->posts as $p) { echo $p->ID . " " . $p->post_title . "\n"; }
'
# Clear the per-product transient cache (forces a live query on next page load)
docker compose run --rm wpcli wp transient delete trendplot_rel_arts_<PRODUCT_ID>
# Confirm block HTML on the product page
curl -sk "https://example.com/product/<slug>/" | grep -A 10 'trendplot-related-articles'
Verification checklist
- [ ]
_trendplot_related_productsmeta is set on at least one published article - [ ] Product page shows
<section class="trendplot-related-articles">in HTML - [ ] Only published articles appear; drafts are absent
- [ ] Articles are ordered newest first
- [ ] Changing Maximum Articles to 1 shows exactly 1 link
- [ ] Changing Placement moves the block to the correct position
- [ ] Changing Block Title updates the
<h2>text - [ ] Disabling the block via Enable Block → unchecked removes all block HTML
- [ ] Re-enabling restores the block
- [ ] A product with no linked articles shows no block and no empty markup
- [ ] Clearing the transient and reloading refreshes the article list
15. Trendplot Content Admin Screen
The Trendplot → Content screen gives WordPress administrators a read-only view of all posts created or managed by Trendplot. It is the primary tool for monitoring sync status, debugging relationships, and tracking which drafts have been published.
Purpose
- See all Trendplot-managed posts in one place
- Monitor draft vs. published vs. scheduled status
- Inspect product and article relationship counts
- Navigate to the WordPress editor for any post
- Debug article IDs and sync timestamps
Location
Trendplot → Content in the WordPress admin sidebar (top-level Trendplot menu, position 26).
Columns
| Column | Description |
|---|---|
| Title | Post title with a "Details ▾" expand link |
| Status | Colour-coded badge: Draft · Pending · Scheduled · Published |
| WP ID | WordPress post ID |
| Article ID | _trendplot_article_id meta value |
| Products | Count of entries in _trendplot_related_products |
| Articles | Count of entries in _trendplot_related_articles |
| Created | Post creation date (YYYY-MM-DD) |
| Modified | Last modified date (YYYY-MM-DD) |
| Actions | Edit (all statuses) · View (published) · Preview (draft/scheduled) |
Filters
Status tabs — All · Draft · Pending · Scheduled · Published. Tabs only appear for statuses that have at least one post. Counts are based on the full Trendplot corpus regardless of any active search.
Search — matches against the post title OR the _trendplot_article_id value.
Sorting — click the Created or Modified column headers to toggle ascending/descending order.
Detail row
Click Details ▾ on any row to expand an inline panel showing:
- Trendplot Article ID
- Trendplot Source
- Trendplot Generated (timestamp)
- Trendplot Last Sync (timestamp)
- Related Product IDs (comma-separated)
- Related Article IDs (comma-separated)
Click Details ▴ to collapse.
Actions (per row)
| Action | Condition |
|---|---|
| Edit | Always shown — opens WordPress post editor |
| View | Post is publish — opens the live URL |
| Preview | Post is draft, pending, or future — opens WordPress preview |
No delete, publish, or status-change actions are available. This screen is intentionally read-only.
Performance
- Queries only posts with
meta_key = _trendplot_article_idvia an indexed JOIN - Status counts use a single aggregate SQL query (not one per status)
- 50 posts per page with standard WordPress pagination
- Search runs at most two queries to resolve title + article-ID matches
Troubleshooting
No posts appear — Confirm at least one post has _trendplot_article_id meta set:
docker compose run --rm wpcli wp post list \
--meta_key=_trendplot_article_id \
--fields=ID,post_title,post_status
Menu not visible — Verify the current user has manage_options capability.
Search returns unexpected results — The search matches any substring of the article ID. Use a more specific prefix to narrow results.
Verification checklist
- [ ] Trendplot → Content appears in the admin sidebar
- [ ] All Trendplot-managed posts appear (draft, pending, future, publish)
- [ ] Non-Trendplot drafts do not appear
- [ ] Status tabs show correct counts
- [ ] Clicking a status tab filters the table correctly
- [ ] Searching by title returns matching posts
- [ ] Searching by
trendplot_article_idreturns matching posts - [ ] Product count column shows correct count for each row
- [ ] Article count column shows correct count for each row
- [ ] "Details ▾" expands the metadata panel; "Details ▴" collapses it
- [ ] Edit link opens the correct WordPress editor URL
- [ ] View link appears only for published posts
- [ ] Preview link appears for draft and scheduled posts
16. Rank Math SEO Integration
The connector writes Rank Math SEO metadata as part of POST /drafts and PATCH /drafts/{id}. Rank Math SEO (v1.0.x+) must be installed and active. If absent, the seo block is silently ignored and the response includes "seo_status": "skipped".
Meta key mapping
seo field |
Rank Math meta key | Storage format |
|---|---|---|
title |
rank_math_title |
plain string |
description |
rank_math_description |
plain string |
focus_keyword |
rank_math_focus_keyword |
plain string |
canonical_url |
rank_math_canonical_url |
plain URL string |
robots |
rank_math_robots |
PHP-serialized string array |
schema_type |
rank_math_rich_snippet |
plain string |
Whitelists
Robots directives: noindex, nofollow, noarchive, nosnippet, noimageindex, noodp
Schema types: off, Article, NewsArticle, BlogPosting
Any value not in the whitelist returns 400 validation_error.
Unknown seo keys
Any key not in the six allowed fields returns 400 validation_error immediately — before any WordPress write:
{
"code": "validation_error",
"message": "Unknown SEO field: \"seo_title\". Allowed fields: title, description, focus_keyword, canonical_url, robots, schema_type."
}
On POST /drafts, the draft is not created. On PATCH /drafts/{id}, the post is left unchanged.
WP-CLI verification
# Confirm Rank Math is active
docker compose run --rm wpcli wp plugin list --name=seo-by-rank-math
# After POST /drafts with seo fields, check written meta
docker compose run --rm wpcli wp post meta get <ID> rank_math_title
docker compose run --rm wpcli wp post meta get <ID> rank_math_description
docker compose run --rm wpcli wp post meta get <ID> rank_math_focus_keyword
docker compose run --rm wpcli wp post meta get <ID> rank_math_rich_snippet
# Verify robots stored as PHP-serialized array
docker compose run --rm wpcli wp post meta get <ID> rank_math_robots
# Expected for robots: ["noindex"] → a:1:{i:0;s:7:"noindex";}
# Test unknown seo key on POST → 400, no draft created
BODY='{"title":"Test","content":"<p>Test.</p>","trendplot_article_id":"seo-test-x",
"seo":{"seo_title":"Wrong key name"}}'
# Expected: 400 validation_error
docker compose run --rm wpcli wp post list \
--meta_key=_trendplot_article_id --meta_value=seo-test-x --fields=ID
# Expected: empty (no post created)
Verification checklist
- [ ]
POST /draftswith seo fields →seo_status: "written", Rank Math meta visible in WP admin - [ ]
PATCH /drafts/{id}withseo.robots: ["noindex"]→rank_math_robots=a:1:{i:0;s:7:"noindex";} - [ ]
PATCH /drafts/{id}withseo.title: ""→rank_math_titlemeta deleted - [ ]
PATCH /drafts/{id}withseo.robots: []→rank_math_robotsmeta deleted - [ ] Rank Math deactivated →
seo_status: "skipped", draft still created, no rankmath* meta written - [ ]
seo: {"robots": ["arbitrary-value"]}→400 validation_error - [ ]
seo: {"schema_type": "WooCommerceProduct"}→400 validation_error - [ ]
seo: {"seo_title": "Wrong key"}on POST →400 validation_error, no post created - [ ]
seo: {"meta_description": "Wrong key"}on PATCH →400 validation_error, post unchanged - [ ]
GET /drafts/{id}→ response includesseoblock with deserializedrobotsarray - [ ]
GET /drafts/{id}with Rank Math deactivated →"seo": null
17. Publishing Workflow
Trendplot manages the full draft → publish → unpublish lifecycle via dedicated endpoints. Only Trendplot-managed posts (those with _trendplot_article_id metadata) are eligible. All content, SEO metadata, Elementor data, and related article metadata are preserved across every status transition.
POST /posts/{id}/publish
Transitions a draft, pending, or scheduled post to publish status.
| Condition | Result |
|---|---|
| Post does not exist or is trash/auto-draft | 404 not_found |
No _trendplot_article_id |
403 not_trendplot_post |
Status is already publish or private |
409 invalid_status_transition |
Status is draft, pending, or future |
✅ Published |
Response (200 OK):
{
"id": 123,
"status": "publish",
"published": true,
"url": "https://example.com/post-slug/",
"edit_url": "https://example.com/wp-admin/post.php?post=123&action=edit"
}
Bash example:
TS=$(date +%s)
ENDPOINT="/wp-json/trendplot/v1/posts/123/publish"
SIG=$(printf 'POST\n%s\n%s\n' "$ENDPOINT" "$TS" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -s -X POST \
-H "Content-Type: application/json" \
-H "X-Trendplot-Site-Id: $SITE_ID" \
-H "X-Trendplot-Timestamp: $TS" \
-H "X-Trendplot-Signature: $SIG" \
"https://example.com${ENDPOINT}"
POST /posts/{id}/unpublish
Reverts a published post back to draft status.
| Condition | Result |
|---|---|
| Post does not exist or is trash/auto-draft | 404 not_found |
No _trendplot_article_id |
403 not_trendplot_post |
Status is not publish |
409 invalid_status_transition |
Status is publish |
✅ Reverted to draft |
Response (200 OK):
{
"id": 123,
"status": "draft",
"published": false
}
What is preserved across transitions
All of the following survive both publish and unpublish:
- Post content (
post_content) - Post title (
post_title) - Elementor layout (
_elementor_data,_elementor_edit_mode,_elementor_template_type) - Rank Math SEO metadata (
rank_math_title,rank_math_description,rank_math_focus_keyword,rank_math_robots,rank_math_rich_snippet,rank_math_canonical_url) - Trendplot metadata (
_trendplot_article_id,_trendplot_source,_trendplot_last_sync, etc.) - Related products and articles
- Categories and tags
Verification checklist
- [x] Draft → Publish via
POST /posts/{id}/publish→{ status: "publish", published: true, url: "..." } - [x] Publish → Draft via
POST /posts/{id}/unpublish→{ status: "draft", published: false } - [x] SEO metadata (
rank_math_title, `rank_m
This README is longer than the copy stored here. Read the rest on GitHub →
Releases
2 releases. Each count is every asset in that release; expand a row for the breakdown.