@hostinger/mcp 3.2.2 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/gemini-extension.json +1 -1
  3. package/package.json +1 -1
  4. package/server.json +2 -2
  5. package/skills/headless/SKILL.md +245 -36
  6. package/skills/headless/connect/SKILL.md +98 -0
  7. package/skills/headless/{entry → connect}/bootstrap.mjs +8 -3
  8. package/skills/headless/connect/tools/antigravity.md +54 -0
  9. package/skills/headless/connect/tools/claude-code.md +46 -0
  10. package/skills/headless/connect/tools/codex.md +48 -0
  11. package/skills/headless/connect/tools/cursor.md +109 -0
  12. package/skills/headless/connect/tools/devin.md +50 -0
  13. package/skills/headless/connect/tools/vscode.md +57 -0
  14. package/skills/headless/report.schema.json +149 -0
  15. package/skills/headless/run-log.mjs +662 -0
  16. package/skills/headless/steps/01-check-state/SKILL.md +209 -0
  17. package/skills/headless/steps/02-refine/SKILL.md +273 -0
  18. package/skills/headless/steps/03-provision/SKILL.md +117 -0
  19. package/skills/headless/steps/03-provision/ecommerce/SKILL.md +142 -0
  20. package/skills/headless/steps/03-provision/webapp/SKILL.md +154 -0
  21. package/skills/headless/steps/03-provision/wordpress/SKILL.md +249 -0
  22. package/skills/headless/steps/04-content-model/SKILL.md +228 -0
  23. package/skills/headless/steps/04-content-model/ecommerce/SKILL.md +75 -0
  24. package/skills/headless/steps/04-content-model/wordpress/SKILL.md +223 -0
  25. package/skills/headless/steps/04-content-model/wordpress/woocommerce.md +153 -0
  26. package/skills/headless/steps/05-design/SKILL.md +464 -0
  27. package/skills/headless/steps/05-design/logo-kit.mjs +201 -0
  28. package/skills/headless/steps/06-frontend/SKILL.md +160 -0
  29. package/skills/headless/steps/07-validate/SKILL.md +56 -0
  30. package/skills/headless/steps/08-deploy/SKILL.md +179 -0
  31. package/skills/headless/steps/09-done/SKILL.md +157 -0
  32. package/src/core/skills.js +171 -24
  33. package/src/core/skills.ts +171 -24
  34. package/src/servers/agency-hosting.js +1 -1
  35. package/src/servers/agency-hosting.ts +1 -1
  36. package/src/servers/agent-readiness.js +1 -1
  37. package/src/servers/agent-readiness.ts +1 -1
  38. package/src/servers/all.js +1 -1
  39. package/src/servers/all.ts +1 -1
  40. package/src/servers/billing.js +1 -1
  41. package/src/servers/billing.ts +1 -1
  42. package/src/servers/dns.js +1 -1
  43. package/src/servers/dns.ts +1 -1
  44. package/src/servers/domains.js +1 -1
  45. package/src/servers/domains.ts +1 -1
  46. package/src/servers/ecommerce.js +1 -1
  47. package/src/servers/ecommerce.ts +1 -1
  48. package/src/servers/horizons.js +1 -1
  49. package/src/servers/horizons.ts +1 -1
  50. package/src/servers/hosting.js +1 -1
  51. package/src/servers/hosting.ts +1 -1
  52. package/src/servers/mail.js +1 -1
  53. package/src/servers/mail.ts +1 -1
  54. package/src/servers/reach.js +1 -1
  55. package/src/servers/reach.ts +1 -1
  56. package/src/servers/vps.js +1 -1
  57. package/src/servers/vps.ts +1 -1
  58. package/src/servers/wordpress.js +1 -1
  59. package/src/servers/wordpress.ts +1 -1
  60. package/skills/headless/entry/skill.md +0 -79
  61. package/skills/headless/references/DATABASE.md +0 -49
  62. package/skills/headless/references/DEPLOYMENT.md +0 -34
  63. package/skills/headless/references/SETUP.md +0 -35
  64. package/skills/headless/references/STORE.md +0 -52
  65. package/skills/headless/references/WORDPRESS.md +0 -51
@@ -0,0 +1,201 @@
1
+ // Hostinger Headless — logo kit (step 05-design).
2
+ //
3
+ // Primitives for building a designed logo as outlined SVG: the letters become vector paths, so the
4
+ // logo needs no web font and renders identically everywhere. The agent composes the design; this
5
+ // file only does the parts that are easy to get wrong.
6
+ //
7
+ // Workflow (from ~/.hostinger/<domain>/brand/, so packages and this kit sit next to the build):
8
+ // cp <installed skill>/steps/05-design/logo-kit.mjs .
9
+ // npm init -y && npm i opentype.js @fontsource/<display-family-slug>
10
+ // node logo-build.mjs // your script: import { … } from './logo-kit.mjs'
11
+ // rm -rf node_modules // package.json keeps the deps; a redesign runs `npm i` again
12
+ //
13
+ // Packages resolve from the current working directory, so run the build from brand/.
14
+
15
+ import { createRequire } from 'node:module';
16
+ import { readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+
19
+ const requireFromCwd = createRequire(join(process.cwd(), 'logo-build.js'));
20
+ const opentype = requireFromCwd('opentype.js');
21
+
22
+ // @fontsource splits each weight into unicode subsets; `latin` alone has no ą č ę ė į š ų ū ž,
23
+ // ő, ł, đ… so load every subset that exists and look each character up across them.
24
+ const SUBSETS = ['latin', 'latin-ext', 'vietnamese', 'cyrillic', 'cyrillic-ext', 'greek', 'greek-ext'];
25
+
26
+ function parseWoff(file) {
27
+ const buf = readFileSync(file);
28
+ return opentype.parse(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
29
+ }
30
+
31
+ /**
32
+ * Load a static @fontsource weight (the static packages ship .woff, which opentype.js reads;
33
+ * variable packages ship only .woff2, which it can't). The returned font is the `latin` subset
34
+ * with `charToGlyph` widened to every other subset in the package, so accented letters resolve.
35
+ * @param {string} slug family slug, e.g. "unbounded"
36
+ * @param {number} weight e.g. 900
37
+ */
38
+ export function loadFont(slug, weight = 400, style = 'normal') {
39
+ const fonts = [];
40
+ for (const subset of SUBSETS) {
41
+ let file;
42
+ try {
43
+ file = requireFromCwd.resolve(`@fontsource/${slug}/files/${slug}-${subset}-${weight}-${style}.woff`);
44
+ } catch {
45
+ continue;
46
+ }
47
+ fonts.push(parseWoff(file));
48
+ }
49
+ if (!fonts.length) throw new Error(`logo-kit: @fontsource/${slug} has no ${weight} ${style} .woff files`);
50
+ const [primary] = fonts;
51
+ const own = primary.charToGlyph.bind(primary);
52
+ primary.charToGlyph = (ch) => {
53
+ for (const font of fonts) {
54
+ const glyph = font === primary ? own(ch) : font.charToGlyph(ch);
55
+ if (glyph.index) {
56
+ glyph.kitFont = font;
57
+ return glyph;
58
+ }
59
+ }
60
+ return own(ch);
61
+ };
62
+ return primary;
63
+ }
64
+
65
+ const num = (v) => {
66
+ if (!Number.isFinite(v)) throw new Error(`logo-kit: non-finite coordinate ${v}`);
67
+ return +v.toFixed(2);
68
+ };
69
+
70
+ /**
71
+ * Serialize an opentype Path to SVG path data. Use this instead of path.toPathData():
72
+ * opentype.js 2.0 emits "NaN" when a coordinate's fraction is a tiny float residue
73
+ * (its roundDecimal builds strings like "5.6e-14e+2"), and the browser silently stops drawing
74
+ * the path at the first NaN.
75
+ */
76
+ export function toD(path) {
77
+ return path.commands
78
+ .map((c) => {
79
+ switch (c.type) {
80
+ case 'Z':
81
+ return 'Z';
82
+ case 'Q':
83
+ return `Q${num(c.x1)} ${num(c.y1)} ${num(c.x)} ${num(c.y)}`;
84
+ case 'C':
85
+ return `C${num(c.x1)} ${num(c.y1)} ${num(c.x2)} ${num(c.y2)} ${num(c.x)} ${num(c.y)}`;
86
+ default:
87
+ return `${c.type}${num(c.x)} ${num(c.y)}`;
88
+ }
89
+ })
90
+ .join('');
91
+ }
92
+
93
+ /**
94
+ * Lay out a string with kerning. Positions are relative to x = 0 on the baseline.
95
+ * One glyph per character: no ligatures or other substitutions, so `char` always matches its glyph
96
+ * (and fonts whose substitution tables opentype.js can't run still work).
97
+ * @returns {{ glyphs: { glyph, char: string, x: number, width: number }[], width: number, ascent: number, capHeight: number, xHeight: number }}
98
+ */
99
+ export function layout(font, text, size, { tracking = 0 } = {}) {
100
+ const scale = size / font.unitsPerEm;
101
+ const chars = [...text];
102
+ const glyphs = chars.map((ch) => font.charToGlyph(ch));
103
+ glyphs.forEach((glyph, i) => {
104
+ if (!glyph.index && chars[i].trim()) {
105
+ throw new Error(`logo-kit: "${chars[i]}" is not in this font (it would render as an empty box); pick a font that has it`);
106
+ }
107
+ });
108
+ let x = 0;
109
+ const out = glyphs.map((glyph, i) => {
110
+ // Kerning pairs only exist inside one subset file; across subsets the indexes don't match.
111
+ if (i && (glyphs[i - 1].kitFont ?? font) === (glyph.kitFont ?? font)) {
112
+ const kern = font.getKerningValue(glyphs[i - 1], glyph);
113
+ x += (Number.isFinite(kern) ? kern : 0) * scale + tracking;
114
+ }
115
+ const item = { glyph, char: chars[i] ?? '', x, width: glyph.advanceWidth * scale };
116
+ x += item.width;
117
+ return item;
118
+ });
119
+ const capHeight = (font.tables.os2?.sCapHeight || font.ascender * 0.7) * scale;
120
+ const xHeight = (font.tables.os2?.sxHeight || font.ascender * 0.5) * scale;
121
+ return { glyphs: out, width: x, ascent: font.ascender * scale, capHeight, xHeight };
122
+ }
123
+
124
+ /**
125
+ * Tight bounding box of a word's outlines at (x, baselineY), before any transform. Use it to size
126
+ * the viewBox; add room for outlines, shadows and rotations yourself.
127
+ * @returns {{ x1: number, y1: number, x2: number, y2: number }}
128
+ */
129
+ export function pathBox(font, text, x, baselineY, size, opts) {
130
+ const path = new opentype.Path();
131
+ for (const g of layout(font, text, size, opts).glyphs) path.extend(g.glyph.getPath(x + g.x, baselineY, size));
132
+ return path.getBoundingBox();
133
+ }
134
+
135
+ /** Path data for one glyph with its origin at (x, baselineY). */
136
+ export const glyphPath = (glyph, x, baselineY, size) => toD(glyph.getPath(x, baselineY, size));
137
+
138
+ /** Path data for a whole string starting at (x, baselineY). */
139
+ export function textPath(font, text, x, baselineY, size, opts) {
140
+ return layout(font, text, size, opts)
141
+ .glyphs.map((g) => glyphPath(g.glyph, x + g.x, baselineY, size))
142
+ .join('');
143
+ }
144
+
145
+ /**
146
+ * Set text around a circle (seals, badges). Each glyph is centred on its arc position and rotated
147
+ * to stand on the circle. `fill: true` stretches the spacing so the text closes the full ring.
148
+ * @returns {{ d: string, transform: string, char: string }[]} style each glyph however you like
149
+ */
150
+ export function arcText(font, text, { cx, cy, r, size, startDeg = -90, fill = true }) {
151
+ const { glyphs, width, capHeight } = layout(font, text, size);
152
+ const k = fill ? (2 * Math.PI * r) / width : 1;
153
+ const offset = fill ? 0 : -width / 2; // centred on startDeg when not filling
154
+ return glyphs.map((g) => {
155
+ const along = (g.x + g.width / 2 + offset) * k;
156
+ const angle = along / r + (startDeg * Math.PI) / 180;
157
+ const px = cx + r * Math.cos(angle);
158
+ const py = cy + r * Math.sin(angle);
159
+ const deg = (angle * 180) / Math.PI + 90;
160
+ return {
161
+ d: glyphPath(g.glyph, -g.width / 2, capHeight / 2, size),
162
+ transform: `translate(${num(px)} ${num(py)}) rotate(${num(deg)})`,
163
+ char: g.char,
164
+ };
165
+ });
166
+ }
167
+
168
+ /**
169
+ * The outlined "cut-out" treatment: an ink outline drawn behind the fill (paint-order) plus an
170
+ * optional hard offset shadow. `shadowColor` defaults to the ink; set it separately on dark
171
+ * backgrounds, where a dark ink outline vanishes but a coloured shadow still reads.
172
+ * Returns SVG markup.
173
+ */
174
+ export function sticker(d, { fill, ink = '#000', strokeWidth = 8, shadow = 8, shadowColor = ink, transform = '' }) {
175
+ const t = transform ? ` transform="${transform}"` : '';
176
+ const shade = shadow
177
+ ? `<path d="${d}" fill="${shadowColor}" stroke="${shadowColor}" stroke-width="${strokeWidth}" stroke-linejoin="round" transform="translate(${shadow} ${shadow})"/>`
178
+ : '';
179
+ return `<g${t}>${shade}<path d="${d}" fill="${fill}" stroke="${ink}" stroke-width="${strokeWidth}" stroke-linejoin="round" paint-order="stroke"/></g>`;
180
+ }
181
+
182
+ /** Root SVG with an accessible name. Pass a distinct `id` if two logos are inlined on one page. */
183
+ export function svgDoc({ width, height, title, body, id = 'logo-title' }) {
184
+ const esc = String(title).replace(/&/g, '&amp;').replace(/</g, '&lt;');
185
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${num(width)} ${num(height)}" role="img" aria-labelledby="${id}"><title id="${id}">${esc}</title>${body}</svg>`;
186
+ }
187
+
188
+ /**
189
+ * Validate a finished SVG string before writing it. Throws on: NaN/undefined/Infinity in the
190
+ * markup, live <text> (the logo must be outlined), a missing <title>, or a missing viewBox.
191
+ * @returns {{ bytes: number }}
192
+ */
193
+ export function check(svg) {
194
+ const problems = [];
195
+ if (/NaN|undefined|Infinity/.test(svg)) problems.push('contains NaN/undefined/Infinity');
196
+ if (/<text[\s>]/.test(svg)) problems.push('contains <text>; outline the letters with textPath/glyphPath');
197
+ if (!/<title[^>]*>[^<]+<\/title>/.test(svg)) problems.push('missing <title>');
198
+ if (!/viewBox="0 0 [\d.]+ [\d.]+"/.test(svg)) problems.push('missing or invalid viewBox');
199
+ if (problems.length) throw new Error(`logo-kit check failed: ${problems.join('; ')}`);
200
+ return { bytes: Buffer.byteLength(svg) };
201
+ }
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: headless-step-frontend
3
+ description: "Step 6 (frontend) of the Hostinger Headless flow. Builds the website that renders the backend's content. Figures out (or confirms) the framework — defaulting to Next.js SSR — implements the look already chosen in design.md (the design step's contract), and builds the source into src/. Only ask for the clarifications you actually need."
4
+ ---
5
+
6
+ # headless-step-frontend
7
+
8
+ Build the site's frontend against the backend from `site.json`, implementing the look the design step
9
+ already chose and wrote to **`design.md`** (plus the logo in `brand/`). The design is decided; this step
10
+ is where it becomes real code. There's no per-framework recipe — use good judgement, follow `design.md`
11
+ as the contract, and ask only for the clarifications you genuinely need. **Out:** source in the site
12
+ workspace `~/.hostinger/<domain>/src/`.
13
+
14
+ > ## ⚠️ Build mode: "Bring your own frontend"? Read this first (any backend)
15
+ > Read `brief.md` → **Build mode**. When it is **Bring your own frontend**, the user already has a
16
+ > frontend (the project in the folder the command was run in) and this step **wires it to the backend
17
+ > from `site.json` — it does not scaffold a new project and there is usually no `design.md` to
18
+ > implement** (the design step deferred to the user). **Use their project as the source**: copy or
19
+ > point `~/.hostinger/<domain>/src/` at it, keep its framework and UI, and collaborate on changes at
20
+ > the user's direction (vibe-code with them). Your job is to connect it to the live backend (WordPress
21
+ > `wp/v2` reads, the WooCommerce/Ecommerce Store API, env/keys) and make it deployable (set
22
+ > `engines.node` to `22`, remove any `.htaccess`), then hand off to deploy. This generalises the Webapp
23
+ > "existing app" case below to WordPress and Ecommerce too. When **Build mode is "Agent designs and
24
+ > builds"**, ignore this box and proceed normally (scaffold + implement `design.md`).
25
+
26
+ > ## ⚠️ Webapp backend? Read this first
27
+ > A `webapp` brief has **no CMS/store and no `content-model.md`** — the app owns its own data (its own
28
+ > API, or the MySQL database recorded in `site.json → database`). It comes in two shapes, decided back in
29
+ > **refine**:
30
+ >
31
+ > - **An existing app the user brought to deploy** (refine routes "deploy my app" here). **Use their
32
+ > project as the source** — copy/point `~/.hostinger/<domain>/src/` at it and keep its own framework and
33
+ > UI. **Do not scaffold a new project, and do not re-implement `design.md` over it** (so **the design
34
+ > step does not apply** to an app that brings its own UI). Your job is only to make it deployable: set
35
+ > `engines.node` to `22`, remove any `.htaccess`, wire its data/env, then hand off to deploy. **Skip the
36
+ > Step 2 design pass**, and ignore the `content-model.md` requirement in Step 3.
37
+ > - **A new app built from the brief.** Scaffold and build it from `brief.md` (and `design.md` if the
38
+ > design step produced one — design *does* run for a from-scratch app). Its data is owned by the app, so
39
+ > there is still **no `content-model.md`** to bind against.
40
+ >
41
+ > Framework choice (Step 1), the build, and hand-off (Step 4) apply to both. The per-backend bullets in
42
+ > Step 3 are **WordPress / WooCommerce / Ecommerce only** — for a webapp follow the Webapp bullet there.
43
+
44
+ ## Step 1 — Pick the framework (guess → default → confirm)
45
+
46
+ **The rendering is already decided in `brief.md` → Rendering** (SSR or SSG, set during refine). Honour
47
+ it, and the backend constraint:
48
+
49
+ - **Ecommerce → SSR required** (carts/search need per-request rendering). Any SSR framework works —
50
+ Next.js is the default, not a requirement (Express, Nuxt, SvelteKit, etc. are all fine).
51
+ - **WordPress + WooCommerce → SSR required** (same reason — cart/checkout need per-request rendering).
52
+ - **WordPress → SSG (Astro)** when the brief says SSG, otherwise **SSR (Next.js default)**.
53
+ - **Webapp → the app's own stack.** An **existing app** keeps its current framework (don't impose one); a
54
+ **new app** uses whatever fits the brief (SSR or SPA). Either way it deploys as a Node.js app (Step 8) —
55
+ an SSR app runs a server, a SPA builds to static output.
56
+ - Map rendering → framework: **`ssr` → Next.js**, **`ssg` → Astro** — unless the brief/prompt explicitly
57
+ names another supported framework, in which case use that.
58
+
59
+ Supported frameworks (what the Hostinger build pipeline can deploy — the `app_type` in Step 8 deploy):
60
+
61
+ - **Server/SSR-capable:** Next.js, Nuxt.js, Astro, SvelteKit, Express, NestJS, Fastify.
62
+ - **Static / SPA:** React, Vue, Angular, Vite, Parcel, Preact, Svelte, (Astro / SvelteKit static too).
63
+
64
+ Confirm the framework with the user in one line only if the brief left it open. Record the choice in
65
+ `state.json`:
66
+
67
+ ```json
68
+ { "frontend": { "framework": "nextjs", "rendering": "ssr" } }
69
+ ```
70
+
71
+ If the brief is silent, default to **Next.js SSR**. Ecommerce must be SSR (never SSG) — but the SSR
72
+ framework itself is free to be any supported one, not just Next.js.
73
+
74
+ ## Step 2 — Implement `design.md` (don't re-design)
75
+
76
+ The look was already chosen and written by the design step. **Read `~/.hostinger/<domain>/design.md` and
77
+ implement it faithfully** — tokens (colour, type, spacing), components, per-page composition, imagery and
78
+ accessibility — plus the logo files in `brand/`. Don't re-run design questions or invent a new direction here.
79
+
80
+ - Treat `design.md` as the contract: the built pages should match its tokens, type scale, components and
81
+ per-page composition, and the 8-point grid / WCAG 2.2 AA rules it carries.
82
+ - Use the **logo** from `brand/` as-is (wordmark, icon mark, favicon); never recolour or redraw it.
83
+ - If `design.md` is **missing** (e.g. a resumed older build), fall back to `brief.md → Design direction`
84
+ (reference site, colours, brand, adjectives) and a tasteful, accessible default — but prefer `design.md`
85
+ whenever it exists.
86
+ - **Don't gate on approval here** — the result is shown at **validate** (Step 7).
87
+
88
+ **Only ask about the code, and only if it matters** (design decisions belong in the design step):
89
+ - Where the **source should live** — default `~/.hostinger/<domain>/src/`; ask only if they want a
90
+ specific folder or an existing repo.
91
+ - **Git** — initialise / connect / push (default: a local project, no remote).
92
+ - A specific **component library** or technical must-have, if they implied one.
93
+
94
+ ## Step 3 — Build against the backend
95
+
96
+ Two phases around the instance barrier (see the top-level `SKILL.md`): scaffold + build the shell while
97
+ provision settles, then wire live data once `provision.status == "completed"`.
98
+
99
+ Read the backend from `site.json`:
100
+
101
+ - **WordPress** → `urls.rest_api` (`wp/v2`), public read. Content is in the posts/pages/categories/media
102
+ the content-model push created; nav/plan come from `content-model.md`. Map each item's **SEO** from its
103
+ `yoast_head_json` into the page `<head>` (title, description, Open Graph, JSON-LD schema) — Yoast is
104
+ installed by default at provision (`site.json → seo.enabled: true`). If `seo.enabled` is `false`
105
+ (Yoast absent), fall back to generating per-page title/description/OG tags from each item's content.
106
+ - **WordPress + WooCommerce** (`site.json → woocommerce.enabled: true`) → the WordPress bullet above **plus**
107
+ the store. Read the catalog over the **public WooCommerce Store API** (`site.json → urls.store_api`,
108
+ `/cms/wp-json/wc/store/v1`, no auth, open CORS): `GET /products` (prices are **integer minor units** on
109
+ `prices.price` with `prices.currency_minor_unit` — divide to display), `GET /products/categories`. Cart,
110
+ coupons and checkout are **client-side** against the Store API with the `Nonce` + `Cart-Token` headers
111
+ (`GET /cart`, `POST /cart/add-item`, `/cart/apply-coupon`, `POST /checkout`) — see
112
+ `../04-content-model/wordpress/woocommerce.md`. **Never embed a consumer key client-side**; the
113
+ storefront is fully public. Blog/pages on the same site still come from `urls.rest_api` (`wp/v2`) + Yoast.
114
+ - **Honour `site.json → woocommerce.payment_enabled`.** It is `false` until the owner enables a payment
115
+ gateway + shipping in wp-admin (that config isn't reachable headlessly — see the provision note). While
116
+ `false`, build the full catalog, product pages and the client-side cart as normal, but render a
117
+ graceful disabled-checkout state (e.g. a disabled "Checkout" with "Checkout isn't set up yet"), **don't
118
+ call `/checkout`**, and don't treat it as a build error. When `true`, wire checkout (`payment_method`
119
+ `"cod"` works out of the box).
120
+ - **Ecommerce** → the **public Hostinger Storefront Core V2 API** (`https://api-ecommerce.hostinger.com/v2`,
121
+ no auth, open CORS), keyed on the **custom sales channel** (`site.json → ecommerce.sales_channel.id`):
122
+ `GET /channels/<scha_*>/products` (prices are **minor units** on variants), `POST /channels/<scha_*>/checkout`
123
+ for buying. Cart is client-side; never embed a token. Read
124
+ `ecommerce_miscellaneous_custom-storefront-setup-instructions` for the authoritative, current guide.
125
+ - **Honour `site.json → ecommerce.payment_enabled`.** When it is `false` (free account / no paid
126
+ Ecommerce plan — see the provision gate), there is **no checkout**: build the full catalog, product
127
+ pages and the client-side cart as normal, but **do not call the checkout endpoint**. Instead render
128
+ a clear, graceful state at the checkout step — e.g. a disabled "Checkout" button with a short note
129
+ like "Online checkout isn't available on this plan yet" (and/or a contact/enquiry fallback). A
130
+ missing checkout is **not** a build error here — never leave the storefront half-built, throw, or
131
+ retry checkout because payment is off. When `payment_enabled` is `true`, wire checkout normally.
132
+ - **Webapp** → **no `content-model.md` and no headless backend to read**. The app uses its own data source:
133
+ the MySQL database from `site.json → database` (through the `DB_*` / `DATABASE_URL` env vars — set at
134
+ provision/deploy, see `../03-provision/webapp/SKILL.md` Step 3; `127.0.0.1` only resolves on the server,
135
+ not locally), or whatever external API the app calls. For an existing app, keep its data layer as-is.
136
+
137
+ Guidelines (framework-agnostic):
138
+
139
+ - **One data module** is the single place that talks to the backend (e.g. `src/lib/data.ts`) — one file
140
+ to inspect, one base URL to switch. Keep it swappable so the shell can build against stubs first.
141
+ - **Fetch at request time for SSR** (fresh content, no rebuild); a static build fetches at build time.
142
+ - **A failed or incomplete fetch is an error, not a silent empty/placeholder.** Planned content missing
143
+ from the API is a build error too — a site that builds with holes ships with holes. *(WordPress/Ecommerce;
144
+ a webapp has no planned content to check.)*
145
+ - **`content-model.md` is the source for what should exist** (pages, nav, collections mapping) — for
146
+ WordPress/Ecommerce only; a **webapp has no `content-model.md`**.
147
+ - **No `.htaccess`** in the project (deploy would overwrite the live one); don't commit `node_modules`
148
+ or build output.
149
+ - Set `engines.node` to `22` (the deploy builds on Node 22).
150
+
151
+ ## Step 4 — Hand off
152
+
153
+ Confirm the project builds clean locally against the live backend, no placeholder/fixture content
154
+ remains, and `state.json → frontend` matches what you built. Update `state.json` (`stage: "validate"`).
155
+ Validate (Step 7) owns the clean rebuild + user preview; deploy (Step 8) ships it with the `app_type`
156
+ and output directory that match the chosen framework + rendering.
157
+
158
+ > **Webapp:** just confirm the app builds clean and `state.json → frontend` is right — there's no
159
+ > `content-model.md` or headless backend to check. A DB-backed app can't reach its database locally
160
+ > (`127.0.0.1` is server-only), so its data layer is verified **after** deploy, not here.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: headless-step-validate
3
+ description: "Step 7 (validate) of the Hostinger Headless flow. OPTIONAL quick gate before deploy: every prior stage already self-verifies, so this is a fast confirmation (instance live + backend API answers with seeded content) plus optional local preview and user sign-off. Skip the heavy parts when nobody needs to approve."
4
+ ---
5
+
6
+ # headless-step-validate
7
+
8
+ > **Optional / fast.** Every stage before this one already verifies its own work (provision checks the
9
+ > backend, content-model push checks the REST counts, frontend builds clean), and **deploy verifies the
10
+ > live result**. So validate is **not** a re-run of all that — it's a quick confirmation, plus the
11
+ > optional human preview/sign-off. **Skip Steps 3–4 (rebuild + approval) entirely** on an autonomous or
12
+ > temp-domain run where no one is signing off; just do the two 10-second checks below and move on. Spend
13
+ > time here only when a person wants to see it before it goes live.
14
+
15
+ **In:** project in `~/.hostinger/<domain>/src/`, `site.json`, `state.json`.
16
+
17
+ ## 1 — Instance status (always, quick)
18
+
19
+ `provision.status == "completed"` **and** re-verify against the connector:
20
+ `hosting_websites_list-setups { "domain": "<domain>" }`, exact-match the row → `completed`. Anything
21
+ else → not ready; return to the instance barrier, don't deploy.
22
+
23
+ ## 2 — Backend API status (WordPress / WooCommerce / Ecommerce, always, quick)
24
+
25
+ Hit the live API from `site.json` and confirm it answers **with the content the push created** — a 200
26
+ with an empty catalog means the push didn't land:
27
+
28
+ - **WordPress:** `GET {urls.rest_api}/posts?per_page=1` → 200 JSON array; check `X-WP-Total` against the
29
+ planned post count, and `GET {urls.rest_api}/media?per_page=1` → `X-WP-Total` matches the planned
30
+ image count (more = duplicates).
31
+ - **WordPress + WooCommerce** (`site.json → woocommerce.enabled`): the WordPress check above **plus**
32
+ `GET {urls.store_api}/products?per_page=100` → 200 with the seeded products and `prices` (minor units);
33
+ count matches the plan. Don't test checkout while `woocommerce.payment_enabled` is `false`.
34
+ - **Ecommerce:** query `urls.storefront_api` for the sales channel → 200 with the seeded products and
35
+ prices *(confirm against the store)*.
36
+
37
+ Fail or counts off → fix content-model push before building against live data.
38
+
39
+ ## 3 — Clean rebuild against the live API (optional — skip unless signing off)
40
+
41
+ Frontend already produced a clean build; only redo this if you want a from-scratch guarantee before a
42
+ human sign-off. Remove the build output and rebuild; a failed or incomplete build is a stop (no
43
+ fixtures, placeholder copy or `.htaccess` in the output).
44
+
45
+ - **Astro (SSG):** `rm -rf dist && npm run build && npm run check:dist`.
46
+ - **Next.js (SSR):** `rm -rf .next && npm run check && npm run build`.
47
+
48
+ ## 4 — Preview & approval (optional — only when a person is signing off)
49
+
50
+ When someone wants to see it first: start the local preview (Astro: `npm run preview`; Next.js:
51
+ `npx next start`), check desktop and mobile widths, navigation and a couple of key routes, and present
52
+ it for **explicit approval**. If preview tooling is unavailable, give the user the command and URL.
53
+ On an autonomous or temp-domain run, skip this — deploy will verify the live result anyway.
54
+
55
+ Then (after approval, or immediately if 3–4 were skipped): `state.json` → `stage: "deploy"`, fresh
56
+ `updated_at`; leave `provision` unchanged.
@@ -0,0 +1,179 @@
1
+ ---
2
+ name: headless-step-deploy
3
+ description: "Step 8 (deploy) of the Hostinger Headless flow. Archives the frontend source, then deploys through the connector's one-shot hosting_deploy (type nodejs — it uploads the source, runs npm install and the build on the server) with the chosen framework's build settings (appType + output dir) passed explicitly, polls to completion with hosting_get-deployment, verifies the live site and that the backend survived, then writes deploy.json."
4
+ ---
5
+
6
+ # headless-step-deploy
7
+
8
+ Ship the frontend to the live domain through the Hostinger connector. You hand the connector a zip of
9
+ the project **source** and it uploads it, runs `npm install` + the build script on the server, and
10
+ serves the result. **Out:** `deploy.json`.
11
+
12
+ This is an **instance-barrier** step: run it only after `provision.status == "completed"` and validate
13
+ (if run) has passed. All operations go through the connector (`execute`).
14
+
15
+ **In:** the project in `~/.hostinger/<domain>/src/`, `site.json` (`username`, `urls`), `state.json`
16
+ (`frontend.framework`, `frontend.rendering`).
17
+
18
+ > ## ⚠️ The one rule that makes this work: pass build settings explicitly
19
+ > If you let `hosting_deploy` **auto-detect** build settings, for a headless site that detection returns
20
+ > **null** (`appType:null`, `outputDirectory:null`, Node 20) — the build "completes" but the **root
21
+ > serves 403** (no `appType`/output → the vhost serves a directory with no entry). This is reproducible.
22
+ > **Always pass `appType`, `outputDirectory`, `nodeVersion`, `buildScript`, `packageManager` and
23
+ > `rootDirectory` on the `hosting_deploy` call** with the framework's values; the deploy then resolves
24
+ > them and serves correctly, and `/cms` is preserved.
25
+
26
+ ## Build configurations
27
+
28
+ Map `frontend.framework` (from `state.json`) to its `appType` + build output directory, and pass those
29
+ on the `hosting_deploy` call. `appType` is the framework's value, e.g. `next`, `nuxt`, `astro`,
30
+ `svelte-kit`, `nest`, `express`, `fastify`, `react`, `create-react-app`, `gatsby`, `vite`, `vue`,
31
+ `angular`, `parcel`, `svelte`, `other`.
32
+
33
+ Common cases (all `nodeVersion: 22`, `buildScript: "build"`, `packageManager: "npm"`, `rootDirectory: "."`):
34
+
35
+ | Framework / mode | Local config | `appType` | `outputDirectory` |
36
+ |---|---|---|---|
37
+ | **Next.js — SSR** *(default)* | `output: 'standalone'` | `next` | `.next` |
38
+ | **Nuxt — SSR** | default | `nuxt` | `.output` |
39
+ | **SvelteKit — SSR** | node adapter | `svelte-kit` | `build` |
40
+ | **Astro — static** | `output: 'static'` | `astro` | `dist` |
41
+ | **Vite / React / Vue — static (SPA)** | — | `vite` (or `react`/`vue`) | `dist` |
42
+ | **Express / NestJS / Fastify — server** | — | `express`/`nest`/`fastify` | build output dir |
43
+
44
+ SSR/server frameworks run a persistent Node process (root sends `x-powered-by` / `cache-control: no-store`);
45
+ static frameworks serve their output dir as files. If unsure of a framework's output dir, check the
46
+ framework's own build output, but **always pass `appType`/`outputDirectory` on the deploy call with the
47
+ framework's correct values** — don't rely on auto-detection (it returns null → 403).
48
+
49
+ ## Step 1 — Pre-flight
50
+
51
+ 1. **Instance live.** `provision.status == "completed"`, re-verified with
52
+ `hosting_websites_list-setups { "domain": "<domain>" }`.
53
+ 2. **Build output exists locally** (the framework's output dir — e.g. `.next/`, `dist/`, `.output/`) and
54
+ the project builds clean (frontend/validate already did this). If not, go back.
55
+ 3. **Env vars (only if used).** Build-time vars (e.g. `NEXT_PUBLIC_*`) must be set **before** the build
56
+ with `hosting_nodejs_replace-environment-variables` (full replace). Never put secrets in the repo or
57
+ the archive. **Webapp with a database:** that full replace wipes the `DB_*`/`DATABASE_URL` that
58
+ `hosting_databases_setup-website` injected (the generated password reads back masked), so set all app
59
+ vars in the same replace and, if you can't, first reset the password with
60
+ `hosting_databases_change-password` and resend every `DB_*`/`DATABASE_URL` — see
61
+ `../03-provision/webapp/SKILL.md` Step 3.
62
+
63
+ ## Step 2 — Archive the source
64
+
65
+ Zip the project **source** with `package.json` at the archive root. **Include** the app/source,
66
+ `public/`, the framework config (`next.config.mjs` / `astro.config.mjs`), `tsconfig.json`,
67
+ `package.json`, `package-lock.json`, `.nvmrc`. **Exclude** `node_modules/`, the build output
68
+ (`.next/`, `dist/`, `out/`), `.git/`, and any `.htaccess` — the server installs and builds from source.
69
+
70
+ ```bash
71
+ cd ~/.hostinger/<domain>/src
72
+ zip -r -X ../app.zip . -x 'node_modules/*' '.next/*' 'dist/*' 'out/*' '.git/*' '**/.htaccess'
73
+ ```
74
+
75
+ ## Step 3 — Deploy (one shot: settings + upload + build)
76
+
77
+ Pass the framework's build settings **inline** on the deploy call — there is no separate settings step.
78
+ `path` is the archive from Step 2 (or the source directory itself; `hosting_deploy` archives a directory
79
+ for you, skipping `node_modules/`, `.git/` and `.env`, but it does **not** read `.gitignore`, so the
80
+ build output must already be absent).
81
+
82
+ ```json
83
+ { "operation": "hosting_deploy",
84
+ "params": {
85
+ "domain": "<domain>",
86
+ "path": "/absolute/path/to/.hostinger/<domain>/app.zip",
87
+ "type": "nodejs",
88
+ "appType": "<framework appType, e.g. next>",
89
+ "outputDirectory": "<framework output dir, e.g. .next>",
90
+ "nodeVersion": 22,
91
+ "buildScript": "build",
92
+ "packageManager": "npm",
93
+ "rootDirectory": "." } }
94
+ ```
95
+
96
+ `hosting_deploy` uploads the source and triggers `npm install` + the build **end-to-end** — do not call
97
+ any upload or build operation yourself. It returns a build `buildUuid` at once. This **overwrites the
98
+ website root** (expected) and keeps `/cms` intact. Passing `appType`/`outputDirectory` explicitly is what
99
+ keeps the root from serving 403 (see the rule above).
100
+
101
+ ## Step 4 — Poll to completion
102
+
103
+ Poll `hosting_get-deployment { "domain": "<domain>", "buildUuid": "<buildUuid>" }` with backoff until
104
+ `build.state` is `completed`. On `failed`, the same response carries the end of the build log and the
105
+ failure analysis — read it, fix locally, re-archive and redeploy (pass `fromLine`, the line count from
106
+ the previous call, to read only new log output). `hosting_get-deployment` also returns the build settings
107
+ stored for the site: `appType`/`outputDirectory`/`nodeVersion` should echo what you passed; if they read
108
+ `null`, the settings didn't take — fix the deploy call before continuing. For an SSR app that built but
109
+ hangs, `hosting_nodejs_restart-application { username, domain }` recovers the server without a rebuild.
110
+
111
+ ## Step 5 — Verify the live site (all must pass)
112
+
113
+ | Check | How | Expect |
114
+ |---|---|---|
115
+ | Root serves | `GET https://<domain>/` | **200** (not 403 / 404) |
116
+ | SSR is live *(server frameworks)* | response headers | `x-powered-by` present, `cache-control: no-store` |
117
+ | Static serves *(static frameworks)* | `GET https://<domain>/` | 200 HTML with real home content |
118
+ | Key routes | a couple of planned routes + one deep route | 200, real content |
119
+ | **Backend survived** *(WordPress / WooCommerce)* | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200 |
120
+
121
+ > **Only WordPress (including WordPress + WooCommerce) has a `/cms` on the domain**, so the *Backend
122
+ > survived* check is WordPress-only.
123
+ > - **WordPress + WooCommerce:** also confirm the store survived the deploy —
124
+ > `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` → 200. There is nothing external to
125
+ > link (no sales-channel step — that's Ecommerce only). **Checkout** stays owner-completed: when
126
+ > `site.json → woocommerce.payment_enabled` is `false`, don't test `/checkout`; a catalog that serves 200
127
+ > is a passing, complete deploy.
128
+ > - **Ecommerce:** the backend is the **Hostinger Ecommerce store**, read over the external Storefront API
129
+ > (`api-ecommerce.hostinger.com`) — nothing a deploy touches lives on this domain. There is no `/cms`;
130
+ > confirm the storefront still fetches products as a frontend check if you like. **Only check that
131
+ > checkout can be reached when `site.json → ecommerce.payment_enabled` is `true`** — on a free account
132
+ > (`payment_enabled: false`) there is no checkout, so **skip that check entirely**; a catalog that serves
133
+ > 200 is a passing, complete deploy. Never fail or hold the deploy because checkout is unavailable.
134
+ > - **Webapp:** no content backend at all — the whole root is the app. Skip the check; `/cms` won't resolve.
135
+
136
+ A `completed` build whose root 403s almost always means the build settings weren't passed to
137
+ `hosting_deploy` (null settings) — pass them and redeploy. Caching can briefly serve a stale root;
138
+ re-check after ~30 s.
139
+
140
+ ## Step 6 — Record `deploy.json`
141
+
142
+ ```json
143
+ {
144
+ "build_uuid": "…",
145
+ "stack": "nextjs",
146
+ "rendering": "ssr",
147
+ "app_type": "next",
148
+ "output_directory": ".next",
149
+ "node_version": 22,
150
+ "method": "hosting_deploy (type: nodejs, settings passed inline)",
151
+ "deployed_at": "…",
152
+ "verified_at": "…",
153
+ "urls": { "live": "https://<domain>", "cms": "https://<domain>/cms" },
154
+ "checks": { "root": 200, "ssr_headers": true, "backend_preserved": true }
155
+ }
156
+ ```
157
+
158
+ No secrets in `deploy.json`. The `urls.cms` field and the `backend_preserved` check above are
159
+ **WordPress-only** — adjust the record to the backend:
160
+
161
+ - **WordPress:** keep `urls.cms` and `checks.backend_preserved` as shown.
162
+ - **WordPress + WooCommerce:** keep `urls.cms` + `checks.backend_preserved` (it *is* WordPress), and add
163
+ `urls.store_api` from `site.json`; no external link step.
164
+ - **Ecommerce:** omit `urls.cms`/`backend_preserved`; record the store URLs instead (`urls.storefront_api`,
165
+ `urls.store_admin` from `site.json`) and link the custom sales channel to the live domain —
166
+ `ecommerce_sales-channels_update { store_id, sales_channel_id, url }` (see the ecommerce provision step).
167
+ - **Webapp:** omit `urls.cms` and `backend_preserved` entirely — `urls` is just `{ "live": "https://<domain>" }`.
168
+
169
+ Update `state.json` (`stage` → `done`), then continue to Step 9 (done).
170
+
171
+ ---
172
+
173
+ ## Note — one deploy path, and never `type: "static"`
174
+
175
+ `hosting_deploy` is the only path: give its `path` either the source **directory** (it archives it for
176
+ you) or a local **archive** (`zip`, `tar`, `tar.gz`, `tgz`, `7z`) — there is no separate upload or build
177
+ step to fall back to. Either way pass the explicit `appType`/`outputDirectory`/`nodeVersion`/
178
+ `buildScript`/`packageManager`/`rootDirectory` from the table. Never deploy a headless site with
179
+ `type: "static"` — it serves the directory as plain files and can overwrite `/cms`.