hostinger-api-mcp 3.10.4 → 3.11.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 +24 -0
- package/README.md +3 -18
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/audit-hosting/SKILL.md +1 -1
- package/skills/deploy-to-hosting/SKILL.md +7 -18
- package/skills/headless/steps/05-design/SKILL.md +43 -19
- package/skills/headless/steps/05-design/logo-kit.mjs +231 -6
- package/skills/headless/steps/08-deploy/SKILL.md +6 -1
- package/skills/migrate-to-hosting/SKILL.md +1 -1
- package/skills/troubleshoot-website/SKILL.md +2 -2
- package/src/core/catalog.js +2 -1
- package/src/core/catalog.ts +3 -2
- package/src/core/instructions.agency-hosting.js +1 -1
- package/src/core/instructions.agency-hosting.ts +1 -1
- package/src/core/instructions.js +1 -1
- package/src/core/instructions.ts +1 -1
- package/src/core/runtime.js +116 -27
- package/src/core/runtime.ts +116 -27
- package/src/core/skills.js +21 -21
- package/src/core/skills.ts +21 -21
- package/src/core/tools/all.js +23 -9
- package/src/core/tools/all.ts +23 -9
- package/src/core/tools/hosting.js +23 -9
- package/src/core/tools/hosting.ts +23 -9
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- package/src/servers/agent-readiness.js +1 -1
- package/src/servers/agent-readiness.ts +1 -1
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/mail.js +1 -1
- package/src/servers/mail.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
- package/src/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
- package/types.d.ts +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ Generated at release time from the diff between the previous and current
|
|
|
4
4
|
build. Tool changes are listed individually; everything else is summarised
|
|
5
5
|
by area.
|
|
6
6
|
|
|
7
|
+
## v3.11.1 — 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Operations
|
|
10
|
+
|
|
11
|
+
- Hid 1 operation from search (it still runs through `execute`): `hosting_nodejs_build-settings-from-repository`
|
|
12
|
+
- Changed the description and input schema of `hosting_deploy`
|
|
13
|
+
|
|
14
|
+
### Also in this release
|
|
15
|
+
|
|
16
|
+
- Documentation: `README.md`
|
|
17
|
+
- Bundled agent skills: `skills/deploy-to-hosting/SKILL.md`
|
|
18
|
+
- Request handling and authentication: `src/core/runtime.js`, `src/core/runtime.ts`
|
|
19
|
+
- Other files: `src/core/skills.js`, `src/core/skills.ts`
|
|
20
|
+
- Build and packaging: `types.d.ts`
|
|
21
|
+
|
|
22
|
+
## v3.11.0 — 2026-10-27
|
|
23
|
+
|
|
24
|
+
### Also in this release
|
|
25
|
+
|
|
26
|
+
- Bundled agent skills: 7 files
|
|
27
|
+
- Request handling and authentication: `src/core/catalog.js`, `src/core/catalog.ts`
|
|
28
|
+
- Server instructions: 4 files
|
|
29
|
+
- Other files: `src/core/skills.js`, `src/core/skills.ts`
|
|
30
|
+
|
|
7
31
|
## v3.10.4 — 2026-10-27
|
|
8
32
|
|
|
9
33
|
### Also in this release
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ pnpm update -g @hostinger/mcp
|
|
|
69
69
|
|
|
70
70
|
This package installs the following MCP server commands:
|
|
71
71
|
|
|
72
|
-
- `hostinger-api-mcp` — unified server over every operation (
|
|
72
|
+
- `hostinger-api-mcp` — unified server over every operation (409 total)
|
|
73
73
|
- `hostinger-agency-hosting-mcp` — 42 operations for agency-hosting
|
|
74
74
|
- `hostinger-agent-readiness-mcp` — 3 operations for agent-readiness
|
|
75
75
|
- `hostinger-billing-mcp` — 9 operations for billing
|
|
@@ -77,7 +77,7 @@ This package installs the following MCP server commands:
|
|
|
77
77
|
- `hostinger-domains-mcp` — 42 operations for domains
|
|
78
78
|
- `hostinger-ecommerce-mcp` — 29 operations for ecommerce
|
|
79
79
|
- `hostinger-horizons-mcp` — 6 operations for horizons
|
|
80
|
-
- `hostinger-hosting-mcp` —
|
|
80
|
+
- `hostinger-hosting-mcp` — 66 operations for hosting
|
|
81
81
|
- `hostinger-mail-mcp` — 38 operations for mail
|
|
82
82
|
- `hostinger-reach-mcp` — 62 operations for reach
|
|
83
83
|
- `hostinger-vps-mcp` — 67 operations for vps
|
|
@@ -1816,7 +1816,7 @@ interface in the provided website URL.
|
|
|
1816
1816
|
|
|
1817
1817
|
#### hosting_deploy
|
|
1818
1818
|
|
|
1819
|
-
Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored,
|
|
1819
|
+
Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, they are detected from the branch's package.json; pass appType, nodeVersion, outputDirectory, buildScript or entryFile to override them). Git needs GitHub or GitLab connected once in hPanel.
|
|
1820
1820
|
|
|
1821
1821
|
- **Method**: `custom`
|
|
1822
1822
|
- **Path**: `custom`
|
|
@@ -2248,21 +2248,6 @@ to it.
|
|
|
2248
2248
|
- **Method**: `POST`
|
|
2249
2249
|
- **Path**: `/api/hosting/v1/headless/agent-reports`
|
|
2250
2250
|
|
|
2251
|
-
#### hosting_nodejs_build-settings-from-repository
|
|
2252
|
-
|
|
2253
|
-
Determine the build settings for a Node.js application from the package.json file of a branch
|
|
2254
|
-
in a Git repository. Use before starting a Git-source build to find out the auto-detected
|
|
2255
|
-
settings instead of guessing them.
|
|
2256
|
-
|
|
2257
|
-
The repository is read through a Git installation: take `installation_uuid` from
|
|
2258
|
-
`List Git installations` and `owner` and `repository` from `List Git installation repositories`.
|
|
2259
|
-
|
|
2260
|
-
Returns 422 when the installation is not found, the repository or branch is not found, or
|
|
2261
|
-
the branch has no package.json.
|
|
2262
|
-
|
|
2263
|
-
- **Method**: `GET`
|
|
2264
|
-
- **Path**: `/api/hosting/v1/accounts/{username}/websites/{domain}/nodejs/builds/settings/from-repository`
|
|
2265
|
-
|
|
2266
2251
|
#### hosting_nodejs_list-environment-variables
|
|
2267
2252
|
|
|
2268
2253
|
Lists the Node.js environment variables currently set for the website. Values are always
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/hostinger/api-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.11.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@hostinger/mcp",
|
|
14
|
-
"version": "3.
|
|
14
|
+
"version": "3.11.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -30,7 +30,7 @@ Tell the user the size (plans, websites, WordPress installs) before the checks.
|
|
|
30
30
|
Per Shared and Cloud website (skip `builder` and `horizons`):
|
|
31
31
|
|
|
32
32
|
- `hosting_ssl_status` — flag `failed` (with `last_error`), `expired`, `not_installed` on a custom domain, `is_https_redirect_enabled: false`, and `expires_at` within 30 days when `is_lifetime` is false. Lifetime certificates renew on their own.
|
|
33
|
-
- Node.js sites: `hosting_get-deployment` (latest build failed? `build.state
|
|
33
|
+
- Node.js sites: `hosting_get-deployment` (latest build failed? `build.state`) and `hosting_nodejs_list-vulnerabilities` with `severities: ["critical", "high"]`.
|
|
34
34
|
|
|
35
35
|
Per hosting account (`username`): `hosting_databases_list` — flag `disk_usage_mb` above 80% of `max_size_mb`.
|
|
36
36
|
|
|
@@ -45,9 +45,9 @@ Archive rules for Agency (name archives `name_YYYYMMDD_HHMMSS.zip`) and for arch
|
|
|
45
45
|
- **Static:** build locally first. `index.html` must be at the archive root, not inside a folder: `cd dist && zip -r ../site_20260101_120000.zip .`
|
|
46
46
|
- **Node.js source:** no `node_modules/`, no build output (`dist/`, `.next/`, `build/`), no `.env*`, nothing matched by `.gitignore`; 50 MB at most. `git archive --format=zip -o app_20260101_120000.zip HEAD` produces exactly the committed files — mention that uncommitted changes are left out.
|
|
47
47
|
|
|
48
|
-
`hosting_deploy` and the `agency-hosting_deploy-*` operations read files from this machine, so only the local server has them.
|
|
48
|
+
`hosting_deploy` with `path` and the `agency-hosting_deploy-*` operations read files from this machine, so only the local server has them. The hosted server (`mcp.hostinger.com`) has `hosting_deploy` for Git repositories only (step 4) and `hosting_get-deployment` (step 3); to deploy files from this machine there, upload them yourself as below.
|
|
49
49
|
|
|
50
|
-
### Without the local deploy operations (
|
|
50
|
+
### Without the local deploy operations (local files on the hosted server; Agency on either server)
|
|
51
51
|
|
|
52
52
|
1. Get upload credentials: `hosting_files_generate-upload-url` (`username`, `domain`); Agency: `agency-hosting_files_generate-upload-url` (`website_uid`). Both return `url`, `auth_key` and `rest_auth_key`, which authenticate the upload instead of the API token.
|
|
53
53
|
2. Upload each file with TUS, where `DEST` is its path in the website's storage:
|
|
@@ -72,35 +72,24 @@ curl -sS -X PATCH "$URL/$DEST?override=true" -H "X-Auth: $AUTH_KEY" -H "X-Auth-R
|
|
|
72
72
|
|
|
73
73
|
## 3. Node.js builds
|
|
74
74
|
|
|
75
|
-
`hosting_deploy` returns a `buildUuid` at once; the build runs on the server and takes minutes. Track it with `hosting_get-deployment` (`domain`, optionally `buildUuid`; the latest build by default), polling every 10–20 s until `build.state` is `completed` or `failed`. It returns the build, its log and the stored build settings; pass the previous `logs.lines` as `fromLine` to read only new output.
|
|
75
|
+
`hosting_deploy` returns a `buildUuid` at once; the build runs on the server and takes minutes. Track it — and a build started with `hosting_nodejs_start-build` after an upload — with `hosting_get-deployment` (`domain`, optionally `buildUuid`; the latest build by default), polling every 10–20 s until `build.state` is `completed` or `failed`. It returns the build, its log and the stored build settings; pass the previous `logs.lines` as `fromLine` to read only new output.
|
|
76
76
|
|
|
77
77
|
- Failed build: the response already carries `analysis` (fetched once per build; the API allows 5 a minute).
|
|
78
78
|
- Wrong detection (framework, `output_directory`, or an entry file inside the build output, such as `dist/server.js` for a compiled app): deploy again with `appType`, `outputDirectory`, `buildScript` or `entryFile` set. An archive deploy sends its settings with the request and leaves the stored ones alone; a Git deploy also stores these values, so later pushes build with them.
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Local files on the hosted server, deployed from an upload (step 2):
|
|
81
81
|
|
|
82
|
-
-
|
|
83
|
-
- Failed build: `hosting_nodejs_analyse-failed-build` once per build (5 calls a minute), then `hosting_nodejs_build-logs` if the analysis is empty.
|
|
84
|
-
- Wrong detection: run `hosting_nodejs_start-build` with explicit values, `source_type: "archive"` and `source_options.archive_path` set to the archive's file name in the document root (check it is there with `hosting_files_list-website-and-directories`). Store the same values with `hosting_nodejs_update-build-settings`.
|
|
82
|
+
- Wrong detection: run `hosting_nodejs_start-build` again with explicit values, `source_type: "archive"` and `source_options.archive_path` set to the archive's file name in the document root (check it is there with `hosting_files_list-website-and-directories`). Store the same values with `hosting_nodejs_update-build-settings`.
|
|
85
83
|
- To review detected settings before any build: upload the archive with `hosting_files_generate-upload-url` (the TUS steps are in its description), then `hosting_nodejs_build-settings-from-archive`.
|
|
86
84
|
|
|
87
85
|
## 4. Auto-deploy from Git
|
|
88
86
|
|
|
89
|
-
On the local server, `hosting_deploy` with `git: { repository: "owner/name", branch }` does all of it: it finds the Git installation that can reach the repository and deploys it as the `type` you give. Always pass `type: "static"` or `"nodejs"` unless the site already runs a Node.js app (or you pass Node.js settings such as `appType`): a new site is not detected as Node.js, and a Node.js repository deployed as static would publish its source. Then:
|
|
87
|
+
On both the local and the hosted server, `hosting_deploy` with `git: { repository: "owner/name", branch }` does all of it: it finds the Git installation that can reach the repository and deploys it as the `type` you give. Always pass `type: "static"` or `"nodejs"` unless the site already runs a Node.js app (or you pass Node.js settings such as `appType`): a new site is not detected as Node.js, and a Node.js repository deployed as static would publish its source. Then:
|
|
90
88
|
- **PHP and static sites:** saves the auto-deployment settings, which deploys the branch immediately and again on every push;
|
|
91
|
-
- **Node.js sites:** starts a build of the branch with the stored build settings; later pushes build the same way.
|
|
89
|
+
- **Node.js sites:** starts a build of the branch with the stored build settings; later pushes build the same way. When none are stored, they are detected from the branch's `package.json` and stored for those pushes; pass `appType`, `nodeVersion`, `outputDirectory`, `buildScript` or `entryFile` to override them (a compiled app still needs `entryFile`).
|
|
92
90
|
|
|
93
91
|
With no active installation it stops and says so: the user connects GitHub or GitLab once in hPanel under Websites → Manage → Advanced → Git; there is no API for that step. Confirm with `hosting_get-deployment`, which returns the stored `gitAutoDeployment`.
|
|
94
92
|
|
|
95
|
-
On the hosted server, by hand:
|
|
96
|
-
|
|
97
|
-
1. `hosting_git_list-installations`. With no `active` installation (check `status: "pending"` and `"suspended"` too), the user connects GitHub or GitLab once in hPanel, as above.
|
|
98
|
-
2. `hosting_git_list-installation-repositories` with the installation `uuid` gives `owner`, `name` and `default_branch` (10 calls a minute).
|
|
99
|
-
3. `hosting_git_update-auto-deployment-settings` with `installation_uuid`, `owner`, `repository`, `branch` and optional `directory`.
|
|
100
|
-
- PHP and static sites deploy the branch immediately and again on every push.
|
|
101
|
-
- Node.js sites clone nothing on save: start the first build with `hosting_nodejs_start-build`, `source_type: "git"` and the same `source_options`. Later pushes build with the stored settings, so keep `hosting_nodejs_update-build-settings` correct.
|
|
102
|
-
4. Confirm with `hosting_git_auto-deployment-settings`.
|
|
103
|
-
|
|
104
93
|
Agency Plan sites have no Git operations; deploy archives.
|
|
105
94
|
|
|
106
95
|
## 5. Environment variables (Node.js)
|
|
@@ -307,14 +307,14 @@ palette (base and surface colours count).
|
|
|
307
307
|
### Build it with the logo kit
|
|
308
308
|
|
|
309
309
|
The kit (`logo-kit.mjs`, next to this file) does the error-prone parts: it outlines letters into
|
|
310
|
-
vector paths, sets text on a circle, draws the outline-and-shadow treatment, measures,
|
|
311
|
-
the output. You write the composition. Copy the kit into
|
|
312
|
-
and a redesign can re-run it:
|
|
310
|
+
vector paths, sets text on a circle, draws the outline-and-shadow treatment, measures, validates
|
|
311
|
+
the output, checks the sizes and renders a preview. You write the composition. Copy the kit into
|
|
312
|
+
`brand/` so the build is self-contained and a redesign can re-run it:
|
|
313
313
|
|
|
314
314
|
```bash
|
|
315
315
|
mkdir -p ~/.hostinger/<domain>/brand && cd ~/.hostinger/<domain>/brand
|
|
316
316
|
cp <the folder this SKILL.md is in>/logo-kit.mjs .
|
|
317
|
-
npm init -y >/dev/null && npm i opentype.js @fontsource/<display-slug>
|
|
317
|
+
npm init -y >/dev/null && npm i opentype.js @resvg/resvg-js @fontsource/<display-slug>
|
|
318
318
|
```
|
|
319
319
|
|
|
320
320
|
Write `brand/logo-build.mjs`, import from `./logo-kit.mjs`, and always run it from `brand/`
|
|
@@ -330,6 +330,9 @@ Write `brand/logo-build.mjs`, import from `./logo-kit.mjs`, and always run it fr
|
|
|
330
330
|
| `sticker(d, { fill, ink, strokeWidth, shadow, shadowColor, transform })` | Ink outline + hard offset shadow, for outlined directions. On dark backgrounds give the outline and shadow a palette colour that shows (`shadowColor` defaults to `ink`) |
|
|
331
331
|
| `svgDoc({ width, height, title, body })` | Root SVG with `role="img"` and a `<title>` |
|
|
332
332
|
| `check(svg)` | Throws on `NaN`, live `<text>`, a missing title or viewBox, or an `&` that isn't an XML entity. Run it on every file before writing |
|
|
333
|
+
| `tile(size, fill)` | The rounded tile for the mark and favicon; draw the symbol after it |
|
|
334
|
+
| `widthBudget(letterHeight, height)` | The widest viewBox that still fits 192 px on small screens, for a logo of that height; see below |
|
|
335
|
+
| `checkLogo({ logo, mark, favicon, letterHeight, background })` | Runs `check()` on all three files, checks their sizes and renders `brand/logo-preview.png`; see below |
|
|
333
336
|
|
|
334
337
|
Wrap every file in `svgDoc()`, never a hand-written `<svg>` or `<title>`: it escapes the site name,
|
|
335
338
|
and one bare `&` (as in "Salt & Pepper") makes the browser show nothing.
|
|
@@ -342,27 +345,48 @@ Never call opentype's `path.toPathData()` directly; the kit's serializer exists
|
|
|
342
345
|
| File | What | Must read clearly at |
|
|
343
346
|
|---|---|---|
|
|
344
347
|
| `brand/logo.svg` | The primary, mostly horizontal logo for the header | Its header display heights (desktop and small screens) |
|
|
345
|
-
| `brand/logo-mark.svg` | A square icon from the same idea (initial, monogram or the swapped symbol) on a
|
|
348
|
+
| `brand/logo-mark.svg` | A square icon from the same idea (initial, monogram or the swapped symbol) on a `tile()` in a palette colour | 32 px |
|
|
346
349
|
| `brand/favicon.svg` | The simplest form of the mark: same tile, no shadow, no thin details, outlines thickened if it has any | 16 px, on both light and dark browser tab bars |
|
|
347
350
|
| `brand/logo-build.mjs` | The script that built them, with `package.json`, kept so a redesign can adjust it | — |
|
|
348
351
|
|
|
349
352
|
Add `brand/logo-seal.svg` only if the direction uses a seal as a secondary mark. Delete
|
|
350
353
|
`brand/node_modules` when you're done; a redesign runs `npm i` in `brand/` first.
|
|
351
354
|
|
|
352
|
-
**
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
355
|
+
**Compose within the budget, so the first build passes.** The small-screen logo is at most 192 px
|
|
356
|
+
wide with letters at least 16 px tall, so the whole logo can be only about 10–12 times as wide as
|
|
357
|
+
its smallest letters are tall, whatever its height. On one line that's about ten letters plus a
|
|
358
|
+
symbol; a longer name stacks or tags, and a second word set at half size halves the room. Size the
|
|
359
|
+
viewBox in `logo-build.mjs` against `widthBudget(letterHeight, viewBoxHeight)`, the exact limit at
|
|
360
|
+
the heights the kit picks; with your own `heights`, the FAIL line gives the limit. Draw the mark's
|
|
361
|
+
symbol from simple, recognisable shapes that still read at 32 px; fine natural forms like petals or
|
|
362
|
+
leaves turn to mush.
|
|
363
|
+
|
|
364
|
+
**Check the sizes and look at it after every build.** `letterHeight` is in viewBox units: the
|
|
365
|
+
`xHeight` from `layout()` when the logo is mostly lowercase, `capHeight` when it's mostly capitals;
|
|
366
|
+
for mixed fonts, the smaller word's. End `logo-build.mjs` with:
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
checkLogo({ logo, mark, favicon, letterHeight, background: '<page background colour>' });
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
It picks the header heights for you: `--logo-h-sm` is the smallest multiple of 8 that makes the
|
|
373
|
+
letters 16 px, and `--logo-h` the same but at least 48 px, both at most 80 px, so a stacked lockup
|
|
374
|
+
keeps its words large or sets the mark beside them. Pass `heights: { desktop, small }` only when the
|
|
375
|
+
direction needs a larger header logo. It first prints `--logo-h`, `--logo-h-sm` and the **Measured**
|
|
376
|
+
line for `design.md`, then PASS or FAIL for the logo's letters ≥ 16 px at both
|
|
377
|
+
heights, width ≤ 192 px on small screens, headroom around everything drawn and heights in multiples
|
|
378
|
+
of 8; for the mark and favicon sitting on a `tile()` with nothing clipped and room around
|
|
379
|
+
the symbol; and for no shadow on the favicon. Every FAIL names its fixes and the number to reach. It
|
|
380
|
+
then renders `logo-preview.png` (the logo at both heights on the page background, the mark at 32 px
|
|
381
|
+
and favicon at 16 px on light and dark strips, each also enlarged), and throws if anything failed.
|
|
382
|
+
|
|
383
|
+
Then read `logo-preview.png`; don't search for another renderer or read the SVG source. Rebuild only
|
|
384
|
+
for a FAIL line or something in the preview that reads wrong, such as collisions, a clipped or
|
|
385
|
+
clearly off-centre mark, or a favicon detail that turns to mush, and fix everything you saw in one
|
|
386
|
+
rebuild. Optical nudges of a few pixels and the header height aren't reasons to rebuild. A symbol
|
|
387
|
+
gets one redraw: if it still reads wrong, replace it with a simpler form of the idea, such as the
|
|
388
|
+
initial, in the next build. If the kit says "preview not rendered", rely on the size check and list
|
|
389
|
+
"logo not visually checked" under gaps in the final report.
|
|
366
390
|
|
|
367
391
|
The logo is exempt from text-contrast minimums, but the header link around it needs `alt` = the site
|
|
368
392
|
name and a target of at least 24 px.
|
|
@@ -6,18 +6,27 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Workflow (from ~/.hostinger/<domain>/brand/, so packages and this kit sit next to the build):
|
|
8
8
|
// cp <installed skill>/steps/05-design/logo-kit.mjs .
|
|
9
|
-
// npm init -y && npm i opentype.js @fontsource/<display-family-slug>
|
|
9
|
+
// npm init -y && npm i opentype.js @resvg/resvg-js @fontsource/<display-family-slug>
|
|
10
10
|
// node logo-build.mjs // your script: import { … } from './logo-kit.mjs'
|
|
11
|
+
// // then read logo-preview.png
|
|
11
12
|
// rm -rf node_modules // package.json keeps the deps; a redesign runs `npm i` again
|
|
12
13
|
//
|
|
13
14
|
// Packages resolve from the current working directory, so run the build from brand/.
|
|
14
15
|
|
|
15
16
|
import { createRequire } from 'node:module';
|
|
16
|
-
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
17
18
|
import { join } from 'node:path';
|
|
18
19
|
|
|
19
20
|
const requireFromCwd = createRequire(join(process.cwd(), 'logo-build.js'));
|
|
20
21
|
const opentype = requireFromCwd('opentype.js');
|
|
22
|
+
// Optional: without resvg the kit still builds logos, it just can't measure what's drawn or render.
|
|
23
|
+
const Resvg = (() => {
|
|
24
|
+
try {
|
|
25
|
+
return requireFromCwd('@resvg/resvg-js').Resvg;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
21
30
|
|
|
22
31
|
// @fontsource splits each weight into unicode subsets; `latin` alone has no ą č ę ė į š ų ū ž,
|
|
23
32
|
// ő, ł, đ… so load every subset that exists and look each character up across them.
|
|
@@ -62,6 +71,8 @@ export function loadFont(slug, weight = 400, style = 'normal') {
|
|
|
62
71
|
return primary;
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
const VIEWBOX = /viewBox="0 0 ([\d.]+) ([\d.]+)"/;
|
|
75
|
+
|
|
65
76
|
const num = (v) => {
|
|
66
77
|
if (!Number.isFinite(v)) throw new Error(`logo-kit: non-finite coordinate ${v}`);
|
|
67
78
|
return +v.toFixed(2);
|
|
@@ -174,11 +185,16 @@ export function arcText(font, text, { cx, cy, r, size, startDeg = -90, fill = tr
|
|
|
174
185
|
export function sticker(d, { fill, ink = '#000', strokeWidth = 8, shadow = 8, shadowColor = ink, transform = '' }) {
|
|
175
186
|
const t = transform ? ` transform="${transform}"` : '';
|
|
176
187
|
const shade = shadow
|
|
177
|
-
? `<path d="${d}" fill="${shadowColor}" stroke="${shadowColor}" stroke-width="${strokeWidth}" stroke-linejoin="round" transform="translate(${shadow} ${shadow})"/>`
|
|
188
|
+
? `<path data-kit="shadow" d="${d}" fill="${shadowColor}" stroke="${shadowColor}" stroke-width="${strokeWidth}" stroke-linejoin="round" transform="translate(${shadow} ${shadow})"/>`
|
|
178
189
|
: '';
|
|
179
190
|
return `<g${t}>${shade}<path d="${d}" fill="${fill}" stroke="${ink}" stroke-width="${strokeWidth}" stroke-linejoin="round" paint-order="stroke"/></g>`;
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
/** The solid rounded tile behind `logo-mark.svg` and `favicon.svg`; draw the symbol after it. */
|
|
194
|
+
export function tile(size, fill) {
|
|
195
|
+
return `<rect data-kit="tile" width="${num(size)}" height="${num(size)}" rx="${num(size / 4)}" fill="${fill}"/>`;
|
|
196
|
+
}
|
|
197
|
+
|
|
182
198
|
/** Root SVG with an accessible name. Pass a distinct `id` if two logos are inlined on one page. */
|
|
183
199
|
export function svgDoc({ width, height, title, body, id = 'logo-title' }) {
|
|
184
200
|
const esc = String(title).replace(/&/g, '&').replace(/</g, '<');
|
|
@@ -192,6 +208,12 @@ export function svgDoc({ width, height, title, body, id = 'logo-title' }) {
|
|
|
192
208
|
* @returns {{ bytes: number }}
|
|
193
209
|
*/
|
|
194
210
|
export function check(svg) {
|
|
211
|
+
const problems = markupProblems(svg);
|
|
212
|
+
if (problems.length) throw new Error(`logo-kit check failed: ${problems.join('; ')}`);
|
|
213
|
+
return { bytes: Buffer.byteLength(svg) };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function markupProblems(svg) {
|
|
195
217
|
const problems = [];
|
|
196
218
|
if (/NaN|undefined|Infinity/.test(svg)) problems.push('contains NaN/undefined/Infinity');
|
|
197
219
|
if (/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[\dA-Fa-f]+);)/.test(svg)) {
|
|
@@ -199,7 +221,210 @@ export function check(svg) {
|
|
|
199
221
|
}
|
|
200
222
|
if (/<text[\s>]/.test(svg)) problems.push('contains <text>; outline the letters with textPath/glyphPath');
|
|
201
223
|
if (!/<title[^>]*>[^<]+<\/title>/.test(svg)) problems.push('missing <title>');
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
if (!VIEWBOX.test(svg)) problems.push('missing or invalid viewBox');
|
|
225
|
+
return problems;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const MIN_LETTER_PX = 16;
|
|
229
|
+
const MAX_SMALL_WIDTH_PX = 192;
|
|
230
|
+
const MIN_DESKTOP_PX = 48;
|
|
231
|
+
const MAX_HEADER_PX = 80; // automatic heights only: taller header logos crowd the nav, and stacked lockups reached 144
|
|
232
|
+
const HEADROOM = 0.02; // of the viewBox height, on every edge
|
|
233
|
+
const CLIP_SLACK = 0.5; // viewBox units of anti-aliasing a "nothing outside the viewBox" check allows
|
|
234
|
+
|
|
235
|
+
function viewBoxOf(svg) {
|
|
236
|
+
const m = svg.match(VIEWBOX);
|
|
237
|
+
if (!m) throw new Error('logo-kit: missing or invalid viewBox');
|
|
238
|
+
return { width: +m[1], height: +m[2] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Gap between everything drawn (strokes, shadows, transforms) and each viewBox edge, smallest first. */
|
|
242
|
+
function edgeGaps(svg, vb) {
|
|
243
|
+
const b = new Resvg(svg).getBBox();
|
|
244
|
+
if (!b) return [];
|
|
245
|
+
const gaps = { top: b.y, bottom: vb.height - b.y - b.height, left: b.x, right: vb.width - b.x - b.width };
|
|
246
|
+
return Object.entries(gaps).sort((a, b) => a[1] - b[1]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const px = (v) => +v.toFixed(1);
|
|
250
|
+
const ceil1 = (v) => Math.ceil(v * 10) / 10;
|
|
251
|
+
const ceil8 = (v) => Math.ceil(v / 8) * 8;
|
|
252
|
+
const floor8 = (v) => Math.floor(v / 8) * 8;
|
|
253
|
+
|
|
254
|
+
const minHeaderPx = (letterHeight, height) => ceil8((MIN_LETTER_PX * height) / letterHeight);
|
|
255
|
+
const maxWidthAt = (height, smallPx) => Math.floor((MAX_SMALL_WIDTH_PX * height) / smallPx);
|
|
256
|
+
|
|
257
|
+
function autoHeights(letterHeight, height) {
|
|
258
|
+
const small = Math.min(minHeaderPx(letterHeight, height), MAX_HEADER_PX);
|
|
259
|
+
return { desktop: Math.max(small, MIN_DESKTOP_PX), small };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The widest viewBox a logo of this height can have and still fit 192 px on small screens at the
|
|
264
|
+
* height `checkLogo` picks. Call it while composing, before the first build: it is roughly
|
|
265
|
+
* 10–12 × `letterHeight`, so a long name stacks or tags instead of running on one line.
|
|
266
|
+
* @param {number} letterHeight viewBox units, as passed to `checkLogo`
|
|
267
|
+
* @param {number} height the viewBox height
|
|
268
|
+
*/
|
|
269
|
+
export function widthBudget(letterHeight, height) {
|
|
270
|
+
return maxWidthAt(height, autoHeights(letterHeight, height).small);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Collects PASS/FAIL/SKIP lines; `print` returns how many failed. */
|
|
274
|
+
function sizeReport(label, vb) {
|
|
275
|
+
const lines = [];
|
|
276
|
+
return {
|
|
277
|
+
rule(ok, text, fix) {
|
|
278
|
+
lines.push(ok ? `PASS ${text}` : `FAIL ${text} → ${fix}`);
|
|
279
|
+
},
|
|
280
|
+
skip(text) {
|
|
281
|
+
lines.push(`SKIP ${text}`);
|
|
282
|
+
},
|
|
283
|
+
headroom(svg, of) {
|
|
284
|
+
if (!Resvg) return this.skip('headroom: install @resvg/resvg-js to measure it');
|
|
285
|
+
const [edge, gap] = edgeGaps(svg, vb)[0] ?? [];
|
|
286
|
+
if (edge === undefined) return this.skip(`headroom: nothing drawn on ${of}`);
|
|
287
|
+
const min = HEADROOM * vb.height;
|
|
288
|
+
this.rule(gap >= min, `headroom ${px(gap)} at the ${edge} of ${of} (≥ ${px(min)} viewBox units)`,
|
|
289
|
+
`add ${ceil1(min - gap)} viewBox units at the ${edge}: grow the viewBox on that side or move the drawing in; accents, descenders, tilt, outline and shadow all need room`);
|
|
290
|
+
},
|
|
291
|
+
print() {
|
|
292
|
+
console.log(`${label} (viewBox ${px(vb.width)} × ${px(vb.height)}):`);
|
|
293
|
+
for (const line of lines) console.log(` ${line}`);
|
|
294
|
+
return lines.filter((l) => l.startsWith('FAIL')).length;
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function logoSizes(svg, { letterHeight, heights }) {
|
|
300
|
+
if (!letterHeight || (heights && (!heights.desktop || !heights.small))) {
|
|
301
|
+
throw new TypeError('logo-kit checkLogo: pass letterHeight, and heights: { desktop, small } only to set both');
|
|
302
|
+
}
|
|
303
|
+
const vb = viewBoxOf(svg);
|
|
304
|
+
const report = sizeReport('logo.svg', vb);
|
|
305
|
+
const minHeaderH = minHeaderPx(letterHeight, vb.height);
|
|
306
|
+
const maxSmall = floor8((MAX_SMALL_WIDTH_PX * vb.height) / vb.width);
|
|
307
|
+
const { desktop, small } = heights ?? autoHeights(letterHeight, vb.height);
|
|
308
|
+
const lettersAt = (h) => (letterHeight / vb.height) * h;
|
|
309
|
+
const width = (vb.width / vb.height) * small;
|
|
310
|
+
const canLowerSmall = small > minHeaderH && minHeaderH <= maxSmall;
|
|
311
|
+
const widthFixes = [
|
|
312
|
+
...(canLowerSmall ? [`--logo-h-sm ${minHeaderH === maxSmall ? minHeaderH : `${minHeaderH}–${maxSmall}`} px passes both`] : []),
|
|
313
|
+
`make the viewBox at most ${maxWidthAt(vb.height, small)} units wide (now ${px(vb.width)}) at this height: stack or tag the words, tighten gaps, or show logo-mark.svg with the name beside it on small screens`,
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
report.rule(desktop % 8 === 0 && small % 8 === 0, `heights ${desktop} / ${small} px are multiples of 8`, 'round them to multiples of 8');
|
|
317
|
+
const capped = !heights && minHeaderH > MAX_HEADER_PX;
|
|
318
|
+
const minLetterAtCap = ceil1((MIN_LETTER_PX * vb.height) / MAX_HEADER_PX);
|
|
319
|
+
const maxHeightAtCap = Math.floor((letterHeight * MAX_HEADER_PX) / MIN_LETTER_PX);
|
|
320
|
+
const lettersFixes = (name) => capped
|
|
321
|
+
? [`the header logo stops at ${MAX_HEADER_PX} px: make letterHeight at least ${minLetterAtCap} viewBox units (now ${px(letterHeight)})`,
|
|
322
|
+
`make the viewBox at most ${maxHeightAtCap} units tall (now ${px(vb.height)}): trim empty space or set the mark beside the words instead of above them`]
|
|
323
|
+
: [`raise ${name} to ${minHeaderH} px or more`, 'enlarge the letters inside the viewBox', 'trim empty viewBox space'];
|
|
324
|
+
const letterChecks = desktop === small ? [['--logo-h and --logo-h-sm', small]] : [['--logo-h', desktop], ['--logo-h-sm', small]];
|
|
325
|
+
for (const [name, h] of letterChecks) {
|
|
326
|
+
report.rule(lettersAt(h) >= MIN_LETTER_PX, `letters ${px(lettersAt(h))} px tall at ${name} ${h} px (≥ ${MIN_LETTER_PX})`,
|
|
327
|
+
lettersFixes(name).join(', or '));
|
|
328
|
+
}
|
|
329
|
+
report.rule(width <= MAX_SMALL_WIDTH_PX, `width ${px(width)} px at --logo-h-sm ${small} px (≤ ${MAX_SMALL_WIDTH_PX})`,
|
|
330
|
+
widthFixes.join(', or '));
|
|
331
|
+
report.headroom(svg, 'the viewBox');
|
|
332
|
+
const measured = `- **Measured:** letters ${px(lettersAt(small))} px tall at \`--logo-h-sm\`; width ${px(width)} px at \`--logo-h-sm\``;
|
|
333
|
+
return { report, heights: { desktop, small }, measured };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function tileChecks(svg, { favicon = false } = {}) {
|
|
337
|
+
const vb = viewBoxOf(svg);
|
|
338
|
+
const report = sizeReport(favicon ? 'favicon.svg' : 'logo-mark.svg', vb);
|
|
339
|
+
const tileTag = svg.match(/<rect data-kit="tile"[^>]*\/>/)?.[0];
|
|
340
|
+
report.rule(Boolean(tileTag), 'drawn on a tile()', "draw the tile with the kit's tile(size, fill)");
|
|
341
|
+
if (!Resvg) report.skip('clipping: install @resvg/resvg-js to measure it');
|
|
342
|
+
else {
|
|
343
|
+
const gap = edgeGaps(svg, vb)[0]?.[1] ?? 0;
|
|
344
|
+
report.rule(gap >= -CLIP_SLACK, 'nothing drawn outside the viewBox', 'shrink or move the drawing in; the tile already fills the viewBox');
|
|
345
|
+
}
|
|
346
|
+
if (tileTag) report.headroom(svg.replace(tileTag, ''), 'the tile');
|
|
347
|
+
if (favicon) {
|
|
348
|
+
report.rule(!/data-kit="shadow"|<filter|drop-shadow/.test(svg), 'no shadow', 'pass shadow: 0 to sticker() and draw no other shadow');
|
|
349
|
+
}
|
|
350
|
+
return report;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function renderPreview({ logo, mark, favicon, heights, background, out }) {
|
|
354
|
+
const render = (svg, h, zoom) => {
|
|
355
|
+
const img = new Resvg(svg, { fitTo: { mode: 'height', value: h } }).render();
|
|
356
|
+
return { w: img.width * zoom, h: img.height * zoom, href: `data:image/png;base64,${img.asPng().toString('base64')}` };
|
|
357
|
+
};
|
|
358
|
+
const tiles = (fill) => ({ fill, items: [render(mark, 32, 1), render(mark, 32, 4), render(favicon, 16, 1), render(favicon, 16, 8)] });
|
|
359
|
+
const bands = [
|
|
360
|
+
{
|
|
361
|
+
label: `logo.svg at --logo-h ${heights.desktop} px and --logo-h-sm ${heights.small} px, then 2×, on the page background`,
|
|
362
|
+
fill: background,
|
|
363
|
+
items: [render(logo, heights.desktop, 1), render(logo, heights.small, 1), render(logo, heights.desktop, 2), render(logo, heights.small, 2)],
|
|
364
|
+
},
|
|
365
|
+
{ label: 'light: logo-mark.svg at 32 px and 4×, favicon.svg at 16 px and 8×', ...tiles('#f1f3f4') },
|
|
366
|
+
{ label: 'dark: the same', ...tiles('#202124') },
|
|
367
|
+
];
|
|
368
|
+
const PAD = 16;
|
|
369
|
+
const LABEL = 28;
|
|
370
|
+
let body = '';
|
|
371
|
+
let y = 0;
|
|
372
|
+
let width = 480;
|
|
373
|
+
for (const { label, fill, items } of bands) {
|
|
374
|
+
const rowH = Math.max(...items.map((i) => i.h)) + PAD * 2;
|
|
375
|
+
body += `<text x="${PAD}" y="${y + 19}" font-family="sans-serif" font-size="13" fill="#555">${label}</text>`;
|
|
376
|
+
y += LABEL;
|
|
377
|
+
body += `<rect y="${y}" width="100%" height="${rowH}" fill="${fill}"/>`;
|
|
378
|
+
let x = PAD;
|
|
379
|
+
for (const i of items) {
|
|
380
|
+
body += `<image href="${i.href}" x="${x}" y="${y + (rowH - i.h) / 2}" width="${i.w}" height="${i.h}" image-rendering="optimizeSpeed"/>`;
|
|
381
|
+
x += i.w + PAD * 2;
|
|
382
|
+
}
|
|
383
|
+
width = Math.max(width, x - PAD);
|
|
384
|
+
y += rowH;
|
|
385
|
+
}
|
|
386
|
+
const sheet = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${y}"><rect width="100%" height="100%" fill="#fff"/>${body}</svg>`;
|
|
387
|
+
writeFileSync(out, new Resvg(sheet).render().asPng());
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check the three finished files and render one PNG to look at instead of the SVG source.
|
|
392
|
+
* Throws first if any file fails `check()`.
|
|
393
|
+
* - logo.svg: letters ≥ 16 px at both header heights, width ≤ 192 px on small screens, 2% headroom
|
|
394
|
+
* around everything drawn, heights in multiples of 8. Without `heights`, `--logo-h-sm` is the
|
|
395
|
+
* smallest multiple of 8 that makes the letters 16 px, and `--logo-h` the same but at least 48 px;
|
|
396
|
+
* both stop at 80 px, so letters too small for 80 px fail.
|
|
397
|
+
* - logo-mark.svg, favicon.svg: drawn on a `tile()`, nothing outside the viewBox, 2% headroom
|
|
398
|
+
* around the symbol; the favicon has no shadow.
|
|
399
|
+
* - logo-preview.png: the logo at both heights on the page background, the mark at 32 px and the
|
|
400
|
+
* favicon at 16 px on a light and a dark strip, each also enlarged with hard pixels.
|
|
401
|
+
* Prints the heights and the Measured line first, then every PASS/FAIL line, each failure with its
|
|
402
|
+
* fixes, renders the preview, then throws if anything failed.
|
|
403
|
+
* @param {object} o
|
|
404
|
+
* @param {number} o.letterHeight viewBox units: `layout().xHeight` (mostly lowercase) or
|
|
405
|
+
* `.capHeight` (capitals) of the smaller word
|
|
406
|
+
* @param {{ desktop: number, small: number }} [o.heights] set both header heights yourself
|
|
407
|
+
* @param {string} [o.background] the page background colour
|
|
408
|
+
* @returns {{ heights: { desktop: number, small: number }, measured: string }} heights for
|
|
409
|
+
* `--logo-h` / `--logo-h-sm`, and the design.md → Logo → **Measured** line
|
|
410
|
+
*/
|
|
411
|
+
export function checkLogo({ logo, mark, favicon, letterHeight, heights, background = '#ffffff', out = 'logo-preview.png' }) {
|
|
412
|
+
// resvg can't measure or render a file check() rejects, so these fail before any size line.
|
|
413
|
+
const broken = Object.entries({ 'logo.svg': logo, 'logo-mark.svg': mark, 'favicon.svg': favicon })
|
|
414
|
+
.flatMap(([name, svg]) => markupProblems(svg).map((p) => `${name}: ${p}`));
|
|
415
|
+
if (broken.length) throw new Error(`logo-kit checkLogo: ${broken.join('; ')}`);
|
|
416
|
+
const sizes = logoSizes(logo, { letterHeight, heights });
|
|
417
|
+
// First, so piping the output through `head` can't cut them off.
|
|
418
|
+
console.log(`--logo-h: ${sizes.heights.desktop}px; --logo-h-sm: ${sizes.heights.small}px`);
|
|
419
|
+
console.log(sizes.measured);
|
|
420
|
+
const reports = [sizes.report, tileChecks(mark), tileChecks(favicon, { favicon: true })];
|
|
421
|
+
const failed = reports.reduce((n, r) => n + r.print(), 0);
|
|
422
|
+
if (Resvg) {
|
|
423
|
+
renderPreview({ logo, mark, favicon, heights: sizes.heights, background, out });
|
|
424
|
+
console.log(`preview: read ${out}`);
|
|
425
|
+
} else {
|
|
426
|
+
console.log('preview not rendered: @resvg/resvg-js is not installed; list "logo not visually checked" under gaps');
|
|
427
|
+
}
|
|
428
|
+
if (failed) throw new Error(`logo-kit checkLogo: ${failed} check(s) failed, see the FAIL lines above`);
|
|
429
|
+
return { heights: sizes.heights, measured: sizes.measured };
|
|
205
430
|
}
|
|
@@ -12,6 +12,11 @@ serves the result. **Out:** `deploy.json`.
|
|
|
12
12
|
This is an **instance-barrier** step: run it only after `provision.status == "completed"` and validate
|
|
13
13
|
(if run) has passed. All operations go through the connector (`execute`).
|
|
14
14
|
|
|
15
|
+
> **Connected to the hosted server (`mcp.hostinger.com`)?** Its `hosting_deploy` deploys Git
|
|
16
|
+
> repositories only and cannot read a local `path`. Run this step through the local
|
|
17
|
+
> `hostinger-api-mcp` server instead (see the connect step:
|
|
18
|
+
> `npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp`).
|
|
19
|
+
|
|
15
20
|
**In:** the project in `~/.hostinger/<domain>/src/`, `site.json` (`username`, `urls`), `state.json`
|
|
16
21
|
(`frontend.framework`, `frontend.rendering`).
|
|
17
22
|
|
|
@@ -188,7 +193,7 @@ Update `state.json` (`stage` → `done`), then continue to Step 9 (done).
|
|
|
188
193
|
|
|
189
194
|
## Note — one deploy path, and never `type: "static"`
|
|
190
195
|
|
|
191
|
-
`hosting_deploy` is the only path: give its `path` either the source **directory** (it archives it for
|
|
196
|
+
`hosting_deploy` on the local server is the only path: give its `path` either the source **directory** (it archives it for
|
|
192
197
|
you) or a local **archive** (`zip`, `tar`, `tar.gz`, `tgz`, `7z`) — there is no separate upload or build
|
|
193
198
|
step to fall back to. Either way pass the explicit `appType`/`outputDirectory`/`nodeVersion`/
|
|
194
199
|
`buildScript`/`packageManager`/`rootDirectory` from the table. Never deploy a headless site with
|
|
@@ -41,7 +41,7 @@ Create the Hostinger website on the domain being migrated, not on a free subdoma
|
|
|
41
41
|
|
|
42
42
|
## 3. Import
|
|
43
43
|
|
|
44
|
-
`hosting_deploy` and `agency-hosting_deploy-php-application` read files from this machine
|
|
44
|
+
`hosting_deploy` with `path` and `agency-hosting_deploy-php-application` read files from this machine, so only the local `hostinger-api-mcp` server has them; the hosted server's `hosting_deploy` deploys Git repositories only. On the hosted server (`mcp.hostinger.com`), upload the files first as described in the `deploy-to-hosting` skill ("Without the local deploy operations") and use the operation named in brackets below.
|
|
45
45
|
|
|
46
46
|
**WordPress, Shared and Cloud:** `hosting_deploy` with `domain`, `path` (the site directory or its archive), `databaseDump` and `type: "wordpress-site"` uploads both, extracts the files and imports the database; large sites take several minutes. Always pass `type`: left to detection, a directory without `wp-config.php` or `wp-content` at its top level is taken for a plain site and the dump is not imported. [Upload `site.zip` and `dump.sql`, then `wordpress_installations_import-website` with `archive_path` and `sql_path`.] Then `wordpress_installations_detect` with `username` and poll `wordpress_installations_list` with `domain` until the install appears with `is_valid: true`.
|
|
47
47
|
|
|
@@ -44,9 +44,9 @@ dig +short NS DOMAIN; dig +short A DOMAIN; dig +short CAA DOMAIN
|
|
|
44
44
|
|
|
45
45
|
### Node.js (`nodejs`)
|
|
46
46
|
|
|
47
|
-
Batch `hosting_get-deployment` (the latest build with its log, failure analysis and stored settings), `hosting_nodejs_runtime-logs` (`period: "1h"`, `levels: ["ERROR", "WARN"]`) and `hosting_nodejs_list-environment-variables` (keys only; values always come back masked).
|
|
47
|
+
Batch `hosting_get-deployment` (the latest build with its log, failure analysis and stored settings), `hosting_nodejs_runtime-logs` (`period: "1h"`, `levels: ["ERROR", "WARN"]`) and `hosting_nodejs_list-environment-variables` (keys only; values always come back masked).
|
|
48
48
|
|
|
49
|
-
- Latest build `failed`: `hosting_get-deployment` already holds the `analysis` (fetched once per build — the API re-runs it on every call and allows 5 a minute). A wrongly detected framework, output directory or missing `entry_file` is the usual cause: compare the returned `buildSettings` with the project and deploy again with corrected `appType`, `outputDirectory`, `buildScript` or `entryFile`; a Git deploy stores them, so pushes build correctly too.
|
|
49
|
+
- Latest build `failed`: `hosting_get-deployment` already holds the `analysis` (fetched once per build — the API re-runs it on every call and allows 5 a minute). A wrongly detected framework, output directory or missing `entry_file` is the usual cause: compare the returned `buildSettings` with the project and deploy again with corrected `appType`, `outputDirectory`, `buildScript` or `entryFile`; a Git deploy stores them, so pushes build correctly too. A site deployed on the hosted server from uploaded files has no `hosting_deploy` to repeat: rebuild through `hosting_nodejs_start-build` with the corrected values and store them with `hosting_nodejs_update-build-settings`.
|
|
50
50
|
- Build `completed` but the app errors: read the runtime logs; entries older than `last_deployed_at` belong to the previous deploy. A variable the code reads but the key list lacks is fixed with the `deploy-to-hosting` skill's environment step. `Access denied for user '…'@'::1'` means the app connects to `localhost` — `DB_HOST` must be `127.0.0.1`.
|
|
51
51
|
- Hung process: `hosting_nodejs_restart-application` (restarts without rebuilding).
|
|
52
52
|
|