hostinger-api-mcp 3.5.0 → 3.6.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 +19 -0
- package/README.md +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/steps/03-provision/ecommerce/SKILL.md +4 -3
- package/skills/headless/steps/03-provision/webapp/SKILL.md +4 -3
- package/skills/headless/steps/03-provision/wordpress/SKILL.md +4 -3
- package/src/core/runtime.js +32 -15
- package/src/core/runtime.ts +32 -15
- package/src/core/skills.js +9 -9
- package/src/core/skills.ts +9 -9
- package/src/core/tools/all.js +1 -5
- package/src/core/tools/all.ts +1 -5
- package/src/core/tools/hosting.js +1 -5
- package/src/core/tools/hosting.ts +1 -5
- 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/types.d.ts +1 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ Generated at release time from the diff between the previous and current
|
|
|
4
4
|
build. Tool changes are listed individually; everything else is summarised
|
|
5
5
|
by area.
|
|
6
6
|
|
|
7
|
+
## v3.6.1 — 2026-10-22
|
|
8
|
+
|
|
9
|
+
### Also in this release
|
|
10
|
+
|
|
11
|
+
- Bundled agent skills: `skills/headless/steps/03-provision/ecommerce/SKILL.md`, `skills/headless/steps/03-provision/webapp/SKILL.md`, `skills/headless/steps/03-provision/wordpress/SKILL.md`
|
|
12
|
+
- Other files: `src/core/skills.js`, `src/core/skills.ts`
|
|
13
|
+
|
|
14
|
+
## v3.6.0 — 2026-10-21
|
|
15
|
+
|
|
16
|
+
### Operations
|
|
17
|
+
|
|
18
|
+
- Changed the description and input schema of `hosting_deploy`
|
|
19
|
+
|
|
20
|
+
### Also in this release
|
|
21
|
+
|
|
22
|
+
- Documentation: `README.md`
|
|
23
|
+
- Request handling and authentication: `src/core/runtime.js`, `src/core/runtime.ts`
|
|
24
|
+
- Build and packaging: `types.d.ts`
|
|
25
|
+
|
|
7
26
|
## v3.5.0 — 2026-10-21
|
|
8
27
|
|
|
9
28
|
### Also in this release
|
package/README.md
CHANGED
|
@@ -1816,7 +1816,7 @@ interface in the provided website URL.
|
|
|
1816
1816
|
|
|
1817
1817
|
#### hosting_deploy
|
|
1818
1818
|
|
|
1819
|
-
Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed.
|
|
1819
|
+
Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, pass appType and nodeVersion from the project's engines, plus outputDirectory and buildScript if the app has a build step). Git needs GitHub or GitLab connected once in hPanel.
|
|
1820
1820
|
|
|
1821
1821
|
- **Method**: `custom`
|
|
1822
1822
|
- **Path**: `custom`
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/hostinger/api-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.6.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@hostinger/mcp",
|
|
14
|
-
"version": "3.
|
|
14
|
+
"version": "3.6.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -51,9 +51,10 @@ Same lifecycle as `../wordpress/SKILL.md` Steps 1–2, but `type: "headless_ecom
|
|
|
51
51
|
→ adopt. `running` → poll. `failed` → stop, point at hPanel. If `website_type` is not `headless`
|
|
52
52
|
(plain WordPress, Node.js, …), stop and tell the user — don't overwrite another kind of site.
|
|
53
53
|
2. Create (only if **no** site exists): `hosting_websites_start-setup { order_id, type: "headless_ecommerce", domain }`
|
|
54
|
-
— **omit `domain`** for a temporary subdomain: the setup generates one itself (don't
|
|
55
|
-
subdomain op first —
|
|
56
|
-
|
|
54
|
+
— **omit `domain`** for a temporary subdomain: the setup generates one itself and returns it (don't
|
|
55
|
+
call a separate subdomain op first — there's no need in this flow; the API would also accept one you
|
|
56
|
+
generated via `hosting_domains_generate-free-subdomain`, valid 24 h from the client that made it). The
|
|
57
|
+
response carries the generated domain; if it differs from the workspace dir, move `~/.hostinger/<old>/` → `<new>/` and
|
|
57
58
|
update `brief.md`. Poll `list-setups` to `completed`. `website_type` will be `headless`.
|
|
58
59
|
|
|
59
60
|
> **Onboarding handoff — an empty headless shell needs no new website.** If the domain already has an
|
|
@@ -45,9 +45,10 @@ All website/database operations below go through the Hostinger connector (`execu
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
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
|
|
49
|
-
first: there's no need
|
|
50
|
-
|
|
48
|
+
**omit `domain`** — the setup generates one itself and returns it in the response. Don't call a
|
|
49
|
+
separate subdomain op to make one first: there's no need in this flow. (The API would also accept a
|
|
50
|
+
subdomain you generated via `hosting_domains_generate-free-subdomain`, valid for 24 h from the client
|
|
51
|
+
that made it — this flow just doesn't use that.) The response then carries the generated domain: if it
|
|
51
52
|
differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,
|
|
52
53
|
update the domain in `brief.md`, and tell the user the new domain.
|
|
53
54
|
3. Poll `hosting_websites_list-setups { "domain": "<domain>" }` every ~15 s until `completed`
|
|
@@ -57,9 +57,10 @@ All operations below go through the Hostinger connector (`execute`).
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
`domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,
|
|
60
|
-
**omit `domain`** — the setup generates one itself. Don't call a
|
|
61
|
-
first: there's no need
|
|
62
|
-
|
|
60
|
+
**omit `domain`** — the setup generates one itself and returns it in the response. Don't call a
|
|
61
|
+
separate subdomain op to make one first: there's no need in this flow. (The API would also accept a
|
|
62
|
+
subdomain you generated via `hosting_domains_generate-free-subdomain`, valid for 24 h from the client
|
|
63
|
+
that made it — this flow just doesn't use that.) The response then carries the generated domain:
|
|
63
64
|
if it differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,
|
|
64
65
|
update the domain in `brief.md`, and tell the user the new domain.
|
|
65
66
|
Admin credentials are generated by the platform; never ask the user for a password.
|
package/src/core/runtime.js
CHANGED
|
@@ -643,9 +643,6 @@ class MCPServer {
|
|
|
643
643
|
if (resolved === 'wordpress-site' && !databaseDump) {
|
|
644
644
|
throw new Error('databaseDump (path to a .sql file) is required to import a WordPress site');
|
|
645
645
|
}
|
|
646
|
-
if (params.headless === true && resolved !== 'wordpress-plugin' && resolved !== 'wordpress-theme') {
|
|
647
|
-
throw new Error('headless is for wordpress-plugin and wordpress-theme deploys only, not ' + resolved);
|
|
648
|
-
}
|
|
649
646
|
return resolved;
|
|
650
647
|
}
|
|
651
648
|
|
|
@@ -826,12 +823,6 @@ class MCPServer {
|
|
|
826
823
|
if (params.slug !== undefined && !(typeof params.slug === 'string' && /^[a-z0-9_-]+$/.test(params.slug))) {
|
|
827
824
|
throw new Error('slug must contain only lower-case letters, digits, "-" and "_"');
|
|
828
825
|
}
|
|
829
|
-
if (params.headless !== undefined && typeof params.headless !== 'boolean') {
|
|
830
|
-
throw new Error('headless must be a boolean');
|
|
831
|
-
}
|
|
832
|
-
if (params.headless === true && git !== undefined) {
|
|
833
|
-
throw new Error('headless is for wordpress-plugin and wordpress-theme deploys only, which need a local directory, not git');
|
|
834
|
-
}
|
|
835
826
|
if (git !== undefined && (typeof git !== 'object' || git === null || typeof git.repository !== 'string' || typeof git.branch !== 'string' || git.branch === '')) {
|
|
836
827
|
throw new Error('git must be an object with repository (owner/name) and branch');
|
|
837
828
|
}
|
|
@@ -855,13 +846,39 @@ class MCPServer {
|
|
|
855
846
|
theme: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/themes/deploy',
|
|
856
847
|
};
|
|
857
848
|
|
|
858
|
-
// A headless WordPress is installed in this folder of the document root, not at the root itself.
|
|
859
|
-
hosting_deploy_headlessDirectory = 'cms';
|
|
860
|
-
|
|
861
849
|
async hosting_deploy_callWordpress(kind, params) {
|
|
862
850
|
return this.executeApiCall({ name: 'wordpress_' + kind, method: 'POST', path: this.hosting_deploy_wordpressEndpoints[kind] }, params);
|
|
863
851
|
}
|
|
864
852
|
|
|
853
|
+
// The folder of the document root WordPress is installed in ('' for the root, e.g. 'cms' on a headless
|
|
854
|
+
// site), from the installation directory wordpress_installations_list reports (public_html or '' for the root,
|
|
855
|
+
// public_html/<folder> or a bare <folder> otherwise). Absolute paths and .. segments are refused.
|
|
856
|
+
async hosting_deploy_wordpressDirectory(username, domain) {
|
|
857
|
+
const response = await this.executeApiCall({ name: 'wordpress_installations_list', method: 'GET', path: '/api/hosting/v1/wordpress/installations' }, { username, domain, ownership: 'all' });
|
|
858
|
+
const list = Array.isArray(response) ? response : (response && Array.isArray(response.data) ? response.data : []);
|
|
859
|
+
const installations = list.filter((installation) => typeof installation.domain === 'string' && installation.domain.toLowerCase() === domain.toLowerCase());
|
|
860
|
+
if (installations.length === 0) {
|
|
861
|
+
return '';
|
|
862
|
+
}
|
|
863
|
+
if (installations.length > 1) {
|
|
864
|
+
throw new Error('Several WordPress installations found for ' + domain + ' (' + installations.map((installation) => installation.directory).join(', ') + '); cannot tell which one to deploy to');
|
|
865
|
+
}
|
|
866
|
+
let directory = typeof installations[0].directory === 'string' ? installations[0].directory : '';
|
|
867
|
+
if (directory.startsWith('/') || directory.split('/').includes('..')) {
|
|
868
|
+
throw new Error('Unrecognized WordPress installation directory for ' + domain + ': ' + String(installations[0].directory));
|
|
869
|
+
}
|
|
870
|
+
while (directory.endsWith('/')) {
|
|
871
|
+
directory = directory.slice(0, -1);
|
|
872
|
+
}
|
|
873
|
+
if (directory === 'public_html') {
|
|
874
|
+
return '';
|
|
875
|
+
}
|
|
876
|
+
if (directory.startsWith('public_html/')) {
|
|
877
|
+
return directory.slice('public_html/'.length);
|
|
878
|
+
}
|
|
879
|
+
return directory;
|
|
880
|
+
}
|
|
881
|
+
|
|
865
882
|
hosting_deploy_randomSuffix() {
|
|
866
883
|
return Math.random().toString(36).slice(2, 10).padEnd(8, '0');
|
|
867
884
|
}
|
|
@@ -927,7 +944,6 @@ class MCPServer {
|
|
|
927
944
|
|
|
928
945
|
async hosting_deploy_wordpressExtension(username, domain, type, params) {
|
|
929
946
|
const isPlugin = type === 'wordpress-plugin';
|
|
930
|
-
const headless = params.headless === true;
|
|
931
947
|
const directory = path.resolve(params.path);
|
|
932
948
|
const slug = params.slug !== undefined ? params.slug : this.hosting_deploy_slug(directory);
|
|
933
949
|
const { files, skipped } = this.hosting_deploy_listFiles(directory);
|
|
@@ -935,9 +951,10 @@ class MCPServer {
|
|
|
935
951
|
throw new Error('No files found in directory: ' + directory);
|
|
936
952
|
}
|
|
937
953
|
|
|
954
|
+
const wordpressDirectory = await this.hosting_deploy_wordpressDirectory(username, domain);
|
|
938
955
|
// The files land in a fresh directory; the deploy call then installs from it.
|
|
939
956
|
const uploadDirName = slug + '-' + this.hosting_deploy_randomSuffix();
|
|
940
|
-
const remoteRoot = (
|
|
957
|
+
const remoteRoot = (wordpressDirectory ? wordpressDirectory + '/' : '') + (isPlugin ? 'wp-content/plugins/' : 'wp-content/themes/') + uploadDirName;
|
|
941
958
|
await this.hosting_deploy_upload(username, domain, files.map((relativePath) => ({
|
|
942
959
|
absolutePath: path.join(directory, relativePath),
|
|
943
960
|
relativePath: remoteRoot + '/' + relativePath,
|
|
@@ -957,7 +974,7 @@ class MCPServer {
|
|
|
957
974
|
slug,
|
|
958
975
|
files: files.length,
|
|
959
976
|
...(isPlugin ? {} : { activated: params.activate === true }),
|
|
960
|
-
...(
|
|
977
|
+
...(wordpressDirectory ? { wordpressDirectory } : {}),
|
|
961
978
|
}, skipped);
|
|
962
979
|
}
|
|
963
980
|
|
package/src/core/runtime.ts
CHANGED
|
@@ -680,9 +680,6 @@ class MCPServer {
|
|
|
680
680
|
if (resolved === 'wordpress-site' && !databaseDump) {
|
|
681
681
|
throw new Error('databaseDump (path to a .sql file) is required to import a WordPress site');
|
|
682
682
|
}
|
|
683
|
-
if (params.headless === true && resolved !== 'wordpress-plugin' && resolved !== 'wordpress-theme') {
|
|
684
|
-
throw new Error('headless is for wordpress-plugin and wordpress-theme deploys only, not ' + resolved);
|
|
685
|
-
}
|
|
686
683
|
return resolved;
|
|
687
684
|
}
|
|
688
685
|
|
|
@@ -863,12 +860,6 @@ class MCPServer {
|
|
|
863
860
|
if (params.slug !== undefined && !(typeof params.slug === 'string' && /^[a-z0-9_-]+$/.test(params.slug))) {
|
|
864
861
|
throw new Error('slug must contain only lower-case letters, digits, "-" and "_"');
|
|
865
862
|
}
|
|
866
|
-
if (params.headless !== undefined && typeof params.headless !== 'boolean') {
|
|
867
|
-
throw new Error('headless must be a boolean');
|
|
868
|
-
}
|
|
869
|
-
if (params.headless === true && git !== undefined) {
|
|
870
|
-
throw new Error('headless is for wordpress-plugin and wordpress-theme deploys only, which need a local directory, not git');
|
|
871
|
-
}
|
|
872
863
|
if (git !== undefined && (typeof git !== 'object' || git === null || typeof git.repository !== 'string' || typeof git.branch !== 'string' || git.branch === '')) {
|
|
873
864
|
throw new Error('git must be an object with repository (owner/name) and branch');
|
|
874
865
|
}
|
|
@@ -892,13 +883,39 @@ class MCPServer {
|
|
|
892
883
|
theme: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/themes/deploy',
|
|
893
884
|
};
|
|
894
885
|
|
|
895
|
-
// A headless WordPress is installed in this folder of the document root, not at the root itself.
|
|
896
|
-
private hosting_deploy_headlessDirectory = 'cms';
|
|
897
|
-
|
|
898
886
|
private async hosting_deploy_callWordpress(kind: string, params: Record<string, any>): Promise<any> {
|
|
899
887
|
return this.executeApiCall({ name: 'wordpress_' + kind, method: 'POST', path: this.hosting_deploy_wordpressEndpoints[kind] } as any, params);
|
|
900
888
|
}
|
|
901
889
|
|
|
890
|
+
// The folder of the document root WordPress is installed in ('' for the root, e.g. 'cms' on a headless
|
|
891
|
+
// site), from the installation directory wordpress_installations_list reports (public_html or '' for the root,
|
|
892
|
+
// public_html/<folder> or a bare <folder> otherwise). Absolute paths and .. segments are refused.
|
|
893
|
+
private async hosting_deploy_wordpressDirectory(username: string, domain: string): Promise<string> {
|
|
894
|
+
const response = await this.executeApiCall({ name: 'wordpress_installations_list', method: 'GET', path: '/api/hosting/v1/wordpress/installations' } as any, { username, domain, ownership: 'all' });
|
|
895
|
+
const list = Array.isArray(response) ? response : (response && Array.isArray(response.data) ? response.data : []);
|
|
896
|
+
const installations = list.filter((installation: any) => typeof installation.domain === 'string' && installation.domain.toLowerCase() === domain.toLowerCase());
|
|
897
|
+
if (installations.length === 0) {
|
|
898
|
+
return '';
|
|
899
|
+
}
|
|
900
|
+
if (installations.length > 1) {
|
|
901
|
+
throw new Error('Several WordPress installations found for ' + domain + ' (' + installations.map((installation: any) => installation.directory).join(', ') + '); cannot tell which one to deploy to');
|
|
902
|
+
}
|
|
903
|
+
let directory = typeof installations[0].directory === 'string' ? installations[0].directory : '';
|
|
904
|
+
if (directory.startsWith('/') || directory.split('/').includes('..')) {
|
|
905
|
+
throw new Error('Unrecognized WordPress installation directory for ' + domain + ': ' + String(installations[0].directory));
|
|
906
|
+
}
|
|
907
|
+
while (directory.endsWith('/')) {
|
|
908
|
+
directory = directory.slice(0, -1);
|
|
909
|
+
}
|
|
910
|
+
if (directory === 'public_html') {
|
|
911
|
+
return '';
|
|
912
|
+
}
|
|
913
|
+
if (directory.startsWith('public_html/')) {
|
|
914
|
+
return directory.slice('public_html/'.length);
|
|
915
|
+
}
|
|
916
|
+
return directory;
|
|
917
|
+
}
|
|
918
|
+
|
|
902
919
|
private hosting_deploy_randomSuffix(): string {
|
|
903
920
|
return Math.random().toString(36).slice(2, 10).padEnd(8, '0');
|
|
904
921
|
}
|
|
@@ -964,7 +981,6 @@ class MCPServer {
|
|
|
964
981
|
|
|
965
982
|
private async hosting_deploy_wordpressExtension(username: string, domain: string, type: string, params: Record<string, any>): Promise<any> {
|
|
966
983
|
const isPlugin = type === 'wordpress-plugin';
|
|
967
|
-
const headless = params.headless === true;
|
|
968
984
|
const directory = path.resolve(params.path);
|
|
969
985
|
const slug = params.slug !== undefined ? params.slug : this.hosting_deploy_slug(directory);
|
|
970
986
|
const { files, skipped } = this.hosting_deploy_listFiles(directory);
|
|
@@ -972,9 +988,10 @@ class MCPServer {
|
|
|
972
988
|
throw new Error('No files found in directory: ' + directory);
|
|
973
989
|
}
|
|
974
990
|
|
|
991
|
+
const wordpressDirectory = await this.hosting_deploy_wordpressDirectory(username, domain);
|
|
975
992
|
// The files land in a fresh directory; the deploy call then installs from it.
|
|
976
993
|
const uploadDirName = slug + '-' + this.hosting_deploy_randomSuffix();
|
|
977
|
-
const remoteRoot = (
|
|
994
|
+
const remoteRoot = (wordpressDirectory ? wordpressDirectory + '/' : '') + (isPlugin ? 'wp-content/plugins/' : 'wp-content/themes/') + uploadDirName;
|
|
978
995
|
await this.hosting_deploy_upload(username, domain, files.map((relativePath) => ({
|
|
979
996
|
absolutePath: path.join(directory, relativePath),
|
|
980
997
|
relativePath: remoteRoot + '/' + relativePath,
|
|
@@ -994,7 +1011,7 @@ class MCPServer {
|
|
|
994
1011
|
slug,
|
|
995
1012
|
files: files.length,
|
|
996
1013
|
...(isPlugin ? {} : { activated: params.activate === true }),
|
|
997
|
-
...(
|
|
1014
|
+
...(wordpressDirectory ? { wordpressDirectory } : {}),
|
|
998
1015
|
}, skipped);
|
|
999
1016
|
}
|
|
1000
1017
|
|
package/src/core/skills.js
CHANGED
|
@@ -155,24 +155,24 @@ export default [
|
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"uri": "skill://hostinger-headless/steps/03-provision/ecommerce/SKILL.md",
|
|
158
|
-
"digest": "sha256:
|
|
159
|
-
"size":
|
|
158
|
+
"digest": "sha256:92079eacad771a4dfb828a187b8d0fa4eb9d914ce232b903d05090ad3519618c",
|
|
159
|
+
"size": 8642,
|
|
160
160
|
"mimeType": "text/markdown",
|
|
161
|
-
"text": "---\nname: headless-step-provision-ecommerce\ndescription: \"Provision a Hostinger Ecommerce backend for the Hostinger Headless flow: ensure the hostinger-ecommerce MCP is connected, create the headless website (frontend host) and a Hostinger Ecommerce STORE with a custom (headless) sales channel, set shipping + payment, and record it all in site.json.\"\n---\n\n# headless-step-provision-ecommerce\n\nProvision a **Hostinger Ecommerce** backend for `<domain>` and prove it is ready. Two distinct things:\n\n1. A **headless website** (`type: \"headless_ecommerce\"`) — this is only the **hosting slot for the\n storefront you'll build**. It installs **no store engine** (no `/cms`, no API on the domain).\n2. A **Hostinger Ecommerce store** — a separate object created through the **Ecommerce API**, with a\n **custom (headless) sales channel**. The storefront reads this store's **public Storefront Core V2\n API**, keyed on the sales channel. This is the real backend.\n\n**Out:** `site.json` (store id, custom sales channel id, URLs).\n\n> ## ⚠️ Prerequisite: the `hostinger-ecommerce` MCP server must be connected\n> Store management is **not** in the `hosting`/`wordpress` MCP groups — it's the separate\n> **`hostinger-ecommerce`** group (`ecommerce_*` ops, OAuth-backed, no API token needed). If those\n> tools aren't available, add the server and **reconnect the session** before continuing:\n> ```bash\n> claude mcp add-json hostinger-ecommerce '{\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"],\"env\":{\"USER_AGENT\":\"extension;claude-code;via-cursor;1.4.2\"}}' --scope user\n> ```\n> (For other tools, enable the **Ecommerce** MCP in the Hostinger Connector.) A newly added MCP server\n> only loads on a session restart/reconnect.\n>\n> **Can't restart your own session? Don't block — drive the server directly.** A coding agent usually\n> can't reconnect itself, so instead of stopping, run the stdio server yourself and speak MCP to it:\n> spawn `npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp` (same `USER_AGENT` as above) and\n> send newline-delimited JSON-RPC over its stdin/stdout — `initialize` → the `notifications/initialized`\n> notification → `tools/list` → `tools/call`, where the tools are `search` / `execute` / `multi-execute`\n> (call ops exactly as in-session: `{ \"name\": \"execute\", \"arguments\": { \"operation\": \"…\", \"params\": {…} } }`).\n> It self-authenticates with the same stored Hostinger OAuth creds as the other connected servers, so\n> there is **no token to handle** (keep any secret off the command line). Your sandbox may ask the user\n> to approve spawning it — ask once, then proceed. Only fall back to asking the user to restart if\n> driving the server directly isn't permitted.\n>\n> Verify the tools work (in-session or via the direct drive) with a search — e.g. \"list stores\" →\n> `ecommerce_stores_list` — before proceeding.\n> The authoritative, always-current build guide is `ecommerce_miscellaneous_custom-storefront-setup-instructions` — read it once per build.\n\nAll website-lifecycle calls go through the `hosting` connector (`execute`); all store calls through the\n`hostinger-ecommerce` MCP.\n\n## Step 1 — The headless website (frontend host)\n\nSame lifecycle as `../wordpress/SKILL.md` Steps 1–2, but `type: \"headless_ecommerce\"`:\n\n1. Detect: `hosting_websites_list` / `hosting_websites_list-setups` (exact-match the domain). `completed`\n → adopt. `running` → poll. `failed` → stop, point at hPanel. If `website_type` is not `headless`\n (plain WordPress, Node.js, …), stop and tell the user — don't overwrite another kind of site.\n2. Create (only if **no** site exists): `hosting_websites_start-setup { order_id, type: \"headless_ecommerce\", domain }`\n — **omit `domain`** for a temporary subdomain: the setup generates one itself (don't
|
|
161
|
+
"text": "---\nname: headless-step-provision-ecommerce\ndescription: \"Provision a Hostinger Ecommerce backend for the Hostinger Headless flow: ensure the hostinger-ecommerce MCP is connected, create the headless website (frontend host) and a Hostinger Ecommerce STORE with a custom (headless) sales channel, set shipping + payment, and record it all in site.json.\"\n---\n\n# headless-step-provision-ecommerce\n\nProvision a **Hostinger Ecommerce** backend for `<domain>` and prove it is ready. Two distinct things:\n\n1. A **headless website** (`type: \"headless_ecommerce\"`) — this is only the **hosting slot for the\n storefront you'll build**. It installs **no store engine** (no `/cms`, no API on the domain).\n2. A **Hostinger Ecommerce store** — a separate object created through the **Ecommerce API**, with a\n **custom (headless) sales channel**. The storefront reads this store's **public Storefront Core V2\n API**, keyed on the sales channel. This is the real backend.\n\n**Out:** `site.json` (store id, custom sales channel id, URLs).\n\n> ## ⚠️ Prerequisite: the `hostinger-ecommerce` MCP server must be connected\n> Store management is **not** in the `hosting`/`wordpress` MCP groups — it's the separate\n> **`hostinger-ecommerce`** group (`ecommerce_*` ops, OAuth-backed, no API token needed). If those\n> tools aren't available, add the server and **reconnect the session** before continuing:\n> ```bash\n> claude mcp add-json hostinger-ecommerce '{\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"],\"env\":{\"USER_AGENT\":\"extension;claude-code;via-cursor;1.4.2\"}}' --scope user\n> ```\n> (For other tools, enable the **Ecommerce** MCP in the Hostinger Connector.) A newly added MCP server\n> only loads on a session restart/reconnect.\n>\n> **Can't restart your own session? Don't block — drive the server directly.** A coding agent usually\n> can't reconnect itself, so instead of stopping, run the stdio server yourself and speak MCP to it:\n> spawn `npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp` (same `USER_AGENT` as above) and\n> send newline-delimited JSON-RPC over its stdin/stdout — `initialize` → the `notifications/initialized`\n> notification → `tools/list` → `tools/call`, where the tools are `search` / `execute` / `multi-execute`\n> (call ops exactly as in-session: `{ \"name\": \"execute\", \"arguments\": { \"operation\": \"…\", \"params\": {…} } }`).\n> It self-authenticates with the same stored Hostinger OAuth creds as the other connected servers, so\n> there is **no token to handle** (keep any secret off the command line). Your sandbox may ask the user\n> to approve spawning it — ask once, then proceed. Only fall back to asking the user to restart if\n> driving the server directly isn't permitted.\n>\n> Verify the tools work (in-session or via the direct drive) with a search — e.g. \"list stores\" →\n> `ecommerce_stores_list` — before proceeding.\n> The authoritative, always-current build guide is `ecommerce_miscellaneous_custom-storefront-setup-instructions` — read it once per build.\n\nAll website-lifecycle calls go through the `hosting` connector (`execute`); all store calls through the\n`hostinger-ecommerce` MCP.\n\n## Step 1 — The headless website (frontend host)\n\nSame lifecycle as `../wordpress/SKILL.md` Steps 1–2, but `type: \"headless_ecommerce\"`:\n\n1. Detect: `hosting_websites_list` / `hosting_websites_list-setups` (exact-match the domain). `completed`\n → adopt. `running` → poll. `failed` → stop, point at hPanel. If `website_type` is not `headless`\n (plain WordPress, Node.js, …), stop and tell the user — don't overwrite another kind of site.\n2. Create (only if **no** site exists): `hosting_websites_start-setup { order_id, type: \"headless_ecommerce\", domain }`\n — **omit `domain`** for a temporary subdomain: the setup generates one itself and returns it (don't\n call a separate subdomain op first — there's no need in this flow; the API would also accept one you\n generated via `hosting_domains_generate-free-subdomain`, valid 24 h from the client that made it). The\n response carries the generated domain; if it differs from the workspace dir, move `~/.hostinger/<old>/` → `<new>/` and\n update `brief.md`. Poll `list-setups` to `completed`. `website_type` will be `headless`.\n\n> **Onboarding handoff — an empty headless shell needs no new website.** If the domain already has an\n> empty headless shell (`website_type: headless`, setup `type: headless_empty`, nothing installed —\n> typically created in onboarding), **adopt it as the storefront host and skip the create.** A\n> `headless_ecommerce` website installs no store engine either, so an empty headless shell *is* the host\n> you need — there is no backend to \"swap\" on the website side. Record its `username`/`order_id` and go\n> straight to Step 2 (store + custom sales channel). Never call `start-setup` on a domain that already\n> has a site.\n\n> Don't expect `/cms` or any API on this domain — it serves the storefront you build (Step 6) and\n> deploy (Step 8).\n\n## Step 2 — Resolve or create the store + custom sales channel\n\n`ecommerce_stores_list` → reuse an existing store if this build already made one; otherwise create:\n\n```json\n{ \"operation\": \"ecommerce_stores_create\",\n \"params\": { \"name\": \"<store name>\", \"country_code\": \"US\", \"company_email\": \"<email>\",\n \"company_name\": \"<store name>\", \"language\": \"en\",\n \"sales_channel\": { \"type\": \"custom\", \"external_id\": \"<slug>-headless\" } } }\n```\n\nThis returns `store.id` (`store_*`) **and** a custom `sales_channel.id` (`scha_*`) in one call — the\ncustom channel is what the headless storefront is keyed on. (You can also add one later with\n`ecommerce_sales-channels_create { store_id, type:\"custom\", name }`.) Currency comes from\n`country_code` (`default_currency_code`); all prices are **minor units** (cents).\n\n## Step 3 — Store readiness: shipping + payment\n\nA store needs ≥1 shipping zone and ≥1 payment method to check out.\n\n- **Shipping:** `ecommerce_shipping_set-store { store_id, price }` (minor units; 0 = free).\n- **Payment:** `ecommerce_payments_enable-manual-method { store_id, title }`.\n\n> **Known gate — free accounts have no payment method, and therefore no checkout.** On a **free\n> account** (and any account/store without a paid Hostinger Ecommerce plan), `enable-manual-method`\n> returns **HTTP 402**, and checkout later fails with `400 payment_provider_not_configured`. This is\n> **expected, not an error** — payment, and therefore checkout, is simply unavailable on the plan. The\n> catalog and the storefront still work fully. **Never block, stop, loop or retry the build on this:**\n> - Record `payment_enabled: false` in `site.json` (set `shipping_enabled` as usual).\n> - Tell the user in one line that checkout needs a paid Ecommerce plan, and carry straight on.\n> - Treat the 402 as a normal outcome — don't surface it as a failure or re-attempt the call.\n> - Never ask the user for payment credentials.\n>\n> Every downstream stage reads `site.json → ecommerce.payment_enabled` and, when it is `false`, skips\n> all checkout wiring and verification (see content-model Step 5, frontend, deploy). A store with\n> `payment_enabled: false` is a **complete, valid build** — a browsable catalog without checkout.\n\n## Step 4 — Verify\n\n- `ecommerce_stores_metadata { store_id }` → `has_shipping` true; `has_payment_methods` (may be false,\n see the gate); read `default_currency` (`decimal_digits`, `template`) here, don't guess.\n- Public Storefront V2 answers for the channel (no auth):\n `GET https://api-ecommerce.hostinger.com/v2/channels/<scha_*>/products?limit=100` → 200 (empty until\n content-model seeds products — fine here).\n- `GET https://<domain>/` returns the Hostinger default page until the storefront is deployed — expected.\n\n## Step 5 — Record `site.json`\n\n```json\n{\n \"backend\": \"ecommerce\",\n \"website_type\": \"headless\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"ecommerce\": {\n \"store_id\": \"store_…\",\n \"sales_channel\": { \"id\": \"scha_…\", \"type\": \"custom\", \"external_id\": \"…-headless\" },\n \"currency\": \"usd\",\n \"payment_enabled\": false,\n \"shipping_enabled\": true\n },\n \"urls\": {\n \"site\": \"https://<domain>\",\n \"storefront_api\": \"https://api-ecommerce.hostinger.com/v2/channels/<scha_*>\",\n \"store_admin\": \"https://ecommerce.hostinger.com/store/<store_*>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo secrets in `site.json`. After deploy, link the channel to the live domain with\n`ecommerce_sales-channels_update { store_id, sales_channel_id, url }` (done in the deploy stage).\n"
|
|
162
162
|
},
|
|
163
163
|
{
|
|
164
164
|
"uri": "skill://hostinger-headless/steps/03-provision/webapp/SKILL.md",
|
|
165
|
-
"digest": "sha256:
|
|
166
|
-
"size":
|
|
165
|
+
"digest": "sha256:f93a16580c8aaa4b8122bf38450cbc763abf9e506065b90de91b619b6edc8a2d",
|
|
166
|
+
"size": 9262,
|
|
167
167
|
"mimeType": "text/markdown",
|
|
168
|
-
"text": "---\nname: headless-step-provision-webapp\ndescription: \"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.\"\n---\n\n# headless-step-provision-webapp\n\nProvision a bare **Node.js app host** for `<domain>` and prove it is ready. This is the backend for\n**any webapp, SPA or app** that brings its own code (and, if it needs one, its own database) — there is\nno CMS and no store engine here. The site is an **empty website**: just the hosting slot that the\nfrontend step builds into and the deploy step ships to as a Node.js application. **Out:** `site.json`.\n\n> **Not WordPress, not Ecommerce.** Never provision `headless_wordpress` / `headless_ecommerce` for a\n> webapp, and never install WordPress at `/cms`. A webapp owns its own routes, data and rendering.\n\nAll website/database operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Steps 1–2 up to `hosting_websites_start-setup` are the\n> **fire** half: once the setup is queued, record `provision.in_progress` and return to do prepare work\n> (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2, plus Steps 3–5, are the\n> **join** half — run them when an instance-barrier step needs the backend (or immediately, if the\n> agent can't interleave). An already-`completed` site detected in Step 1 means provision is done:\n> record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. `hosting_websites_list-setups { \"domain\": \"<domain>\" }` — exact-match the row:\n - `completed` → already provisioned → skip to Step 3 (adopt it).\n - `running` → skip to Step 2 (poll).\n - `failed` → tell the user and point them at hPanel; do **not** call start-setup again on your own.\n3. If the site exists but is a different kind (`website_type` is `headless`, or a WordPress/store site),\n stop and tell the user — this domain already hosts something else.\n\n## Step 2 — Create (only when nothing exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup as an **empty website** — **omit `type`** (no WordPress, no store):\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself. Don't call a
|
|
168
|
+
"text": "---\nname: headless-step-provision-webapp\ndescription: \"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.\"\n---\n\n# headless-step-provision-webapp\n\nProvision a bare **Node.js app host** for `<domain>` and prove it is ready. This is the backend for\n**any webapp, SPA or app** that brings its own code (and, if it needs one, its own database) — there is\nno CMS and no store engine here. The site is an **empty website**: just the hosting slot that the\nfrontend step builds into and the deploy step ships to as a Node.js application. **Out:** `site.json`.\n\n> **Not WordPress, not Ecommerce.** Never provision `headless_wordpress` / `headless_ecommerce` for a\n> webapp, and never install WordPress at `/cms`. A webapp owns its own routes, data and rendering.\n\nAll website/database operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Steps 1–2 up to `hosting_websites_start-setup` are the\n> **fire** half: once the setup is queued, record `provision.in_progress` and return to do prepare work\n> (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2, plus Steps 3–5, are the\n> **join** half — run them when an instance-barrier step needs the backend (or immediately, if the\n> agent can't interleave). An already-`completed` site detected in Step 1 means provision is done:\n> record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. `hosting_websites_list-setups { \"domain\": \"<domain>\" }` — exact-match the row:\n - `completed` → already provisioned → skip to Step 3 (adopt it).\n - `running` → skip to Step 2 (poll).\n - `failed` → tell the user and point them at hPanel; do **not** call start-setup again on your own.\n3. If the site exists but is a different kind (`website_type` is `headless`, or a WordPress/store site),\n stop and tell the user — this domain already hosts something else.\n\n## Step 2 — Create (only when nothing exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup as an **empty website** — **omit `type`** (no WordPress, no store):\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself and returns it in the response. Don't call a\n separate subdomain op to make one first: there's no need in this flow. (The API would also accept a\n subdomain you generated via `hosting_domains_generate-free-subdomain`, valid for 24 h from the client\n that made it — this flow just doesn't use that.) The response then carries the generated domain: if it\n differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,\n update the domain in `brief.md`, and tell the user the new domain.\n3. Poll `hosting_websites_list-setups { \"domain\": \"<domain>\" }` every ~15 s until `completed`\n (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.\n\n## Step 3 — Database (only if the app needs one)\n\nSkip this step entirely for an app with no database (static SPA, an app that talks to an external API).\nThis is **join-half** work — it needs the live website, so run it once `provision.status == \"completed\"`.\n\n**Preferred — let the platform create the database and inject the credentials:**\n`hosting_databases_setup-website { username, domain }` creates a MySQL database, **generates the\npassword, writes `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASSWORD` / `DATABASE_URL` into the\nwebsite's environment variables, and restarts the app — in one call.** The password is never returned,\nso there is **nothing to persist and nothing to lose across sessions** — the app reads it from the\nenvironment. (It 422s if any of those env vars already exist; clear them first with\n`hosting_nodejs_replace-environment-variables`. Frameworks that bake vars into the build — Next.js,\n`NEXT_PUBLIC_*` — only pick up the values on the deploy build.)\n\n> **⚠️ A later `hosting_nodejs_replace-environment-variables` wipes these credentials.** That call is a\n> **full replace**, and when you list the current vars first the `DB_PASSWORD` (and `DATABASE_URL`) read\n> back **masked** (`********`) — the generated password is unknown, so you cannot resend it and the DB\n> env is lost. One rule, pick one:\n> - **(a) Set every other app env var first, and call `setup-website` last** — then nothing has to\n> replace the env again.\n> - **(b) If a later replace is unavoidable**, reset the password with\n> `hosting_databases_change-password { username, name, password }` and resend **all** of `DB_HOST`,\n> `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` with the new password in that same\n> full replace.\n\n**Manual alternative** — only when you need a specific name/user or finer control:\n\n1. `hosting_databases_create` with a name, a user, and a password you generate — name and user are\n prefixed with the account username automatically. Read the full, prefixed `name`/`user` back from\n `hosting_databases_list`.\n2. **Which host the app connects to:** the app runs **on the website**, so Node.js connects to\n **`127.0.0.1:3306`** — never `localhost` (it can resolve to IPv6 `::1`, and not every database user\n has that grant), and never the external `host` from `hosting_databases_list` (`srvNNNN.hstgr.io`,\n which has no grant for connections from the server itself).\n3. **Set the env vars now, while you still hold the generated password.** The password is **not persisted\n anywhere** (never in `site.json`), so set `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_NAME`, `DB_USER`,\n `DB_PASSWORD`, plus `DATABASE_URL=mysql://USER:PASSWORD@127.0.0.1:3306/NAME` (URL-encode USER/PASSWORD)\n right here with `hosting_nodejs_replace-environment-variables` (a **full replace** — list current keys\n first; values come back masked; send the complete set with real values). Saving restarts the process;\n build-time vars need a fresh deploy (`hosting_deploy`) afterwards. **Don't defer this to deploy:** on\n a **cross-session resume** the generated password is gone, so you must **reset it** with\n `hosting_databases_change-password { username, name, password }` and set the env vars from the new one.\n4. **Never** put the password in source, a committed `.env`, client-side JavaScript, or a chat reply;\n never print it once stored.\n\n## Step 4 — Verify\n\n| Check | How | Expect |\n|---|---|---|\n| Website exists | `hosting_websites_list` exact row | the row is present, `username` known |\n| Setup finished | `hosting_websites_list-setups` exact row | `status: completed` |\n| Database reachable *(only if created)* | `hosting_databases_list { username }` | the prefixed db + user are listed |\n\n`GET https://<domain>/` returns the Hostinger default page (or 403) until the frontend is deployed —\nexpected, not a failure. There is **no `/cms` and no API** on this domain; the app itself serves every\nroute once it is deployed (Step 8).\n\nIf a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.\n\n## Step 5 — Record `site.json`\n\n```json\n{\n \"backend\": \"webapp\",\n \"website_type\": \"empty\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"runtime\": \"nodejs\",\n \"database\": {\n \"enabled\": false,\n \"engine\": \"mysql\",\n \"name\": \"u…_appdb\",\n \"user\": \"u…_app\",\n \"host\": \"127.0.0.1\",\n \"port\": 3306\n },\n \"urls\": {\n \"site\": \"https://<domain>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo passwords or tokens in `site.json` — only the non-secret connection shape (host/port/name/user). Set\n`\"database\": { \"enabled\": false }` (and drop the rest) when the app needs no database.\n\n## Deploy & content model (what happens next)\n\n- **Content model is skipped for a webapp** — there is no CMS or store to seed; the app owns its own\n data. The content-model router (`../../04-content-model/SKILL.md`) passes straight through to the\n frontend step for this backend.\n- **Deploy** is a standard Node.js application deploy, handled by `../../08-deploy/SKILL.md`: it archives\n the **source** (not the build output; exclude `node_modules/`, `.next/`, `dist/`, build output and\n `.git/`), then deploys with `hosting_deploy` (`type: \"nodejs\"`, upload + server-side `npm install` +\n build, one shot) passing the framework's `appType` + `outputDirectory` inline, and polls\n `hosting_get-deployment` until the build completes. Any supported `appType` works (Next.js, Nuxt,\n Express, NestJS, Fastify, Vite/React/Vue SPA, …). There is no `/cms` to preserve here, so the whole\n root is the app.\n"
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"uri": "skill://hostinger-headless/steps/03-provision/wordpress/SKILL.md",
|
|
172
|
-
"digest": "sha256:
|
|
173
|
-
"size":
|
|
172
|
+
"digest": "sha256:4bfd1496aaf6b3507cd7cd5120b0c50372a0bf336afc9a6f445325335aa4cbcf",
|
|
173
|
+
"size": 20310,
|
|
174
174
|
"mimeType": "text/markdown",
|
|
175
|
-
"text": "---\nname: headless-step-provision-wordpress\ndescription: \"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.\"\n---\n\n# headless-step-provision-wordpress\n\nCreate the headless WordPress backend for `<domain>` and prove it is ready. One platform call lays\nevery headless site out identically: WordPress in `public_html/cms`, Hostinger AI Assistant + MCP\nadapter, site marked `headless`. **Out:** `site.json`.\n\n> **WordPress + WooCommerce?** This same recipe serves it. When the brief's **Suggested backend** is\n> `**WordPress + WooCommerce**`, provision the WordPress site exactly as below, then run the **optional\n> WooCommerce tail in Step 5** (install + activate the `woocommerce` plugin and verify the store\n> endpoints; the catalog is seeded in content-model via the per-site WP MCP).\n> WooCommerce is a plugin on this WordPress install, **not** a separate backend: `site.json → backend`\n> stays `\"wordpress\"` and the store is recorded under `site.json → woocommerce`. For a plain WordPress\n> brief, skip Step 5 entirely.\n\nAll operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Step 1, plus the queuing call — `hosting_websites_start-setup`\n> for a fresh site (Step 2) **or** `wordpress_installations_install` to swap an empty headless shell\n> (Step 2b) — are the **fire** half: once the job is queued, record `provision.in_progress` and return\n> to do prepare work (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2 / 2b,\n> plus Steps 3–6, are the **join** half — run them when an instance-barrier step needs the backend (or\n> immediately, if the agent can't interleave). An already-`completed` site with WordPress at `/cms`\n> detected in Step 1 means provision is already done: record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. **No exact row** → nothing exists on this domain → **create fresh** (Step 2).\n3. **A row exists.** If `website_type` is **not** `headless`, stop and tell the user — this domain\n already hosts a different kind of site (plain WordPress, Node.js, builder, …); never overwrite it.\n If it **is** `headless`, work out whether WordPress is already installed or the shell is still empty\n (see the router's *Onboarding handoff*): check `hosting_websites_list-setups { \"domain\": \"<domain>\" }`\n (exact row) and `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row):\n - Setup `running` → provisioning is underway → skip to Step 2's poll.\n - Setup `failed` → tell the user and point them at hPanel; do **not** call start-setup again yourself.\n - A WordPress install with `directory: \"cms\"` already present (or setup `type: headless_wordpress`)\n → **already provisioned** → skip to Step 3 (adopt it).\n - A `headless` site with **no** `/cms` install (an **empty headless shell** — setup\n `type: headless_empty`, typically from onboarding) → **swap it to WordPress** (Step 2b). Do **not**\n call `start-setup` again — the website already exists; you only install WordPress into it.\n\n## Step 2 — Create a fresh headless WordPress site (only when no site exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup:\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"type\": \"headless_wordpress\", \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself. Don't call a separate subdomain op to make one\n first: there's no need, and passing a generated free subdomain back as `domain` fails with\n `422 … Free subdomains cannot be provided`. The response then carries the generated domain:\n if it differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,\n update the domain in `brief.md`, and tell the user the new domain.\n Admin credentials are generated by the platform; never ask the user for a password.\n3. Poll `hosting_websites_list-setups { \"domain\": \"<domain>\" }` every ~15 s until `completed`\n (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.\n\n## Step 2b — Swap an empty headless shell to WordPress (only when onboarding already made the shell)\n\nThe website already exists as an empty `headless` shell, so there is **no `start-setup`** here — you\ninstall WordPress straight into its `/cms` directory. This is the onboarding handoff path.\n\n1. Use the shell's `username` and `order_id` from Step 1's row — no new order pick, no new website.\n2. Install WordPress into `/cms`, flagged headless:\n\n ```json\n { \"operation\": \"wordpress_installations_install\",\n \"params\": { \"username\": \"<username>\", \"domain\": \"<domain>\", \"type\": \"headless\",\n \"site_title\": \"<site title from brief.md>\",\n \"credentials\": { \"login\": \"<admin login>\", \"email\": \"<account email>\",\n \"password\": \"<strong generated password>\" } } }\n ```\n\n - `type: \"headless\"` installs into `/cms` with the Hostinger headless MU plugin and marks the site\n headless-WordPress; **`directory` is not allowed with it** (it is always `cms`). The target must\n already be `headless` — the headless install rewrites the root `.htaccess`, so the API rejects it\n with `422` on any non-headless site (which is why Step 1 confirms `website_type: headless` first).\n - **You generate the admin password** (strong, 8–50 chars, mixed case + a digit) — never ask the user\n for one and never print it. The user signs in later through a one-click login link (Step 6), so the\n password only has to exist, not be remembered.\n - A new database is created for the install unless you pass `database.name` of an existing one; the\n generated database counts against the plan's limit.\n3. The call is **asynchronous** — a success response only means the install job was **queued**. This is\n the *fire*: record `provision.in_progress` and return to prepare work. Poll\n `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row) at the join; when a\n row with `directory: \"cms\"` and `is_valid: true` appears, WordPress is ready. Allow 1–2 minutes;\n `wordpress_installations_list` can lag a few seconds behind the job finishing and return `[]` — retry\n briefly before failing.\n\n## Step 3 — Verify (all must pass)\n\n| Check | How | Expect |\n|---|---|---|\n| Site is headless | `hosting_websites_list` exact row | `website_type: \"headless\"` |\n| WordPress at `/cms` | `wordpress_installations_list { \"username\": \"<username>\", \"ownership\": \"all\" }`, exact-match `domain` | row with `directory: \"cms\"`, `is_valid: true`; note its `id` |\n| REST API answers | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200, JSON array |\n| AI Assistant + MCP loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `hostinger-ai-assistant/v1` and `mcp` |\n| JWT mintable | `wordpress_installations_jwt-token { username, software: <id> }` | returns `token`, `mcp_url` |\n\nNotes:\n\n- `wordpress_installations_list` can lag a few seconds behind `completed` and return `[]` — retry\n briefly before failing. Its `domain` filter is a substring match, so exact-match the row.\n- The install record's `url` may say `http://`; always use `https://` URLs.\n- `GET https://<domain>/` returns 403 until the frontend is deployed — expected, not a failure.\n- **Never print, log or store the JWT.** It is minted on demand (24 h lifetime) by whichever stage\n needs the per-site WP MCP; only `mcp_url` is recorded.\n\nIf a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.\n\n## Step 4 — SEO plugin (Yoast) *(every WordPress site — on by default)*\n\nEvery headless WordPress site gets **Yoast SEO** (`wordpress-seo`) installed and activated by default,\nso per-page title/description/Open Graph/JSON-LD come from a maintained plugin's `yoast_head_json`\nrather than being hand-generated from each item's content by the frontend. This is **join-half** work\n(needs the live `/cms` install), so run it once `provision.status == \"completed\"`, the same way as the\nWooCommerce tail. Tell the user in one line that SEO (Yoast) is being set up; if they explicitly don't\nwant it, skip this step and record `seo.enabled: false` — but the default is to install it.\n\n1. **Install + activate Yoast** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"wordpress-seo\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `wordpress-seo` shows\n `active` (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"wordpress-seo\" }`.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` → `namespaces` once more, and that a sample post/page\n response carries `yoast_head_json`. The per-item Yoast meta (focus title/description per post) is set\n later in the **content-model push** — provision only installs + activates the plugin here. Don't fail\n provision over SEO: if the install can't be confirmed after one retry, record `seo.enabled: false`,\n tell the user, and let the frontend fall back to generating tags from content.\n\n## Step 5 — WooCommerce store *(only for a `WordPress + WooCommerce` brief — skip otherwise)*\n\nRun this only when the brief's **Suggested backend** is `**WordPress + WooCommerce**`. It turns the\nheadless WordPress site into a headless **store**: WooCommerce is the `woocommerce` plugin on this same\n`/cms` install — **not a separate backend**. This is **join-half** work (needs the live instance), so run\nit once `provision.status == \"completed\"`. The authoritative data-layer reference is\n[`../../04-content-model/wordpress/woocommerce.md`](../../04-content-model/wordpress/woocommerce.md).\n\n> **No custom plugin — the catalog is driven through the per-site WP MCP.** A custom plugin can't be\n> installed into a headless `/cms` from a coding agent, so there is **no seed/config plugin and no REST\n> consumer key in this flow.** Instead, the Hostinger AI Assistant (the per-site WP MCP, same JWT session\n> content-model uses) exposes **native WooCommerce abilities** — `wc-products-*`, `wc-product-categories-*`,\n> `wc-coupons-*`, `wc-orders-*`, `media-upload-from-url`, `wp-settings-*`. Provision just **installs\n> WooCommerce and verifies the store endpoints**; the catalog + coupons are seeded in the content-model\n> push through those abilities (see the cookbook).\n\n1. **Install + activate WooCommerce** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"woocommerce\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `woocommerce` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"woocommerce\" }`.\n - Confirm with `wordpress_plugins_check-if-woo-commerce-is-installed { domain: \"<domain>\" }`. Allow a\n moment for WooCommerce's installer to create its tables and the Shop/Cart/Checkout pages.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded** (seen on both `plugins_install` and `jwt-token`). Treat a 500 as \"verify, don't assume\":\n poll `list-installed` / re-check before retrying, and never fire a second install blindly.\n2. **Verify the store endpoints (public, no auth):**\n\n | Check | How | Expect |\n |---|---|---|\n | WooCommerce active | `wordpress_plugins_check-if-woo-commerce-is-installed { domain }` | installed + active |\n | Store API answers | `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` | 200 (empty until content-model seeds — fine) |\n | Both namespaces loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `wc/store/v1` **and** `wc/v3` |\n\n Pretty permalinks are on by default on a headless install, so the Store API resolves immediately; if a\n store route ever 404s, the permalink structure is the thing to check.\n\n> **Checkout needs owner setup — treat it like the Ecommerce payment gate.** A fresh WooCommerce install\n> has **no payment gateway and no shipping zone enabled**, and that configuration is **not reachable from\n> the headless tooling** (there is no payment/shipping MCP ability, and `wp-settings-update` only writes\n> standard WP settings). So **checkout is unavailable until the owner enables a payment method + shipping\n> in wp-admin** (hand them the one-click login in Step 6). Record `woocommerce.payment_enabled: false`\n> accordingly. This is **not a build failure**: the catalog, product pages, client-side cart and coupon\n> application all work headlessly — the downstream frontend renders a graceful disabled-checkout state and\n> never calls `/checkout` while `payment_enabled` is `false` (see content-model + frontend). Never ask the\n> user for payment credentials. Store currency and address (`woocommerce_*` options) likewise aren't\n> settable headlessly — they follow the install default and the owner can change them in wp-admin.\n\n## Step 5b — Contact form (Contact Form 7) *(only when the brief's Must-haves include a contact form — skip otherwise)*\n\nRun this only when the brief's **Must-haves** include a contact form. It installs **Contact Form 7**\n(`contact-form-7`) on this same `/cms` install, so the frontend can render a contact page whose form\nemails the site owner through WordPress's built-in `wp_mail()` — no third-party service and no API keys.\nLike Yoast/WooCommerce this is **join-half** work: run it once `provision.status == \"completed\"`. The form\nitself (id + fields + recipient) — and a tiny **spam-guard plugin** that makes a honeypot work headlessly —\nare resolved/deployed in the content-model push — see\n[`../../04-content-model/wordpress/contact-form.md`](../../04-content-model/wordpress/contact-form.md).\n\n> **Why CF7 and not a bespoke mailer.** A contact form is maintained, spam-hardened functionality worth\n> reusing rather than reinventing (you *can* deploy a custom plugin to a headless `/cms` — see the\n> content-model cookbook — but you needn't write your own mailer). CF7 is a wordpress.org-slug plugin,\n> installs through the same management MCP as Yoast, and ships a **public, unauthenticated** submit\n> endpoint (`POST /cms/wp-json/contact-form-7/v1/contact-forms/<id>/feedback`). The frontend and `/cms`\n> are same-origin, so the browser posts to it directly — no proxy, no CORS, no secret on the client.\n\n1. **Install + activate CF7** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"contact-form-7\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `contact-form-7` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"contact-form-7\" }`.\n - **Don't install the `contact-form-7-honeypot` add-on.** It only acts when a `[honeypot]` form-tag is\n present in the form — a wp-admin edit the headless agent can't make — so on the default form it does\n nothing. Headless spam protection instead comes from a small **spam-guard plugin deployed in the\n content-model push** (it hooks `wpcf7_spam`); see the cookbook.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` lists the **`contact-form-7/v1`** namespace, then\n record `contact_form.enabled: true` in `site.json` (Step 6). If the install can't be confirmed after\n one retry, record `contact_form.enabled: false`, tell the user in one line, and let the frontend\n render the contact page without a form (graceful — not a build failure). Don't fail provision over it.\n\nThe owner's email is **CF7's default recipient** (`[_site_admin_email]`, the admin address from\ninstall) — fine out of the box, so \"does Hostinger set up the email?\" is **yes** for basic delivery via\n`wp_mail`. Deliverability (SPF/DKIM) is most reliable when that address is a mailbox **on the same\ndomain**; the content-model push keeps the default recipient and validate sends a test submission to\nconfirm mail actually leaves. Never ask the user for mail credentials.\n\n## Step 6 — Record `site.json`\n\n```json\n{\n \"backend\": \"wordpress\",\n \"website_type\": \"headless\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"wordpress\": {\n \"install_id\": \"30…\",\n \"directory\": \"cms\",\n \"admin_login\": \"…\"\n },\n \"seo\": {\n \"plugin\": \"wordpress-seo\",\n \"enabled\": true\n },\n \"urls\": {\n \"site\": \"https://<domain>\",\n \"cms\": \"https://<domain>/cms\",\n \"wp_admin\": \"https://<domain>/cms/wp-admin\",\n \"rest_api\": \"https://<domain>/cms/wp-json/wp/v2\",\n \"wp_mcp\": \"<mcp_url from the JWT call>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo passwords or tokens in `site.json`. When the user needs wp-admin, mint a one-click link with\n`wordpress_login_create-links { username, software: <install_id> }` — never hand over a password.\n\n**For a `WordPress + WooCommerce` brief, also add a `woocommerce` block and the store URLs** (the\n`backend` field still stays `\"wordpress\"`):\n\n```json\n{\n \"woocommerce\": {\n \"enabled\": true,\n \"seeded_via\": \"per-site-wp-mcp\",\n \"currency\": \"<currency_code from GET {store_api}/products or the install default>\",\n \"payment_enabled\": false,\n \"checkout_setup\": \"owner enables a payment method + shipping in wp-admin\"\n },\n \"urls\": {\n \"store_api\": \"https://<domain>/cms/wp-json/wc/store/v1\",\n \"wc_rest_api\": \"https://<domain>/cms/wp-json/wc/v3\"\n }\n}\n```\n\n**For a brief with a contact form (CF7 installed in Step 5b), also add a `contact_form` block.**\nProvision records only `enabled` + `plugin` here; the content-model push fills `form_id`,\n`feedback_url`, `fields` and `honeypot_field` once it resolves the form and deploys the spam guard:\n\n```json\n{\n \"contact_form\": {\n \"enabled\": true,\n \"plugin\": \"contact-form-7\",\n \"form_id\": null,\n \"feedback_url\": null,\n \"fields\": [],\n \"honeypot_field\": null,\n \"recipient\": \"site-admin-email\"\n }\n}\n```\n\n`form_id` / `feedback_url` / `fields` / `honeypot_field` stay `null` / `[]` until content-model resolves the form (the\nsubmit URL is `https://<domain>/cms/wp-json/contact-form-7/v1/contact-forms/<form_id>/feedback`).\n`recipient` is informational — CF7's `To` is `[_site_admin_email]`; no address, token or secret is\nstored in `site.json`.\n\n`rest_api` stays `wp/v2` (the WordPress content contract, for the blog/pages on the same site);\n`store_api` is the public storefront surface (browse/cart/coupons/checkout, no auth). `wc_rest_api`\n(`wc/v3`) is recorded only as the surface the **owner's own** integrations use with a consumer key they\ngenerate in wp-admin — **this flow never creates or stores a key** (the catalog is seeded through the\nper-site WP MCP's `wc-*` abilities; the storefront uses the public Store API). `payment_enabled` starts\n`false` and flips to `true` only once a gateway + shipping are configured — no secrets in `site.json`.\n"
|
|
175
|
+
"text": "---\nname: headless-step-provision-wordpress\ndescription: \"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.\"\n---\n\n# headless-step-provision-wordpress\n\nCreate the headless WordPress backend for `<domain>` and prove it is ready. One platform call lays\nevery headless site out identically: WordPress in `public_html/cms`, Hostinger AI Assistant + MCP\nadapter, site marked `headless`. **Out:** `site.json`.\n\n> **WordPress + WooCommerce?** This same recipe serves it. When the brief's **Suggested backend** is\n> `**WordPress + WooCommerce**`, provision the WordPress site exactly as below, then run the **optional\n> WooCommerce tail in Step 5** (install + activate the `woocommerce` plugin and verify the store\n> endpoints; the catalog is seeded in content-model via the per-site WP MCP).\n> WooCommerce is a plugin on this WordPress install, **not** a separate backend: `site.json → backend`\n> stays `\"wordpress\"` and the store is recorded under `site.json → woocommerce`. For a plain WordPress\n> brief, skip Step 5 entirely.\n\nAll operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Step 1, plus the queuing call — `hosting_websites_start-setup`\n> for a fresh site (Step 2) **or** `wordpress_installations_install` to swap an empty headless shell\n> (Step 2b) — are the **fire** half: once the job is queued, record `provision.in_progress` and return\n> to do prepare work (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2 / 2b,\n> plus Steps 3–6, are the **join** half — run them when an instance-barrier step needs the backend (or\n> immediately, if the agent can't interleave). An already-`completed` site with WordPress at `/cms`\n> detected in Step 1 means provision is already done: record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. **No exact row** → nothing exists on this domain → **create fresh** (Step 2).\n3. **A row exists.** If `website_type` is **not** `headless`, stop and tell the user — this domain\n already hosts a different kind of site (plain WordPress, Node.js, builder, …); never overwrite it.\n If it **is** `headless`, work out whether WordPress is already installed or the shell is still empty\n (see the router's *Onboarding handoff*): check `hosting_websites_list-setups { \"domain\": \"<domain>\" }`\n (exact row) and `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row):\n - Setup `running` → provisioning is underway → skip to Step 2's poll.\n - Setup `failed` → tell the user and point them at hPanel; do **not** call start-setup again yourself.\n - A WordPress install with `directory: \"cms\"` already present (or setup `type: headless_wordpress`)\n → **already provisioned** → skip to Step 3 (adopt it).\n - A `headless` site with **no** `/cms` install (an **empty headless shell** — setup\n `type: headless_empty`, typically from onboarding) → **swap it to WordPress** (Step 2b). Do **not**\n call `start-setup` again — the website already exists; you only install WordPress into it.\n\n## Step 2 — Create a fresh headless WordPress site (only when no site exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup:\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"type\": \"headless_wordpress\", \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself and returns it in the response. Don't call a\n separate subdomain op to make one first: there's no need in this flow. (The API would also accept a\n subdomain you generated via `hosting_domains_generate-free-subdomain`, valid for 24 h from the client\n that made it — this flow just doesn't use that.) The response then carries the generated domain:\n if it differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,\n update the domain in `brief.md`, and tell the user the new domain.\n Admin credentials are generated by the platform; never ask the user for a password.\n3. Poll `hosting_websites_list-setups { \"domain\": \"<domain>\" }` every ~15 s until `completed`\n (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.\n\n## Step 2b — Swap an empty headless shell to WordPress (only when onboarding already made the shell)\n\nThe website already exists as an empty `headless` shell, so there is **no `start-setup`** here — you\ninstall WordPress straight into its `/cms` directory. This is the onboarding handoff path.\n\n1. Use the shell's `username` and `order_id` from Step 1's row — no new order pick, no new website.\n2. Install WordPress into `/cms`, flagged headless:\n\n ```json\n { \"operation\": \"wordpress_installations_install\",\n \"params\": { \"username\": \"<username>\", \"domain\": \"<domain>\", \"type\": \"headless\",\n \"site_title\": \"<site title from brief.md>\",\n \"credentials\": { \"login\": \"<admin login>\", \"email\": \"<account email>\",\n \"password\": \"<strong generated password>\" } } }\n ```\n\n - `type: \"headless\"` installs into `/cms` with the Hostinger headless MU plugin and marks the site\n headless-WordPress; **`directory` is not allowed with it** (it is always `cms`). The target must\n already be `headless` — the headless install rewrites the root `.htaccess`, so the API rejects it\n with `422` on any non-headless site (which is why Step 1 confirms `website_type: headless` first).\n - **You generate the admin password** (strong, 8–50 chars, mixed case + a digit) — never ask the user\n for one and never print it. The user signs in later through a one-click login link (Step 6), so the\n password only has to exist, not be remembered.\n - A new database is created for the install unless you pass `database.name` of an existing one; the\n generated database counts against the plan's limit.\n3. The call is **asynchronous** — a success response only means the install job was **queued**. This is\n the *fire*: record `provision.in_progress` and return to prepare work. Poll\n `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row) at the join; when a\n row with `directory: \"cms\"` and `is_valid: true` appears, WordPress is ready. Allow 1–2 minutes;\n `wordpress_installations_list` can lag a few seconds behind the job finishing and return `[]` — retry\n briefly before failing.\n\n## Step 3 — Verify (all must pass)\n\n| Check | How | Expect |\n|---|---|---|\n| Site is headless | `hosting_websites_list` exact row | `website_type: \"headless\"` |\n| WordPress at `/cms` | `wordpress_installations_list { \"username\": \"<username>\", \"ownership\": \"all\" }`, exact-match `domain` | row with `directory: \"cms\"`, `is_valid: true`; note its `id` |\n| REST API answers | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200, JSON array |\n| AI Assistant + MCP loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `hostinger-ai-assistant/v1` and `mcp` |\n| JWT mintable | `wordpress_installations_jwt-token { username, software: <id> }` | returns `token`, `mcp_url` |\n\nNotes:\n\n- `wordpress_installations_list` can lag a few seconds behind `completed` and return `[]` — retry\n briefly before failing. Its `domain` filter is a substring match, so exact-match the row.\n- The install record's `url` may say `http://`; always use `https://` URLs.\n- `GET https://<domain>/` returns 403 until the frontend is deployed — expected, not a failure.\n- **Never print, log or store the JWT.** It is minted on demand (24 h lifetime) by whichever stage\n needs the per-site WP MCP; only `mcp_url` is recorded.\n\nIf a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.\n\n## Step 4 — SEO plugin (Yoast) *(every WordPress site — on by default)*\n\nEvery headless WordPress site gets **Yoast SEO** (`wordpress-seo`) installed and activated by default,\nso per-page title/description/Open Graph/JSON-LD come from a maintained plugin's `yoast_head_json`\nrather than being hand-generated from each item's content by the frontend. This is **join-half** work\n(needs the live `/cms` install), so run it once `provision.status == \"completed\"`, the same way as the\nWooCommerce tail. Tell the user in one line that SEO (Yoast) is being set up; if they explicitly don't\nwant it, skip this step and record `seo.enabled: false` — but the default is to install it.\n\n1. **Install + activate Yoast** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"wordpress-seo\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `wordpress-seo` shows\n `active` (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"wordpress-seo\" }`.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` → `namespaces` once more, and that a sample post/page\n response carries `yoast_head_json`. The per-item Yoast meta (focus title/description per post) is set\n later in the **content-model push** — provision only installs + activates the plugin here. Don't fail\n provision over SEO: if the install can't be confirmed after one retry, record `seo.enabled: false`,\n tell the user, and let the frontend fall back to generating tags from content.\n\n## Step 5 — WooCommerce store *(only for a `WordPress + WooCommerce` brief — skip otherwise)*\n\nRun this only when the brief's **Suggested backend** is `**WordPress + WooCommerce**`. It turns the\nheadless WordPress site into a headless **store**: WooCommerce is the `woocommerce` plugin on this same\n`/cms` install — **not a separate backend**. This is **join-half** work (needs the live instance), so run\nit once `provision.status == \"completed\"`. The authoritative data-layer reference is\n[`../../04-content-model/wordpress/woocommerce.md`](../../04-content-model/wordpress/woocommerce.md).\n\n> **No custom plugin — the catalog is driven through the per-site WP MCP.** A custom plugin can't be\n> installed into a headless `/cms` from a coding agent, so there is **no seed/config plugin and no REST\n> consumer key in this flow.** Instead, the Hostinger AI Assistant (the per-site WP MCP, same JWT session\n> content-model uses) exposes **native WooCommerce abilities** — `wc-products-*`, `wc-product-categories-*`,\n> `wc-coupons-*`, `wc-orders-*`, `media-upload-from-url`, `wp-settings-*`. Provision just **installs\n> WooCommerce and verifies the store endpoints**; the catalog + coupons are seeded in the content-model\n> push through those abilities (see the cookbook).\n\n1. **Install + activate WooCommerce** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"woocommerce\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `woocommerce` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"woocommerce\" }`.\n - Confirm with `wordpress_plugins_check-if-woo-commerce-is-installed { domain: \"<domain>\" }`. Allow a\n moment for WooCommerce's installer to create its tables and the Shop/Cart/Checkout pages.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded** (seen on both `plugins_install` and `jwt-token`). Treat a 500 as \"verify, don't assume\":\n poll `list-installed` / re-check before retrying, and never fire a second install blindly.\n2. **Verify the store endpoints (public, no auth):**\n\n | Check | How | Expect |\n |---|---|---|\n | WooCommerce active | `wordpress_plugins_check-if-woo-commerce-is-installed { domain }` | installed + active |\n | Store API answers | `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` | 200 (empty until content-model seeds — fine) |\n | Both namespaces loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `wc/store/v1` **and** `wc/v3` |\n\n Pretty permalinks are on by default on a headless install, so the Store API resolves immediately; if a\n store route ever 404s, the permalink structure is the thing to check.\n\n> **Checkout needs owner setup — treat it like the Ecommerce payment gate.** A fresh WooCommerce install\n> has **no payment gateway and no shipping zone enabled**, and that configuration is **not reachable from\n> the headless tooling** (there is no payment/shipping MCP ability, and `wp-settings-update` only writes\n> standard WP settings). So **checkout is unavailable until the owner enables a payment method + shipping\n> in wp-admin** (hand them the one-click login in Step 6). Record `woocommerce.payment_enabled: false`\n> accordingly. This is **not a build failure**: the catalog, product pages, client-side cart and coupon\n> application all work headlessly — the downstream frontend renders a graceful disabled-checkout state and\n> never calls `/checkout` while `payment_enabled` is `false` (see content-model + frontend). Never ask the\n> user for payment credentials. Store currency and address (`woocommerce_*` options) likewise aren't\n> settable headlessly — they follow the install default and the owner can change them in wp-admin.\n\n## Step 5b — Contact form (Contact Form 7) *(only when the brief's Must-haves include a contact form — skip otherwise)*\n\nRun this only when the brief's **Must-haves** include a contact form. It installs **Contact Form 7**\n(`contact-form-7`) on this same `/cms` install, so the frontend can render a contact page whose form\nemails the site owner through WordPress's built-in `wp_mail()` — no third-party service and no API keys.\nLike Yoast/WooCommerce this is **join-half** work: run it once `provision.status == \"completed\"`. The form\nitself (id + fields + recipient) — and a tiny **spam-guard plugin** that makes a honeypot work headlessly —\nare resolved/deployed in the content-model push — see\n[`../../04-content-model/wordpress/contact-form.md`](../../04-content-model/wordpress/contact-form.md).\n\n> **Why CF7 and not a bespoke mailer.** A contact form is maintained, spam-hardened functionality worth\n> reusing rather than reinventing (you *can* deploy a custom plugin to a headless `/cms` — see the\n> content-model cookbook — but you needn't write your own mailer). CF7 is a wordpress.org-slug plugin,\n> installs through the same management MCP as Yoast, and ships a **public, unauthenticated** submit\n> endpoint (`POST /cms/wp-json/contact-form-7/v1/contact-forms/<id>/feedback`). The frontend and `/cms`\n> are same-origin, so the browser posts to it directly — no proxy, no CORS, no secret on the client.\n\n1. **Install + activate CF7** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"contact-form-7\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `contact-form-7` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"contact-form-7\" }`.\n - **Don't install the `contact-form-7-honeypot` add-on.** It only acts when a `[honeypot]` form-tag is\n present in the form — a wp-admin edit the headless agent can't make — so on the default form it does\n nothing. Headless spam protection instead comes from a small **spam-guard plugin deployed in the\n content-model push** (it hooks `wpcf7_spam`); see the cookbook.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` lists the **`contact-form-7/v1`** namespace, then\n record `contact_form.enabled: true` in `site.json` (Step 6). If the install can't be confirmed after\n one retry, record `contact_form.enabled: false`, tell the user in one line, and let the frontend\n render the contact page without a form (graceful — not a build failure). Don't fail provision over it.\n\nThe owner's email is **CF7's default recipient** (`[_site_admin_email]`, the admin address from\ninstall) — fine out of the box, so \"does Hostinger set up the email?\" is **yes** for basic delivery via\n`wp_mail`. Deliverability (SPF/DKIM) is most reliable when that address is a mailbox **on the same\ndomain**; the content-model push keeps the default recipient and validate sends a test submission to\nconfirm mail actually leaves. Never ask the user for mail credentials.\n\n## Step 6 — Record `site.json`\n\n```json\n{\n \"backend\": \"wordpress\",\n \"website_type\": \"headless\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"wordpress\": {\n \"install_id\": \"30…\",\n \"directory\": \"cms\",\n \"admin_login\": \"…\"\n },\n \"seo\": {\n \"plugin\": \"wordpress-seo\",\n \"enabled\": true\n },\n \"urls\": {\n \"site\": \"https://<domain>\",\n \"cms\": \"https://<domain>/cms\",\n \"wp_admin\": \"https://<domain>/cms/wp-admin\",\n \"rest_api\": \"https://<domain>/cms/wp-json/wp/v2\",\n \"wp_mcp\": \"<mcp_url from the JWT call>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo passwords or tokens in `site.json`. When the user needs wp-admin, mint a one-click link with\n`wordpress_login_create-links { username, software: <install_id> }` — never hand over a password.\n\n**For a `WordPress + WooCommerce` brief, also add a `woocommerce` block and the store URLs** (the\n`backend` field still stays `\"wordpress\"`):\n\n```json\n{\n \"woocommerce\": {\n \"enabled\": true,\n \"seeded_via\": \"per-site-wp-mcp\",\n \"currency\": \"<currency_code from GET {store_api}/products or the install default>\",\n \"payment_enabled\": false,\n \"checkout_setup\": \"owner enables a payment method + shipping in wp-admin\"\n },\n \"urls\": {\n \"store_api\": \"https://<domain>/cms/wp-json/wc/store/v1\",\n \"wc_rest_api\": \"https://<domain>/cms/wp-json/wc/v3\"\n }\n}\n```\n\n**For a brief with a contact form (CF7 installed in Step 5b), also add a `contact_form` block.**\nProvision records only `enabled` + `plugin` here; the content-model push fills `form_id`,\n`feedback_url`, `fields` and `honeypot_field` once it resolves the form and deploys the spam guard:\n\n```json\n{\n \"contact_form\": {\n \"enabled\": true,\n \"plugin\": \"contact-form-7\",\n \"form_id\": null,\n \"feedback_url\": null,\n \"fields\": [],\n \"honeypot_field\": null,\n \"recipient\": \"site-admin-email\"\n }\n}\n```\n\n`form_id` / `feedback_url` / `fields` / `honeypot_field` stay `null` / `[]` until content-model resolves the form (the\nsubmit URL is `https://<domain>/cms/wp-json/contact-form-7/v1/contact-forms/<form_id>/feedback`).\n`recipient` is informational — CF7's `To` is `[_site_admin_email]`; no address, token or secret is\nstored in `site.json`.\n\n`rest_api` stays `wp/v2` (the WordPress content contract, for the blog/pages on the same site);\n`store_api` is the public storefront surface (browse/cart/coupons/checkout, no auth). `wc_rest_api`\n(`wc/v3`) is recorded only as the surface the **owner's own** integrations use with a consumer key they\ngenerate in wp-admin — **this flow never creates or stores a key** (the catalog is seeded through the\nper-site WP MCP's `wc-*` abilities; the storefront uses the public Store API). `payment_enabled` starts\n`false` and flips to `true` only once a gateway + shipping are configured — no secrets in `site.json`.\n"
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
"uri": "skill://hostinger-headless/steps/04-content-model/SKILL.md",
|
package/src/core/skills.ts
CHANGED
|
@@ -169,24 +169,24 @@ const skills: Skill[] = [
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"uri": "skill://hostinger-headless/steps/03-provision/ecommerce/SKILL.md",
|
|
172
|
-
"digest": "sha256:
|
|
173
|
-
"size":
|
|
172
|
+
"digest": "sha256:92079eacad771a4dfb828a187b8d0fa4eb9d914ce232b903d05090ad3519618c",
|
|
173
|
+
"size": 8642,
|
|
174
174
|
"mimeType": "text/markdown",
|
|
175
|
-
"text": "---\nname: headless-step-provision-ecommerce\ndescription: \"Provision a Hostinger Ecommerce backend for the Hostinger Headless flow: ensure the hostinger-ecommerce MCP is connected, create the headless website (frontend host) and a Hostinger Ecommerce STORE with a custom (headless) sales channel, set shipping + payment, and record it all in site.json.\"\n---\n\n# headless-step-provision-ecommerce\n\nProvision a **Hostinger Ecommerce** backend for `<domain>` and prove it is ready. Two distinct things:\n\n1. A **headless website** (`type: \"headless_ecommerce\"`) — this is only the **hosting slot for the\n storefront you'll build**. It installs **no store engine** (no `/cms`, no API on the domain).\n2. A **Hostinger Ecommerce store** — a separate object created through the **Ecommerce API**, with a\n **custom (headless) sales channel**. The storefront reads this store's **public Storefront Core V2\n API**, keyed on the sales channel. This is the real backend.\n\n**Out:** `site.json` (store id, custom sales channel id, URLs).\n\n> ## ⚠️ Prerequisite: the `hostinger-ecommerce` MCP server must be connected\n> Store management is **not** in the `hosting`/`wordpress` MCP groups — it's the separate\n> **`hostinger-ecommerce`** group (`ecommerce_*` ops, OAuth-backed, no API token needed). If those\n> tools aren't available, add the server and **reconnect the session** before continuing:\n> ```bash\n> claude mcp add-json hostinger-ecommerce '{\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"],\"env\":{\"USER_AGENT\":\"extension;claude-code;via-cursor;1.4.2\"}}' --scope user\n> ```\n> (For other tools, enable the **Ecommerce** MCP in the Hostinger Connector.) A newly added MCP server\n> only loads on a session restart/reconnect.\n>\n> **Can't restart your own session? Don't block — drive the server directly.** A coding agent usually\n> can't reconnect itself, so instead of stopping, run the stdio server yourself and speak MCP to it:\n> spawn `npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp` (same `USER_AGENT` as above) and\n> send newline-delimited JSON-RPC over its stdin/stdout — `initialize` → the `notifications/initialized`\n> notification → `tools/list` → `tools/call`, where the tools are `search` / `execute` / `multi-execute`\n> (call ops exactly as in-session: `{ \"name\": \"execute\", \"arguments\": { \"operation\": \"…\", \"params\": {…} } }`).\n> It self-authenticates with the same stored Hostinger OAuth creds as the other connected servers, so\n> there is **no token to handle** (keep any secret off the command line). Your sandbox may ask the user\n> to approve spawning it — ask once, then proceed. Only fall back to asking the user to restart if\n> driving the server directly isn't permitted.\n>\n> Verify the tools work (in-session or via the direct drive) with a search — e.g. \"list stores\" →\n> `ecommerce_stores_list` — before proceeding.\n> The authoritative, always-current build guide is `ecommerce_miscellaneous_custom-storefront-setup-instructions` — read it once per build.\n\nAll website-lifecycle calls go through the `hosting` connector (`execute`); all store calls through the\n`hostinger-ecommerce` MCP.\n\n## Step 1 — The headless website (frontend host)\n\nSame lifecycle as `../wordpress/SKILL.md` Steps 1–2, but `type: \"headless_ecommerce\"`:\n\n1. Detect: `hosting_websites_list` / `hosting_websites_list-setups` (exact-match the domain). `completed`\n → adopt. `running` → poll. `failed` → stop, point at hPanel. If `website_type` is not `headless`\n (plain WordPress, Node.js, …), stop and tell the user — don't overwrite another kind of site.\n2. Create (only if **no** site exists): `hosting_websites_start-setup { order_id, type: \"headless_ecommerce\", domain }`\n — **omit `domain`** for a temporary subdomain: the setup generates one itself (don't
|
|
175
|
+
"text": "---\nname: headless-step-provision-ecommerce\ndescription: \"Provision a Hostinger Ecommerce backend for the Hostinger Headless flow: ensure the hostinger-ecommerce MCP is connected, create the headless website (frontend host) and a Hostinger Ecommerce STORE with a custom (headless) sales channel, set shipping + payment, and record it all in site.json.\"\n---\n\n# headless-step-provision-ecommerce\n\nProvision a **Hostinger Ecommerce** backend for `<domain>` and prove it is ready. Two distinct things:\n\n1. A **headless website** (`type: \"headless_ecommerce\"`) — this is only the **hosting slot for the\n storefront you'll build**. It installs **no store engine** (no `/cms`, no API on the domain).\n2. A **Hostinger Ecommerce store** — a separate object created through the **Ecommerce API**, with a\n **custom (headless) sales channel**. The storefront reads this store's **public Storefront Core V2\n API**, keyed on the sales channel. This is the real backend.\n\n**Out:** `site.json` (store id, custom sales channel id, URLs).\n\n> ## ⚠️ Prerequisite: the `hostinger-ecommerce` MCP server must be connected\n> Store management is **not** in the `hosting`/`wordpress` MCP groups — it's the separate\n> **`hostinger-ecommerce`** group (`ecommerce_*` ops, OAuth-backed, no API token needed). If those\n> tools aren't available, add the server and **reconnect the session** before continuing:\n> ```bash\n> claude mcp add-json hostinger-ecommerce '{\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"],\"env\":{\"USER_AGENT\":\"extension;claude-code;via-cursor;1.4.2\"}}' --scope user\n> ```\n> (For other tools, enable the **Ecommerce** MCP in the Hostinger Connector.) A newly added MCP server\n> only loads on a session restart/reconnect.\n>\n> **Can't restart your own session? Don't block — drive the server directly.** A coding agent usually\n> can't reconnect itself, so instead of stopping, run the stdio server yourself and speak MCP to it:\n> spawn `npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp` (same `USER_AGENT` as above) and\n> send newline-delimited JSON-RPC over its stdin/stdout — `initialize` → the `notifications/initialized`\n> notification → `tools/list` → `tools/call`, where the tools are `search` / `execute` / `multi-execute`\n> (call ops exactly as in-session: `{ \"name\": \"execute\", \"arguments\": { \"operation\": \"…\", \"params\": {…} } }`).\n> It self-authenticates with the same stored Hostinger OAuth creds as the other connected servers, so\n> there is **no token to handle** (keep any secret off the command line). Your sandbox may ask the user\n> to approve spawning it — ask once, then proceed. Only fall back to asking the user to restart if\n> driving the server directly isn't permitted.\n>\n> Verify the tools work (in-session or via the direct drive) with a search — e.g. \"list stores\" →\n> `ecommerce_stores_list` — before proceeding.\n> The authoritative, always-current build guide is `ecommerce_miscellaneous_custom-storefront-setup-instructions` — read it once per build.\n\nAll website-lifecycle calls go through the `hosting` connector (`execute`); all store calls through the\n`hostinger-ecommerce` MCP.\n\n## Step 1 — The headless website (frontend host)\n\nSame lifecycle as `../wordpress/SKILL.md` Steps 1–2, but `type: \"headless_ecommerce\"`:\n\n1. Detect: `hosting_websites_list` / `hosting_websites_list-setups` (exact-match the domain). `completed`\n → adopt. `running` → poll. `failed` → stop, point at hPanel. If `website_type` is not `headless`\n (plain WordPress, Node.js, …), stop and tell the user — don't overwrite another kind of site.\n2. Create (only if **no** site exists): `hosting_websites_start-setup { order_id, type: \"headless_ecommerce\", domain }`\n — **omit `domain`** for a temporary subdomain: the setup generates one itself and returns it (don't\n call a separate subdomain op first — there's no need in this flow; the API would also accept one you\n generated via `hosting_domains_generate-free-subdomain`, valid 24 h from the client that made it). The\n response carries the generated domain; if it differs from the workspace dir, move `~/.hostinger/<old>/` → `<new>/` and\n update `brief.md`. Poll `list-setups` to `completed`. `website_type` will be `headless`.\n\n> **Onboarding handoff — an empty headless shell needs no new website.** If the domain already has an\n> empty headless shell (`website_type: headless`, setup `type: headless_empty`, nothing installed —\n> typically created in onboarding), **adopt it as the storefront host and skip the create.** A\n> `headless_ecommerce` website installs no store engine either, so an empty headless shell *is* the host\n> you need — there is no backend to \"swap\" on the website side. Record its `username`/`order_id` and go\n> straight to Step 2 (store + custom sales channel). Never call `start-setup` on a domain that already\n> has a site.\n\n> Don't expect `/cms` or any API on this domain — it serves the storefront you build (Step 6) and\n> deploy (Step 8).\n\n## Step 2 — Resolve or create the store + custom sales channel\n\n`ecommerce_stores_list` → reuse an existing store if this build already made one; otherwise create:\n\n```json\n{ \"operation\": \"ecommerce_stores_create\",\n \"params\": { \"name\": \"<store name>\", \"country_code\": \"US\", \"company_email\": \"<email>\",\n \"company_name\": \"<store name>\", \"language\": \"en\",\n \"sales_channel\": { \"type\": \"custom\", \"external_id\": \"<slug>-headless\" } } }\n```\n\nThis returns `store.id` (`store_*`) **and** a custom `sales_channel.id` (`scha_*`) in one call — the\ncustom channel is what the headless storefront is keyed on. (You can also add one later with\n`ecommerce_sales-channels_create { store_id, type:\"custom\", name }`.) Currency comes from\n`country_code` (`default_currency_code`); all prices are **minor units** (cents).\n\n## Step 3 — Store readiness: shipping + payment\n\nA store needs ≥1 shipping zone and ≥1 payment method to check out.\n\n- **Shipping:** `ecommerce_shipping_set-store { store_id, price }` (minor units; 0 = free).\n- **Payment:** `ecommerce_payments_enable-manual-method { store_id, title }`.\n\n> **Known gate — free accounts have no payment method, and therefore no checkout.** On a **free\n> account** (and any account/store without a paid Hostinger Ecommerce plan), `enable-manual-method`\n> returns **HTTP 402**, and checkout later fails with `400 payment_provider_not_configured`. This is\n> **expected, not an error** — payment, and therefore checkout, is simply unavailable on the plan. The\n> catalog and the storefront still work fully. **Never block, stop, loop or retry the build on this:**\n> - Record `payment_enabled: false` in `site.json` (set `shipping_enabled` as usual).\n> - Tell the user in one line that checkout needs a paid Ecommerce plan, and carry straight on.\n> - Treat the 402 as a normal outcome — don't surface it as a failure or re-attempt the call.\n> - Never ask the user for payment credentials.\n>\n> Every downstream stage reads `site.json → ecommerce.payment_enabled` and, when it is `false`, skips\n> all checkout wiring and verification (see content-model Step 5, frontend, deploy). A store with\n> `payment_enabled: false` is a **complete, valid build** — a browsable catalog without checkout.\n\n## Step 4 — Verify\n\n- `ecommerce_stores_metadata { store_id }` → `has_shipping` true; `has_payment_methods` (may be false,\n see the gate); read `default_currency` (`decimal_digits`, `template`) here, don't guess.\n- Public Storefront V2 answers for the channel (no auth):\n `GET https://api-ecommerce.hostinger.com/v2/channels/<scha_*>/products?limit=100` → 200 (empty until\n content-model seeds products — fine here).\n- `GET https://<domain>/` returns the Hostinger default page until the storefront is deployed — expected.\n\n## Step 5 — Record `site.json`\n\n```json\n{\n \"backend\": \"ecommerce\",\n \"website_type\": \"headless\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"ecommerce\": {\n \"store_id\": \"store_…\",\n \"sales_channel\": { \"id\": \"scha_…\", \"type\": \"custom\", \"external_id\": \"…-headless\" },\n \"currency\": \"usd\",\n \"payment_enabled\": false,\n \"shipping_enabled\": true\n },\n \"urls\": {\n \"site\": \"https://<domain>\",\n \"storefront_api\": \"https://api-ecommerce.hostinger.com/v2/channels/<scha_*>\",\n \"store_admin\": \"https://ecommerce.hostinger.com/store/<store_*>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo secrets in `site.json`. After deploy, link the channel to the live domain with\n`ecommerce_sales-channels_update { store_id, sales_channel_id, url }` (done in the deploy stage).\n"
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
"uri": "skill://hostinger-headless/steps/03-provision/webapp/SKILL.md",
|
|
179
|
-
"digest": "sha256:
|
|
180
|
-
"size":
|
|
179
|
+
"digest": "sha256:f93a16580c8aaa4b8122bf38450cbc763abf9e506065b90de91b619b6edc8a2d",
|
|
180
|
+
"size": 9262,
|
|
181
181
|
"mimeType": "text/markdown",
|
|
182
|
-
"text": "---\nname: headless-step-provision-webapp\ndescription: \"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.\"\n---\n\n# headless-step-provision-webapp\n\nProvision a bare **Node.js app host** for `<domain>` and prove it is ready. This is the backend for\n**any webapp, SPA or app** that brings its own code (and, if it needs one, its own database) — there is\nno CMS and no store engine here. The site is an **empty website**: just the hosting slot that the\nfrontend step builds into and the deploy step ships to as a Node.js application. **Out:** `site.json`.\n\n> **Not WordPress, not Ecommerce.** Never provision `headless_wordpress` / `headless_ecommerce` for a\n> webapp, and never install WordPress at `/cms`. A webapp owns its own routes, data and rendering.\n\nAll website/database operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Steps 1–2 up to `hosting_websites_start-setup` are the\n> **fire** half: once the setup is queued, record `provision.in_progress` and return to do prepare work\n> (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2, plus Steps 3–5, are the\n> **join** half — run them when an instance-barrier step needs the backend (or immediately, if the\n> agent can't interleave). An already-`completed` site detected in Step 1 means provision is done:\n> record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. `hosting_websites_list-setups { \"domain\": \"<domain>\" }` — exact-match the row:\n - `completed` → already provisioned → skip to Step 3 (adopt it).\n - `running` → skip to Step 2 (poll).\n - `failed` → tell the user and point them at hPanel; do **not** call start-setup again on your own.\n3. If the site exists but is a different kind (`website_type` is `headless`, or a WordPress/store site),\n stop and tell the user — this domain already hosts something else.\n\n## Step 2 — Create (only when nothing exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup as an **empty website** — **omit `type`** (no WordPress, no store):\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself. Don't call a
|
|
182
|
+
"text": "---\nname: headless-step-provision-webapp\ndescription: \"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.\"\n---\n\n# headless-step-provision-webapp\n\nProvision a bare **Node.js app host** for `<domain>` and prove it is ready. This is the backend for\n**any webapp, SPA or app** that brings its own code (and, if it needs one, its own database) — there is\nno CMS and no store engine here. The site is an **empty website**: just the hosting slot that the\nfrontend step builds into and the deploy step ships to as a Node.js application. **Out:** `site.json`.\n\n> **Not WordPress, not Ecommerce.** Never provision `headless_wordpress` / `headless_ecommerce` for a\n> webapp, and never install WordPress at `/cms`. A webapp owns its own routes, data and rendering.\n\nAll website/database operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Steps 1–2 up to `hosting_websites_start-setup` are the\n> **fire** half: once the setup is queued, record `provision.in_progress` and return to do prepare work\n> (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2, plus Steps 3–5, are the\n> **join** half — run them when an instance-barrier step needs the backend (or immediately, if the\n> agent can't interleave). An already-`completed` site detected in Step 1 means provision is done:\n> record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. `hosting_websites_list-setups { \"domain\": \"<domain>\" }` — exact-match the row:\n - `completed` → already provisioned → skip to Step 3 (adopt it).\n - `running` → skip to Step 2 (poll).\n - `failed` → tell the user and point them at hPanel; do **not** call start-setup again on your own.\n3. If the site exists but is a different kind (`website_type` is `headless`, or a WordPress/store site),\n stop and tell the user — this domain already hosts something else.\n\n## Step 2 — Create (only when nothing exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup as an **empty website** — **omit `type`** (no WordPress, no store):\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself and returns it in the response. Don't call a\n separate subdomain op to make one first: there's no need in this flow. (The API would also accept a\n subdomain you generated via `hosting_domains_generate-free-subdomain`, valid for 24 h from the client\n that made it — this flow just doesn't use that.) The response then carries the generated domain: if it\n differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,\n update the domain in `brief.md`, and tell the user the new domain.\n3. Poll `hosting_websites_list-setups { \"domain\": \"<domain>\" }` every ~15 s until `completed`\n (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.\n\n## Step 3 — Database (only if the app needs one)\n\nSkip this step entirely for an app with no database (static SPA, an app that talks to an external API).\nThis is **join-half** work — it needs the live website, so run it once `provision.status == \"completed\"`.\n\n**Preferred — let the platform create the database and inject the credentials:**\n`hosting_databases_setup-website { username, domain }` creates a MySQL database, **generates the\npassword, writes `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASSWORD` / `DATABASE_URL` into the\nwebsite's environment variables, and restarts the app — in one call.** The password is never returned,\nso there is **nothing to persist and nothing to lose across sessions** — the app reads it from the\nenvironment. (It 422s if any of those env vars already exist; clear them first with\n`hosting_nodejs_replace-environment-variables`. Frameworks that bake vars into the build — Next.js,\n`NEXT_PUBLIC_*` — only pick up the values on the deploy build.)\n\n> **⚠️ A later `hosting_nodejs_replace-environment-variables` wipes these credentials.** That call is a\n> **full replace**, and when you list the current vars first the `DB_PASSWORD` (and `DATABASE_URL`) read\n> back **masked** (`********`) — the generated password is unknown, so you cannot resend it and the DB\n> env is lost. One rule, pick one:\n> - **(a) Set every other app env var first, and call `setup-website` last** — then nothing has to\n> replace the env again.\n> - **(b) If a later replace is unavoidable**, reset the password with\n> `hosting_databases_change-password { username, name, password }` and resend **all** of `DB_HOST`,\n> `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` with the new password in that same\n> full replace.\n\n**Manual alternative** — only when you need a specific name/user or finer control:\n\n1. `hosting_databases_create` with a name, a user, and a password you generate — name and user are\n prefixed with the account username automatically. Read the full, prefixed `name`/`user` back from\n `hosting_databases_list`.\n2. **Which host the app connects to:** the app runs **on the website**, so Node.js connects to\n **`127.0.0.1:3306`** — never `localhost` (it can resolve to IPv6 `::1`, and not every database user\n has that grant), and never the external `host` from `hosting_databases_list` (`srvNNNN.hstgr.io`,\n which has no grant for connections from the server itself).\n3. **Set the env vars now, while you still hold the generated password.** The password is **not persisted\n anywhere** (never in `site.json`), so set `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_NAME`, `DB_USER`,\n `DB_PASSWORD`, plus `DATABASE_URL=mysql://USER:PASSWORD@127.0.0.1:3306/NAME` (URL-encode USER/PASSWORD)\n right here with `hosting_nodejs_replace-environment-variables` (a **full replace** — list current keys\n first; values come back masked; send the complete set with real values). Saving restarts the process;\n build-time vars need a fresh deploy (`hosting_deploy`) afterwards. **Don't defer this to deploy:** on\n a **cross-session resume** the generated password is gone, so you must **reset it** with\n `hosting_databases_change-password { username, name, password }` and set the env vars from the new one.\n4. **Never** put the password in source, a committed `.env`, client-side JavaScript, or a chat reply;\n never print it once stored.\n\n## Step 4 — Verify\n\n| Check | How | Expect |\n|---|---|---|\n| Website exists | `hosting_websites_list` exact row | the row is present, `username` known |\n| Setup finished | `hosting_websites_list-setups` exact row | `status: completed` |\n| Database reachable *(only if created)* | `hosting_databases_list { username }` | the prefixed db + user are listed |\n\n`GET https://<domain>/` returns the Hostinger default page (or 403) until the frontend is deployed —\nexpected, not a failure. There is **no `/cms` and no API** on this domain; the app itself serves every\nroute once it is deployed (Step 8).\n\nIf a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.\n\n## Step 5 — Record `site.json`\n\n```json\n{\n \"backend\": \"webapp\",\n \"website_type\": \"empty\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"runtime\": \"nodejs\",\n \"database\": {\n \"enabled\": false,\n \"engine\": \"mysql\",\n \"name\": \"u…_appdb\",\n \"user\": \"u…_app\",\n \"host\": \"127.0.0.1\",\n \"port\": 3306\n },\n \"urls\": {\n \"site\": \"https://<domain>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo passwords or tokens in `site.json` — only the non-secret connection shape (host/port/name/user). Set\n`\"database\": { \"enabled\": false }` (and drop the rest) when the app needs no database.\n\n## Deploy & content model (what happens next)\n\n- **Content model is skipped for a webapp** — there is no CMS or store to seed; the app owns its own\n data. The content-model router (`../../04-content-model/SKILL.md`) passes straight through to the\n frontend step for this backend.\n- **Deploy** is a standard Node.js application deploy, handled by `../../08-deploy/SKILL.md`: it archives\n the **source** (not the build output; exclude `node_modules/`, `.next/`, `dist/`, build output and\n `.git/`), then deploys with `hosting_deploy` (`type: \"nodejs\"`, upload + server-side `npm install` +\n build, one shot) passing the framework's `appType` + `outputDirectory` inline, and polls\n `hosting_get-deployment` until the build completes. Any supported `appType` works (Next.js, Nuxt,\n Express, NestJS, Fastify, Vite/React/Vue SPA, …). There is no `/cms` to preserve here, so the whole\n root is the app.\n"
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"uri": "skill://hostinger-headless/steps/03-provision/wordpress/SKILL.md",
|
|
186
|
-
"digest": "sha256:
|
|
187
|
-
"size":
|
|
186
|
+
"digest": "sha256:4bfd1496aaf6b3507cd7cd5120b0c50372a0bf336afc9a6f445325335aa4cbcf",
|
|
187
|
+
"size": 20310,
|
|
188
188
|
"mimeType": "text/markdown",
|
|
189
|
-
"text": "---\nname: headless-step-provision-wordpress\ndescription: \"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.\"\n---\n\n# headless-step-provision-wordpress\n\nCreate the headless WordPress backend for `<domain>` and prove it is ready. One platform call lays\nevery headless site out identically: WordPress in `public_html/cms`, Hostinger AI Assistant + MCP\nadapter, site marked `headless`. **Out:** `site.json`.\n\n> **WordPress + WooCommerce?** This same recipe serves it. When the brief's **Suggested backend** is\n> `**WordPress + WooCommerce**`, provision the WordPress site exactly as below, then run the **optional\n> WooCommerce tail in Step 5** (install + activate the `woocommerce` plugin and verify the store\n> endpoints; the catalog is seeded in content-model via the per-site WP MCP).\n> WooCommerce is a plugin on this WordPress install, **not** a separate backend: `site.json → backend`\n> stays `\"wordpress\"` and the store is recorded under `site.json → woocommerce`. For a plain WordPress\n> brief, skip Step 5 entirely.\n\nAll operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Step 1, plus the queuing call — `hosting_websites_start-setup`\n> for a fresh site (Step 2) **or** `wordpress_installations_install` to swap an empty headless shell\n> (Step 2b) — are the **fire** half: once the job is queued, record `provision.in_progress` and return\n> to do prepare work (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2 / 2b,\n> plus Steps 3–6, are the **join** half — run them when an instance-barrier step needs the backend (or\n> immediately, if the agent can't interleave). An already-`completed` site with WordPress at `/cms`\n> detected in Step 1 means provision is already done: record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. **No exact row** → nothing exists on this domain → **create fresh** (Step 2).\n3. **A row exists.** If `website_type` is **not** `headless`, stop and tell the user — this domain\n already hosts a different kind of site (plain WordPress, Node.js, builder, …); never overwrite it.\n If it **is** `headless`, work out whether WordPress is already installed or the shell is still empty\n (see the router's *Onboarding handoff*): check `hosting_websites_list-setups { \"domain\": \"<domain>\" }`\n (exact row) and `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row):\n - Setup `running` → provisioning is underway → skip to Step 2's poll.\n - Setup `failed` → tell the user and point them at hPanel; do **not** call start-setup again yourself.\n - A WordPress install with `directory: \"cms\"` already present (or setup `type: headless_wordpress`)\n → **already provisioned** → skip to Step 3 (adopt it).\n - A `headless` site with **no** `/cms` install (an **empty headless shell** — setup\n `type: headless_empty`, typically from onboarding) → **swap it to WordPress** (Step 2b). Do **not**\n call `start-setup` again — the website already exists; you only install WordPress into it.\n\n## Step 2 — Create a fresh headless WordPress site (only when no site exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup:\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"type\": \"headless_wordpress\", \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself. Don't call a separate subdomain op to make one\n first: there's no need, and passing a generated free subdomain back as `domain` fails with\n `422 … Free subdomains cannot be provided`. The response then carries the generated domain:\n if it differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,\n update the domain in `brief.md`, and tell the user the new domain.\n Admin credentials are generated by the platform; never ask the user for a password.\n3. Poll `hosting_websites_list-setups { \"domain\": \"<domain>\" }` every ~15 s until `completed`\n (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.\n\n## Step 2b — Swap an empty headless shell to WordPress (only when onboarding already made the shell)\n\nThe website already exists as an empty `headless` shell, so there is **no `start-setup`** here — you\ninstall WordPress straight into its `/cms` directory. This is the onboarding handoff path.\n\n1. Use the shell's `username` and `order_id` from Step 1's row — no new order pick, no new website.\n2. Install WordPress into `/cms`, flagged headless:\n\n ```json\n { \"operation\": \"wordpress_installations_install\",\n \"params\": { \"username\": \"<username>\", \"domain\": \"<domain>\", \"type\": \"headless\",\n \"site_title\": \"<site title from brief.md>\",\n \"credentials\": { \"login\": \"<admin login>\", \"email\": \"<account email>\",\n \"password\": \"<strong generated password>\" } } }\n ```\n\n - `type: \"headless\"` installs into `/cms` with the Hostinger headless MU plugin and marks the site\n headless-WordPress; **`directory` is not allowed with it** (it is always `cms`). The target must\n already be `headless` — the headless install rewrites the root `.htaccess`, so the API rejects it\n with `422` on any non-headless site (which is why Step 1 confirms `website_type: headless` first).\n - **You generate the admin password** (strong, 8–50 chars, mixed case + a digit) — never ask the user\n for one and never print it. The user signs in later through a one-click login link (Step 6), so the\n password only has to exist, not be remembered.\n - A new database is created for the install unless you pass `database.name` of an existing one; the\n generated database counts against the plan's limit.\n3. The call is **asynchronous** — a success response only means the install job was **queued**. This is\n the *fire*: record `provision.in_progress` and return to prepare work. Poll\n `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row) at the join; when a\n row with `directory: \"cms\"` and `is_valid: true` appears, WordPress is ready. Allow 1–2 minutes;\n `wordpress_installations_list` can lag a few seconds behind the job finishing and return `[]` — retry\n briefly before failing.\n\n## Step 3 — Verify (all must pass)\n\n| Check | How | Expect |\n|---|---|---|\n| Site is headless | `hosting_websites_list` exact row | `website_type: \"headless\"` |\n| WordPress at `/cms` | `wordpress_installations_list { \"username\": \"<username>\", \"ownership\": \"all\" }`, exact-match `domain` | row with `directory: \"cms\"`, `is_valid: true`; note its `id` |\n| REST API answers | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200, JSON array |\n| AI Assistant + MCP loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `hostinger-ai-assistant/v1` and `mcp` |\n| JWT mintable | `wordpress_installations_jwt-token { username, software: <id> }` | returns `token`, `mcp_url` |\n\nNotes:\n\n- `wordpress_installations_list` can lag a few seconds behind `completed` and return `[]` — retry\n briefly before failing. Its `domain` filter is a substring match, so exact-match the row.\n- The install record's `url` may say `http://`; always use `https://` URLs.\n- `GET https://<domain>/` returns 403 until the frontend is deployed — expected, not a failure.\n- **Never print, log or store the JWT.** It is minted on demand (24 h lifetime) by whichever stage\n needs the per-site WP MCP; only `mcp_url` is recorded.\n\nIf a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.\n\n## Step 4 — SEO plugin (Yoast) *(every WordPress site — on by default)*\n\nEvery headless WordPress site gets **Yoast SEO** (`wordpress-seo`) installed and activated by default,\nso per-page title/description/Open Graph/JSON-LD come from a maintained plugin's `yoast_head_json`\nrather than being hand-generated from each item's content by the frontend. This is **join-half** work\n(needs the live `/cms` install), so run it once `provision.status == \"completed\"`, the same way as the\nWooCommerce tail. Tell the user in one line that SEO (Yoast) is being set up; if they explicitly don't\nwant it, skip this step and record `seo.enabled: false` — but the default is to install it.\n\n1. **Install + activate Yoast** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"wordpress-seo\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `wordpress-seo` shows\n `active` (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"wordpress-seo\" }`.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` → `namespaces` once more, and that a sample post/page\n response carries `yoast_head_json`. The per-item Yoast meta (focus title/description per post) is set\n later in the **content-model push** — provision only installs + activates the plugin here. Don't fail\n provision over SEO: if the install can't be confirmed after one retry, record `seo.enabled: false`,\n tell the user, and let the frontend fall back to generating tags from content.\n\n## Step 5 — WooCommerce store *(only for a `WordPress + WooCommerce` brief — skip otherwise)*\n\nRun this only when the brief's **Suggested backend** is `**WordPress + WooCommerce**`. It turns the\nheadless WordPress site into a headless **store**: WooCommerce is the `woocommerce` plugin on this same\n`/cms` install — **not a separate backend**. This is **join-half** work (needs the live instance), so run\nit once `provision.status == \"completed\"`. The authoritative data-layer reference is\n[`../../04-content-model/wordpress/woocommerce.md`](../../04-content-model/wordpress/woocommerce.md).\n\n> **No custom plugin — the catalog is driven through the per-site WP MCP.** A custom plugin can't be\n> installed into a headless `/cms` from a coding agent, so there is **no seed/config plugin and no REST\n> consumer key in this flow.** Instead, the Hostinger AI Assistant (the per-site WP MCP, same JWT session\n> content-model uses) exposes **native WooCommerce abilities** — `wc-products-*`, `wc-product-categories-*`,\n> `wc-coupons-*`, `wc-orders-*`, `media-upload-from-url`, `wp-settings-*`. Provision just **installs\n> WooCommerce and verifies the store endpoints**; the catalog + coupons are seeded in the content-model\n> push through those abilities (see the cookbook).\n\n1. **Install + activate WooCommerce** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"woocommerce\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `woocommerce` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"woocommerce\" }`.\n - Confirm with `wordpress_plugins_check-if-woo-commerce-is-installed { domain: \"<domain>\" }`. Allow a\n moment for WooCommerce's installer to create its tables and the Shop/Cart/Checkout pages.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded** (seen on both `plugins_install` and `jwt-token`). Treat a 500 as \"verify, don't assume\":\n poll `list-installed` / re-check before retrying, and never fire a second install blindly.\n2. **Verify the store endpoints (public, no auth):**\n\n | Check | How | Expect |\n |---|---|---|\n | WooCommerce active | `wordpress_plugins_check-if-woo-commerce-is-installed { domain }` | installed + active |\n | Store API answers | `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` | 200 (empty until content-model seeds — fine) |\n | Both namespaces loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `wc/store/v1` **and** `wc/v3` |\n\n Pretty permalinks are on by default on a headless install, so the Store API resolves immediately; if a\n store route ever 404s, the permalink structure is the thing to check.\n\n> **Checkout needs owner setup — treat it like the Ecommerce payment gate.** A fresh WooCommerce install\n> has **no payment gateway and no shipping zone enabled**, and that configuration is **not reachable from\n> the headless tooling** (there is no payment/shipping MCP ability, and `wp-settings-update` only writes\n> standard WP settings). So **checkout is unavailable until the owner enables a payment method + shipping\n> in wp-admin** (hand them the one-click login in Step 6). Record `woocommerce.payment_enabled: false`\n> accordingly. This is **not a build failure**: the catalog, product pages, client-side cart and coupon\n> application all work headlessly — the downstream frontend renders a graceful disabled-checkout state and\n> never calls `/checkout` while `payment_enabled` is `false` (see content-model + frontend). Never ask the\n> user for payment credentials. Store currency and address (`woocommerce_*` options) likewise aren't\n> settable headlessly — they follow the install default and the owner can change them in wp-admin.\n\n## Step 5b — Contact form (Contact Form 7) *(only when the brief's Must-haves include a contact form — skip otherwise)*\n\nRun this only when the brief's **Must-haves** include a contact form. It installs **Contact Form 7**\n(`contact-form-7`) on this same `/cms` install, so the frontend can render a contact page whose form\nemails the site owner through WordPress's built-in `wp_mail()` — no third-party service and no API keys.\nLike Yoast/WooCommerce this is **join-half** work: run it once `provision.status == \"completed\"`. The form\nitself (id + fields + recipient) — and a tiny **spam-guard plugin** that makes a honeypot work headlessly —\nare resolved/deployed in the content-model push — see\n[`../../04-content-model/wordpress/contact-form.md`](../../04-content-model/wordpress/contact-form.md).\n\n> **Why CF7 and not a bespoke mailer.** A contact form is maintained, spam-hardened functionality worth\n> reusing rather than reinventing (you *can* deploy a custom plugin to a headless `/cms` — see the\n> content-model cookbook — but you needn't write your own mailer). CF7 is a wordpress.org-slug plugin,\n> installs through the same management MCP as Yoast, and ships a **public, unauthenticated** submit\n> endpoint (`POST /cms/wp-json/contact-form-7/v1/contact-forms/<id>/feedback`). The frontend and `/cms`\n> are same-origin, so the browser posts to it directly — no proxy, no CORS, no secret on the client.\n\n1. **Install + activate CF7** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"contact-form-7\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `contact-form-7` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"contact-form-7\" }`.\n - **Don't install the `contact-form-7-honeypot` add-on.** It only acts when a `[honeypot]` form-tag is\n present in the form — a wp-admin edit the headless agent can't make — so on the default form it does\n nothing. Headless spam protection instead comes from a small **spam-guard plugin deployed in the\n content-model push** (it hooks `wpcf7_spam`); see the cookbook.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` lists the **`contact-form-7/v1`** namespace, then\n record `contact_form.enabled: true` in `site.json` (Step 6). If the install can't be confirmed after\n one retry, record `contact_form.enabled: false`, tell the user in one line, and let the frontend\n render the contact page without a form (graceful — not a build failure). Don't fail provision over it.\n\nThe owner's email is **CF7's default recipient** (`[_site_admin_email]`, the admin address from\ninstall) — fine out of the box, so \"does Hostinger set up the email?\" is **yes** for basic delivery via\n`wp_mail`. Deliverability (SPF/DKIM) is most reliable when that address is a mailbox **on the same\ndomain**; the content-model push keeps the default recipient and validate sends a test submission to\nconfirm mail actually leaves. Never ask the user for mail credentials.\n\n## Step 6 — Record `site.json`\n\n```json\n{\n \"backend\": \"wordpress\",\n \"website_type\": \"headless\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"wordpress\": {\n \"install_id\": \"30…\",\n \"directory\": \"cms\",\n \"admin_login\": \"…\"\n },\n \"seo\": {\n \"plugin\": \"wordpress-seo\",\n \"enabled\": true\n },\n \"urls\": {\n \"site\": \"https://<domain>\",\n \"cms\": \"https://<domain>/cms\",\n \"wp_admin\": \"https://<domain>/cms/wp-admin\",\n \"rest_api\": \"https://<domain>/cms/wp-json/wp/v2\",\n \"wp_mcp\": \"<mcp_url from the JWT call>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo passwords or tokens in `site.json`. When the user needs wp-admin, mint a one-click link with\n`wordpress_login_create-links { username, software: <install_id> }` — never hand over a password.\n\n**For a `WordPress + WooCommerce` brief, also add a `woocommerce` block and the store URLs** (the\n`backend` field still stays `\"wordpress\"`):\n\n```json\n{\n \"woocommerce\": {\n \"enabled\": true,\n \"seeded_via\": \"per-site-wp-mcp\",\n \"currency\": \"<currency_code from GET {store_api}/products or the install default>\",\n \"payment_enabled\": false,\n \"checkout_setup\": \"owner enables a payment method + shipping in wp-admin\"\n },\n \"urls\": {\n \"store_api\": \"https://<domain>/cms/wp-json/wc/store/v1\",\n \"wc_rest_api\": \"https://<domain>/cms/wp-json/wc/v3\"\n }\n}\n```\n\n**For a brief with a contact form (CF7 installed in Step 5b), also add a `contact_form` block.**\nProvision records only `enabled` + `plugin` here; the content-model push fills `form_id`,\n`feedback_url`, `fields` and `honeypot_field` once it resolves the form and deploys the spam guard:\n\n```json\n{\n \"contact_form\": {\n \"enabled\": true,\n \"plugin\": \"contact-form-7\",\n \"form_id\": null,\n \"feedback_url\": null,\n \"fields\": [],\n \"honeypot_field\": null,\n \"recipient\": \"site-admin-email\"\n }\n}\n```\n\n`form_id` / `feedback_url` / `fields` / `honeypot_field` stay `null` / `[]` until content-model resolves the form (the\nsubmit URL is `https://<domain>/cms/wp-json/contact-form-7/v1/contact-forms/<form_id>/feedback`).\n`recipient` is informational — CF7's `To` is `[_site_admin_email]`; no address, token or secret is\nstored in `site.json`.\n\n`rest_api` stays `wp/v2` (the WordPress content contract, for the blog/pages on the same site);\n`store_api` is the public storefront surface (browse/cart/coupons/checkout, no auth). `wc_rest_api`\n(`wc/v3`) is recorded only as the surface the **owner's own** integrations use with a consumer key they\ngenerate in wp-admin — **this flow never creates or stores a key** (the catalog is seeded through the\nper-site WP MCP's `wc-*` abilities; the storefront uses the public Store API). `payment_enabled` starts\n`false` and flips to `true` only once a gateway + shipping are configured — no secrets in `site.json`.\n"
|
|
189
|
+
"text": "---\nname: headless-step-provision-wordpress\ndescription: \"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.\"\n---\n\n# headless-step-provision-wordpress\n\nCreate the headless WordPress backend for `<domain>` and prove it is ready. One platform call lays\nevery headless site out identically: WordPress in `public_html/cms`, Hostinger AI Assistant + MCP\nadapter, site marked `headless`. **Out:** `site.json`.\n\n> **WordPress + WooCommerce?** This same recipe serves it. When the brief's **Suggested backend** is\n> `**WordPress + WooCommerce**`, provision the WordPress site exactly as below, then run the **optional\n> WooCommerce tail in Step 5** (install + activate the `woocommerce` plugin and verify the store\n> endpoints; the catalog is seeded in content-model via the per-site WP MCP).\n> WooCommerce is a plugin on this WordPress install, **not** a separate backend: `site.json → backend`\n> stays `\"wordpress\"` and the store is recorded under `site.json → woocommerce`. For a plain WordPress\n> brief, skip Step 5 entirely.\n\nAll operations below go through the Hostinger connector (`execute`).\n\n> **Fire vs join (background provision).** Step 1, plus the queuing call — `hosting_websites_start-setup`\n> for a fresh site (Step 2) **or** `wordpress_installations_install` to swap an empty headless shell\n> (Step 2b) — are the **fire** half: once the job is queued, record `provision.in_progress` and return\n> to do prepare work (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2 / 2b,\n> plus Steps 3–6, are the **join** half — run them when an instance-barrier step needs the backend (or\n> immediately, if the agent can't interleave). An already-`completed` site with WordPress at `/cms`\n> detected in Step 1 means provision is already done: record `provision.completed` and adopt it.\n\n## Step 1 — Detect before you create (never provision twice)\n\n1. `hosting_websites_list { \"domain\": \"<domain>\" }` — substring filter, so **exact-match** the row's\n `domain`. If a row exists, note `username`, `order_id`, `website_type`.\n2. **No exact row** → nothing exists on this domain → **create fresh** (Step 2).\n3. **A row exists.** If `website_type` is **not** `headless`, stop and tell the user — this domain\n already hosts a different kind of site (plain WordPress, Node.js, builder, …); never overwrite it.\n If it **is** `headless`, work out whether WordPress is already installed or the shell is still empty\n (see the router's *Onboarding handoff*): check `hosting_websites_list-setups { \"domain\": \"<domain>\" }`\n (exact row) and `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row):\n - Setup `running` → provisioning is underway → skip to Step 2's poll.\n - Setup `failed` → tell the user and point them at hPanel; do **not** call start-setup again yourself.\n - A WordPress install with `directory: \"cms\"` already present (or setup `type: headless_wordpress`)\n → **already provisioned** → skip to Step 3 (adopt it).\n - A `headless` site with **no** `/cms` install (an **empty headless shell** — setup\n `type: headless_empty`, typically from onboarding) → **swap it to WordPress** (Step 2b). Do **not**\n call `start-setup` again — the website already exists; you only install WordPress into it.\n\n## Step 2 — Create a fresh headless WordPress site (only when no site exists)\n\n1. Pick the plan: `hosting_orders_list { \"statuses\": [\"active\"] }`. One active order → use it; several\n → ask the user which.\n2. Start the setup:\n\n ```json\n { \"operation\": \"hosting_websites_start-setup\",\n \"params\": { \"order_id\": <order_id>, \"type\": \"headless_wordpress\", \"domain\": \"<domain>\" } }\n ```\n\n `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,\n **omit `domain`** — the setup generates one itself and returns it in the response. Don't call a\n separate subdomain op to make one first: there's no need in this flow. (The API would also accept a\n subdomain you generated via `hosting_domains_generate-free-subdomain`, valid for 24 h from the client\n that made it — this flow just doesn't use that.) The response then carries the generated domain:\n if it differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,\n update the domain in `brief.md`, and tell the user the new domain.\n Admin credentials are generated by the platform; never ask the user for a password.\n3. Poll `hosting_websites_list-setups { \"domain\": \"<domain>\" }` every ~15 s until `completed`\n (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.\n\n## Step 2b — Swap an empty headless shell to WordPress (only when onboarding already made the shell)\n\nThe website already exists as an empty `headless` shell, so there is **no `start-setup`** here — you\ninstall WordPress straight into its `/cms` directory. This is the onboarding handoff path.\n\n1. Use the shell's `username` and `order_id` from Step 1's row — no new order pick, no new website.\n2. Install WordPress into `/cms`, flagged headless:\n\n ```json\n { \"operation\": \"wordpress_installations_install\",\n \"params\": { \"username\": \"<username>\", \"domain\": \"<domain>\", \"type\": \"headless\",\n \"site_title\": \"<site title from brief.md>\",\n \"credentials\": { \"login\": \"<admin login>\", \"email\": \"<account email>\",\n \"password\": \"<strong generated password>\" } } }\n ```\n\n - `type: \"headless\"` installs into `/cms` with the Hostinger headless MU plugin and marks the site\n headless-WordPress; **`directory` is not allowed with it** (it is always `cms`). The target must\n already be `headless` — the headless install rewrites the root `.htaccess`, so the API rejects it\n with `422` on any non-headless site (which is why Step 1 confirms `website_type: headless` first).\n - **You generate the admin password** (strong, 8–50 chars, mixed case + a digit) — never ask the user\n for one and never print it. The user signs in later through a one-click login link (Step 6), so the\n password only has to exist, not be remembered.\n - A new database is created for the install unless you pass `database.name` of an existing one; the\n generated database counts against the plan's limit.\n3. The call is **asynchronous** — a success response only means the install job was **queued**. This is\n the *fire*: record `provision.in_progress` and return to prepare work. Poll\n `wordpress_installations_list { username, domain, ownership: \"all\" }` (exact row) at the join; when a\n row with `directory: \"cms\"` and `is_valid: true` appears, WordPress is ready. Allow 1–2 minutes;\n `wordpress_installations_list` can lag a few seconds behind the job finishing and return `[]` — retry\n briefly before failing.\n\n## Step 3 — Verify (all must pass)\n\n| Check | How | Expect |\n|---|---|---|\n| Site is headless | `hosting_websites_list` exact row | `website_type: \"headless\"` |\n| WordPress at `/cms` | `wordpress_installations_list { \"username\": \"<username>\", \"ownership\": \"all\" }`, exact-match `domain` | row with `directory: \"cms\"`, `is_valid: true`; note its `id` |\n| REST API answers | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200, JSON array |\n| AI Assistant + MCP loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `hostinger-ai-assistant/v1` and `mcp` |\n| JWT mintable | `wordpress_installations_jwt-token { username, software: <id> }` | returns `token`, `mcp_url` |\n\nNotes:\n\n- `wordpress_installations_list` can lag a few seconds behind `completed` and return `[]` — retry\n briefly before failing. Its `domain` filter is a substring match, so exact-match the row.\n- The install record's `url` may say `http://`; always use `https://` URLs.\n- `GET https://<domain>/` returns 403 until the frontend is deployed — expected, not a failure.\n- **Never print, log or store the JWT.** It is minted on demand (24 h lifetime) by whichever stage\n needs the per-site WP MCP; only `mcp_url` is recorded.\n\nIf a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.\n\n## Step 4 — SEO plugin (Yoast) *(every WordPress site — on by default)*\n\nEvery headless WordPress site gets **Yoast SEO** (`wordpress-seo`) installed and activated by default,\nso per-page title/description/Open Graph/JSON-LD come from a maintained plugin's `yoast_head_json`\nrather than being hand-generated from each item's content by the frontend. This is **join-half** work\n(needs the live `/cms` install), so run it once `provision.status == \"completed\"`, the same way as the\nWooCommerce tail. Tell the user in one line that SEO (Yoast) is being set up; if they explicitly don't\nwant it, skip this step and record `seo.enabled: false` — but the default is to install it.\n\n1. **Install + activate Yoast** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"wordpress-seo\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `wordpress-seo` shows\n `active` (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"wordpress-seo\" }`.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` → `namespaces` once more, and that a sample post/page\n response carries `yoast_head_json`. The per-item Yoast meta (focus title/description per post) is set\n later in the **content-model push** — provision only installs + activates the plugin here. Don't fail\n provision over SEO: if the install can't be confirmed after one retry, record `seo.enabled: false`,\n tell the user, and let the frontend fall back to generating tags from content.\n\n## Step 5 — WooCommerce store *(only for a `WordPress + WooCommerce` brief — skip otherwise)*\n\nRun this only when the brief's **Suggested backend** is `**WordPress + WooCommerce**`. It turns the\nheadless WordPress site into a headless **store**: WooCommerce is the `woocommerce` plugin on this same\n`/cms` install — **not a separate backend**. This is **join-half** work (needs the live instance), so run\nit once `provision.status == \"completed\"`. The authoritative data-layer reference is\n[`../../04-content-model/wordpress/woocommerce.md`](../../04-content-model/wordpress/woocommerce.md).\n\n> **No custom plugin — the catalog is driven through the per-site WP MCP.** A custom plugin can't be\n> installed into a headless `/cms` from a coding agent, so there is **no seed/config plugin and no REST\n> consumer key in this flow.** Instead, the Hostinger AI Assistant (the per-site WP MCP, same JWT session\n> content-model uses) exposes **native WooCommerce abilities** — `wc-products-*`, `wc-product-categories-*`,\n> `wc-coupons-*`, `wc-orders-*`, `media-upload-from-url`, `wp-settings-*`. Provision just **installs\n> WooCommerce and verifies the store endpoints**; the catalog + coupons are seeded in the content-model\n> push through those abilities (see the cookbook).\n\n1. **Install + activate WooCommerce** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"woocommerce\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `woocommerce` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"woocommerce\" }`.\n - Confirm with `wordpress_plugins_check-if-woo-commerce-is-installed { domain: \"<domain>\" }`. Allow a\n moment for WooCommerce's installer to create its tables and the Shop/Cart/Checkout pages.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded** (seen on both `plugins_install` and `jwt-token`). Treat a 500 as \"verify, don't assume\":\n poll `list-installed` / re-check before retrying, and never fire a second install blindly.\n2. **Verify the store endpoints (public, no auth):**\n\n | Check | How | Expect |\n |---|---|---|\n | WooCommerce active | `wordpress_plugins_check-if-woo-commerce-is-installed { domain }` | installed + active |\n | Store API answers | `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` | 200 (empty until content-model seeds — fine) |\n | Both namespaces loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `wc/store/v1` **and** `wc/v3` |\n\n Pretty permalinks are on by default on a headless install, so the Store API resolves immediately; if a\n store route ever 404s, the permalink structure is the thing to check.\n\n> **Checkout needs owner setup — treat it like the Ecommerce payment gate.** A fresh WooCommerce install\n> has **no payment gateway and no shipping zone enabled**, and that configuration is **not reachable from\n> the headless tooling** (there is no payment/shipping MCP ability, and `wp-settings-update` only writes\n> standard WP settings). So **checkout is unavailable until the owner enables a payment method + shipping\n> in wp-admin** (hand them the one-click login in Step 6). Record `woocommerce.payment_enabled: false`\n> accordingly. This is **not a build failure**: the catalog, product pages, client-side cart and coupon\n> application all work headlessly — the downstream frontend renders a graceful disabled-checkout state and\n> never calls `/checkout` while `payment_enabled` is `false` (see content-model + frontend). Never ask the\n> user for payment credentials. Store currency and address (`woocommerce_*` options) likewise aren't\n> settable headlessly — they follow the install default and the owner can change them in wp-admin.\n\n## Step 5b — Contact form (Contact Form 7) *(only when the brief's Must-haves include a contact form — skip otherwise)*\n\nRun this only when the brief's **Must-haves** include a contact form. It installs **Contact Form 7**\n(`contact-form-7`) on this same `/cms` install, so the frontend can render a contact page whose form\nemails the site owner through WordPress's built-in `wp_mail()` — no third-party service and no API keys.\nLike Yoast/WooCommerce this is **join-half** work: run it once `provision.status == \"completed\"`. The form\nitself (id + fields + recipient) — and a tiny **spam-guard plugin** that makes a honeypot work headlessly —\nare resolved/deployed in the content-model push — see\n[`../../04-content-model/wordpress/contact-form.md`](../../04-content-model/wordpress/contact-form.md).\n\n> **Why CF7 and not a bespoke mailer.** A contact form is maintained, spam-hardened functionality worth\n> reusing rather than reinventing (you *can* deploy a custom plugin to a headless `/cms` — see the\n> content-model cookbook — but you needn't write your own mailer). CF7 is a wordpress.org-slug plugin,\n> installs through the same management MCP as Yoast, and ships a **public, unauthenticated** submit\n> endpoint (`POST /cms/wp-json/contact-form-7/v1/contact-forms/<id>/feedback`). The frontend and `/cms`\n> are same-origin, so the browser posts to it directly — no proxy, no CORS, no secret on the client.\n\n1. **Install + activate CF7** (management MCP, async — poll, never blind-retry):\n - `wordpress_plugins_install { username, software: <install_id>, plugins: [\"contact-form-7\"] }`\n - Poll `wordpress_plugins_list-installed { username, software }` until `contact-form-7` shows `active`\n (install + activate can complete together), or activate it with\n `wordpress_plugins_activate { username, software, plugin: \"contact-form-7\" }`.\n - **Don't install the `contact-form-7-honeypot` add-on.** It only acts when a `[honeypot]` form-tag is\n present in the form — a wp-admin edit the headless agent can't make — so on the default form it does\n nothing. Headless spam protection instead comes from a small **spam-guard plugin deployed in the\n content-model push** (it hooks `wpcf7_spam`); see the cookbook.\n - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job\n succeeded**. Treat a 500 as \"verify, don't assume\": poll `list-installed` before retrying, and\n never fire a second install blindly.\n2. **Verify** `GET https://<domain>/cms/wp-json/` lists the **`contact-form-7/v1`** namespace, then\n record `contact_form.enabled: true` in `site.json` (Step 6). If the install can't be confirmed after\n one retry, record `contact_form.enabled: false`, tell the user in one line, and let the frontend\n render the contact page without a form (graceful — not a build failure). Don't fail provision over it.\n\nThe owner's email is **CF7's default recipient** (`[_site_admin_email]`, the admin address from\ninstall) — fine out of the box, so \"does Hostinger set up the email?\" is **yes** for basic delivery via\n`wp_mail`. Deliverability (SPF/DKIM) is most reliable when that address is a mailbox **on the same\ndomain**; the content-model push keeps the default recipient and validate sends a test submission to\nconfirm mail actually leaves. Never ask the user for mail credentials.\n\n## Step 6 — Record `site.json`\n\n```json\n{\n \"backend\": \"wordpress\",\n \"website_type\": \"headless\",\n \"order_id\": 1007223428,\n \"username\": \"u…\",\n \"wordpress\": {\n \"install_id\": \"30…\",\n \"directory\": \"cms\",\n \"admin_login\": \"…\"\n },\n \"seo\": {\n \"plugin\": \"wordpress-seo\",\n \"enabled\": true\n },\n \"urls\": {\n \"site\": \"https://<domain>\",\n \"cms\": \"https://<domain>/cms\",\n \"wp_admin\": \"https://<domain>/cms/wp-admin\",\n \"rest_api\": \"https://<domain>/cms/wp-json/wp/v2\",\n \"wp_mcp\": \"<mcp_url from the JWT call>\"\n },\n \"provisioned_at\": \"…\",\n \"verified_at\": \"…\"\n}\n```\n\nNo passwords or tokens in `site.json`. When the user needs wp-admin, mint a one-click link with\n`wordpress_login_create-links { username, software: <install_id> }` — never hand over a password.\n\n**For a `WordPress + WooCommerce` brief, also add a `woocommerce` block and the store URLs** (the\n`backend` field still stays `\"wordpress\"`):\n\n```json\n{\n \"woocommerce\": {\n \"enabled\": true,\n \"seeded_via\": \"per-site-wp-mcp\",\n \"currency\": \"<currency_code from GET {store_api}/products or the install default>\",\n \"payment_enabled\": false,\n \"checkout_setup\": \"owner enables a payment method + shipping in wp-admin\"\n },\n \"urls\": {\n \"store_api\": \"https://<domain>/cms/wp-json/wc/store/v1\",\n \"wc_rest_api\": \"https://<domain>/cms/wp-json/wc/v3\"\n }\n}\n```\n\n**For a brief with a contact form (CF7 installed in Step 5b), also add a `contact_form` block.**\nProvision records only `enabled` + `plugin` here; the content-model push fills `form_id`,\n`feedback_url`, `fields` and `honeypot_field` once it resolves the form and deploys the spam guard:\n\n```json\n{\n \"contact_form\": {\n \"enabled\": true,\n \"plugin\": \"contact-form-7\",\n \"form_id\": null,\n \"feedback_url\": null,\n \"fields\": [],\n \"honeypot_field\": null,\n \"recipient\": \"site-admin-email\"\n }\n}\n```\n\n`form_id` / `feedback_url` / `fields` / `honeypot_field` stay `null` / `[]` until content-model resolves the form (the\nsubmit URL is `https://<domain>/cms/wp-json/contact-form-7/v1/contact-forms/<form_id>/feedback`).\n`recipient` is informational — CF7's `To` is `[_site_admin_email]`; no address, token or secret is\nstored in `site.json`.\n\n`rest_api` stays `wp/v2` (the WordPress content contract, for the blog/pages on the same site);\n`store_api` is the public storefront surface (browse/cart/coupons/checkout, no auth). `wc_rest_api`\n(`wc/v3`) is recorded only as the surface the **owner's own** integrations use with a consumer key they\ngenerate in wp-admin — **this flow never creates or stores a key** (the catalog is seeded through the\nper-site WP MCP's `wc-*` abilities; the storefront uses the public Store API). `payment_enabled` starts\n`false` and flips to `true` only once a gateway + shipping are configured — no secrets in `site.json`.\n"
|
|
190
190
|
},
|
|
191
191
|
{
|
|
192
192
|
"uri": "skill://hostinger-headless/steps/04-content-model/SKILL.md",
|
package/src/core/tools/all.js
CHANGED
|
@@ -9,7 +9,7 @@ export default [
|
|
|
9
9
|
"destructiveHint": true
|
|
10
10
|
},
|
|
11
11
|
"topic": "hosting",
|
|
12
|
-
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed.
|
|
12
|
+
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, pass appType and nodeVersion from the project's engines, plus outputDirectory and buildScript if the app has a build step). Git needs GitHub or GitLab connected once in hPanel.",
|
|
13
13
|
"method": "",
|
|
14
14
|
"path": "",
|
|
15
15
|
"inputSchema": {
|
|
@@ -64,10 +64,6 @@ export default [
|
|
|
64
64
|
"type": "boolean",
|
|
65
65
|
"description": "Activate the theme after deployment (WordPress themes only, default: false)"
|
|
66
66
|
},
|
|
67
|
-
"headless": {
|
|
68
|
-
"type": "boolean",
|
|
69
|
-
"description": "The site runs headless WordPress, installed in the cms folder of the document root: the plugin or theme is uploaded to cms/wp-content/plugins or cms/wp-content/themes and installed there (wordpress-plugin and wordpress-theme only, default: false)"
|
|
70
|
-
},
|
|
71
67
|
"databaseDump": {
|
|
72
68
|
"type": "string",
|
|
73
69
|
"description": "Path to a .sql dump; required for a WordPress site import"
|
package/src/core/tools/all.ts
CHANGED
|
@@ -25,7 +25,7 @@ const tools: OpenApiTool[] = [
|
|
|
25
25
|
"destructiveHint": true
|
|
26
26
|
},
|
|
27
27
|
"topic": "hosting",
|
|
28
|
-
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed.
|
|
28
|
+
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, pass appType and nodeVersion from the project's engines, plus outputDirectory and buildScript if the app has a build step). Git needs GitHub or GitLab connected once in hPanel.",
|
|
29
29
|
"method": "",
|
|
30
30
|
"path": "",
|
|
31
31
|
"inputSchema": {
|
|
@@ -80,10 +80,6 @@ const tools: OpenApiTool[] = [
|
|
|
80
80
|
"type": "boolean",
|
|
81
81
|
"description": "Activate the theme after deployment (WordPress themes only, default: false)"
|
|
82
82
|
},
|
|
83
|
-
"headless": {
|
|
84
|
-
"type": "boolean",
|
|
85
|
-
"description": "The site runs headless WordPress, installed in the cms folder of the document root: the plugin or theme is uploaded to cms/wp-content/plugins or cms/wp-content/themes and installed there (wordpress-plugin and wordpress-theme only, default: false)"
|
|
86
|
-
},
|
|
87
83
|
"databaseDump": {
|
|
88
84
|
"type": "string",
|
|
89
85
|
"description": "Path to a .sql dump; required for a WordPress site import"
|
|
@@ -9,7 +9,7 @@ export default [
|
|
|
9
9
|
"destructiveHint": true
|
|
10
10
|
},
|
|
11
11
|
"topic": "hosting",
|
|
12
|
-
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed.
|
|
12
|
+
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, pass appType and nodeVersion from the project's engines, plus outputDirectory and buildScript if the app has a build step). Git needs GitHub or GitLab connected once in hPanel.",
|
|
13
13
|
"method": "",
|
|
14
14
|
"path": "",
|
|
15
15
|
"inputSchema": {
|
|
@@ -64,10 +64,6 @@ export default [
|
|
|
64
64
|
"type": "boolean",
|
|
65
65
|
"description": "Activate the theme after deployment (WordPress themes only, default: false)"
|
|
66
66
|
},
|
|
67
|
-
"headless": {
|
|
68
|
-
"type": "boolean",
|
|
69
|
-
"description": "The site runs headless WordPress, installed in the cms folder of the document root: the plugin or theme is uploaded to cms/wp-content/plugins or cms/wp-content/themes and installed there (wordpress-plugin and wordpress-theme only, default: false)"
|
|
70
|
-
},
|
|
71
67
|
"databaseDump": {
|
|
72
68
|
"type": "string",
|
|
73
69
|
"description": "Path to a .sql dump; required for a WordPress site import"
|
|
@@ -25,7 +25,7 @@ const tools: OpenApiTool[] = [
|
|
|
25
25
|
"destructiveHint": true
|
|
26
26
|
},
|
|
27
27
|
"topic": "hosting",
|
|
28
|
-
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed.
|
|
28
|
+
"description": "Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, pass appType and nodeVersion from the project's engines, plus outputDirectory and buildScript if the app has a build step). Git needs GitHub or GitLab connected once in hPanel.",
|
|
29
29
|
"method": "",
|
|
30
30
|
"path": "",
|
|
31
31
|
"inputSchema": {
|
|
@@ -80,10 +80,6 @@ const tools: OpenApiTool[] = [
|
|
|
80
80
|
"type": "boolean",
|
|
81
81
|
"description": "Activate the theme after deployment (WordPress themes only, default: false)"
|
|
82
82
|
},
|
|
83
|
-
"headless": {
|
|
84
|
-
"type": "boolean",
|
|
85
|
-
"description": "The site runs headless WordPress, installed in the cms folder of the document root: the plugin or theme is uploaded to cms/wp-content/plugins or cms/wp-content/themes and installed there (wordpress-plugin and wordpress-theme only, default: false)"
|
|
86
|
-
},
|
|
87
83
|
"databaseDump": {
|
|
88
84
|
"type": "string",
|
|
89
85
|
"description": "Path to a .sql dump; required for a WordPress site import"
|
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/agency-hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.agency-hosting.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.
|
|
7
|
+
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.6.1', tools, instructions });
|
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/agency-hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.agency-hosting.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.
|
|
7
|
+
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.6.1', tools, instructions });
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
import { startServer } from '../core/runtime.js';
|
|
4
4
|
import tools from '../core/tools/agent-readiness.js';
|
|
5
5
|
|
|
6
|
-
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.
|
|
6
|
+
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.6.1', tools });
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
import { startServer } from '../core/runtime.js';
|
|
4
4
|
import tools from '../core/tools/agent-readiness.js';
|
|
5
5
|
|
|
6
|
-
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.
|
|
6
|
+
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.6.1', tools });
|
package/src/servers/all.js
CHANGED
|
@@ -5,4 +5,4 @@ import tools from '../core/tools/all.js';
|
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
import skills from '../core/skills.js';
|
|
7
7
|
|
|
8
|
-
startServer({ name: 'hostinger-api-mcp', version: '3.
|
|
8
|
+
startServer({ name: 'hostinger-api-mcp', version: '3.6.1', tools, instructions, skills });
|
package/src/servers/all.ts
CHANGED
|
@@ -5,4 +5,4 @@ import tools from '../core/tools/all.js';
|
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
import skills from '../core/skills.js';
|
|
7
7
|
|
|
8
|
-
startServer({ name: 'hostinger-api-mcp', version: '3.
|
|
8
|
+
startServer({ name: 'hostinger-api-mcp', version: '3.6.1', tools, instructions, skills });
|
package/src/servers/billing.js
CHANGED
package/src/servers/billing.ts
CHANGED
package/src/servers/dns.js
CHANGED
package/src/servers/dns.ts
CHANGED
package/src/servers/domains.js
CHANGED
package/src/servers/domains.ts
CHANGED
package/src/servers/ecommerce.js
CHANGED
package/src/servers/ecommerce.ts
CHANGED
package/src/servers/horizons.js
CHANGED
package/src/servers/horizons.ts
CHANGED
package/src/servers/hosting.js
CHANGED
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-hosting-mcp', version: '3.
|
|
7
|
+
startServer({ name: 'hostinger-hosting-mcp', version: '3.6.1', tools, instructions });
|
package/src/servers/hosting.ts
CHANGED
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-hosting-mcp', version: '3.
|
|
7
|
+
startServer({ name: 'hostinger-hosting-mcp', version: '3.6.1', tools, instructions });
|
package/src/servers/mail.js
CHANGED
package/src/servers/mail.ts
CHANGED
package/src/servers/reach.js
CHANGED
package/src/servers/reach.ts
CHANGED
package/src/servers/vps.js
CHANGED
package/src/servers/vps.ts
CHANGED
package/src/servers/wordpress.js
CHANGED
package/src/servers/wordpress.ts
CHANGED
package/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
export interface APITools {
|
|
7
7
|
/**
|
|
8
|
-
* Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed.
|
|
8
|
+
* Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, pass appType and nodeVersion from the project's engines, plus outputDirectory and buildScript if the app has a build step). Git needs GitHub or GitLab connected once in hPanel.
|
|
9
9
|
*/
|
|
10
10
|
"hosting_deploy": {
|
|
11
11
|
params: {
|
|
@@ -33,10 +33,6 @@ export interface APITools {
|
|
|
33
33
|
* Activate the theme after deployment (WordPress themes only, default: false)
|
|
34
34
|
*/
|
|
35
35
|
activate?: boolean;
|
|
36
|
-
/**
|
|
37
|
-
* The site runs headless WordPress, installed in the cms folder of the document root: the plugin or theme is uploaded to cms/wp-content/plugins or cms/wp-content/themes and installed there (wordpress-plugin and wordpress-theme only, default: false)
|
|
38
|
-
*/
|
|
39
|
-
headless?: boolean;
|
|
40
36
|
/**
|
|
41
37
|
* Path to a .sql dump; required for a WordPress site import
|
|
42
38
|
*/
|