wp-mcp-gateway 0.1.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/README.md +21 -0
- package/dist/client/pluginApiClient.js +150 -0
- package/dist/config.js +71 -0
- package/dist/guides/generic-guide.md +204 -0
- package/dist/guides/guides/generic-guide.md +204 -0
- package/dist/guides/guides/xmpro-page-guide.md +342 -0
- package/dist/guides/guides/xmpro-post-guide.md +176 -0
- package/dist/guides/xmpro-page-guide.md +342 -0
- package/dist/guides/xmpro-post-guide.md +176 -0
- package/dist/index.js +42 -0
- package/dist/prompts/registerPrompts.js +101 -0
- package/dist/services/confirmationService.js +40 -0
- package/dist/services/markdownService.js +37 -0
- package/dist/services/policyService.js +75 -0
- package/dist/services/rateLimiter.js +46 -0
- package/dist/tools/registerTools.js +384 -0
- package/dist/types/contracts.js +1 -0
- package/dist/utils/errors.js +33 -0
- package/dist/utils/logger.js +40 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MCP Server (TypeScript)
|
|
2
|
+
|
|
3
|
+
Local MCP server that connects Claude to a scoped WordPress plugin gateway.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
1. Copy `.env.example` to `.env` and fill credentials.
|
|
8
|
+
2. Install dependencies:
|
|
9
|
+
- `npm install`
|
|
10
|
+
3. Start development server:
|
|
11
|
+
- `npm run dev`
|
|
12
|
+
|
|
13
|
+
## Test
|
|
14
|
+
|
|
15
|
+
- `npm test`
|
|
16
|
+
|
|
17
|
+
## Key constraints
|
|
18
|
+
|
|
19
|
+
- Allowed content types default to: `post,page,featured_item`
|
|
20
|
+
- Permanent delete is intentionally not exposed in v1
|
|
21
|
+
- Yoast operations are limited to allowlisted Tier A/B routes
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { ToolError } from "../utils/errors.js";
|
|
2
|
+
export class PluginApiClient {
|
|
3
|
+
config;
|
|
4
|
+
authHeader;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
const raw = `${config.wpUsername}:${config.wpAppPassword}`;
|
|
8
|
+
this.authHeader = `Basic ${Buffer.from(raw).toString("base64")}`;
|
|
9
|
+
}
|
|
10
|
+
async siteInfo() {
|
|
11
|
+
return this.request("GET", "/site-info");
|
|
12
|
+
}
|
|
13
|
+
async listContentTypes() {
|
|
14
|
+
return this.request("GET", "/content-types");
|
|
15
|
+
}
|
|
16
|
+
async findContent(query) {
|
|
17
|
+
return this.request("GET", "/content", { query: query });
|
|
18
|
+
}
|
|
19
|
+
async getContent(id) {
|
|
20
|
+
return this.request("GET", `/content/${id}`);
|
|
21
|
+
}
|
|
22
|
+
async createContent(body) {
|
|
23
|
+
return this.request("POST", "/content", { body });
|
|
24
|
+
}
|
|
25
|
+
async updateContent(id, body) {
|
|
26
|
+
return this.request("PATCH", `/content/${id}`, { body });
|
|
27
|
+
}
|
|
28
|
+
async publishContent(id, body) {
|
|
29
|
+
return this.request("POST", `/content/${id}/publish`, { body });
|
|
30
|
+
}
|
|
31
|
+
async trashContent(id) {
|
|
32
|
+
return this.request("POST", `/content/${id}/trash`, { body: {} });
|
|
33
|
+
}
|
|
34
|
+
async restoreContent(id) {
|
|
35
|
+
return this.request("POST", `/content/${id}/restore`, { body: {} });
|
|
36
|
+
}
|
|
37
|
+
async listRevisions(id) {
|
|
38
|
+
return this.request("GET", `/content/${id}/revisions`);
|
|
39
|
+
}
|
|
40
|
+
async restoreRevision(id, revisionId) {
|
|
41
|
+
return this.request("POST", `/content/${id}/revisions/${revisionId}/restore`, { body: {} });
|
|
42
|
+
}
|
|
43
|
+
async listAuthors() {
|
|
44
|
+
return this.request("GET", "/authors");
|
|
45
|
+
}
|
|
46
|
+
async assignAuthor(id, authorId) {
|
|
47
|
+
return this.request("POST", `/content/${id}/author`, { body: { author_id: authorId } });
|
|
48
|
+
}
|
|
49
|
+
async listTerms(taxonomy, query) {
|
|
50
|
+
return this.request("GET", `/taxonomies/${encodeURIComponent(taxonomy)}/terms`, {
|
|
51
|
+
query: query,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async createTerm(taxonomy, body) {
|
|
55
|
+
return this.request("POST", `/taxonomies/${encodeURIComponent(taxonomy)}/terms`, { body });
|
|
56
|
+
}
|
|
57
|
+
async assignTerms(id, body) {
|
|
58
|
+
return this.request("POST", `/content/${id}/terms`, { body });
|
|
59
|
+
}
|
|
60
|
+
async uploadMedia(body) {
|
|
61
|
+
return this.request("POST", "/media", { body });
|
|
62
|
+
}
|
|
63
|
+
async searchMedia(query) {
|
|
64
|
+
return this.request("GET", "/media", { query: query });
|
|
65
|
+
}
|
|
66
|
+
async updateMedia(id, body) {
|
|
67
|
+
return this.request("PATCH", `/media/${id}`, { body });
|
|
68
|
+
}
|
|
69
|
+
async setFeaturedImage(id, mediaId) {
|
|
70
|
+
return this.request("POST", `/content/${id}/featured-image`, { body: { media_id: mediaId } });
|
|
71
|
+
}
|
|
72
|
+
async insertInlineImage(id, body) {
|
|
73
|
+
return this.request("POST", `/content/${id}/inline-image/insert`, { body });
|
|
74
|
+
}
|
|
75
|
+
async replaceInlineImage(id, body) {
|
|
76
|
+
return this.request("POST", `/content/${id}/inline-image/replace`, { body });
|
|
77
|
+
}
|
|
78
|
+
async removeInlineImage(id, body) {
|
|
79
|
+
return this.request("POST", `/content/${id}/inline-image/remove`, { body });
|
|
80
|
+
}
|
|
81
|
+
async getYoastAnalysis(id) {
|
|
82
|
+
return this.request("GET", `/yoast/analysis/${id}`);
|
|
83
|
+
}
|
|
84
|
+
async updateYoastMetadata(id, body) {
|
|
85
|
+
return this.request("PATCH", `/yoast/metadata/${id}`, { body });
|
|
86
|
+
}
|
|
87
|
+
async getYoastHeadPreview(id) {
|
|
88
|
+
return this.request("GET", `/yoast/head/${id}`);
|
|
89
|
+
}
|
|
90
|
+
async getPreviewLink(id) {
|
|
91
|
+
return this.request("POST", `/content/${id}/preview-link`, { body: {} });
|
|
92
|
+
}
|
|
93
|
+
async getMedia(mediaId) {
|
|
94
|
+
return this.request("GET", `/media/${mediaId}`);
|
|
95
|
+
}
|
|
96
|
+
async uploadMediaFromUrl(params) {
|
|
97
|
+
return this.request("POST", "/media/from-url", params);
|
|
98
|
+
}
|
|
99
|
+
async cloneContent(id, params) {
|
|
100
|
+
return this.request("POST", `/content/${id}/clone`, params);
|
|
101
|
+
}
|
|
102
|
+
async request(method, path, options = {}) {
|
|
103
|
+
const url = new URL(`${this.config.wpPluginBaseUrl}${path}`);
|
|
104
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
105
|
+
if (value === null || value === undefined || value === "") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
url.searchParams.set(key, String(value));
|
|
109
|
+
}
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(url, {
|
|
114
|
+
method,
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: this.authHeader,
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
},
|
|
119
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
});
|
|
122
|
+
const payload = (await response.json().catch(() => ({})));
|
|
123
|
+
if (!response.ok || !payload.success) {
|
|
124
|
+
const error = payload.error;
|
|
125
|
+
throw new ToolError(error?.code ?? "PLUGIN_REQUEST_FAILED", error?.message ?? "Plugin request failed", {
|
|
126
|
+
details: error?.details ?? payload,
|
|
127
|
+
httpStatus: error?.http_status ?? response.status,
|
|
128
|
+
retryable: Boolean(error?.retryable),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return payload.data;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof ToolError) {
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
138
|
+
throw new ToolError("PLUGIN_REQUEST_TIMEOUT", `Request timed out after ${this.config.requestTimeoutMs}ms`, {
|
|
139
|
+
retryable: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
throw new ToolError("PLUGIN_REQUEST_FAILED", "Failed to call WordPress plugin API", {
|
|
143
|
+
details: error,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const DEFAULT_ALLOWED_CONTENT_TYPES = ["post", "page", "featured_item"];
|
|
2
|
+
const DEFAULT_ALLOWED_TAXONOMIES = [
|
|
3
|
+
"category",
|
|
4
|
+
"post_tag",
|
|
5
|
+
"featured_item_category",
|
|
6
|
+
"featured_item_tag",
|
|
7
|
+
];
|
|
8
|
+
const DEFAULT_YOAST_ALLOWED_PATHS = ["/yoast/analysis", "/yoast/metadata", "/yoast/head"];
|
|
9
|
+
const DEFAULT_MEDIA_ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|
10
|
+
function readList(raw, fallback) {
|
|
11
|
+
if (!raw || !raw.trim()) {
|
|
12
|
+
return [...fallback];
|
|
13
|
+
}
|
|
14
|
+
return raw
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((item) => item.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
function readNumber(raw, fallback) {
|
|
20
|
+
if (!raw || !raw.trim()) {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
const parsed = Number(raw);
|
|
24
|
+
if (!Number.isFinite(parsed)) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
function readBoolean(raw, fallback) {
|
|
30
|
+
if (!raw || !raw.trim()) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
const lowered = raw.toLowerCase();
|
|
34
|
+
if (["true", "1", "yes", "y", "on"].includes(lowered)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (["false", "0", "no", "n", "off"].includes(lowered)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
function ensure(name, value) {
|
|
43
|
+
if (!value || !value.trim()) {
|
|
44
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
export function loadConfig(env = process.env) {
|
|
49
|
+
const allowedAuthorIds = readList(env.ALLOWED_AUTHOR_IDS, [])
|
|
50
|
+
.map((raw) => Number(raw))
|
|
51
|
+
.filter((value) => Number.isInteger(value) && value > 0);
|
|
52
|
+
return {
|
|
53
|
+
wpPluginBaseUrl: ensure("WP_PLUGIN_BASE_URL", env.WP_PLUGIN_BASE_URL).replace(/\/$/, ""),
|
|
54
|
+
wpUsername: ensure("WP_USERNAME", env.WP_USERNAME),
|
|
55
|
+
wpAppPassword: ensure("WP_APP_PASSWORD", env.WP_APP_PASSWORD),
|
|
56
|
+
allowedContentTypes: readList(env.ALLOWED_CONTENT_TYPES, DEFAULT_ALLOWED_CONTENT_TYPES),
|
|
57
|
+
allowedTaxonomies: readList(env.ALLOWED_TAXONOMIES, DEFAULT_ALLOWED_TAXONOMIES),
|
|
58
|
+
allowedAuthorIds,
|
|
59
|
+
solutionsRootSlug: env.SOLUTIONS_ROOT_SLUG?.trim() || "solutions",
|
|
60
|
+
yoastAllowedPaths: readList(env.YOAST_ALLOWED_PATHS, DEFAULT_YOAST_ALLOWED_PATHS),
|
|
61
|
+
mediaAllowedMimeTypes: readList(env.MEDIA_ALLOWED_MIME_TYPES, DEFAULT_MEDIA_ALLOWED_MIME_TYPES),
|
|
62
|
+
mediaMaxSizeMb: readNumber(env.MEDIA_MAX_SIZE_MB, 10),
|
|
63
|
+
mediaRequireAltText: readBoolean(env.MEDIA_REQUIRE_ALT_TEXT, true),
|
|
64
|
+
mediaAllowedImportDomains: readList(env.MEDIA_ALLOWED_IMPORT_DOMAINS, []),
|
|
65
|
+
previewTokenTtlMinutes: readNumber(env.PREVIEW_TOKEN_TTL_MINUTES, 1440),
|
|
66
|
+
confirmationTtlSeconds: readNumber(env.CONFIRMATION_TTL_SECONDS, 300),
|
|
67
|
+
requestTimeoutMs: readNumber(env.REQUEST_TIMEOUT_MS, 30_000),
|
|
68
|
+
rateLimitMaxBurst: readNumber(env.RATE_LIMIT_MAX_BURST, 30),
|
|
69
|
+
rateLimitRefillPerSecond: readNumber(env.RATE_LIMIT_REFILL_PER_SECOND, 1),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Generic WordPress Content Format Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
For standard WordPress sites without custom shortcodes, use plain HTML or Gutenberg block markup. No special theme-specific shortcuts—just semantic HTML.
|
|
5
|
+
|
|
6
|
+
## Content Formats Supported
|
|
7
|
+
|
|
8
|
+
### Option 1: HTML (Recommended for Simplicity)
|
|
9
|
+
```html
|
|
10
|
+
<p>Regular paragraph text.</p>
|
|
11
|
+
|
|
12
|
+
<h2>Heading</h2>
|
|
13
|
+
|
|
14
|
+
<p>More paragraph text with <strong>bold</strong> and <em>italic</em>.</p>
|
|
15
|
+
|
|
16
|
+
<ul>
|
|
17
|
+
<li>List item 1</li>
|
|
18
|
+
<li>List item 2</li>
|
|
19
|
+
</ul>
|
|
20
|
+
|
|
21
|
+
<img src="https://..." alt="Image description">
|
|
22
|
+
|
|
23
|
+
<a href="https://...">Link text</a>
|
|
24
|
+
|
|
25
|
+
<blockquote>Quote text</blockquote>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `content_format: "html"` with this approach.
|
|
29
|
+
|
|
30
|
+
### Option 2: Markdown (Auto-Converted to HTML)
|
|
31
|
+
```markdown
|
|
32
|
+
# Heading 1
|
|
33
|
+
|
|
34
|
+
## Heading 2
|
|
35
|
+
|
|
36
|
+
Regular paragraph text with **bold** and *italic*.
|
|
37
|
+
|
|
38
|
+
- List item 1
|
|
39
|
+
- List item 2
|
|
40
|
+
|
|
41
|
+
[Link text](https://...)
|
|
42
|
+
|
|
43
|
+
> Blockquote text
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `content_format: "markdown"` with this approach.
|
|
49
|
+
|
|
50
|
+
### Option 3: Gutenberg Block Markup
|
|
51
|
+
```html
|
|
52
|
+
<!-- wp:paragraph -->
|
|
53
|
+
<p>Paragraph content</p>
|
|
54
|
+
<!-- /wp:paragraph -->
|
|
55
|
+
|
|
56
|
+
<!-- wp:heading {"level":2} -->
|
|
57
|
+
<h2>Heading</h2>
|
|
58
|
+
<!-- /wp:heading -->
|
|
59
|
+
|
|
60
|
+
<!-- wp:image {"id":123,"sizeSlug":"large"} -->
|
|
61
|
+
<figure class="wp-block-image size-large">
|
|
62
|
+
<img src="https://..." alt=""/>
|
|
63
|
+
</figure>
|
|
64
|
+
<!-- /wp:image -->
|
|
65
|
+
|
|
66
|
+
<!-- wp:list -->
|
|
67
|
+
<ul>
|
|
68
|
+
<li>Item 1</li>
|
|
69
|
+
<li>Item 2</li>
|
|
70
|
+
</ul>
|
|
71
|
+
<!-- /wp:list -->
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use `content_format: "html"` with this approach.
|
|
75
|
+
|
|
76
|
+
## Common HTML Elements
|
|
77
|
+
|
|
78
|
+
### Headings
|
|
79
|
+
```html
|
|
80
|
+
<h1>Page Title</h1>
|
|
81
|
+
<h2>Section Heading</h2>
|
|
82
|
+
<h3>Subsection</h3>
|
|
83
|
+
<h4>Small Heading</h4>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Text Formatting
|
|
87
|
+
```html
|
|
88
|
+
<p>Regular paragraph.</p>
|
|
89
|
+
<strong>Bold text</strong>
|
|
90
|
+
<em>Italic text</em>
|
|
91
|
+
<u>Underlined text</u>
|
|
92
|
+
<code>Code snippet</code>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Lists
|
|
96
|
+
```html
|
|
97
|
+
<ul>
|
|
98
|
+
<li>Unordered item 1</li>
|
|
99
|
+
<li>Unordered item 2</li>
|
|
100
|
+
</ul>
|
|
101
|
+
|
|
102
|
+
<ol>
|
|
103
|
+
<li>Ordered item 1</li>
|
|
104
|
+
<li>Ordered item 2</li>
|
|
105
|
+
</ol>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Images
|
|
109
|
+
```html
|
|
110
|
+
<img src="https://example.com/image.jpg" alt="Image description">
|
|
111
|
+
|
|
112
|
+
<!-- Or with figure element -->
|
|
113
|
+
<figure>
|
|
114
|
+
<img src="https://example.com/image.jpg" alt="">
|
|
115
|
+
<figcaption>Optional image caption</figcaption>
|
|
116
|
+
</figure>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Links
|
|
120
|
+
```html
|
|
121
|
+
<a href="https://example.com">Link text</a>
|
|
122
|
+
<a href="https://example.com" target="_blank">External link</a>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Blockquote
|
|
126
|
+
```html
|
|
127
|
+
<blockquote>
|
|
128
|
+
<p>Quote text here</p>
|
|
129
|
+
<cite>— Author Name</cite>
|
|
130
|
+
</blockquote>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Separator/Divider
|
|
134
|
+
```html
|
|
135
|
+
<hr>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Image Workflow
|
|
139
|
+
|
|
140
|
+
### Selecting images from the media library
|
|
141
|
+
1. Call `wp_search_media` with relevant keywords
|
|
142
|
+
2. **Display the thumbnail URLs visually** — show the images inline so the user can see their options
|
|
143
|
+
3. Present as numbered choices with titles and dimensions
|
|
144
|
+
4. The user picks by number, or drops a new image into the chat
|
|
145
|
+
|
|
146
|
+
### Uploading new images
|
|
147
|
+
- **User drops image in chat**: Read the file, base64-encode it, call `wp_upload_media`
|
|
148
|
+
- **Image at a URL**: Call `wp_upload_media_from_url` (server-side download)
|
|
149
|
+
- Use the returned source_url in `<img src="RETURNED_URL" alt="description">`
|
|
150
|
+
|
|
151
|
+
### Featured image
|
|
152
|
+
- Call `wp_set_featured_image` with the media ID (separate from inline content images)
|
|
153
|
+
|
|
154
|
+
## Simple Content Template
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<h1>Post Title</h1>
|
|
158
|
+
|
|
159
|
+
<p>Opening paragraph introducing the topic.</p>
|
|
160
|
+
|
|
161
|
+
<h2>First Section</h2>
|
|
162
|
+
|
|
163
|
+
<p>Section content goes here. Use natural paragraphs.</p>
|
|
164
|
+
|
|
165
|
+
<img src="https://..." alt="Relevant image">
|
|
166
|
+
|
|
167
|
+
<h2>Second Section</h2>
|
|
168
|
+
|
|
169
|
+
<p>More content.</p>
|
|
170
|
+
|
|
171
|
+
<ul>
|
|
172
|
+
<li>Key point 1</li>
|
|
173
|
+
<li>Key point 2</li>
|
|
174
|
+
<li>Key point 3</li>
|
|
175
|
+
</ul>
|
|
176
|
+
|
|
177
|
+
<h2>Conclusion</h2>
|
|
178
|
+
|
|
179
|
+
<p>Closing thoughts.</p>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Content Format Selection
|
|
183
|
+
|
|
184
|
+
- **`content_format: "html"`** — Use for HTML, Gutenberg blocks, or mixed content
|
|
185
|
+
- **`content_format: "markdown"`** — Use for markdown-formatted content (will auto-convert to HTML)
|
|
186
|
+
- **`content_format: "auto"`** — WordPress auto-detects format (usually works fine)
|
|
187
|
+
|
|
188
|
+
## Pro Tips
|
|
189
|
+
|
|
190
|
+
- **Keep it semantic**: Use proper HTML tags (h1-h6 for headings, p for paragraphs, etc.)
|
|
191
|
+
- **Alt text required**: Always include `alt` attribute on images for accessibility
|
|
192
|
+
- **Link target**: Use `target="_blank"` for external links only
|
|
193
|
+
- **No inline styles**: Let WordPress/theme handle styling via CSS classes
|
|
194
|
+
- **Use lists**: Break up text with lists for readability
|
|
195
|
+
- **Short paragraphs**: 2-4 sentences per paragraph is ideal for web reading
|
|
196
|
+
|
|
197
|
+
## When to Use Custom Theme Guides
|
|
198
|
+
|
|
199
|
+
If the WordPress site uses:
|
|
200
|
+
- **XMPro + Flatsome theme** → Use `xmpro-post-guide.md` or `xmpro-page-guide.md`
|
|
201
|
+
- **Custom shortcodes** → Ask the user for theme/plugin documentation
|
|
202
|
+
- **Gutenberg patterns** → This generic guide works fine
|
|
203
|
+
|
|
204
|
+
Otherwise, this generic HTML approach works for any WordPress site.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Generic WordPress Content Format Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
For standard WordPress sites without custom shortcodes, use plain HTML or Gutenberg block markup. No special theme-specific shortcuts—just semantic HTML.
|
|
5
|
+
|
|
6
|
+
## Content Formats Supported
|
|
7
|
+
|
|
8
|
+
### Option 1: HTML (Recommended for Simplicity)
|
|
9
|
+
```html
|
|
10
|
+
<p>Regular paragraph text.</p>
|
|
11
|
+
|
|
12
|
+
<h2>Heading</h2>
|
|
13
|
+
|
|
14
|
+
<p>More paragraph text with <strong>bold</strong> and <em>italic</em>.</p>
|
|
15
|
+
|
|
16
|
+
<ul>
|
|
17
|
+
<li>List item 1</li>
|
|
18
|
+
<li>List item 2</li>
|
|
19
|
+
</ul>
|
|
20
|
+
|
|
21
|
+
<img src="https://..." alt="Image description">
|
|
22
|
+
|
|
23
|
+
<a href="https://...">Link text</a>
|
|
24
|
+
|
|
25
|
+
<blockquote>Quote text</blockquote>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `content_format: "html"` with this approach.
|
|
29
|
+
|
|
30
|
+
### Option 2: Markdown (Auto-Converted to HTML)
|
|
31
|
+
```markdown
|
|
32
|
+
# Heading 1
|
|
33
|
+
|
|
34
|
+
## Heading 2
|
|
35
|
+
|
|
36
|
+
Regular paragraph text with **bold** and *italic*.
|
|
37
|
+
|
|
38
|
+
- List item 1
|
|
39
|
+
- List item 2
|
|
40
|
+
|
|
41
|
+
[Link text](https://...)
|
|
42
|
+
|
|
43
|
+
> Blockquote text
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `content_format: "markdown"` with this approach.
|
|
49
|
+
|
|
50
|
+
### Option 3: Gutenberg Block Markup
|
|
51
|
+
```html
|
|
52
|
+
<!-- wp:paragraph -->
|
|
53
|
+
<p>Paragraph content</p>
|
|
54
|
+
<!-- /wp:paragraph -->
|
|
55
|
+
|
|
56
|
+
<!-- wp:heading {"level":2} -->
|
|
57
|
+
<h2>Heading</h2>
|
|
58
|
+
<!-- /wp:heading -->
|
|
59
|
+
|
|
60
|
+
<!-- wp:image {"id":123,"sizeSlug":"large"} -->
|
|
61
|
+
<figure class="wp-block-image size-large">
|
|
62
|
+
<img src="https://..." alt=""/>
|
|
63
|
+
</figure>
|
|
64
|
+
<!-- /wp:image -->
|
|
65
|
+
|
|
66
|
+
<!-- wp:list -->
|
|
67
|
+
<ul>
|
|
68
|
+
<li>Item 1</li>
|
|
69
|
+
<li>Item 2</li>
|
|
70
|
+
</ul>
|
|
71
|
+
<!-- /wp:list -->
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use `content_format: "html"` with this approach.
|
|
75
|
+
|
|
76
|
+
## Common HTML Elements
|
|
77
|
+
|
|
78
|
+
### Headings
|
|
79
|
+
```html
|
|
80
|
+
<h1>Page Title</h1>
|
|
81
|
+
<h2>Section Heading</h2>
|
|
82
|
+
<h3>Subsection</h3>
|
|
83
|
+
<h4>Small Heading</h4>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Text Formatting
|
|
87
|
+
```html
|
|
88
|
+
<p>Regular paragraph.</p>
|
|
89
|
+
<strong>Bold text</strong>
|
|
90
|
+
<em>Italic text</em>
|
|
91
|
+
<u>Underlined text</u>
|
|
92
|
+
<code>Code snippet</code>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Lists
|
|
96
|
+
```html
|
|
97
|
+
<ul>
|
|
98
|
+
<li>Unordered item 1</li>
|
|
99
|
+
<li>Unordered item 2</li>
|
|
100
|
+
</ul>
|
|
101
|
+
|
|
102
|
+
<ol>
|
|
103
|
+
<li>Ordered item 1</li>
|
|
104
|
+
<li>Ordered item 2</li>
|
|
105
|
+
</ol>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Images
|
|
109
|
+
```html
|
|
110
|
+
<img src="https://example.com/image.jpg" alt="Image description">
|
|
111
|
+
|
|
112
|
+
<!-- Or with figure element -->
|
|
113
|
+
<figure>
|
|
114
|
+
<img src="https://example.com/image.jpg" alt="">
|
|
115
|
+
<figcaption>Optional image caption</figcaption>
|
|
116
|
+
</figure>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Links
|
|
120
|
+
```html
|
|
121
|
+
<a href="https://example.com">Link text</a>
|
|
122
|
+
<a href="https://example.com" target="_blank">External link</a>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Blockquote
|
|
126
|
+
```html
|
|
127
|
+
<blockquote>
|
|
128
|
+
<p>Quote text here</p>
|
|
129
|
+
<cite>— Author Name</cite>
|
|
130
|
+
</blockquote>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Separator/Divider
|
|
134
|
+
```html
|
|
135
|
+
<hr>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Image Workflow
|
|
139
|
+
|
|
140
|
+
### Selecting images from the media library
|
|
141
|
+
1. Call `wp_search_media` with relevant keywords
|
|
142
|
+
2. **Display the thumbnail URLs visually** — show the images inline so the user can see their options
|
|
143
|
+
3. Present as numbered choices with titles and dimensions
|
|
144
|
+
4. The user picks by number, or drops a new image into the chat
|
|
145
|
+
|
|
146
|
+
### Uploading new images
|
|
147
|
+
- **User drops image in chat**: Read the file, base64-encode it, call `wp_upload_media`
|
|
148
|
+
- **Image at a URL**: Call `wp_upload_media_from_url` (server-side download)
|
|
149
|
+
- Use the returned source_url in `<img src="RETURNED_URL" alt="description">`
|
|
150
|
+
|
|
151
|
+
### Featured image
|
|
152
|
+
- Call `wp_set_featured_image` with the media ID (separate from inline content images)
|
|
153
|
+
|
|
154
|
+
## Simple Content Template
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<h1>Post Title</h1>
|
|
158
|
+
|
|
159
|
+
<p>Opening paragraph introducing the topic.</p>
|
|
160
|
+
|
|
161
|
+
<h2>First Section</h2>
|
|
162
|
+
|
|
163
|
+
<p>Section content goes here. Use natural paragraphs.</p>
|
|
164
|
+
|
|
165
|
+
<img src="https://..." alt="Relevant image">
|
|
166
|
+
|
|
167
|
+
<h2>Second Section</h2>
|
|
168
|
+
|
|
169
|
+
<p>More content.</p>
|
|
170
|
+
|
|
171
|
+
<ul>
|
|
172
|
+
<li>Key point 1</li>
|
|
173
|
+
<li>Key point 2</li>
|
|
174
|
+
<li>Key point 3</li>
|
|
175
|
+
</ul>
|
|
176
|
+
|
|
177
|
+
<h2>Conclusion</h2>
|
|
178
|
+
|
|
179
|
+
<p>Closing thoughts.</p>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Content Format Selection
|
|
183
|
+
|
|
184
|
+
- **`content_format: "html"`** — Use for HTML, Gutenberg blocks, or mixed content
|
|
185
|
+
- **`content_format: "markdown"`** — Use for markdown-formatted content (will auto-convert to HTML)
|
|
186
|
+
- **`content_format: "auto"`** — WordPress auto-detects format (usually works fine)
|
|
187
|
+
|
|
188
|
+
## Pro Tips
|
|
189
|
+
|
|
190
|
+
- **Keep it semantic**: Use proper HTML tags (h1-h6 for headings, p for paragraphs, etc.)
|
|
191
|
+
- **Alt text required**: Always include `alt` attribute on images for accessibility
|
|
192
|
+
- **Link target**: Use `target="_blank"` for external links only
|
|
193
|
+
- **No inline styles**: Let WordPress/theme handle styling via CSS classes
|
|
194
|
+
- **Use lists**: Break up text with lists for readability
|
|
195
|
+
- **Short paragraphs**: 2-4 sentences per paragraph is ideal for web reading
|
|
196
|
+
|
|
197
|
+
## When to Use Custom Theme Guides
|
|
198
|
+
|
|
199
|
+
If the WordPress site uses:
|
|
200
|
+
- **XMPro + Flatsome theme** → Use `xmpro-post-guide.md` or `xmpro-page-guide.md`
|
|
201
|
+
- **Custom shortcodes** → Ask the user for theme/plugin documentation
|
|
202
|
+
- **Gutenberg patterns** → This generic guide works fine
|
|
203
|
+
|
|
204
|
+
Otherwise, this generic HTML approach works for any WordPress site.
|