@hostinger/mcp 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/gemini-extension.json +1 -1
  3. package/package.json +1 -1
  4. package/server.json +2 -2
  5. package/skills/headless/SKILL.md +247 -36
  6. package/skills/headless/connect/SKILL.md +103 -0
  7. package/skills/headless/{entry → connect}/bootstrap.mjs +8 -3
  8. package/skills/headless/connect/tools/antigravity.md +54 -0
  9. package/skills/headless/connect/tools/claude-code.md +46 -0
  10. package/skills/headless/connect/tools/codex.md +48 -0
  11. package/skills/headless/connect/tools/cursor.md +109 -0
  12. package/skills/headless/connect/tools/devin.md +50 -0
  13. package/skills/headless/connect/tools/vscode.md +57 -0
  14. package/skills/headless/report.schema.json +149 -0
  15. package/skills/headless/run-log.mjs +662 -0
  16. package/skills/headless/steps/01-check-state/SKILL.md +193 -0
  17. package/skills/headless/steps/02-refine/SKILL.md +206 -0
  18. package/skills/headless/steps/03-provision/SKILL.md +62 -0
  19. package/skills/headless/steps/03-provision/ecommerce/SKILL.md +133 -0
  20. package/skills/headless/steps/03-provision/webapp/SKILL.md +154 -0
  21. package/skills/headless/steps/03-provision/wordpress/SKILL.md +100 -0
  22. package/skills/headless/steps/04-content-model/SKILL.md +150 -0
  23. package/skills/headless/steps/04-content-model/ecommerce/SKILL.md +75 -0
  24. package/skills/headless/steps/04-content-model/wordpress/SKILL.md +178 -0
  25. package/skills/headless/steps/05-design/SKILL.md +455 -0
  26. package/skills/headless/steps/05-design/logo-kit.mjs +201 -0
  27. package/skills/headless/steps/06-frontend/SKILL.md +131 -0
  28. package/skills/headless/steps/07-validate/SKILL.md +53 -0
  29. package/skills/headless/steps/08-deploy/SKILL.md +171 -0
  30. package/skills/headless/steps/09-done/SKILL.md +151 -0
  31. package/src/core/skills.js +164 -24
  32. package/src/core/skills.ts +164 -24
  33. package/src/servers/agency-hosting.js +1 -1
  34. package/src/servers/agency-hosting.ts +1 -1
  35. package/src/servers/agent-readiness.js +1 -1
  36. package/src/servers/agent-readiness.ts +1 -1
  37. package/src/servers/all.js +1 -1
  38. package/src/servers/all.ts +1 -1
  39. package/src/servers/billing.js +1 -1
  40. package/src/servers/billing.ts +1 -1
  41. package/src/servers/dns.js +1 -1
  42. package/src/servers/dns.ts +1 -1
  43. package/src/servers/domains.js +1 -1
  44. package/src/servers/domains.ts +1 -1
  45. package/src/servers/ecommerce.js +1 -1
  46. package/src/servers/ecommerce.ts +1 -1
  47. package/src/servers/horizons.js +1 -1
  48. package/src/servers/horizons.ts +1 -1
  49. package/src/servers/hosting.js +1 -1
  50. package/src/servers/hosting.ts +1 -1
  51. package/src/servers/mail.js +1 -1
  52. package/src/servers/mail.ts +1 -1
  53. package/src/servers/reach.js +1 -1
  54. package/src/servers/reach.ts +1 -1
  55. package/src/servers/vps.js +1 -1
  56. package/src/servers/vps.ts +1 -1
  57. package/src/servers/wordpress.js +1 -1
  58. package/src/servers/wordpress.ts +1 -1
  59. package/skills/headless/entry/skill.md +0 -79
  60. package/skills/headless/references/DATABASE.md +0 -49
  61. package/skills/headless/references/DEPLOYMENT.md +0 -34
  62. package/skills/headless/references/SETUP.md +0 -35
  63. package/skills/headless/references/STORE.md +0 -52
  64. package/skills/headless/references/WORDPRESS.md +0 -51
@@ -0,0 +1,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,131 @@
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
+ > ## ⚠️ Webapp backend? Read this first
15
+ > A `webapp` brief has **no CMS/store and no `content-model.md`** — the app owns its own data (its own
16
+ > API, or the MySQL database recorded in `site.json → database`). It comes in two shapes, decided back in
17
+ > **refine**:
18
+ >
19
+ > - **An existing app the user brought to deploy** (refine routes "deploy my app" here). **Use their
20
+ > project as the source** — copy/point `~/.hostinger/<domain>/src/` at it and keep its own framework and
21
+ > UI. **Do not scaffold a new project, and do not re-implement `design.md` over it** (so **the design
22
+ > step does not apply** to an app that brings its own UI). Your job is only to make it deployable: set
23
+ > `engines.node` to `22`, remove any `.htaccess`, wire its data/env, then hand off to deploy. **Skip the
24
+ > Step 2 design pass**, and ignore the `content-model.md` requirement in Step 3.
25
+ > - **A new app built from the brief.** Scaffold and build it from `brief.md` (and `design.md` if the
26
+ > design step produced one — design *does* run for a from-scratch app). Its data is owned by the app, so
27
+ > there is still **no `content-model.md`** to bind against.
28
+ >
29
+ > Framework choice (Step 1), the build, and hand-off (Step 4) apply to both. The per-backend bullets in
30
+ > Step 3 are **WordPress/Ecommerce only** — for a webapp follow the Webapp bullet there.
31
+
32
+ ## Step 1 — Pick the framework (guess → default → confirm)
33
+
34
+ **The rendering is already decided in `brief.md` → Rendering** (SSR or SSG, set during refine). Honour
35
+ it, and the backend constraint:
36
+
37
+ - **Ecommerce → SSR required** (carts/search need per-request rendering). Any SSR framework works —
38
+ Next.js is the default, not a requirement (Express, Nuxt, SvelteKit, etc. are all fine).
39
+ - **WordPress → SSG (Astro)** when the brief says SSG, otherwise **SSR (Next.js default)**.
40
+ - **Webapp → the app's own stack.** An **existing app** keeps its current framework (don't impose one); a
41
+ **new app** uses whatever fits the brief (SSR or SPA). Either way it deploys as a Node.js app (Step 8) —
42
+ an SSR app runs a server, a SPA builds to static output.
43
+ - Map rendering → framework: **`ssr` → Next.js**, **`ssg` → Astro** — unless the brief/prompt explicitly
44
+ names another supported framework, in which case use that.
45
+
46
+ Supported frameworks (what the Hostinger build pipeline can deploy — the `app_type` in Step 8 deploy):
47
+
48
+ - **Server/SSR-capable:** Next.js, Nuxt.js, Astro, SvelteKit, Express, NestJS, Fastify.
49
+ - **Static / SPA:** React, Vue, Angular, Vite, Parcel, Preact, Svelte, (Astro / SvelteKit static too).
50
+
51
+ Confirm the framework with the user in one line only if the brief left it open. Record the choice in
52
+ `state.json`:
53
+
54
+ ```json
55
+ { "frontend": { "framework": "nextjs", "rendering": "ssr" } }
56
+ ```
57
+
58
+ If the brief is silent, default to **Next.js SSR**. Ecommerce must be SSR (never SSG) — but the SSR
59
+ framework itself is free to be any supported one, not just Next.js.
60
+
61
+ ## Step 2 — Implement `design.md` (don't re-design)
62
+
63
+ The look was already chosen and written by the design step. **Read `~/.hostinger/<domain>/design.md` and
64
+ implement it faithfully** — tokens (colour, type, spacing), components, per-page composition, imagery and
65
+ accessibility — plus the logo files in `brand/`. Don't re-run design questions or invent a new direction here.
66
+
67
+ - Treat `design.md` as the contract: the built pages should match its tokens, type scale, components and
68
+ per-page composition, and the 8-point grid / WCAG 2.2 AA rules it carries.
69
+ - Use the **logo** from `brand/` as-is (wordmark, icon mark, favicon); never recolour or redraw it.
70
+ - If `design.md` is **missing** (e.g. a resumed older build), fall back to `brief.md → Design direction`
71
+ (reference site, colours, brand, adjectives) and a tasteful, accessible default — but prefer `design.md`
72
+ whenever it exists.
73
+ - **Don't gate on approval here** — the result is shown at **validate** (Step 7).
74
+
75
+ **Only ask about the code, and only if it matters** (design decisions belong in the design step):
76
+ - Where the **source should live** — default `~/.hostinger/<domain>/src/`; ask only if they want a
77
+ specific folder or an existing repo.
78
+ - **Git** — initialise / connect / push (default: a local project, no remote).
79
+ - A specific **component library** or technical must-have, if they implied one.
80
+
81
+ ## Step 3 — Build against the backend
82
+
83
+ Two phases around the instance barrier (see the top-level `SKILL.md`): scaffold + build the shell while
84
+ provision settles, then wire live data once `provision.status == "completed"`.
85
+
86
+ Read the backend from `site.json`:
87
+
88
+ - **WordPress** → `urls.rest_api` (`wp/v2`), public read. Content is in the posts/pages/categories/media
89
+ the content-model push created; nav/plan come from `content-model.md`. Map each item's **SEO** from its
90
+ `yoast_head_json` into the page `<head>` (title, description, Open Graph, JSON-LD schema).
91
+ - **Ecommerce** → the **public Hostinger Storefront Core V2 API** (`https://api-ecommerce.hostinger.com/v2`,
92
+ no auth, open CORS), keyed on the **custom sales channel** (`site.json → ecommerce.sales_channel.id`):
93
+ `GET /channels/<scha_*>/products` (prices are **minor units** on variants), `POST /channels/<scha_*>/checkout`
94
+ for buying. Cart is client-side; never embed a token. Read
95
+ `ecommerce_miscellaneous_custom-storefront-setup-instructions` for the authoritative, current guide.
96
+ - **Honour `site.json → ecommerce.payment_enabled`.** When it is `false` (free account / no paid
97
+ Ecommerce plan — see the provision gate), there is **no checkout**: build the full catalog, product
98
+ pages and the client-side cart as normal, but **do not call the checkout endpoint**. Instead render
99
+ a clear, graceful state at the checkout step — e.g. a disabled "Checkout" button with a short note
100
+ like "Online checkout isn't available on this plan yet" (and/or a contact/enquiry fallback). A
101
+ missing checkout is **not** a build error here — never leave the storefront half-built, throw, or
102
+ retry checkout because payment is off. When `payment_enabled` is `true`, wire checkout normally.
103
+ - **Webapp** → **no `content-model.md` and no headless backend to read**. The app uses its own data source:
104
+ the MySQL database from `site.json → database` (through the `DB_*` / `DATABASE_URL` env vars — set at
105
+ provision/deploy, see `../03-provision/webapp/SKILL.md` Step 3; `127.0.0.1` only resolves on the server,
106
+ not locally), or whatever external API the app calls. For an existing app, keep its data layer as-is.
107
+
108
+ Guidelines (framework-agnostic):
109
+
110
+ - **One data module** is the single place that talks to the backend (e.g. `src/lib/data.ts`) — one file
111
+ to inspect, one base URL to switch. Keep it swappable so the shell can build against stubs first.
112
+ - **Fetch at request time for SSR** (fresh content, no rebuild); a static build fetches at build time.
113
+ - **A failed or incomplete fetch is an error, not a silent empty/placeholder.** Planned content missing
114
+ from the API is a build error too — a site that builds with holes ships with holes. *(WordPress/Ecommerce;
115
+ a webapp has no planned content to check.)*
116
+ - **`content-model.md` is the source for what should exist** (pages, nav, collections mapping) — for
117
+ WordPress/Ecommerce only; a **webapp has no `content-model.md`**.
118
+ - **No `.htaccess`** in the project (deploy would overwrite the live one); don't commit `node_modules`
119
+ or build output.
120
+ - Set `engines.node` to `22` (the deploy builds on Node 22).
121
+
122
+ ## Step 4 — Hand off
123
+
124
+ Confirm the project builds clean locally against the live backend, no placeholder/fixture content
125
+ remains, and `state.json → frontend` matches what you built. Update `state.json` (`stage: "validate"`).
126
+ Validate (Step 7) owns the clean rebuild + user preview; deploy (Step 8) ships it with the `app_type`
127
+ and output directory that match the chosen framework + rendering.
128
+
129
+ > **Webapp:** just confirm the app builds clean and `state.json → frontend` is right — there's no
130
+ > `content-model.md` or headless backend to check. A DB-backed app can't reach its database locally
131
+ > (`127.0.0.1` is server-only), so its data layer is verified **after** deploy, not here.
@@ -0,0 +1,53 @@
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 or 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
+ - **Ecommerce:** query `urls.storefront_api` for the sales channel → 200 with the seeded products and
32
+ prices *(confirm against the store)*.
33
+
34
+ Fail or counts off → fix content-model push before building against live data.
35
+
36
+ ## 3 — Clean rebuild against the live API (optional — skip unless signing off)
37
+
38
+ Frontend already produced a clean build; only redo this if you want a from-scratch guarantee before a
39
+ human sign-off. Remove the build output and rebuild; a failed or incomplete build is a stop (no
40
+ fixtures, placeholder copy or `.htaccess` in the output).
41
+
42
+ - **Astro (SSG):** `rm -rf dist && npm run build && npm run check:dist`.
43
+ - **Next.js (SSR):** `rm -rf .next && npm run check && npm run build`.
44
+
45
+ ## 4 — Preview & approval (optional — only when a person is signing off)
46
+
47
+ When someone wants to see it first: start the local preview (Astro: `npm run preview`; Next.js:
48
+ `npx next start`), check desktop and mobile widths, navigation and a couple of key routes, and present
49
+ it for **explicit approval**. If preview tooling is unavailable, give the user the command and URL.
50
+ On an autonomous or temp-domain run, skip this — deploy will verify the live result anyway.
51
+
52
+ Then (after approval, or immediately if 3–4 were skipped): `state.json` → `stage: "deploy"`, fresh
53
+ `updated_at`; leave `provision` unchanged.
@@ -0,0 +1,171 @@
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 only)* | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200 |
120
+
121
+ > **Only WordPress has a `/cms` on the domain**, so the *Backend survived* check is WordPress-only.
122
+ > - **Ecommerce:** the backend is the **Hostinger Ecommerce store**, read over the external Storefront API
123
+ > (`api-ecommerce.hostinger.com`) — nothing a deploy touches lives on this domain. There is no `/cms`;
124
+ > confirm the storefront still fetches products as a frontend check if you like. **Only check that
125
+ > checkout can be reached when `site.json → ecommerce.payment_enabled` is `true`** — on a free account
126
+ > (`payment_enabled: false`) there is no checkout, so **skip that check entirely**; a catalog that serves
127
+ > 200 is a passing, complete deploy. Never fail or hold the deploy because checkout is unavailable.
128
+ > - **Webapp:** no content backend at all — the whole root is the app. Skip the check; `/cms` won't resolve.
129
+
130
+ A `completed` build whose root 403s almost always means the build settings weren't passed to
131
+ `hosting_deploy` (null settings) — pass them and redeploy. Caching can briefly serve a stale root;
132
+ re-check after ~30 s.
133
+
134
+ ## Step 6 — Record `deploy.json`
135
+
136
+ ```json
137
+ {
138
+ "build_uuid": "…",
139
+ "stack": "nextjs",
140
+ "rendering": "ssr",
141
+ "app_type": "next",
142
+ "output_directory": ".next",
143
+ "node_version": 22,
144
+ "method": "hosting_deploy (type: nodejs, settings passed inline)",
145
+ "deployed_at": "…",
146
+ "verified_at": "…",
147
+ "urls": { "live": "https://<domain>", "cms": "https://<domain>/cms" },
148
+ "checks": { "root": 200, "ssr_headers": true, "backend_preserved": true }
149
+ }
150
+ ```
151
+
152
+ No secrets in `deploy.json`. The `urls.cms` field and the `backend_preserved` check above are
153
+ **WordPress-only** — adjust the record to the backend:
154
+
155
+ - **WordPress:** keep `urls.cms` and `checks.backend_preserved` as shown.
156
+ - **Ecommerce:** omit `urls.cms`/`backend_preserved`; record the store URLs instead (`urls.storefront_api`,
157
+ `urls.store_admin` from `site.json`) and link the custom sales channel to the live domain —
158
+ `ecommerce_sales-channels_update { store_id, sales_channel_id, url }` (see the ecommerce provision step).
159
+ - **Webapp:** omit `urls.cms` and `backend_preserved` entirely — `urls` is just `{ "live": "https://<domain>" }`.
160
+
161
+ Update `state.json` (`stage` → `done`), then continue to Step 9 (done).
162
+
163
+ ---
164
+
165
+ ## Note — one deploy path, and never `type: "static"`
166
+
167
+ `hosting_deploy` is the only path: give its `path` either the source **directory** (it archives it for
168
+ you) or a local **archive** (`zip`, `tar`, `tar.gz`, `tgz`, `7z`) — there is no separate upload or build
169
+ step to fall back to. Either way pass the explicit `appType`/`outputDirectory`/`nodeVersion`/
170
+ `buildScript`/`packageManager`/`rootDirectory` from the table. Never deploy a headless site with
171
+ `type: "static"` — it serves the directory as plain files and can overwrite `/cms`.