@hostinger/mcp 3.2.2 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/SKILL.md +245 -36
- package/skills/headless/connect/SKILL.md +98 -0
- package/skills/headless/{entry → connect}/bootstrap.mjs +8 -3
- package/skills/headless/connect/tools/antigravity.md +54 -0
- package/skills/headless/connect/tools/claude-code.md +46 -0
- package/skills/headless/connect/tools/codex.md +48 -0
- package/skills/headless/connect/tools/cursor.md +109 -0
- package/skills/headless/connect/tools/devin.md +50 -0
- package/skills/headless/connect/tools/vscode.md +57 -0
- package/skills/headless/report.schema.json +149 -0
- package/skills/headless/run-log.mjs +662 -0
- package/skills/headless/steps/01-check-state/SKILL.md +209 -0
- package/skills/headless/steps/02-refine/SKILL.md +273 -0
- package/skills/headless/steps/03-provision/SKILL.md +117 -0
- package/skills/headless/steps/03-provision/ecommerce/SKILL.md +142 -0
- package/skills/headless/steps/03-provision/webapp/SKILL.md +154 -0
- package/skills/headless/steps/03-provision/wordpress/SKILL.md +249 -0
- package/skills/headless/steps/04-content-model/SKILL.md +228 -0
- package/skills/headless/steps/04-content-model/ecommerce/SKILL.md +75 -0
- package/skills/headless/steps/04-content-model/wordpress/SKILL.md +223 -0
- package/skills/headless/steps/04-content-model/wordpress/woocommerce.md +153 -0
- package/skills/headless/steps/05-design/SKILL.md +464 -0
- package/skills/headless/steps/05-design/logo-kit.mjs +201 -0
- package/skills/headless/steps/06-frontend/SKILL.md +160 -0
- package/skills/headless/steps/07-validate/SKILL.md +56 -0
- package/skills/headless/steps/08-deploy/SKILL.md +179 -0
- package/skills/headless/steps/09-done/SKILL.md +157 -0
- package/src/core/skills.js +171 -24
- package/src/core/skills.ts +171 -24
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- package/src/servers/agent-readiness.js +1 -1
- package/src/servers/agent-readiness.ts +1 -1
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/mail.js +1 -1
- package/src/servers/mail.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
- package/src/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
- package/skills/headless/entry/skill.md +0 -79
- package/skills/headless/references/DATABASE.md +0 -49
- package/skills/headless/references/DEPLOYMENT.md +0 -34
- package/skills/headless/references/SETUP.md +0 -35
- package/skills/headless/references/STORE.md +0 -52
- package/skills/headless/references/WORDPRESS.md +0 -51
|
@@ -0,0 +1,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, '&').replace(/</g, '<');
|
|
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`.
|