hostinger-api-mcp 3.28.6 → 3.29.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/CHANGELOG.md +7 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/SKILL.md +1 -0
- package/skills/headless/VERSION +2 -2
- package/skills/headless/hostinger-mcp.mjs +88 -0
- package/skills/headless/push-content.mjs +530 -0
- package/skills/headless/steps/04-content-model/SKILL.md +9 -9
- package/skills/headless/steps/04-content-model/wordpress/SKILL.md +83 -252
- package/skills/headless/steps/04-content-model/wordpress/by-hand.md +31 -0
- package/skills/headless/steps/04-content-model/wordpress/custom-plugins.md +2 -2
- package/skills/headless/steps/04-content-model/wordpress/woocommerce.md +21 -49
- package/skills/headless/steps/05-design/SKILL.md +1 -1
- package/skills/headless/wait.mjs +15 -95
- package/skills/headless/wp-mcp.mjs +125 -0
- package/src/core/skills.js +49 -21
- package/src/core/skills.ts +49 -21
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- package/src/servers/agent-readiness.js +1 -1
- package/src/servers/agent-readiness.ts +1 -1
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/gpu.js +1 -1
- package/src/servers/gpu.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/mail.js +1 -1
- package/src/servers/mail.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
- package/src/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
|
@@ -1,275 +1,106 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: headless-step-content-model-wordpress
|
|
3
|
-
description: "Push phase of Step 4 for a headless WordPress backend:
|
|
3
|
+
description: "Push phase of Step 4 for a headless WordPress backend: write the drafted content into one content.json and run push-content.mjs, which creates the site title, categories, tags, images, pages, posts, menu, Yoast fields and the WooCommerce catalog through the per-site WP MCP and records the created ids. By-hand recipe only when the script can't run. Idempotent, instance-gated."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# headless-step-content-model-wordpress
|
|
7
7
|
|
|
8
|
-
**Push phase — needs the live instance.** Run only after `provision.status == "completed"`.
|
|
9
|
-
WordPress objects.
|
|
8
|
+
**Push phase — needs the live instance.** Run only after `provision.status == "completed"`.
|
|
10
9
|
|
|
11
|
-
**In:** `content-model.md`, `site.json
|
|
12
|
-
|
|
13
|
-
`content-model.md`.
|
|
14
|
-
|
|
15
|
-
## What creates what — and what authenticates it
|
|
16
|
-
|
|
17
|
-
The Hostinger **management** MCP (`wordpress_*`) is infra-only: it installs WordPress, plugins and
|
|
18
|
-
themes but has **no content endpoints**.
|
|
10
|
+
**In:** `content-model.md`, `site.json`, the drafts from the prepare phase. **Out:** the content in
|
|
11
|
+
WordPress, and the created-id map in `content-model.md`.
|
|
19
12
|
|
|
20
13
|
Writes go through the **per-site WP MCP** (the Hostinger AI Assistant plugin), authenticated with the
|
|
21
|
-
installation JWT.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
authenticated; the admin user context (`wp_set_current_user`) exists only *inside* an MCP session.
|
|
25
|
-
|
|
26
|
-
| Surface | Base (from `site.json`) | Role |
|
|
27
|
-
|---|---|---|
|
|
28
|
-
| **Per-site WP MCP** | `urls.wp_mcp` (`mcp_url`) | **all writes** — create content through its ability tools |
|
|
29
|
-
| **WP REST API** `wp/v2` | `urls.rest_api` | **public reads only** (no auth needed) — what the Step 6 frontend consumes; do not write to it with the JWT |
|
|
14
|
+
installation JWT. The management MCP (`wordpress_*`) has no content endpoints, and `wp/v2` REST is the
|
|
15
|
+
public read surface the frontend uses, not a write path for that token. `push-content.mjs` handles all
|
|
16
|
+
of this: you write one file and run one command.
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
> is already installed + activated at provision (every headless WordPress site — see provision Step 4),
|
|
33
|
-
> so meta + schema come from a maintained plugin and the frontend reads each item's `yoast_head_json`.
|
|
34
|
-
> This push only sets per-item Yoast meta (Step 4.7 below).
|
|
18
|
+
Also on this step, when the brief calls for them:
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
20
|
+
- **A store** (`site.json → woocommerce.enabled: true`): the catalog goes in the same content file
|
|
21
|
+
(`store` below). Checkout stays owner-completed; see [`woocommerce.md`](woocommerce.md) §4.
|
|
22
|
+
- **A contact form** (`site.json → contact_form.enabled: true`): after the push, follow
|
|
23
|
+
[`contact-form.md`](contact-form.md) to find the form id and deploy the spam guard.
|
|
24
|
+
- **Custom post types, taxonomies or a settings page:** deploy the plugin from
|
|
25
|
+
[`custom-plugins.md`](custom-plugins.md) before the push, and create its entries with `cpt-create` after.
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
> create content *data* but can't register a **custom post type**, a **custom taxonomy**, or a **global
|
|
45
|
-
> settings page** (footer text, phone, social links the frontend reads). When the content model calls for
|
|
46
|
-
> those, author a small custom plugin and deploy it into the headless `/cms` with
|
|
47
|
-
> `hosting_deploy { type: "wordpress-plugin", headless: true }`, exposing everything over REST for the
|
|
48
|
-
> frontend. **Deploy the plugin first** (the schema — the deploy activates it), then the steps below populate
|
|
49
|
-
> its entries through the MCP's native `cpt-create` ability (custom-taxonomy terms are seeded in the plugin — see the cookbook).
|
|
27
|
+
## Step 1 — Write `content.json`
|
|
50
28
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
> and records them into `site.json` so the Step 6 frontend can submit to CF7's public feedback endpoint.
|
|
55
|
-
> Run the **Contact form** section after the WordPress content below.
|
|
56
|
-
|
|
57
|
-
## Step 1 — Open the authenticated MCP session
|
|
58
|
-
|
|
59
|
-
Mint a short-lived token through the Hostinger connector:
|
|
29
|
+
Write `~/.hostinger/<domain>/content.json` in one go, from the drafts you generated in the prepare phase.
|
|
30
|
+
Leave out any section the site doesn't have. Every field below is what the script accepts; anything
|
|
31
|
+
else is refused, with the field named.
|
|
60
32
|
|
|
61
33
|
```json
|
|
62
|
-
{
|
|
63
|
-
"
|
|
34
|
+
{
|
|
35
|
+
"site": { "title": "Crumb & Co", "description": "Sourdough baked daily in Lisbon" },
|
|
36
|
+
"categories": [ { "name": "Bread", "slug": "bread" } ],
|
|
37
|
+
"tags": [ { "name": "Sourdough", "slug": "sourdough" } ],
|
|
38
|
+
"pages": [
|
|
39
|
+
{ "slug": "about", "title": "About us", "content": "<p>…</p>", "excerpt": "…",
|
|
40
|
+
"image": { "url": "https://…", "alt": "…" },
|
|
41
|
+
"seo": { "title": "…", "description": "…" } }
|
|
42
|
+
],
|
|
43
|
+
"posts": [
|
|
44
|
+
{ "slug": "why-sourdough", "title": "Why sourdough", "content": "<h2>…</h2><p>…</p>", "excerpt": "…",
|
|
45
|
+
"categories": ["bread"], "tags": ["sourdough"],
|
|
46
|
+
"image": { "url": "https://images.unsplash.com/photo-…?w=1600&q=80&fm=jpg&fit=max",
|
|
47
|
+
"alt": "Loaves on a wooden shelf", "title": "Loaves on a shelf",
|
|
48
|
+
"filename": "crumb-loaves.jpg", "caption": "Photo by <Photographer> on Unsplash" },
|
|
49
|
+
"seo": { "title": "…", "description": "…", "focus_keyword": "…" } }
|
|
50
|
+
],
|
|
51
|
+
"menu": { "name": "Primary", "items": [ { "title": "About", "page": "about" }, { "title": "Journal", "url": "/journal" } ] },
|
|
52
|
+
"store": {
|
|
53
|
+
"categories": [ { "name": "Breads", "slug": "breads" } ],
|
|
54
|
+
"products": [ { "sku": "CR-LOAF", "name": "Country loaf", "price": "6.50", "description": "<p>…</p>",
|
|
55
|
+
"short_description": "…", "categories": ["breads"], "image": { "url": "https://…", "alt": "…" } } ],
|
|
56
|
+
"coupons": [ { "code": "WELCOME10", "discount_type": "percent", "amount": "10" } ]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
64
59
|
```
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- `
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Step 2 —
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
what's there; with discovery, list abilities, then execute by ability name. The content tools that
|
|
88
|
-
cover this step (exact names come from discovery — don't hardcode them):
|
|
89
|
-
|
|
90
|
-
`SettingsTools` · `CategoriesTools` · `TagsTools` · `MediaUploadTools` / `MediaTools` · `PagesTools` ·
|
|
91
|
-
`PostsTools` · `MenusTools` / `NavigationTools` · `SeoTools`, plus **`cpt-*`** for custom post types. There
|
|
92
|
-
is **no** generic `wp/v2` passthrough tool — every write goes through a dedicated ability, so a `wp/v2`
|
|
93
|
-
route with no matching ability (e.g. the terms of a *custom* taxonomy) isn't reachable from the MCP;
|
|
94
|
-
register and seed those in a custom plugin instead (see the backend's custom-plugins cookbook).
|
|
95
|
-
|
|
96
|
-
Seen on a live install (Hostinger AI Assistant, discovery mode): the meta-tools are
|
|
97
|
-
`hostinger-ai-discover-abilities`, `hostinger-ai-get-ability-info { name }` and
|
|
98
|
-
`hostinger-ai-execute-ability { name, parameters }`. The abilities are named
|
|
99
|
-
`hostinger-ai/<thing>-<action>`, e.g. `posts-create`, `posts-update`, `media-upload-from-url`,
|
|
100
|
-
`media-delete`. Call `get-ability-info` for each schema before using it. Results come back as
|
|
101
|
-
`{ ability, result: { data, … } }` for lists and `{ ability, result: {…} }` for single objects — so a
|
|
102
|
-
create answers **`200`** (not `201`) and the new object's `id` is **inside** `result` (`result.id`, or
|
|
103
|
-
`result.data.id`), never at the top level. A missing top-level `id` is not a failure; read it from
|
|
104
|
-
`result`.
|
|
105
|
-
|
|
106
|
-
## Step 3 — Idempotency rule (makes resume safe)
|
|
107
|
-
|
|
108
|
-
Every create is **list/get-by-slug first, then create only if absent** — a re-run must not duplicate.
|
|
109
|
-
Use the matching read ability (`posts-search`, `pages-search`, `categories-list`, `tags-list`,
|
|
110
|
-
`cpt-search`, `media-search`, …): a hit → reuse its `id`; empty → create. Capture each returned `id` and record the id map back into
|
|
111
|
-
`content-model.md` so later references resolve without re-querying.
|
|
112
|
-
|
|
113
|
-
## Step 4 — Create in dependency order
|
|
114
|
-
|
|
115
|
-
> **Custom types/settings? Deploy the plugin before this list.** If the content model needs a custom post
|
|
116
|
-
> type, taxonomy or global settings page, deploy that plugin **first** (the deploy activates it — see
|
|
117
|
-
> [`custom-plugins.md`](custom-plugins.md)) so its REST routes exist — then the steps below populate its
|
|
118
|
-
> entries through the MCP's native `cpt-create` ability, as they do core posts and pages (custom-taxonomy
|
|
119
|
-
> terms are seeded in the plugin — the MCP has no taxonomy-term path; see the cookbook).
|
|
120
|
-
|
|
121
|
-
0. **Remove WordPress's default content first.** A fresh install ships a **"Hello world!" post**
|
|
122
|
-
(id 1), a **"Sample Page"**, and a default comment — they will otherwise show up on the live site.
|
|
123
|
-
Delete the default post (`posts-delete { id: 1, force: true }`) and, if present, the sample page
|
|
124
|
-
(find it via `pages-search` for slug `sample-page`, then `pages-delete`). This is idempotent: if
|
|
125
|
-
they're already gone, skip. Leave the default `Uncategorized` category — it's harmless once no post
|
|
126
|
-
uses it (the frontend only lists categories with posts).
|
|
127
|
-
1. **Site Title & Description** (the helper's global part) → the Settings ability.
|
|
128
|
-
2. **Categories / tags** — one per term in the helper → Categories / Tags abilities. Keep `name → id`.
|
|
129
|
-
3. **Media.** Upload every image in the helper, set `alt_text`, and keep `filename → id`. See
|
|
130
|
-
*Media upload* below for all sources: a Hostinger-AI image URL, an Unsplash URL, or a generated
|
|
131
|
-
file in `media/`. For a Hostinger-AI image, generate it now (its URL is temporary — see the
|
|
132
|
-
content-model `SKILL.md`, *Images* Option A) and upload straight away, before `expires_at`.
|
|
133
|
-
4. **Pages** — each static/index page → Pages ability. Pass `title`, `content` and `excerpt` as
|
|
134
|
-
**`{ "raw": "…" }` objects** (see the note below), plus `slug`, `status: publish`. Keep `slug → id`.
|
|
135
|
-
5. **Posts (articles)** — each → Posts ability: `title`, `content` and `excerpt` as **`{ "raw": "…" }`**
|
|
136
|
-
(note below), plus `slug`, `status: publish`, `categories: [id]`, `tags: [id…]`,
|
|
137
|
-
`featured_media: <id>`, `date`. (The plugin block-converts post content on insert via
|
|
138
|
-
`rest_pre_insert_post`, so HTML bodies in `content.raw` are fine.)
|
|
139
|
-
|
|
140
|
-
> **Create payload gotcha (`pages-create` / `posts-create`).** These abilities take `title`,
|
|
141
|
-
> `content` and `excerpt` as WordPress's core **`{ "raw": "…" }` objects**, not bare strings. Pass a
|
|
142
|
-
> bare string and the field is silently dropped — the create still answers `200`, so you get a
|
|
143
|
-
> *published-but-empty* page or post with nothing to key off. Always send `{ "raw": "…" }` and
|
|
144
|
-
> read the new `id` from `result` (see Step 2). Confirm the exact field shape with
|
|
145
|
-
> `get-ability-info` if a schema differs.
|
|
146
|
-
6. **Navigation** — Menus / Navigation abilities: create the menu, then an item per nav entry
|
|
147
|
-
referencing a page `id` (or a custom URL for listing routes). **Headless note:** the Step 6
|
|
148
|
-
frontend renders nav from `content-model.md`, so the WP menu is for owners editing in wp-admin, not
|
|
149
|
-
what the live site reads — create it, but it isn't the source of truth.
|
|
150
|
-
7. **SEO** — **Yoast SEO** (`wordpress-seo`) is already installed + activated at provision (Step 4 of
|
|
151
|
-
the WordPress provision recipe), so it generates meta + schema and the frontend reads
|
|
152
|
-
`yoast_head_json` per post/page. Here, just set each post's Yoast title/description if an ability
|
|
153
|
-
exposes it; otherwise the excerpt + title are a fine fallback. Don't fail the push over SEO fields.
|
|
154
|
-
If `site.json → seo.enabled` is `false` (the owner declined Yoast, or provision couldn't confirm the
|
|
155
|
-
install), skip this and let the frontend generate tags from content — don't try to install the
|
|
156
|
-
plugin from the content stage.
|
|
157
|
-
|
|
158
|
-
### Media upload
|
|
159
|
-
|
|
160
|
-
**From a URL (a Hostinger-AI image, Unsplash, or any direct file URL).** Use
|
|
161
|
-
`hostinger-ai/media-upload-from-url`. WordPress downloads the file and stores its own copy, so the
|
|
162
|
-
site never depends on the external URL staying up — which is exactly why a **Hostinger-AI** image
|
|
163
|
-
(whose `url` from `wordpress_ai-content_generation-result` is temporary) must be uploaded here, while
|
|
164
|
-
it is still valid; drop the Unsplash-only `caption`/`description` for AI images:
|
|
165
|
-
|
|
166
|
-
```json
|
|
167
|
-
{ "name": "hostinger-ai/media-upload-from-url",
|
|
168
|
-
"parameters": {
|
|
169
|
-
"url": "https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max",
|
|
170
|
-
"filename": "<site>-<subject>.jpg",
|
|
171
|
-
"title": "<short title>",
|
|
172
|
-
"alt_text": "<what the photo shows>",
|
|
173
|
-
"caption": "Photo by <Photographer> on Unsplash",
|
|
174
|
-
"description": "Source: https://unsplash.com/photos/<id> (Unsplash License)",
|
|
175
|
-
"post_id": <post id, if the post already exists> } }
|
|
61
|
+
- **Slugs** are lower-case ASCII with dashes (`about-us`). References are slugs defined in the same
|
|
62
|
+
file: a post's `categories` and `tags`, a menu item's `page`, a product's `categories`, and a page's
|
|
63
|
+
or term's `parent`, which must be listed before its child.
|
|
64
|
+
- `content` and `excerpt` are HTML strings. A page can also take `parent` and `menu_order`; a term can take
|
|
65
|
+
`description` and `parent`.
|
|
66
|
+
- **Images:** `url` must be a direct `https` file URL that answers 200 with an image type: an Unsplash
|
|
67
|
+
web-size URL, or a Hostinger-AI image `url` (it expires, so generate right before the push; see the
|
|
68
|
+
content-model `SKILL.md`, *Images* Option A, and leave `caption` empty). `alt` is required. `title` (it
|
|
69
|
+
defaults to `alt`) is how a re-run finds the upload, so keep it unique per image. The same `url` used
|
|
70
|
+
twice uploads once.
|
|
71
|
+
- **`seo`** sets the Yoast title, description and focus keyword. It's skipped when `site.json →
|
|
72
|
+
seo.enabled` is `false`.
|
|
73
|
+
- **`store`** is only for a WooCommerce brief. `price` is a decimal string in major units (`"6.50"`). A
|
|
74
|
+
product passes any other WooCommerce product field as given (`sale_price`, `stock_quantity`,
|
|
75
|
+
`attributes`, …). Variations of a variable product are by hand. `discount_type` is `percent`,
|
|
76
|
+
`fixed_cart` or `fixed_product`.
|
|
77
|
+
|
|
78
|
+
## Step 2 — Run the push
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node <skill>/push-content.mjs <domain>
|
|
176
82
|
```
|
|
177
83
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
files.
|
|
186
|
-
|
|
187
|
-
**Set the featured image.** Pass `featured_media: <id>` when creating the post (Step 4.5). If the
|
|
188
|
-
post already exists (on resume, or when images are added later), use
|
|
189
|
-
`hostinger-ai/posts-update { id, featured_media }`.
|
|
190
|
-
|
|
191
|
-
**Idempotency trap: don't look up earlier uploads by filename.** `media-search` matches titles and
|
|
192
|
-
content, not filenames or the slug you passed. A search for `<site>-lisbon-tram` finds nothing even
|
|
193
|
-
though the file exists, so a re-run would upload the image again. WordPress then stores the duplicate
|
|
194
|
-
as `…-1.jpg` and silently repoints the post at it. Check this way instead:
|
|
195
|
-
|
|
196
|
-
1. `posts-get { id }`. If `featured_media` is already set, skip that post. Its image exists.
|
|
197
|
-
2. Otherwise, search by the **title** you set on upload (`media-search { search: "<title>" }`),
|
|
198
|
-
and reuse a hit.
|
|
199
|
-
3. Only upload when both checks come up empty.
|
|
200
|
-
|
|
201
|
-
If duplicates do slip in, point each post back at the original with `posts-update`, then delete the
|
|
202
|
-
duplicates with `media-delete { id, force: true }`.
|
|
203
|
-
|
|
204
|
-
## Step 5 — Verify and record
|
|
84
|
+
It mints the token itself (never printed), removes WordPress's "Hello world!" post and "Sample Page",
|
|
85
|
+
then creates the site title, terms, images, pages, posts with their featured images, the menu, the
|
|
86
|
+
Yoast fields and the store, each looked up first, so a re-run reuses what exists and updates pages,
|
|
87
|
+
posts and products to match the file. Then it checks the result over public `wp/v2` and the Store API
|
|
88
|
+
(every slug and SKU there once, featured images set, no duplicate uploads), and writes the id map into
|
|
89
|
+
`content-model.md` under `## Created in WordPress` and `## Created in WooCommerce`. Progress goes to
|
|
90
|
+
stderr; stdout is one JSON line with `counts`, `verify` and `ids`.
|
|
205
91
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`featured_media` must point at its image.
|
|
213
|
-
- Write the created-id map into `content-model.md` (a `## Created in WordPress` section: object → slug
|
|
214
|
-
→ id) so the frontend data phase and any re-run resolve references directly. Include one row per
|
|
215
|
-
media item: filename, credit and source page, the post it's featured on, and its id.
|
|
216
|
-
|
|
217
|
-
## WooCommerce catalog *(only for a `WordPress + WooCommerce` brief — skip otherwise)*
|
|
218
|
-
|
|
219
|
-
When `site.json → woocommerce.enabled` is `true`, also seed the store **in this same authenticated MCP
|
|
220
|
-
session**, using WooCommerce abilities the Assistant exposes alongside the content ones. Full recipe (incl.
|
|
221
|
-
the storefront Store API and the checkout gate): [`woocommerce.md`](woocommerce.md). In short:
|
|
222
|
-
|
|
223
|
-
1. **Product categories** — `wc-product-categories-list` (reuse by slug/name) else
|
|
224
|
-
`wc-product-categories-create { name, slug }`. Keep `name → id`.
|
|
225
|
-
2. **Products** — **idempotent by SKU**: `wc-products-search { sku }` → reuse, else
|
|
226
|
-
`wc-products-create { name, type:"simple", sku, regular_price:"<decimal>", description, status:"publish",
|
|
227
|
-
categories:[{id}] }`. For images, `media-upload-from-url` then `wc-products-update { id, images:[{id}] }`.
|
|
228
|
-
3. **Coupons** — **idempotent by code**: `wc-coupons-search { code }` → reuse, else
|
|
229
|
-
`wc-coupons-create { code, discount_type, amount }`.
|
|
230
|
-
4. **Verify over the public Store API** (no auth): `GET {urls.store_api}/products?per_page=100` count +
|
|
231
|
-
`prices` (minor units), `GET {urls.store_api}/products/categories`. Record the SKU→id map, category ids
|
|
232
|
-
and coupon codes into `content-model.md` under `## Created in WooCommerce`.
|
|
233
|
-
|
|
234
|
-
> **Don't seed or test checkout here.** A fresh WooCommerce install has no payment gateway/shipping and
|
|
235
|
-
> that config isn't reachable headlessly (`site.json → woocommerce.payment_enabled` is `false` — the owner
|
|
236
|
-
> finishes it in wp-admin). Seeding + verifying the catalog over the Store API is the whole job; never call
|
|
237
|
-
> `/checkout` or block on it. See the cookbook §4.
|
|
238
|
-
|
|
239
|
-
## Contact form (Contact Form 7) *(only when `site.json → contact_form.enabled` is true — skip otherwise)*
|
|
240
|
-
|
|
241
|
-
Provision installed Contact Form 7. Here, resolve the form the frontend submits to, deploy a tiny
|
|
242
|
-
spam-guard plugin (a headless honeypot), and record both into `site.json`. Full recipe:
|
|
243
|
-
[`contact-form.md`](contact-form.md). In short:
|
|
244
|
-
|
|
245
|
-
1. **Find the form id (idempotent).** The per-site MCP has **no generic REST ability** and
|
|
246
|
-
`wpcf7_contact_form` is not a public CPT, so read the id by **probing the public feedback endpoint**:
|
|
247
|
-
a **`multipart/form-data`** `POST {urls.cms}/wp-json/contact-form-7/v1/contact-forms/<id>/feedback` with
|
|
248
|
-
`_wpcf7_unit_tag=wpcf7-f<id>-o1`, for `id` in `1`‒`30`. The hit is the id whose `200` JSON reply carries
|
|
249
|
-
`"contact_form_id": <id>` (plus a CF7 `"status"`) — the default **"Contact form 1"** (fields `your-name`,
|
|
250
|
-
`your-email`, `your-subject`, `your-message`); `404`/`wpcf7_not_found` is a miss, and anything else
|
|
251
|
-
(`415`, `403`, `429`, `5xx`) is inconclusive, never a hit. **The content type matters:** CF7 answers
|
|
252
|
-
`415` to a non-multipart body *before* it looks up the id, so a bare POST would wrongly pick id 1. Reuse
|
|
253
|
-
it on re-run. CF7's default mail already sends to `[_site_admin_email]`, so no recipient change is needed.
|
|
254
|
-
*(Fallback if nothing answers: hand the owner the one-click wp-admin login — `wordpress_login_create-links`
|
|
255
|
-
— to read the id from Contact → Contact Forms, the WooCommerce-checkout-gate owner-handoff pattern.)* See
|
|
256
|
-
the cookbook for the full method.
|
|
257
|
-
2. **Deploy the spam guard (honeypot).** The `contact-form-7-honeypot` add-on can't work headlessly (it
|
|
258
|
-
needs a `[honeypot]` form-tag only an owner can add), so deploy a ~15-line `wpcf7_spam` plugin to `/cms`
|
|
259
|
-
with `hosting_deploy { type: "wordpress-plugin", slug: "cf7-headless-spamguard", headless: true }` — it
|
|
260
|
-
flags a submission as `spam` (no mail) when a fixed decoy field (`your_website`) is filled. Cookbook §2b.
|
|
261
|
-
3. **Record into `site.json`** — fill the `contact_form` block provision started: `form_id`,
|
|
262
|
-
`feedback_url` (`{urls.cms}/wp-json/contact-form-7/v1/contact-forms/<form_id>/feedback`), `fields`
|
|
263
|
-
(the form's input names, e.g. `["your-name","your-email","your-subject","your-message"]`) and
|
|
264
|
-
`honeypot_field` (`"your_website"`, the decoy the frontend renders empty). Note the form id in
|
|
265
|
-
`content-model.md` under `## Created in WordPress`.
|
|
92
|
+
| Exit | Meaning | Do |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `0` | Everything pushed and verified | Go on to Step 3 |
|
|
95
|
+
| `1` with `invalid` or `error` | The call or a file is wrong; nothing was pushed | Fix what it names and re-run |
|
|
96
|
+
| `1` with `failed` | Those items failed; the rest is in | Fix the cause the `error` names and re-run. If the same item fails twice, create just that item with [`by-hand.md`](by-hand.md) |
|
|
97
|
+
| `2` | It couldn't reach the site (sign-in, the site's MCP, a missing ability); `error` says why | Push with [`by-hand.md`](by-hand.md) |
|
|
266
98
|
|
|
267
|
-
|
|
268
|
-
> throwaway `mail_sent` test belongs to **validate** (Step 7), so one dummy email reaches the owner, not
|
|
269
|
-
> several. See the cookbook for the feedback contract and the hidden `_wpcf7*` fields the frontend needs.
|
|
99
|
+
A run cut short (a tool timeout, a kill) is safe to repeat: just run it again.
|
|
270
100
|
|
|
271
|
-
## Step
|
|
101
|
+
## Step 3 — Hand off
|
|
272
102
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
frontend
|
|
103
|
+
Run the contact-form and custom-type work above if the brief has them. Then update `state.json`
|
|
104
|
+
(`provision` stays `completed`; advance `stage` toward the frontend **data** phase) with a fresh
|
|
105
|
+
`updated_at`. The frontend reads `urls.rest_api` (and, for a store, `urls.store_api`), and nav from
|
|
106
|
+
`content-model.md`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# headless — WordPress content push by hand
|
|
2
|
+
|
|
3
|
+
The recipe `push-content.mjs` runs, for when it exits 2, or for one item that failed twice (create only
|
|
4
|
+
that item). The content is the same as in `content.json` (`SKILL.md` Step 1).
|
|
5
|
+
|
|
6
|
+
1. **Session.** Mint the token with `wordpress_installations_jwt-token { username, software: <site.json
|
|
7
|
+
wordpress.install_id> }`; never print, log or store it. POST JSON-RPC to `urls.wp_mcp` with the header
|
|
8
|
+
`X-Hostinger-JWT-Token: <token>` and `Accept: application/json, text/event-stream`. `initialize`
|
|
9
|
+
answers an `Mcp-Session-Id` header: send it on every later request. Then post
|
|
10
|
+
`notifications/initialized`; a 404 on it is harmless. A 401/403 means the token expired: re-mint.
|
|
11
|
+
2. **Abilities.** `tools/list` shows either one tool per ability or the meta-tools
|
|
12
|
+
`hostinger-ai-discover-abilities`, `hostinger-ai-get-ability-info { name }` and
|
|
13
|
+
`hostinger-ai-execute-ability { name, parameters }`. Abilities are named `hostinger-ai/<thing>-<action>`;
|
|
14
|
+
read each schema before use. A result is `{ ability, result }`: the new `id` is `result.id`, a list is
|
|
15
|
+
`result.data`, and an error is a normal result holding `{ code, message, data: { status } }`.
|
|
16
|
+
3. **Look up, then create.** Search first by slug (`posts-search`, `pages-search`, `categories-list`,
|
|
17
|
+
`tags-list`, `menus-search` take `slug: [<slug>]`), products by `sku`, coupons by `code`, and reuse a hit.
|
|
18
|
+
4. **In this order:** delete `hello-world` (`posts-search`, `posts-delete`) and `sample-page`; then
|
|
19
|
+
`wp-settings-update { title, description }`; categories and tags; images with
|
|
20
|
+
`media-upload-from-url { url, filename, title, alt_text, caption }`; `pages-create` and
|
|
21
|
+
`posts-create` with `slug`, `status: "publish"`, `categories`, `tags`, `featured_media`; `menus-create`,
|
|
22
|
+
then `menu-items-create { title: { raw }, type: "post_type", object: "page", object_id, menus }` (or
|
|
23
|
+
`type: "custom", url`); `seo-bulk-update { posts: [ { id, seo_title, meta_description } ] }`; then
|
|
24
|
+
`wc-product-categories-create`, `wc-products-create { sku, name, type: "simple", regular_price,
|
|
25
|
+
categories: [ { id } ], images: [ { id } ] }` and `wc-coupons-create`.
|
|
26
|
+
5. **Two traps.** Pass `title`, `content` and `excerpt` as `{ "raw": "…" }` objects: a bare string is
|
|
27
|
+
dropped while the create still succeeds. And `media-search` matches titles, not filenames, so before
|
|
28
|
+
uploading again, check the post's `featured_media` (`posts-get`), then search by the title you set.
|
|
29
|
+
6. **Verify and record.** `GET {urls.rest_api}/posts?per_page=100` (and `/pages`, `/media`) and
|
|
30
|
+
`GET {urls.store_api}/products?per_page=100` show each item once with its image. Write the id map into
|
|
31
|
+
`content-model.md` under `## Created in WordPress` (and `## Created in WooCommerce`).
|
|
@@ -13,7 +13,7 @@ custom post types — but there is **no generic `wp/v2` passthrough** (no `RestE
|
|
|
13
13
|
|
|
14
14
|
| Need | Plugin? | Do it with |
|
|
15
15
|
|---|---|---|
|
|
16
|
-
| Blog posts, static pages, categories, media, nav | **no** | per-site WP MCP (`SKILL.md`
|
|
16
|
+
| Blog posts, static pages, categories, media, nav | **no** | per-site WP MCP (`push-content.mjs`, `SKILL.md`) |
|
|
17
17
|
| A **custom post type** — Team, Services, Properties, Testimonials, a restaurant's dishes | **yes** | *Custom post type* below |
|
|
18
18
|
| A **custom taxonomy** — Property Type, Cuisine, Service Area | **yes** | *Custom taxonomy* below |
|
|
19
19
|
| **Global site content** the frontend needs — footer text, phone, address, opening hours, social links, a CTA line | **yes** | *Settings page + public settings route* below |
|
|
@@ -25,7 +25,7 @@ custom post types — but there is **no generic `wp/v2` passthrough** (no `RestE
|
|
|
25
25
|
> headless `/cms` with `hosting_deploy { type: "wordpress-plugin", headless: true }` (see *Deploy* below).
|
|
26
26
|
> Author one only for the *structure and behaviour* the per-site MCP can't express — types, taxonomies,
|
|
27
27
|
> settings, helpers. Don't write a **seed/config plugin** to load WooCommerce catalog data or page content;
|
|
28
|
-
> that goes through the per-site MCP's native abilities (`woocommerce.md`, `
|
|
28
|
+
> that goes through the per-site MCP's native abilities (`woocommerce.md`, `push-content.mjs`), never a plugin.
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -13,11 +13,10 @@ the cart, coupons and checkout.
|
|
|
13
13
|
|
|
14
14
|
## 1 — The one rule: drive the store through the per-site WP MCP
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
catalog through them, exactly as posts/pages are created.
|
|
16
|
+
There is **no seed plugin, no WP-CLI, and no REST consumer key in this flow**: catalog data goes through
|
|
17
|
+
the WP MCP. Instead, the **per-site WP MCP** (the Hostinger AI Assistant — the same JWT session the WordPress
|
|
18
|
+
content push uses, see `SKILL.md`) exposes **native WooCommerce abilities**. The push seeds the
|
|
19
|
+
catalog through them (§3); call them yourself only by hand or for work after the push.
|
|
21
20
|
|
|
22
21
|
Relevant abilities (names from `hostinger-ai-discover-abilities`; call `get-ability-info` for each schema
|
|
23
22
|
before use — they mirror the WooCommerce REST v3 shapes):
|
|
@@ -65,42 +64,18 @@ divisor: `prices.price` (e.g. `"1999"`) with `prices.currency_minor_unit` (e.g.
|
|
|
65
64
|
|
|
66
65
|
## 3 — Seeding the catalog (content-model push)
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"categories": [ { "id": <catId> } ] }
|
|
81
|
-
```
|
|
82
|
-
- `regular_price` is a **decimal string in major units** (`"39.99"`) on the ability side — the Store
|
|
83
|
-
API re-exposes it as minor units to the storefront.
|
|
84
|
-
- **Variable products:** create the parent (`type: "variable"`) with its attributes, then the
|
|
85
|
-
variations (via the product update / variations ability that discovery lists).
|
|
86
|
-
3. **Product images** — `media-upload-from-url { url, filename, alt_text, title }` (WordPress downloads
|
|
87
|
-
and stores its own copy; raster, HTTPS, direct file URL — source like the parent `SKILL.md` Unsplash
|
|
88
|
-
flow). Attach the returned media id to the product (`wc-products-update { id, images: [ { id } ] }`).
|
|
89
|
-
Idempotency: skip when the product already has images.
|
|
90
|
-
4. **Coupons** — for each coupon: `wc-coupons-search { code }`; empty → `wc-coupons-create`:
|
|
91
|
-
```json
|
|
92
|
-
{ "code": "WELCOME10", "discount_type": "percent", "amount": "10" }
|
|
93
|
-
```
|
|
94
|
-
(`discount_type`: `percent` | `fixed_cart` | `fixed_product`.)
|
|
95
|
-
|
|
96
|
-
Record the SKU → id map, category ids and coupon codes into `content-model.md` under
|
|
97
|
-
`## Created in WooCommerce` so the frontend and any re-run resolve references directly.
|
|
98
|
-
|
|
99
|
-
**Verify over the public Store API (no auth):**
|
|
100
|
-
- `GET {store_api}/products?per_page=100` → count matches the plan; each has `prices` (minor units +
|
|
101
|
-
`currency_minor_unit`) and, where seeded, an image.
|
|
102
|
-
- `GET {store_api}/products/categories` → every planned category present.
|
|
103
|
-
- Coupon: `wc-coupons-search { code }` returns it, or apply it to a throwaway cart and see the total drop.
|
|
67
|
+
The push seeds the catalog with the rest of the content: put product categories, products and coupons
|
|
68
|
+
under `store` in `content.json` and run `push-content.mjs` (`SKILL.md` Steps 1–2). It looks categories up
|
|
69
|
+
by slug, products by **SKU** and coupons by **code** before creating, so a re-run never duplicates; uploads
|
|
70
|
+
each product image once; checks every SKU and category over the Store API; and records the ids under
|
|
71
|
+
`## Created in WooCommerce` in `content-model.md`.
|
|
72
|
+
|
|
73
|
+
- `price` is a decimal string in **major units** (`"39.99"`); the Store API re-exposes it in minor units.
|
|
74
|
+
- Any other WooCommerce product field passes through as given (`sale_price`, `stock_quantity`,
|
|
75
|
+
`attributes`, …). For a **variable product**, the push creates the parent; add its variations by hand
|
|
76
|
+
through the product update / variations ability that discovery lists.
|
|
77
|
+
- Coupons: `discount_type` is `percent`, `fixed_cart` or `fixed_product`. WooCommerce stores codes in
|
|
78
|
+
lower case.
|
|
104
79
|
|
|
105
80
|
---
|
|
106
81
|
|
|
@@ -112,7 +87,7 @@ only writes standard WordPress settings (it ignores `woocommerce_*` options). So
|
|
|
112
87
|
|
|
113
88
|
- **The catalog, product pages, client-side cart and coupon application all work headlessly.**
|
|
114
89
|
- **Checkout (`POST /checkout`) does not work until the owner enables a payment method + a shipping zone
|
|
115
|
-
in wp-admin** (WooCommerce → Settings → Payments / Shipping; reach it via the one-click login in
|
|
90
|
+
in wp-admin** (WooCommerce → Settings → Payments / Shipping; reach it via the one-click login in `steps/09-done/SKILL.md`).
|
|
116
91
|
Cash on Delivery is the zero-config choice once enabled.
|
|
117
92
|
- Treat this exactly like the Hostinger Ecommerce `payment_enabled: false` gate: record
|
|
118
93
|
`woocommerce.payment_enabled: false`, tell the user in one line that checkout needs that one setup step,
|
|
@@ -144,10 +119,7 @@ through the per-site MCP and the storefront uses the public Store API. Document
|
|
|
144
119
|
|
|
145
120
|
## 6 — Idempotency & verification checklist
|
|
146
121
|
|
|
147
|
-
- **Idempotency keys:** products →
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
a cart apply. Record the id/SKU map into `content-model.md`.
|
|
152
|
-
- **Checkout** is verified only when `payment_enabled` is `true`; while `false`, confirming the catalog +
|
|
153
|
-
cart over the Store API is the whole job.
|
|
122
|
+
- **Idempotency keys:** products → SKU, coupons → code, categories → slug, images → skip when the product
|
|
123
|
+
already has `images`. `push-content.mjs` applies them; follow them too when seeding by hand.
|
|
124
|
+
- **Checkout** is verified only when `payment_enabled` is `true`; while `false`, the catalog and cart over
|
|
125
|
+
the Store API are the whole job.
|
|
@@ -117,7 +117,7 @@ Before inventing anything, gather what already constrains the design:
|
|
|
117
117
|
| Brand assets | `brief.md` → *Assets* | Logo, brand colours and fonts are **fixed** in every direction. Never recolour a supplied logo; Step 6 uses it instead of drawing an icon |
|
|
118
118
|
| Pages and entities | `content-model.md` → *Navigation*, *Pages*, *Articles*; if it doesn't exist yet, `brief.md` → *Pages* | Drives the component inventory and page composition |
|
|
119
119
|
| Reference sites | URLs in the brief or the conversation | Cues to borrow (see below) |
|
|
120
|
-
| Images already sourced | `content-model.md` → the image lines under *Articles
|
|
120
|
+
| Images already sourced | `content-model.md` → the image lines under *Articles*, or the `image` URLs in `content.json` on WordPress (skip if none yet) | Glance at a few at small size; keep accents from clashing with their dominant colours |
|
|
121
121
|
| Current design (redesign only) | existing `design.md` | What to keep, what to move away from |
|
|
122
122
|
|
|
123
123
|
**Reference sites.** Open each one in a real browser if you have one. Static fetches are often
|