@hostinger/mcp 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/gemini-extension.json +1 -1
  3. package/package.json +1 -1
  4. package/server.json +2 -2
  5. package/skills/headless/SKILL.md +247 -36
  6. package/skills/headless/connect/SKILL.md +103 -0
  7. package/skills/headless/{entry → connect}/bootstrap.mjs +8 -3
  8. package/skills/headless/connect/tools/antigravity.md +54 -0
  9. package/skills/headless/connect/tools/claude-code.md +46 -0
  10. package/skills/headless/connect/tools/codex.md +48 -0
  11. package/skills/headless/connect/tools/cursor.md +109 -0
  12. package/skills/headless/connect/tools/devin.md +50 -0
  13. package/skills/headless/connect/tools/vscode.md +57 -0
  14. package/skills/headless/report.schema.json +149 -0
  15. package/skills/headless/run-log.mjs +662 -0
  16. package/skills/headless/steps/01-check-state/SKILL.md +193 -0
  17. package/skills/headless/steps/02-refine/SKILL.md +206 -0
  18. package/skills/headless/steps/03-provision/SKILL.md +62 -0
  19. package/skills/headless/steps/03-provision/ecommerce/SKILL.md +133 -0
  20. package/skills/headless/steps/03-provision/webapp/SKILL.md +154 -0
  21. package/skills/headless/steps/03-provision/wordpress/SKILL.md +100 -0
  22. package/skills/headless/steps/04-content-model/SKILL.md +150 -0
  23. package/skills/headless/steps/04-content-model/ecommerce/SKILL.md +75 -0
  24. package/skills/headless/steps/04-content-model/wordpress/SKILL.md +178 -0
  25. package/skills/headless/steps/05-design/SKILL.md +455 -0
  26. package/skills/headless/steps/05-design/logo-kit.mjs +201 -0
  27. package/skills/headless/steps/06-frontend/SKILL.md +131 -0
  28. package/skills/headless/steps/07-validate/SKILL.md +53 -0
  29. package/skills/headless/steps/08-deploy/SKILL.md +171 -0
  30. package/skills/headless/steps/09-done/SKILL.md +151 -0
  31. package/src/core/skills.js +164 -24
  32. package/src/core/skills.ts +164 -24
  33. package/src/servers/agency-hosting.js +1 -1
  34. package/src/servers/agency-hosting.ts +1 -1
  35. package/src/servers/agent-readiness.js +1 -1
  36. package/src/servers/agent-readiness.ts +1 -1
  37. package/src/servers/all.js +1 -1
  38. package/src/servers/all.ts +1 -1
  39. package/src/servers/billing.js +1 -1
  40. package/src/servers/billing.ts +1 -1
  41. package/src/servers/dns.js +1 -1
  42. package/src/servers/dns.ts +1 -1
  43. package/src/servers/domains.js +1 -1
  44. package/src/servers/domains.ts +1 -1
  45. package/src/servers/ecommerce.js +1 -1
  46. package/src/servers/ecommerce.ts +1 -1
  47. package/src/servers/horizons.js +1 -1
  48. package/src/servers/horizons.ts +1 -1
  49. package/src/servers/hosting.js +1 -1
  50. package/src/servers/hosting.ts +1 -1
  51. package/src/servers/mail.js +1 -1
  52. package/src/servers/mail.ts +1 -1
  53. package/src/servers/reach.js +1 -1
  54. package/src/servers/reach.ts +1 -1
  55. package/src/servers/vps.js +1 -1
  56. package/src/servers/vps.ts +1 -1
  57. package/src/servers/wordpress.js +1 -1
  58. package/src/servers/wordpress.ts +1 -1
  59. package/skills/headless/entry/skill.md +0 -79
  60. package/skills/headless/references/DATABASE.md +0 -49
  61. package/skills/headless/references/DEPLOYMENT.md +0 -34
  62. package/skills/headless/references/SETUP.md +0 -35
  63. package/skills/headless/references/STORE.md +0 -52
  64. package/skills/headless/references/WORDPRESS.md +0 -51
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: headless-step-provision-webapp
3
+ description: "Provision a plain Node.js app host for the Hostinger Headless flow: create an empty website (no WordPress, no store engine, no /cms), optionally a MySQL database, then record it in site.json. For any webapp / SPA / app that brings its own code and data."
4
+ ---
5
+
6
+ # headless-step-provision-webapp
7
+
8
+ Provision a bare **Node.js app host** for `<domain>` and prove it is ready. This is the backend for
9
+ **any webapp, SPA or app** that brings its own code (and, if it needs one, its own database) — there is
10
+ no CMS and no store engine here. The site is an **empty website**: just the hosting slot that the
11
+ frontend step builds into and the deploy step ships to as a Node.js application. **Out:** `site.json`.
12
+
13
+ > **Not WordPress, not Ecommerce.** Never provision `headless_wordpress` / `headless_ecommerce` for a
14
+ > webapp, and never install WordPress at `/cms`. A webapp owns its own routes, data and rendering.
15
+
16
+ All website/database operations below go through the Hostinger connector (`execute`).
17
+
18
+ > **Fire vs join (background provision).** Steps 1–2 up to `hosting_websites_start-setup` are the
19
+ > **fire** half: once the setup is queued, record `provision.in_progress` and return to do prepare work
20
+ > (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2, plus Steps 3–5, are the
21
+ > **join** half — run them when an instance-barrier step needs the backend (or immediately, if the
22
+ > agent can't interleave). An already-`completed` site detected in Step 1 means provision is done:
23
+ > record `provision.completed` and adopt it.
24
+
25
+ ## Step 1 — Detect before you create (never provision twice)
26
+
27
+ 1. `hosting_websites_list { "domain": "<domain>" }` — substring filter, so **exact-match** the row's
28
+ `domain`. If a row exists, note `username`, `order_id`, `website_type`.
29
+ 2. `hosting_websites_list-setups { "domain": "<domain>" }` — exact-match the row:
30
+ - `completed` → already provisioned → skip to Step 3 (adopt it).
31
+ - `running` → skip to Step 2 (poll).
32
+ - `failed` → tell the user and point them at hPanel; do **not** call start-setup again on your own.
33
+ 3. If the site exists but is a different kind (`website_type` is `headless`, or a WordPress/store site),
34
+ stop and tell the user — this domain already hosts something else.
35
+
36
+ ## Step 2 — Create (only when nothing exists)
37
+
38
+ 1. Pick the plan: `hosting_orders_list { "statuses": ["active"] }`. One active order → use it; several
39
+ → ask the user which.
40
+ 2. Start the setup as an **empty website** — **omit `type`** (no WordPress, no store):
41
+
42
+ ```json
43
+ { "operation": "hosting_websites_start-setup",
44
+ "params": { "order_id": <order_id>, "domain": "<domain>" } }
45
+ ```
46
+
47
+ `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,
48
+ **omit `domain`** — the setup generates one itself. Don't call a separate subdomain op to make one
49
+ first: there's no need, and passing a generated free subdomain back as `domain` fails with
50
+ `422 … Free subdomains cannot be provided`. The response then carries the generated domain: if it
51
+ differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,
52
+ update the domain in `brief.md`, and tell the user the new domain.
53
+ 3. Poll `hosting_websites_list-setups { "domain": "<domain>" }` every ~15 s until `completed`
54
+ (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.
55
+
56
+ ## Step 3 — Database (only if the app needs one)
57
+
58
+ Skip this step entirely for an app with no database (static SPA, an app that talks to an external API).
59
+ This is **join-half** work — it needs the live website, so run it once `provision.status == "completed"`.
60
+
61
+ **Preferred — let the platform create the database and inject the credentials:**
62
+ `hosting_databases_setup-website { username, domain }` creates a MySQL database, **generates the
63
+ password, writes `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASSWORD` / `DATABASE_URL` into the
64
+ website's environment variables, and restarts the app — in one call.** The password is never returned,
65
+ so there is **nothing to persist and nothing to lose across sessions** — the app reads it from the
66
+ environment. (It 422s if any of those env vars already exist; clear them first with
67
+ `hosting_nodejs_replace-environment-variables`. Frameworks that bake vars into the build — Next.js,
68
+ `NEXT_PUBLIC_*` — only pick up the values on the deploy build.)
69
+
70
+ > **⚠️ A later `hosting_nodejs_replace-environment-variables` wipes these credentials.** That call is a
71
+ > **full replace**, and when you list the current vars first the `DB_PASSWORD` (and `DATABASE_URL`) read
72
+ > back **masked** (`********`) — the generated password is unknown, so you cannot resend it and the DB
73
+ > env is lost. One rule, pick one:
74
+ > - **(a) Set every other app env var first, and call `setup-website` last** — then nothing has to
75
+ > replace the env again.
76
+ > - **(b) If a later replace is unavoidable**, reset the password with
77
+ > `hosting_databases_change-password { username, name, password }` and resend **all** of `DB_HOST`,
78
+ > `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` with the new password in that same
79
+ > full replace.
80
+
81
+ **Manual alternative** — only when you need a specific name/user or finer control:
82
+
83
+ 1. `hosting_databases_create` with a name, a user, and a password you generate — name and user are
84
+ prefixed with the account username automatically. Read the full, prefixed `name`/`user` back from
85
+ `hosting_databases_list`.
86
+ 2. **Which host the app connects to:** the app runs **on the website**, so Node.js connects to
87
+ **`127.0.0.1:3306`** — never `localhost` (it can resolve to IPv6 `::1`, and not every database user
88
+ has that grant), and never the external `host` from `hosting_databases_list` (`srvNNNN.hstgr.io`,
89
+ which has no grant for connections from the server itself).
90
+ 3. **Set the env vars now, while you still hold the generated password.** The password is **not persisted
91
+ anywhere** (never in `site.json`), so set `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_NAME`, `DB_USER`,
92
+ `DB_PASSWORD`, plus `DATABASE_URL=mysql://USER:PASSWORD@127.0.0.1:3306/NAME` (URL-encode USER/PASSWORD)
93
+ right here with `hosting_nodejs_replace-environment-variables` (a **full replace** — list current keys
94
+ first; values come back masked; send the complete set with real values). Saving restarts the process;
95
+ build-time vars need a fresh deploy (`hosting_deploy`) afterwards. **Don't defer this to deploy:** on
96
+ a **cross-session resume** the generated password is gone, so you must **reset it** with
97
+ `hosting_databases_change-password { username, name, password }` and set the env vars from the new one.
98
+ 4. **Never** put the password in source, a committed `.env`, client-side JavaScript, or a chat reply;
99
+ never print it once stored.
100
+
101
+ ## Step 4 — Verify
102
+
103
+ | Check | How | Expect |
104
+ |---|---|---|
105
+ | Website exists | `hosting_websites_list` exact row | the row is present, `username` known |
106
+ | Setup finished | `hosting_websites_list-setups` exact row | `status: completed` |
107
+ | Database reachable *(only if created)* | `hosting_databases_list { username }` | the prefixed db + user are listed |
108
+
109
+ `GET https://<domain>/` returns the Hostinger default page (or 403) until the frontend is deployed —
110
+ expected, not a failure. There is **no `/cms` and no API** on this domain; the app itself serves every
111
+ route once it is deployed (Step 8).
112
+
113
+ If a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.
114
+
115
+ ## Step 5 — Record `site.json`
116
+
117
+ ```json
118
+ {
119
+ "backend": "webapp",
120
+ "website_type": "empty",
121
+ "order_id": 1007223428,
122
+ "username": "u…",
123
+ "runtime": "nodejs",
124
+ "database": {
125
+ "enabled": false,
126
+ "engine": "mysql",
127
+ "name": "u…_appdb",
128
+ "user": "u…_app",
129
+ "host": "127.0.0.1",
130
+ "port": 3306
131
+ },
132
+ "urls": {
133
+ "site": "https://<domain>"
134
+ },
135
+ "provisioned_at": "…",
136
+ "verified_at": "…"
137
+ }
138
+ ```
139
+
140
+ No passwords or tokens in `site.json` — only the non-secret connection shape (host/port/name/user). Set
141
+ `"database": { "enabled": false }` (and drop the rest) when the app needs no database.
142
+
143
+ ## Deploy & content model (what happens next)
144
+
145
+ - **Content model is skipped for a webapp** — there is no CMS or store to seed; the app owns its own
146
+ data. The content-model router (`../../04-content-model/SKILL.md`) passes straight through to the
147
+ frontend step for this backend.
148
+ - **Deploy** is a standard Node.js application deploy, handled by `../../08-deploy/SKILL.md`: it archives
149
+ the **source** (not the build output; exclude `node_modules/`, `.next/`, `dist/`, build output and
150
+ `.git/`), then deploys with `hosting_deploy` (`type: "nodejs"`, upload + server-side `npm install` +
151
+ build, one shot) passing the framework's `appType` + `outputDirectory` inline, and polls
152
+ `hosting_get-deployment` until the build completes. Any supported `appType` works (Next.js, Nuxt,
153
+ Express, NestJS, Fastify, Vite/React/Vue SPA, …). There is no `/cms` to preserve here, so the whole
154
+ root is the app.
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: headless-step-provision-wordpress
3
+ description: "Provision a headless WordPress backend for the Hostinger Headless flow: create the headless website with WordPress at /cms, verify and self-heal, then record it in site.json."
4
+ ---
5
+
6
+ # headless-step-provision-wordpress
7
+
8
+ Create the headless WordPress backend for `<domain>` and prove it is ready. One platform call lays
9
+ every headless site out identically: WordPress in `public_html/cms`, Hostinger AI Assistant + MCP
10
+ adapter, site marked `headless`. **Out:** `site.json`.
11
+
12
+ All operations below go through the Hostinger connector (`execute`).
13
+
14
+ > **Fire vs join (background provision).** Steps 1–2 up to `hosting_websites_start-setup` are the
15
+ > **fire** half: after the setup is queued, record `provision.in_progress` and return to do prepare
16
+ > work (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2, plus Steps 3–4,
17
+ > are the **join** half — run them when an instance-barrier step needs the backend (or immediately, if
18
+ > the agent can't interleave). An already-`completed` site detected in Step 1 means provision is
19
+ > already done: record `provision.completed` and adopt it.
20
+
21
+ ## Step 1 — Detect before you create (never provision twice)
22
+
23
+ 1. `hosting_websites_list { "domain": "<domain>" }` — substring filter, so **exact-match** the row's
24
+ `domain`. If a row exists, note `username`, `order_id`, `website_type`.
25
+ 2. `hosting_websites_list-setups { "domain": "<domain>" }` — exact-match the row:
26
+ - `completed` → already provisioned → skip to Step 3 (adopt it).
27
+ - `running` → skip to Step 2 (poll).
28
+ - `failed` → tell the user and point them at hPanel; do **not** call start-setup again on your own.
29
+ 3. If the site exists but `website_type` is not `headless`, stop and tell the user — this domain
30
+ already hosts a different kind of site.
31
+
32
+ ## Step 2 — Create (only when nothing exists)
33
+
34
+ 1. Pick the plan: `hosting_orders_list { "statuses": ["active"] }`. One active order → use it; several
35
+ → ask the user which.
36
+ 2. Start the setup:
37
+
38
+ ```json
39
+ { "operation": "hosting_websites_start-setup",
40
+ "params": { "order_id": <order_id>, "type": "headless_wordpress", "domain": "<domain>" } }
41
+ ```
42
+
43
+ `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,
44
+ **omit `domain`** — the setup generates one itself. Don't call a separate subdomain op to make one
45
+ first: there's no need, and passing a generated free subdomain back as `domain` fails with
46
+ `422 … Free subdomains cannot be provided`. The response then carries the generated domain:
47
+ if it differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,
48
+ update the domain in `brief.md`, and tell the user the new domain.
49
+ Admin credentials are generated by the platform; never ask the user for a password.
50
+ 3. Poll `hosting_websites_list-setups { "domain": "<domain>" }` every ~15 s until `completed`
51
+ (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.
52
+
53
+ ## Step 3 — Verify (all must pass)
54
+
55
+ | Check | How | Expect |
56
+ |---|---|---|
57
+ | Site is headless | `hosting_websites_list` exact row | `website_type: "headless"` |
58
+ | WordPress at `/cms` | `wordpress_installations_list { "username": "<username>", "ownership": "all" }`, exact-match `domain` | row with `directory: "cms"`, `is_valid: true`; note its `id` |
59
+ | REST API answers | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200, JSON array |
60
+ | AI Assistant + MCP loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `hostinger-ai-assistant/v1` and `mcp` |
61
+ | JWT mintable | `wordpress_installations_jwt-token { username, software: <id> }` | returns `token`, `mcp_url` |
62
+
63
+ Notes:
64
+
65
+ - `wordpress_installations_list` can lag a few seconds behind `completed` and return `[]` — retry
66
+ briefly before failing. Its `domain` filter is a substring match, so exact-match the row.
67
+ - The install record's `url` may say `http://`; always use `https://` URLs.
68
+ - `GET https://<domain>/` returns 403 until the frontend is deployed — expected, not a failure.
69
+ - **Never print, log or store the JWT.** It is minted on demand (24 h lifetime) by whichever stage
70
+ needs the per-site WP MCP; only `mcp_url` is recorded.
71
+
72
+ If a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.
73
+
74
+ ## Step 4 — Record `site.json`
75
+
76
+ ```json
77
+ {
78
+ "backend": "wordpress",
79
+ "website_type": "headless",
80
+ "order_id": 1007223428,
81
+ "username": "u…",
82
+ "wordpress": {
83
+ "install_id": "30…",
84
+ "directory": "cms",
85
+ "admin_login": "…"
86
+ },
87
+ "urls": {
88
+ "site": "https://<domain>",
89
+ "cms": "https://<domain>/cms",
90
+ "wp_admin": "https://<domain>/cms/wp-admin",
91
+ "rest_api": "https://<domain>/cms/wp-json/wp/v2",
92
+ "wp_mcp": "<mcp_url from the JWT call>"
93
+ },
94
+ "provisioned_at": "…",
95
+ "verified_at": "…"
96
+ }
97
+ ```
98
+
99
+ No passwords or tokens in `site.json`. When the user needs wp-admin, mint a one-click link with
100
+ `wordpress_login_create-links { username, software: <install_id> }` — never hand over a password.
@@ -0,0 +1,150 @@
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 (generated into media/, or sourced from Unsplash — see *Images* below) 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 (generated into
63
+ > media/, or sourced from Unsplash — see *Images*) with alt text. Group products into the collections
64
+ > the brief implies. The `ecommerce` push phase creates these on the store's sales channel.
65
+ ```
66
+
67
+ Guidance:
68
+
69
+ - **Fixed vs. prompt.** Title and Description are concrete values. Navigation, Pages and Articles are
70
+ prompts because their volume/wording is generated — keep them as prompts so the push phase (or a
71
+ rerun) can regenerate consistently.
72
+ - **Run the prompts now.** Generate the actual page copy, article drafts and images during the wait —
73
+ this is the slowest work and the whole reason it runs in parallel. Put generated media in
74
+ `~/.hostinger/<domain>/media/` and reference it by filename. Record Unsplash images by URL (see
75
+ *Images* below). Record alt text for every image.
76
+ - **Optional sections.** Drop **Articles** if the brief has no article-like content. Drop **Products**
77
+ for a non-commerce brief; for a commerce brief, generate the product catalog here (names, prices,
78
+ descriptions, collections, images) — that's prepare work, and the `ecommerce` push phase then creates
79
+ it on the store.
80
+ - **Don't block on design.** The design step (5) owns the look, and it runs in parallel during the
81
+ provision wait, so never wait for it here. If `design.md` already exists when you source images,
82
+ follow its **Imagery** section; otherwise generate neutral, on-topic imagery from the brief.
83
+
84
+ ### Images — source from Unsplash when you can't generate them
85
+
86
+ Every article needs a featured image. If you can generate images, put them in `media/`. If you
87
+ can't (most coding agents can't), source real photos from Unsplash. This is still prepare work and
88
+ needs no live backend: you only pick the photos and record their URLs here. The push phase uploads
89
+ them.
90
+
91
+ 1. **Find candidates.** Use one photo per article, matching its subject and destination.
92
+ - With an `UNSPLASH_ACCESS_KEY` in the environment, use the official API:
93
+ `GET https://api.unsplash.com/search/photos?query=<subject>&orientation=landscape` with header
94
+ `Authorization: Client-ID <key>`.
95
+ - Without a key, use web search limited to `unsplash.com` (e.g. "unsplash photo Lisbon tram") and
96
+ take the photo id from the result URL: the last segment of `unsplash.com/photos/<slug>-<id>`, or
97
+ the whole segment for `unsplash.com/photos/<id>`.
98
+ - Don't call `unsplash.com/napi/…`. It sits behind a bot challenge and returns 401.
99
+ 2. **Resolve a direct file URL.** `GET https://unsplash.com/photos/<id>/download?force=true` (don't
100
+ follow the redirect) answers `302` with `Location: https://images.unsplash.com/photo-<…>?…`. The
101
+ `dl=` parameter in it also carries the photographer's name. Drop the query string and request a
102
+ web size: `https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max`. The upload ability
103
+ needs this direct file URL, which returns 200 with `image/jpeg`. A photo *page* URL won't work.
104
+ 3. **Look before you keep it.** Download a small preview (`?w=500`) and view it. Search snippets
105
+ only show captions, and a photo often isn't quite what its title says. Reject anything off-topic.
106
+ If nothing fits exactly, take the closest real photo and note the gap rather than mislabel it.
107
+ 4. **Write alt text from what the photo shows**, not from the article's image brief. For example,
108
+ use "Shopfronts and parked scooters on a Hanoi street" even if the article is about rain.
109
+ 5. **Record it** under each article in `content-model.md`:
110
+
111
+ ```markdown
112
+ Image: https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max
113
+ Image credit: <Photographer> — https://unsplash.com/photos/<id> (Unsplash License)
114
+ Image alt: <what the photo shows>
115
+ Image file: <site>-<subject>.jpg
116
+ ```
117
+
118
+ Unsplash photos are free to use under the Unsplash License. Still credit the photographer: the push
119
+ phase stores "Photo by <Photographer> on Unsplash" as the image caption.
120
+
121
+ Then update `state.json`: set `stage` to the next prepare stage still to do, fresh `updated_at`. Do
122
+ **not** advance past the barrier here.
123
+
124
+ ---
125
+
126
+ ## Push phase — create it the backend's way (needs the instance)
127
+
128
+ Only once `provision.status == "completed"` (re-verified against `hosting_websites_list-setups`),
129
+ route on the brief's **Suggested backend** and follow that subfolder, which maps the helper onto the
130
+ live backend — for WordPress, through the per-site WP MCP's ability tools (the installation JWT
131
+ authenticates that MCP session; `wp/v2` REST is the public read surface, not a write path for the
132
+ token) to create the menu, pages, posts, categories and media:
133
+
134
+ | Suggested backend | Follow |
135
+ |---|---|
136
+ | WordPress | `wordpress/SKILL.md` |
137
+ | Ecommerce | `ecommerce/SKILL.md` |
138
+ | Webapp | — (no push; skip to frontend) |
139
+
140
+ > **Supported backends: `wordpress`, `ecommerce`, and `webapp`** — where `webapp` is "supported" by
141
+ > **skipping this whole stage** (it has no content backend; see the note below). PocketBase is not
142
+ > available in this version. Push to whichever single backend provision set up (see the provision
143
+ > router) — don't try to push to two backends here.
144
+ >
145
+ > **Webapp has no content backend.** There is no CMS or store to seed, so a `webapp` brief **skips this
146
+ > whole stage** — both prepare and push. The app owns its own data (its own API or database). Leave
147
+ > `content-model.md` unwritten, mark content-model `skipped` in the run log, and go straight to the
148
+ > frontend step.
149
+
150
+ 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.