@hostinger/mcp 3.2.2 → 3.3.1
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 +14 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/SKILL.md +245 -36
- package/skills/headless/connect/SKILL.md +98 -0
- package/skills/headless/{entry → connect}/bootstrap.mjs +8 -3
- package/skills/headless/connect/tools/antigravity.md +54 -0
- package/skills/headless/connect/tools/claude-code.md +46 -0
- package/skills/headless/connect/tools/codex.md +48 -0
- package/skills/headless/connect/tools/cursor.md +109 -0
- package/skills/headless/connect/tools/devin.md +50 -0
- package/skills/headless/connect/tools/vscode.md +57 -0
- package/skills/headless/report.schema.json +149 -0
- package/skills/headless/run-log.mjs +662 -0
- package/skills/headless/steps/01-check-state/SKILL.md +209 -0
- package/skills/headless/steps/02-refine/SKILL.md +273 -0
- package/skills/headless/steps/03-provision/SKILL.md +117 -0
- package/skills/headless/steps/03-provision/ecommerce/SKILL.md +142 -0
- package/skills/headless/steps/03-provision/webapp/SKILL.md +154 -0
- package/skills/headless/steps/03-provision/wordpress/SKILL.md +249 -0
- package/skills/headless/steps/04-content-model/SKILL.md +228 -0
- package/skills/headless/steps/04-content-model/ecommerce/SKILL.md +75 -0
- package/skills/headless/steps/04-content-model/wordpress/SKILL.md +223 -0
- package/skills/headless/steps/04-content-model/wordpress/woocommerce.md +153 -0
- package/skills/headless/steps/05-design/SKILL.md +464 -0
- package/skills/headless/steps/05-design/logo-kit.mjs +201 -0
- package/skills/headless/steps/06-frontend/SKILL.md +160 -0
- package/skills/headless/steps/07-validate/SKILL.md +56 -0
- package/skills/headless/steps/08-deploy/SKILL.md +179 -0
- package/skills/headless/steps/09-done/SKILL.md +157 -0
- package/src/core/skills.js +171 -24
- package/src/core/skills.ts +171 -24
- 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/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
- package/skills/headless/entry/skill.md +0 -79
- package/skills/headless/references/DATABASE.md +0 -49
- package/skills/headless/references/DEPLOYMENT.md +0 -34
- package/skills/headless/references/SETUP.md +0 -35
- package/skills/headless/references/STORE.md +0 -52
- package/skills/headless/references/WORDPRESS.md +0 -51
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: headless-step-content-model
|
|
3
|
+
description: "Step 4 (content model) of the Hostinger Headless flow. Prepare phase: turns brief.md into a content helper (content-model.md) — fixed Title/Description plus a tailored generation prompt per section (Navigation, Pages, Articles) — and generates the drafts/images, no live backend needed. Push phase: routes to the backend subfolder to create it the backend's way once provision is ready."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# headless-step-content-model
|
|
7
|
+
|
|
8
|
+
Two phases around the **instance barrier** (see the top-level `SKILL.md`):
|
|
9
|
+
|
|
10
|
+
- **Prepare — no instance.** Build the content helper from `brief.md` and generate the content it
|
|
11
|
+
calls for. Runs *during* the provision wait; needs only the brief.
|
|
12
|
+
- **Push — needs the instance.** Create that content in the backend. Gated on
|
|
13
|
+
`provision.status == "completed"`; routes to `wordpress/` or `ecommerce/`.
|
|
14
|
+
|
|
15
|
+
**In:** `brief.md`. **Out:** `content-model.md` (+ any generated media under
|
|
16
|
+
`~/.hostinger/<domain>/media/`).
|
|
17
|
+
|
|
18
|
+
> Don't wait for provisioning — the helper comes from the brief alone.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Prepare phase — write the content helper (no instance)
|
|
23
|
+
|
|
24
|
+
`content-model.md` is a lightweight helper, not a schema. Fill the **fixed values** from the brief and
|
|
25
|
+
write a **generation prompt** for each section — tailored to *this* site, not generic. Then run those
|
|
26
|
+
prompts to produce the drafts (and images), so the push phase is just upload.
|
|
27
|
+
|
|
28
|
+
Write `content-model.md` from this template, adapting every prompt to the brief's purpose, audience
|
|
29
|
+
and tone:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
# Content — <domain>
|
|
33
|
+
|
|
34
|
+
> Content helper prepared from brief.md on <today>. Fixed values are set; each prompt is run
|
|
35
|
+
> during the provision wait to generate the content and media. Push to the backend happens later.
|
|
36
|
+
|
|
37
|
+
## Title
|
|
38
|
+
<site name, from the brief>
|
|
39
|
+
|
|
40
|
+
## Description
|
|
41
|
+
<one–two line site description / default meta, from the brief>
|
|
42
|
+
|
|
43
|
+
## Navigation
|
|
44
|
+
> Generate the primary menu from the site's pages in reading order (<list the brief's pages>).
|
|
45
|
+
> Short labels; note which items are static pages vs. listing pages. Add a footer menu if the
|
|
46
|
+
> brief implies one (socials, contact).
|
|
47
|
+
|
|
48
|
+
## Pages
|
|
49
|
+
> Generate each page the brief lists. For every page: title, slug, a short section outline, the
|
|
50
|
+
> copy for static pages (e.g. About, Contact), and SEO title + description.
|
|
51
|
+
> <bullet the brief's pages with their one-line purpose>
|
|
52
|
+
|
|
53
|
+
## Articles (optional)
|
|
54
|
+
> Generate <N> articles across the brief's categories (<list them>). For each: title, slug,
|
|
55
|
+
> body in the brief's tone, category, any relationship (e.g. destination), published date, SEO,
|
|
56
|
+
> and a featured image (see *Images* below — Hostinger AI generation or Unsplash stock) with
|
|
57
|
+
> alt text. Omit if the brief has no article-like content.
|
|
58
|
+
|
|
59
|
+
## Products (optional — required for a commerce brief)
|
|
60
|
+
> Skip for a non-commerce brief. For a commerce brief, generate the catalog content here (prepare
|
|
61
|
+
> work, no instance): for each product — name, slug, SKU, short + long description in the brief's
|
|
62
|
+
> tone, price, the collection it belongs to, variants if any, and a product image (see *Images* —
|
|
63
|
+
> Hostinger AI generation or Unsplash stock) with alt text. Group products into the collections
|
|
64
|
+
> the brief implies, and generate any **coupons** the brief calls for (code, type, amount). The
|
|
65
|
+
> `ecommerce` push phase creates these on the store's sales channel; a **WordPress + WooCommerce** brief
|
|
66
|
+
> seeds them into WooCommerce instead (see `wordpress/SKILL.md` + `wordpress/woocommerce.md`) — there
|
|
67
|
+
> the **SKU is the idempotency key** and coupons + product categories are part of the catalog.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Guidance:
|
|
71
|
+
|
|
72
|
+
- **Fixed vs. prompt.** Title and Description are concrete values. Navigation, Pages and Articles are
|
|
73
|
+
prompts because their volume/wording is generated — keep them as prompts so the push phase (or a
|
|
74
|
+
rerun) can regenerate consistently.
|
|
75
|
+
- **Run the prompts now.** Generate the actual page copy and article drafts during the wait — this is
|
|
76
|
+
the slowest work and the whole reason it runs in parallel. Images depend on the chosen source (see
|
|
77
|
+
*Images* below): Unsplash photos and your own generated files can be produced now; Hostinger AI
|
|
78
|
+
images only generate once the instance exists, so write their brief now and generate at the barrier.
|
|
79
|
+
Put any generated/own media in `~/.hostinger/<domain>/media/` and reference it by filename. Record
|
|
80
|
+
alt text for every image.
|
|
81
|
+
- **Optional sections.** Drop **Articles** if the brief has no article-like content. Drop **Products**
|
|
82
|
+
for a non-commerce brief; for a commerce brief, generate the product catalog here (names, prices,
|
|
83
|
+
descriptions, collections, images) — that's prepare work, and the `ecommerce` push phase then creates
|
|
84
|
+
it on the store.
|
|
85
|
+
- **Don't block on design.** The design step (5) owns the look, and it runs in parallel during the
|
|
86
|
+
provision wait, so never wait for it here. If `design.md` already exists when you source images,
|
|
87
|
+
follow its **Imagery** section; otherwise generate neutral, on-topic imagery from the brief.
|
|
88
|
+
|
|
89
|
+
### Images — generate with Hostinger AI, or source stock from Unsplash
|
|
90
|
+
|
|
91
|
+
Every article (and every product) needs an image. There are two sources, and **you MUST offer the user
|
|
92
|
+
both and let them choose — never pick silently.** State the default for this backend in one line, name
|
|
93
|
+
the other option, and proceed on their answer (a skipped answer / "you decide" = use the default):
|
|
94
|
+
|
|
95
|
+
| Option | Tool | Backends | Needs the live instance? |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| **A — Hostinger AI image generation** *(default for WordPress / WooCommerce)* | `wordpress_ai-content_generate-images` | WordPress, WordPress + WooCommerce | **yes** — runs at the instance barrier |
|
|
98
|
+
| **B — Unsplash stock photos** *(default for Ecommerce / Webapp; the fallback everywhere)* | Unsplash API / web search | any | no — pure prepare work |
|
|
99
|
+
|
|
100
|
+
> **Do not silently fall back to stock on a WordPress site.** On WordPress / WooCommerce the default is
|
|
101
|
+
> **Hostinger AI (Option A)** — on-brand, generated from `design.md`. Using Unsplash on a WordPress build
|
|
102
|
+
> is only correct when **the user chose it**, or when **AI generation actually failed** (and then you say
|
|
103
|
+
> so). Taking stock because it is faster, synchronous, or avoids the async poll is a **defect**, not a
|
|
104
|
+
> shortcut: the async start → poll → upload-before-expiry flow (Option A below) is the expected work, not
|
|
105
|
+
> an optional extra. If you are tempted to skip it "for speed", stop and offer the user the choice instead.
|
|
106
|
+
|
|
107
|
+
If you can generate images yourself (some agents can), that's a third path: put the files in `media/`
|
|
108
|
+
and record alt text — no tool needed. **Record alt text for every image whichever source you use.**
|
|
109
|
+
|
|
110
|
+
**During the prepare wait, write an image brief for each image** into `content-model.md` no matter the
|
|
111
|
+
option (subject, `image_type`, aspect, and the alt text you expect). Option B can also *fetch* the
|
|
112
|
+
photo now (below); Option A only generates once the instance exists, so its briefs wait for the barrier.
|
|
113
|
+
|
|
114
|
+
#### Option A — Hostinger AI image generation (WordPress backends)
|
|
115
|
+
|
|
116
|
+
`wordpress_ai-content_generate-images` generates on-brand images for a live WordPress install, so it
|
|
117
|
+
runs **at the instance barrier** (after `provision.status == "completed"`), using the install from
|
|
118
|
+
`site.json` (`username`, and `wordpress.install_id` → the `software` path param). Follow `design.md`'s
|
|
119
|
+
**Imagery** section for look and palette. It is **asynchronous**:
|
|
120
|
+
|
|
121
|
+
1. **Start** one generation per image:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{ "operation": "wordpress_ai-content_generate-images",
|
|
125
|
+
"params": {
|
|
126
|
+
"username": "<site.json username>",
|
|
127
|
+
"software": "<site.json wordpress.install_id>",
|
|
128
|
+
"prompt": "<what the image shows; for a logo, the wordmark + mark to render>",
|
|
129
|
+
"image_type": "hero|background|inline|gallery|product|logo",
|
|
130
|
+
"num_of_images": 1,
|
|
131
|
+
"style": "<photographic style tokens from design.md; ignored for logo>",
|
|
132
|
+
"color_guidance": "<logo only — colour direction, not rendered as text>" } }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`prompt` (3–2500 chars) and `image_type` are required; `width`/`height` (128–2048) default per type
|
|
136
|
+
and are ignored for `logo`. Returns `202` with a `generation_id` and `status: in-progress`.
|
|
137
|
+
**Only one generation runs per install at a time** — a `409` means one is already running (and it
|
|
138
|
+
returns no id), so wait for it to finish before starting the next. The endpoint allows 10
|
|
139
|
+
requests/minute.
|
|
140
|
+
|
|
141
|
+
2. **Poll** `wordpress_ai-content_generation-result` with `{ username, software, generation_id }`
|
|
142
|
+
every few seconds until `status` is `completed` (or `failed`). A completed result carries
|
|
143
|
+
`result.images[]` — each with a **temporary** `url`, plus `width`, `height`, `content_type` — and
|
|
144
|
+
`result.expires_at`.
|
|
145
|
+
|
|
146
|
+
3. **Use the images before they expire.** The URLs are short-lived, so in the push phase hand each
|
|
147
|
+
`url` straight to WordPress with `media-upload-from-url`, which downloads and stores WP's own copy
|
|
148
|
+
(see the wordpress push `SKILL.md`). Don't persist the temporary URL as the live image. There is no
|
|
149
|
+
photographer to credit — leave `caption` empty or a short on-brand one. Record under the
|
|
150
|
+
article/product in `content-model.md`:
|
|
151
|
+
|
|
152
|
+
```markdown
|
|
153
|
+
Image: Hostinger AI (image_type: <type>, generation_id: <uuid>)
|
|
154
|
+
Image alt: <what the image shows>
|
|
155
|
+
Image file: <site>-<subject>.jpg
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
On `failed`, retry once with a tightened prompt; if it fails again, fall back to Option B and note
|
|
159
|
+
the gap. Never block the whole push on a single image.
|
|
160
|
+
|
|
161
|
+
#### Option B — stock photos from Unsplash (any backend, no instance)
|
|
162
|
+
|
|
163
|
+
This is still prepare work and needs no live backend: you pick the photos and record their URLs here,
|
|
164
|
+
and the push phase uploads them.
|
|
165
|
+
|
|
166
|
+
1. **Find candidates.** Use one photo per article, matching its subject and destination.
|
|
167
|
+
- With an `UNSPLASH_ACCESS_KEY` in the environment, use the official API:
|
|
168
|
+
`GET https://api.unsplash.com/search/photos?query=<subject>&orientation=landscape` with header
|
|
169
|
+
`Authorization: Client-ID <key>`.
|
|
170
|
+
- Without a key, use web search limited to `unsplash.com` (e.g. "unsplash photo Lisbon tram") and
|
|
171
|
+
take the photo id from the result URL: the last segment of `unsplash.com/photos/<slug>-<id>`, or
|
|
172
|
+
the whole segment for `unsplash.com/photos/<id>`.
|
|
173
|
+
- Don't call `unsplash.com/napi/…`. It sits behind a bot challenge and returns 401.
|
|
174
|
+
2. **Resolve a direct file URL.** `GET https://unsplash.com/photos/<id>/download?force=true` (don't
|
|
175
|
+
follow the redirect) answers `302` with `Location: https://images.unsplash.com/photo-<…>?…`. The
|
|
176
|
+
`dl=` parameter in it also carries the photographer's name. Drop the query string and request a
|
|
177
|
+
web size: `https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max`. The upload ability
|
|
178
|
+
needs this direct file URL, which returns 200 with `image/jpeg`. A photo *page* URL won't work.
|
|
179
|
+
3. **Look before you keep it.** Download a small preview (`?w=500`) and view it. Search snippets
|
|
180
|
+
only show captions, and a photo often isn't quite what its title says. Reject anything off-topic.
|
|
181
|
+
If nothing fits exactly, take the closest real photo and note the gap rather than mislabel it.
|
|
182
|
+
4. **Write alt text from what the photo shows**, not from the article's image brief. For example,
|
|
183
|
+
use "Shopfronts and parked scooters on a Hanoi street" even if the article is about rain.
|
|
184
|
+
5. **Record it** under each article in `content-model.md`:
|
|
185
|
+
|
|
186
|
+
```markdown
|
|
187
|
+
Image: https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max
|
|
188
|
+
Image credit: <Photographer> — https://unsplash.com/photos/<id> (Unsplash License)
|
|
189
|
+
Image alt: <what the photo shows>
|
|
190
|
+
Image file: <site>-<subject>.jpg
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Unsplash photos are free to use under the Unsplash License. Still credit the photographer: the push
|
|
194
|
+
phase stores "Photo by <Photographer> on Unsplash" as the image caption.
|
|
195
|
+
|
|
196
|
+
Then update `state.json`: set `stage` to the next prepare stage still to do, fresh `updated_at`. Do
|
|
197
|
+
**not** advance past the barrier here.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Push phase — create it the backend's way (needs the instance)
|
|
202
|
+
|
|
203
|
+
Only once `provision.status == "completed"` (re-verified against `hosting_websites_list-setups`),
|
|
204
|
+
route on the brief's **Suggested backend** and follow that subfolder, which maps the helper onto the
|
|
205
|
+
live backend — for WordPress, through the per-site WP MCP's ability tools (the installation JWT
|
|
206
|
+
authenticates that MCP session; `wp/v2` REST is the public read surface, not a write path for the
|
|
207
|
+
token) to create the menu, pages, posts, categories and media:
|
|
208
|
+
|
|
209
|
+
| Suggested backend | Follow |
|
|
210
|
+
|---|---|
|
|
211
|
+
| WordPress | `wordpress/SKILL.md` |
|
|
212
|
+
| WordPress + WooCommerce | `wordpress/SKILL.md` — pushes WP content **and** seeds the WooCommerce catalog (its WooCommerce section + `wordpress/woocommerce.md`) |
|
|
213
|
+
| Ecommerce | `ecommerce/SKILL.md` |
|
|
214
|
+
| Webapp | — (no push; skip to frontend) |
|
|
215
|
+
|
|
216
|
+
> **Supported backends: `wordpress` (optionally with a WooCommerce store), `ecommerce`, and `webapp`** —
|
|
217
|
+
> where `webapp` is "supported" by **skipping this whole stage** (it has no content backend; see the note
|
|
218
|
+
> below). A **WordPress + WooCommerce** brief routes to `wordpress/SKILL.md`, which pushes the WP content
|
|
219
|
+
> **and** seeds the WooCommerce catalog in the same push. PocketBase is not available in this version.
|
|
220
|
+
> Push to whichever single backend provision set up (see the provision router) — don't try to push to two
|
|
221
|
+
> backends here.
|
|
222
|
+
>
|
|
223
|
+
> **Webapp has no content backend.** There is no CMS or store to seed, so a `webapp` brief **skips this
|
|
224
|
+
> whole stage** — both prepare and push. The app owns its own data (its own API or database). Leave
|
|
225
|
+
> `content-model.md` unwritten, mark content-model `skipped` in the run log, and go straight to the
|
|
226
|
+
> frontend step.
|
|
227
|
+
|
|
228
|
+
After the push completes, update `state.json` and continue to the frontend **data** phase.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: headless-step-content-model-ecommerce
|
|
3
|
+
description: "Push phase of Step 4 for a Hostinger Ecommerce backend: create the catalog through the hostinger-ecommerce MCP — products (physical/digital) priced in minor units, product images from URLs, optional variants — on the store from site.json. Verified against the public Storefront V2 API. Idempotent, instance-gated."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# headless-step-content-model-ecommerce
|
|
7
|
+
|
|
8
|
+
**Push phase — needs the store.** Run after provision created the store + custom sales channel (ids in
|
|
9
|
+
`site.json → ecommerce`). All writes go through the **`hostinger-ecommerce`** MCP (`ecommerce_*`,
|
|
10
|
+
OAuth). Prices are **integers in minor units** (cents) and live on **variants**.
|
|
11
|
+
|
|
12
|
+
**In:** `content-model.md` (prepared catalog), `site.json` (`ecommerce.store_id`). **Out:** products +
|
|
13
|
+
images created in the store; ids recorded back into `content-model.md`.
|
|
14
|
+
|
|
15
|
+
> If the `hostinger-ecommerce` MCP tools aren't available, don't stop — see the prerequisite in
|
|
16
|
+
> `../../03-provision/ecommerce/SKILL.md` (add the server, and if you can't reconnect your session,
|
|
17
|
+
> drive the stdio server directly rather than blocking).
|
|
18
|
+
|
|
19
|
+
## Step 1 — Create products (idempotent)
|
|
20
|
+
|
|
21
|
+
List first so a re-run doesn't duplicate: `ecommerce_products_list { store_id }` — match by title/slug;
|
|
22
|
+
reuse existing ids. For each product in the helper:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{ "operation": "ecommerce_products_create-physical",
|
|
26
|
+
"params": { "store_id": "store_…", "name": "…", "price": 3999, "description": "…" } }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- `price` is minor units (3999 = $39.99); currency defaults to the store's. Each create makes one
|
|
30
|
+
**variant** (its `id` is what checkout needs — read it from the product detail/list).
|
|
31
|
+
- Digital goods: `ecommerce_products_create-digital` (takes a `download_url`).
|
|
32
|
+
- Multiple variants (sizes/flavours): `ecommerce_product-variants_create { store_id, product_id,
|
|
33
|
+
options:[{name,value}], prices:[{amount}] }`.
|
|
34
|
+
- Batch several creates with `multi-execute` to keep it fast.
|
|
35
|
+
|
|
36
|
+
## Step 2 — Product images (one call each)
|
|
37
|
+
|
|
38
|
+
`ecommerce_products_upload-and-attach-image { store_id, product_id, image_url, is_thumbnail: true }` —
|
|
39
|
+
fetches the image from a public URL and attaches it (raster only, no SVG, ≤15MB, HTTPS, ≤1 redirect).
|
|
40
|
+
Unsplash direct URLs (`https://images.unsplash.com/photo-…?w=1200&q=80&fm=jpg&fit=max`) work directly.
|
|
41
|
+
Source images as in the parent `../SKILL.md` (Unsplash). The store stores its own CDN copy
|
|
42
|
+
(`cdn.zyrosite.com`), so the storefront never depends on the source URL.
|
|
43
|
+
|
|
44
|
+
> Idempotency: check the product's existing `images`/`thumbnail` before attaching so a re-run doesn't pile
|
|
45
|
+
> on duplicates.
|
|
46
|
+
|
|
47
|
+
## Step 3 — Collections
|
|
48
|
+
|
|
49
|
+
The API does not expose a collection-create op in this flow, and `product_collections` comes back empty.
|
|
50
|
+
Represent collections (e.g. Proteins / Supplements) as a **frontend mapping** in the storefront
|
|
51
|
+
(`lib/site.ts` → product-slug → collection), and record that mapping in `content-model.md`. If a real
|
|
52
|
+
collection API becomes available, prefer it.
|
|
53
|
+
|
|
54
|
+
## Step 4 — Verify (public Storefront V2, no auth)
|
|
55
|
+
|
|
56
|
+
Keyed on the **sales channel** (`site.json → ecommerce.sales_channel.id`):
|
|
57
|
+
|
|
58
|
+
- `GET https://api-ecommerce.hostinger.com/v2/channels/<scha_*>/products?limit=100` → all products, each
|
|
59
|
+
with `variants[].prices[]` (amount in minor units + currency `decimal_digits`/`template`), `thumbnail`
|
|
60
|
+
and `images[]`. Count matches the plan.
|
|
61
|
+
- Detail resolves by **product id** (`…/products/<id>`); a **slug 404s** on detail — map slug→id from the
|
|
62
|
+
list in the frontend.
|
|
63
|
+
|
|
64
|
+
## Step 5 — Hand off
|
|
65
|
+
|
|
66
|
+
Record the id map in `content-model.md` (`## Created in the store`: product → slug → id → variant id →
|
|
67
|
+
price → image). Update `state.json` (advance `stage` toward frontend; leave `provision`). The storefront
|
|
68
|
+
(Step 6) reads the public Storefront V2 API for the channel; checkout is
|
|
69
|
+
`POST /channels/<scha_*>/checkout` (needs a payment method — see the provision payment gate).
|
|
70
|
+
|
|
71
|
+
> **No payment method = no checkout to seed or verify.** When `site.json → ecommerce.payment_enabled`
|
|
72
|
+
> is `false` (free account / no paid Ecommerce plan — see the provision gate), the catalog push here is
|
|
73
|
+
> **complete on its own**. There is nothing to seed for checkout, and you must **not** test the checkout
|
|
74
|
+
> endpoint or wait on it — seeding products and verifying them over the Storefront V2 API is the whole
|
|
75
|
+
> job. Hand off normally.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: headless-step-content-model-wordpress
|
|
3
|
+
description: "Push phase of Step 4 for a headless WordPress backend: open an authenticated session to the per-site WP MCP with the installation JWT, then create the content helper's Title/Description, categories, media, pages, posts and navigation through the MCP ability tools. wp/v2 REST is the public read surface, not a write path for this token. Idempotent, instance-gated."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# headless-step-content-model-wordpress
|
|
7
|
+
|
|
8
|
+
**Push phase — needs the live instance.** Run only after `provision.status == "completed"` (re-verify
|
|
9
|
+
against `hosting_websites_list-setups`). It maps the backend-agnostic `content-model.md` onto real
|
|
10
|
+
WordPress objects.
|
|
11
|
+
|
|
12
|
+
**In:** `content-model.md`, `site.json` (both prepared earlier), generated media under
|
|
13
|
+
`~/.hostinger/<domain>/media/`. **Out:** content created in WordPress; created IDs recorded back into
|
|
14
|
+
`content-model.md`.
|
|
15
|
+
|
|
16
|
+
## What creates what — and what authenticates it
|
|
17
|
+
|
|
18
|
+
The Hostinger **management** MCP (`wordpress_*`) is infra-only: it installs WordPress, plugins and
|
|
19
|
+
themes but has **no content endpoints**.
|
|
20
|
+
|
|
21
|
+
Writes go through the **per-site WP MCP** (the Hostinger AI Assistant plugin), authenticated with the
|
|
22
|
+
installation JWT. **The JWT authenticates the MCP endpoint only** — it is wired as the MCP server's
|
|
23
|
+
`authenticate_request`, and the plugin registers no global `determine_current_user` /
|
|
24
|
+
`rest_authentication_errors` filter. So a raw `POST /wp/v2/posts` with this token is **not**
|
|
25
|
+
authenticated; the admin user context (`wp_set_current_user`) exists only *inside* an MCP session.
|
|
26
|
+
|
|
27
|
+
| Surface | Base (from `site.json`) | Role |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **Per-site WP MCP** | `urls.wp_mcp` (`mcp_url`) | **all writes** — create content through its ability tools |
|
|
30
|
+
| **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 |
|
|
31
|
+
|
|
32
|
+
> **The WordPress content model is posts, pages and categories** (plus media). For **SEO**, **Yoast**
|
|
33
|
+
> is already installed + activated at provision (every headless WordPress site — see provision Step 4),
|
|
34
|
+
> so meta + schema come from a maintained plugin and the frontend reads each item's `yoast_head_json`.
|
|
35
|
+
> This push only sets per-item Yoast meta (Step 4.7 below).
|
|
36
|
+
|
|
37
|
+
> **A store on this site? See [`woocommerce.md`](woocommerce.md).** When the brief's **Suggested
|
|
38
|
+
> backend** is `**WordPress + WooCommerce**` (`site.json → woocommerce.enabled: true`), this push also
|
|
39
|
+
> seeds the WooCommerce catalog — products, categories and coupons — through the **same per-site WP MCP
|
|
40
|
+
> session**, which exposes native `wc-products-*`, `wc-product-categories-*` and `wc-coupons-*` abilities
|
|
41
|
+
> (no custom plugin, no consumer key). Do the WordPress content below as normal, then run the **WooCommerce
|
|
42
|
+
> catalog** section before you hand off.
|
|
43
|
+
|
|
44
|
+
## Step 1 — Open the authenticated MCP session
|
|
45
|
+
|
|
46
|
+
Mint a short-lived token through the Hostinger connector:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{ "operation": "wordpress_installations_jwt-token",
|
|
50
|
+
"params": { "username": "<site.json username>", "software": "<site.json wordpress.install_id>" } }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Returns `token` (+ `mcp_url`, already stored as `urls.wp_mcp`). Connect to `urls.wp_mcp` as an MCP
|
|
54
|
+
client (`initialize` → `tools/list` → `tools/call`) and send the token on **every** request as:
|
|
55
|
+
|
|
56
|
+
- `X-Hostinger-JWT-Token: <token>` — the plugin's preferred header (checked first), **or**
|
|
57
|
+
- `Authorization: Bearer <token>` — accepted as a fallback (case-insensitive `bearer ` prefix).
|
|
58
|
+
|
|
59
|
+
The token lives ~24 h — mint on demand, **never print, log, or write it to `site.json` /
|
|
60
|
+
`content-model.md`**. If a call returns `401/403` with `token_invalid`/`invalid_api_key`, the token
|
|
61
|
+
expired or was revoked → re-mint and reconnect.
|
|
62
|
+
|
|
63
|
+
> **Transport gotchas (streamable HTTP MCP).** The endpoint is session-based: `initialize` returns an
|
|
64
|
+
> **`Mcp-Session-Id` response header** — capture it and send it on **every** subsequent request
|
|
65
|
+
> (`tools/list`, `tools/call`), or they fail with `-32600 Missing Mcp-Session-Id header`. Send
|
|
66
|
+
> `Accept: application/json, text/event-stream`. After `initialize`, post the `notifications/initialized`
|
|
67
|
+
> notification, and **tolerate a non-2xx on it** (it can answer 404 with no body — harmless; don't let
|
|
68
|
+
> it abort the session). A raw curl/HTTP client works fine for all this.
|
|
69
|
+
|
|
70
|
+
## Step 2 — Discover the ability tools
|
|
71
|
+
|
|
72
|
+
The server advertises either every ability as its own tool, or three **discovery meta-tools**
|
|
73
|
+
(discover → get-info → execute) to keep `tools/list` small. Call `tools/list` first and work with
|
|
74
|
+
what's there; with discovery, list abilities, then execute by ability name. The content tools that
|
|
75
|
+
cover this step (exact names come from discovery — don't hardcode them):
|
|
76
|
+
|
|
77
|
+
`SettingsTools` · `CategoriesTools` · `TagsTools` · `MediaUploadTools` / `MediaTools` · `PagesTools` ·
|
|
78
|
+
`PostsTools` · `MenusTools` / `NavigationTools` · `SeoTools`. A generic
|
|
79
|
+
`RestEndpointTool` can reach any `wp/v2` route *through the authenticated session* if no dedicated tool
|
|
80
|
+
fits.
|
|
81
|
+
|
|
82
|
+
Seen on a live install (Hostinger AI Assistant, discovery mode): the meta-tools are
|
|
83
|
+
`hostinger-ai-discover-abilities`, `hostinger-ai-get-ability-info { name }` and
|
|
84
|
+
`hostinger-ai-execute-ability { name, parameters }`. The abilities are named
|
|
85
|
+
`hostinger-ai/<thing>-<action>`, e.g. `posts-create`, `posts-update`, `media-upload-from-url`,
|
|
86
|
+
`media-delete`. Call `get-ability-info` for each schema before using it. Results come back as
|
|
87
|
+
`{ ability, result: { data, … } }` for lists and `{ ability, result: {…} }` for single objects — so a
|
|
88
|
+
create answers **`200`** (not `201`) and the new object's `id` is **inside** `result` (`result.id`, or
|
|
89
|
+
`result.data.id`), never at the top level. A missing top-level `id` is not a failure; read it from
|
|
90
|
+
`result`.
|
|
91
|
+
|
|
92
|
+
## Step 3 — Idempotency rule (makes resume safe)
|
|
93
|
+
|
|
94
|
+
Every create is **list/get-by-slug first, then create only if absent** — a re-run must not duplicate.
|
|
95
|
+
Use the matching read ability (or `RestEndpointTool` → `GET /wp/v2/posts?slug=…&status=any`): a hit →
|
|
96
|
+
reuse its `id`; empty → create. Capture each returned `id` and record the id map back into
|
|
97
|
+
`content-model.md` so later references resolve without re-querying.
|
|
98
|
+
|
|
99
|
+
## Step 4 — Create in dependency order
|
|
100
|
+
|
|
101
|
+
0. **Remove WordPress's default content first.** A fresh install ships a **"Hello world!" post**
|
|
102
|
+
(id 1), a **"Sample Page"**, and a default comment — they will otherwise show up on the live site.
|
|
103
|
+
Delete the default post (`posts-delete { id: 1, force: true }`) and, if present, the sample page
|
|
104
|
+
(find it via `pages-search` for slug `sample-page`, then `pages-delete`). This is idempotent: if
|
|
105
|
+
they're already gone, skip. Leave the default `Uncategorized` category — it's harmless once no post
|
|
106
|
+
uses it (the frontend only lists categories with posts).
|
|
107
|
+
1. **Site Title & Description** (the helper's global part) → the Settings ability.
|
|
108
|
+
2. **Categories / tags** — one per term in the helper → Categories / Tags abilities. Keep `name → id`.
|
|
109
|
+
3. **Media.** Upload every image in the helper, set `alt_text`, and keep `filename → id`. See
|
|
110
|
+
*Media upload* below for all sources: a Hostinger-AI image URL, an Unsplash URL, or a generated
|
|
111
|
+
file in `media/`. For a Hostinger-AI image, generate it now (its URL is temporary — see the
|
|
112
|
+
content-model `SKILL.md`, *Images* Option A) and upload straight away, before `expires_at`.
|
|
113
|
+
4. **Pages** — each static/index page → Pages ability. Pass `title`, `content` and `excerpt` as
|
|
114
|
+
**`{ "raw": "…" }` objects** (see the note below), plus `slug`, `status: publish`. Keep `slug → id`.
|
|
115
|
+
5. **Posts (articles)** — each → Posts ability: `title`, `content` and `excerpt` as **`{ "raw": "…" }`**
|
|
116
|
+
(note below), plus `slug`, `status: publish`, `categories: [id]`, `tags: [id…]`,
|
|
117
|
+
`featured_media: <id>`, `date`. (The plugin block-converts post content on insert via
|
|
118
|
+
`rest_pre_insert_post`, so HTML bodies in `content.raw` are fine.)
|
|
119
|
+
|
|
120
|
+
> **Create payload gotcha (`pages-create` / `posts-create`).** These abilities take `title`,
|
|
121
|
+
> `content` and `excerpt` as WordPress's core **`{ "raw": "…" }` objects**, not bare strings. Pass a
|
|
122
|
+
> bare string and the field is silently dropped — the create still answers `200`, so you get a
|
|
123
|
+
> *published-but-empty* page or post with nothing to key off. Always send `{ "raw": "…" }` and
|
|
124
|
+
> read the new `id` from `result` (see Step 2). Confirm the exact field shape with
|
|
125
|
+
> `get-ability-info` if a schema differs.
|
|
126
|
+
6. **Navigation** — Menus / Navigation abilities: create the menu, then an item per nav entry
|
|
127
|
+
referencing a page `id` (or a custom URL for listing routes). **Headless note:** the Step 6
|
|
128
|
+
frontend renders nav from `content-model.md`, so the WP menu is for owners editing in wp-admin, not
|
|
129
|
+
what the live site reads — create it, but it isn't the source of truth.
|
|
130
|
+
7. **SEO** — **Yoast SEO** (`wordpress-seo`) is already installed + activated at provision (Step 4 of
|
|
131
|
+
the WordPress provision recipe), so it generates meta + schema and the frontend reads
|
|
132
|
+
`yoast_head_json` per post/page. Here, just set each post's Yoast title/description if an ability
|
|
133
|
+
exposes it; otherwise the excerpt + title are a fine fallback. Don't fail the push over SEO fields.
|
|
134
|
+
If `site.json → seo.enabled` is `false` (the owner declined Yoast, or provision couldn't confirm the
|
|
135
|
+
install), skip this and let the frontend generate tags from content — don't try to install the
|
|
136
|
+
plugin from the content stage.
|
|
137
|
+
|
|
138
|
+
### Media upload
|
|
139
|
+
|
|
140
|
+
**From a URL (a Hostinger-AI image, Unsplash, or any direct file URL).** Use
|
|
141
|
+
`hostinger-ai/media-upload-from-url`. WordPress downloads the file and stores its own copy, so the
|
|
142
|
+
site never depends on the external URL staying up — which is exactly why a **Hostinger-AI** image
|
|
143
|
+
(whose `url` from `wordpress_ai-content_generation-result` is temporary) must be uploaded here, while
|
|
144
|
+
it is still valid; drop the Unsplash-only `caption`/`description` for AI images:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "name": "hostinger-ai/media-upload-from-url",
|
|
148
|
+
"parameters": {
|
|
149
|
+
"url": "https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max",
|
|
150
|
+
"filename": "<site>-<subject>.jpg",
|
|
151
|
+
"title": "<short title>",
|
|
152
|
+
"alt_text": "<what the photo shows>",
|
|
153
|
+
"caption": "Photo by <Photographer> on Unsplash",
|
|
154
|
+
"description": "Source: https://unsplash.com/photos/<id> (Unsplash License)",
|
|
155
|
+
"post_id": <post id, if the post already exists> } }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- `url` must return 200 with an image content type. A photo *page* URL fails.
|
|
159
|
+
- The result carries the attachment `id` and `source_url`. WordPress also generates the smaller
|
|
160
|
+
sizes (`300x200`, `768x…`, `1024x…`, …) under `media_details.sizes`, which the frontend can use
|
|
161
|
+
for `srcset`.
|
|
162
|
+
- Set `filename` yourself, so files in `wp-content/uploads/` have readable names.
|
|
163
|
+
|
|
164
|
+
**From a local file in `media/`.** Use the Media-upload ability that discovery lists for local
|
|
165
|
+
files.
|
|
166
|
+
|
|
167
|
+
**Set the featured image.** Pass `featured_media: <id>` when creating the post (Step 4.5). If the
|
|
168
|
+
post already exists (on resume, or when images are added later), use
|
|
169
|
+
`hostinger-ai/posts-update { id, featured_media }`.
|
|
170
|
+
|
|
171
|
+
**Idempotency trap: don't look up earlier uploads by filename.** `media-search` matches titles and
|
|
172
|
+
content, not filenames or the slug you passed. A search for `<site>-lisbon-tram` finds nothing even
|
|
173
|
+
though the file exists, so a re-run would upload the image again. WordPress then stores the duplicate
|
|
174
|
+
as `…-1.jpg` and silently repoints the post at it. Check this way instead:
|
|
175
|
+
|
|
176
|
+
1. `posts-get { id }`. If `featured_media` is already set, skip that post. Its image exists.
|
|
177
|
+
2. Otherwise, search by the **title** you set on upload (`media-search { search: "<title>" }`),
|
|
178
|
+
and reuse a hit.
|
|
179
|
+
3. Only upload when both checks come up empty.
|
|
180
|
+
|
|
181
|
+
If duplicates do slip in, point each post back at the original with `posts-update`, then delete the
|
|
182
|
+
duplicates with `media-delete { id, force: true }`.
|
|
183
|
+
|
|
184
|
+
## Step 5 — Verify and record
|
|
185
|
+
|
|
186
|
+
- Read a couple of created objects back through `wp/v2` (public GET, no auth): e.g.
|
|
187
|
+
`GET {urls.rest_api}/posts?per_page=1` and check the `X-WP-Total` header against the planned article
|
|
188
|
+
count; fetch one post with `_embed` to confirm `featured_media` and `categories` resolved.
|
|
189
|
+
- Media: `GET {urls.rest_api}/media?per_page=1` → `X-WP-Total` equals the number of images planned
|
|
190
|
+
(more means duplicates). `GET {urls.rest_api}/media?include=<ids>` confirms `alt_text` and
|
|
191
|
+
`caption` on each image. Each `source_url` must return 200 with an image type. Each post's
|
|
192
|
+
`featured_media` must point at its image.
|
|
193
|
+
- Write the created-id map into `content-model.md` (a `## Created in WordPress` section: object → slug
|
|
194
|
+
→ id) so the frontend data phase and any re-run resolve references directly. Include one row per
|
|
195
|
+
media item: filename, credit and source page, the post it's featured on, and its id.
|
|
196
|
+
|
|
197
|
+
## WooCommerce catalog *(only for a `WordPress + WooCommerce` brief — skip otherwise)*
|
|
198
|
+
|
|
199
|
+
When `site.json → woocommerce.enabled` is `true`, also seed the store **in this same authenticated MCP
|
|
200
|
+
session**, using WooCommerce abilities the Assistant exposes alongside the content ones. Full recipe (incl.
|
|
201
|
+
the storefront Store API and the checkout gate): [`woocommerce.md`](woocommerce.md). In short:
|
|
202
|
+
|
|
203
|
+
1. **Product categories** — `wc-product-categories-list` (reuse by slug/name) else
|
|
204
|
+
`wc-product-categories-create { name, slug }`. Keep `name → id`.
|
|
205
|
+
2. **Products** — **idempotent by SKU**: `wc-products-search { sku }` → reuse, else
|
|
206
|
+
`wc-products-create { name, type:"simple", sku, regular_price:"<decimal>", description, status:"publish",
|
|
207
|
+
categories:[{id}] }`. For images, `media-upload-from-url` then `wc-products-update { id, images:[{id}] }`.
|
|
208
|
+
3. **Coupons** — **idempotent by code**: `wc-coupons-search { code }` → reuse, else
|
|
209
|
+
`wc-coupons-create { code, discount_type, amount }`.
|
|
210
|
+
4. **Verify over the public Store API** (no auth): `GET {urls.store_api}/products?per_page=100` count +
|
|
211
|
+
`prices` (minor units), `GET {urls.store_api}/products/categories`. Record the SKU→id map, category ids
|
|
212
|
+
and coupon codes into `content-model.md` under `## Created in WooCommerce`.
|
|
213
|
+
|
|
214
|
+
> **Don't seed or test checkout here.** A fresh WooCommerce install has no payment gateway/shipping and
|
|
215
|
+
> that config isn't reachable headlessly (`site.json → woocommerce.payment_enabled` is `false` — the owner
|
|
216
|
+
> finishes it in wp-admin). Seeding + verifying the catalog over the Store API is the whole job; never call
|
|
217
|
+
> `/checkout` or block on it. See the cookbook §4.
|
|
218
|
+
|
|
219
|
+
## Step 6 — Hand off
|
|
220
|
+
|
|
221
|
+
Update `state.json` (`provision` stays `completed`; advance `stage` toward the frontend **data**
|
|
222
|
+
phase) with a fresh `updated_at`. The backend now holds the content; Step 6's data wiring points the
|
|
223
|
+
frontend at `urls.rest_api` (and, for a store, `urls.store_api`).
|