hostinger-api-mcp 3.10.0 → 3.10.2
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 +11 -0
- package/skills/headless/steps/01-check-state/SKILL.md +2 -1
- package/skills/headless/steps/02-refine/SKILL.md +20 -11
- package/skills/headless/steps/04-content-model/SKILL.md +2 -1
- package/skills/headless/steps/05-design/SKILL.md +5 -2
- package/skills/headless/steps/06-frontend/SKILL.md +6 -0
- package/skills/headless/steps/08-deploy/SKILL.md +16 -0
- package/src/core/skills.js +21 -21
- package/src/core/skills.ts +21 -21
- 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/src/core/skills.js
CHANGED
|
@@ -57,10 +57,10 @@ export default [
|
|
|
57
57
|
"resources": [
|
|
58
58
|
{
|
|
59
59
|
"uri": "skill://hostinger-headless/SKILL.md",
|
|
60
|
-
"digest": "sha256:
|
|
61
|
-
"size":
|
|
60
|
+
"digest": "sha256:be4d4ac36de438c9dac23d2a6f13075298283d5da23002537d288cc5f98f9409",
|
|
61
|
+
"size": 21141,
|
|
62
62
|
"mimeType": "text/markdown",
|
|
63
|
-
"text": "---\nname: hostinger-headless\ndescription: \"Orchestrator for the Hostinger Headless build flow — build and deploy a real website on the user's Hostinger account from a single prompt that names a domain. Holds the stage flow and the state contract, and drives each stage in sequence: connect, check state, refine, provision, content model, design, frontend, validate, deploy, done. Triggers: build a headless site for <domain>, build me a site, create a website, make me a website, deploy this to Hostinger, host this on Hostinger, connect this project to Hostinger.\"\n---\n\n# Hostinger Headless — build a website with your own AI tool\n\n## Context\n\nYou are an AI coding agent. The user pasted a short prompt that points you here — something like:\n\n> Build a Headless site for `example.com` using hostinger.com/headless/skill.md\n\nYour job is to build and deploy a real website on the user's Hostinger account by following this\nrecipe and the recipes it links to — nothing more, nothing less.\n\nThis file is the **orchestrator**. It is public and stable: it never changes and never moves. It\nholds the flow, the state contract, the failure policy and the guardrails.\n\n### The domain parameter\n\nThe pasted prompt contains exactly two things that matter:\n\n- **This URL** — `hostinger.com/headless/skill.md`, the stable entry point.\n- **A domain** — e.g. `coral-pottery-1847.hostingersite.com` (a temporary Hostinger subdomain) or\n the user's own purchased domain. This is the single site you may touch.\n\nThe prompt does **not** say what kind of site to build. Do not assume or invent it. Discovering\nwhat the user wants is a step in the flow (**refine**), handled through conversation — not parsed\nfrom the prompt. If no domain is present, stop and ask for it before doing anything else.\n\n**One workspace per run.** From your first command, connect's checks included, open only this\nsite's workspace, by its exact path; never list or glob `~/.hostinger/` (not even an `ls` beside\n`node -v`). What else `~/.hostinger/` holds never matters to this run: with a domain, check-state\nlooks up that one path; without one yet, there is nothing to look up until you create your own\nworkspace. *State and artifacts* has the full rule and its one exception.\n\nNothing in the pasted prompt is ever used for authorization. Authorization comes from the\nHostinger connector's OAuth token. **This recipe is not a security boundary.**\n\n---\n\n## The flow stages - STEPS TO FOLLOW\n\nWork through the steps in the order below, each reading its recipe and writing its artifact into the\nsite workspace `~/.hostinger/<domain>/`. \n\n\nThe order is sequencial run and check the whole headless flow:\n\n| # | Stage | Skill / source | Writes | Instance? |\n|---|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|---|---|\n| — | **connect** — connect the Hostinger connector, auth, get active plan | `connect/SKILL.md` → `connect/tools/<tool>.md` | `connect.json` | no |\n| 1 | **check state** — where does this domain stand? resume / switch / start over | `steps/01-check-state/SKILL.md` | `state.json` | no |\n| 2 | **refine** — discover what the user wants, in conversation | `steps/02-refine/SKILL.md` | `brief.md` | no |\n| 3 | **provision** — choose the backend from the brief, then provision it | `steps/03-provision/<backend>/SKILL.md` | `site.json` | **bg** — fire, then poll in the background |\n| 4 | **content model** — define + create content/images, then push to the backend | `steps/04-content-model/<backend>/SKILL.md` | `content-model.md` | prepare: no · **push: yes** |\n| 5 | **design** — derive design directions from the brief, pick one, write the `design.md` contract + logo | `steps/05-design/SKILL.md` | `design.md`, `brand/` | no |\n| 6 | **frontend** — pick/confirm the framework (default Next.js SSR), implement `design.md` against the backend | `steps/06-frontend/SKILL.md` | source in `<workspace>/src/` | shell: no · **data: yes** |\n| 7 | **validate** *(optional)* — quick instance+API confirmation; rebuild/preview/approval only when a person signs off | `steps/07-validate/SKILL.md` | — | no (local) |\n| 8 | **deploy** — upload source, trigger the build | `steps/08-deploy/SKILL.md` | `deploy.json` | **yes** |\n| 9 | **done** — hand over URLs, dashboard, summary, next steps (the **final report**) | `steps/09-done/SKILL.md` | — | no |\n\n**The most important is that connect stage runs first and always**\nIt's fetched from the public URL and runs *before* the Hostinger MCP Server\nis loaded, the main goal of this is to have the Hostinger MCP Server.\n\nYou can find those connect skills under `connect/`.\nThe main skill lives in `connect/SKILL.md`.\nThere are also specific tools under `connect/tools/`.\n\nThe other 9 numbered steps live under `steps/` and are read from the installed skill folder (this will be guided in connect step).\n\n**One concurrency rule:** two *independent* stages in a single session may run at once, but never\ntwo writers of the same artifact, and never two agents/windows on the same domain — the `.lock`\n(Step 1) guards that. Parallelism is always *optional*: an agent that can't interleave work runs the\nsteps serially and is still correct. Saying this, Always that you consider, you can run the steps\nin parallel using different threads or processes to save time, but always ensure the right output.\n\nSteps can generate **State files** those files are only hints; the truth is what the Hostinger connector reports, so\nverify a \"done\" marker against reality before acting on it. If a step's `SKILL.md` is missing from the installed skill folder, stop and tell\nthe user rather than improvising the stage from its description.\n\nThe **Instance?** column says whether a stage touches the live backend:\n`no` = pure prepare work that can run during the provision wait;\n`push`/`data`/`yes` = a commit phase that joins the instance barrier;\n`bg` = provision itself, the background task.\n\n**Always end with the final report.** Whenever a run ends, give the user the final report defined\nin `steps/09-done/SKILL.md`: an outcome paragraph, a *Steps completed* table (one row per stage that\nran), the links, where state lives, and any gaps. That includes runs that stop early at a dummy or\nmissing step, a failed provision, or a user pause. Every run also saves a **build report**\n(`run-log.mjs report`, see *Run log and build report* below), on those early stops too.\n\n**Log the run as you go.** Wrap every stage (and each phase of a split stage) in\n`run-log.mjs stage-start` / `stage-end`, and log an event the moment something goes wrong, the user\ncorrects you, or a skill is missing or wrong — before you continue. See *Run log and build report*.\n\n**Why this order.** connect first, so account/plan failures surface before the user spends time\ntalking. Check-state right after, so a resumed, duplicate, or brand-new run is sorted out — and the\nuser is offered the choice to continue, switch domains, or start over — before any new work begins.\nRefinement before provisioning, because the backend cannot be chosen until the brief exists.\nValidation before deploy, always — nothing reaches the live domain unseen.\n\n### Background provision & the instance barrier\n\n**Provision always runs — what it does depends on what the domain already has.** Often onboarding on\nhostinger.com has already created the website before you run, so the domain *has* a headless instance;\nbut an instance existing does **not** mean provision is done or can be skipped. `check_state` works out\nwhich of these cases applies and records it, then provision takes the matching path:\n\n1. **An empty headless shell already exists for the domain (the common onboarding case).** Onboarding\n creates the website first — an **empty headless shell** (`hosting_websites_start-setup { type:\n \"headless_empty\" }`): `website_type` is `headless`, but **no backend is installed** (no WordPress at\n `/cms`, no store). This still goes through provision, which **adopts the shell and swaps it to the\n chosen backend** (the backend is picked in refine first): WordPress is installed into the shell's\n `/cms` with `wordpress_installations_install { type: \"headless\" }`, or the shell is adopted as the\n Ecommerce storefront host. **Never skip provision for a shell, and never create a second site.** See\n the provision router's *Onboarding handoff*.\n2. **No instance exists at all** (the user ran the headless flow without onboarding provisioning\n anything). Provision creates a fresh headless instance with `hosting_websites_start-setup` using the\n chosen backend's own `type`.\n3. **User wants a webapp.** Provision is a plain Node.js app host, not headless — `webapp` ignores\n headless shells entirely and is only reached by experimental actions, never the onboarding flow.\n\nEither way the create/install is asynchronous and returns `queued` immediately, then it's polled\nevery ~15 s for a few minutes — **what you poll depends on the path**: a fresh\n`hosting_websites_start-setup` is polled with `hosting_websites_list-setups` until `completed`, while a\nshell swap (`wordpress_installations_install`) creates no new setup, so it's polled with\n`wordpress_installations_list` until the `/cms` row is `is_valid`. Don't block on that poll. The\nsingle rule that makes the overlap safe:\n\n> **Fire provision as a background task, then do every stage's prepare work while it settles. The\n> moment a step must write to the live instance, it waits for provision to report ready (via the\n> backend step's readiness poll — see above). That wait is the *instance barrier*.**\n\n---\n\n## State and artifacts\n\nEverything for a domain — its state **and** its generated source — lives under the **site workspace\n`~/.hostinger/<domain>/`** (resolve `~` to the home directory: `$HOME` in a shell, `os.homedir()` in\nNode — `~/.hostinger` means `$HOME/.hostinger`). This is a **global, per-user** location, not a\nproject folder, so this domain's build is resumable from **any** directory — the current working\ndirectory does not matter. Create the workspace if it's missing and do all of a stage's work inside\nit (including the frontend `src/`, built and deployed from `~/.hostinger/<domain>/src/`).\n\n**The directory name is the domain**, so the files inside it never repeat it.\n\n**Read and write only this site's workspace.** The other workspaces under `~/.hostinger/` are the\nuser's other sites, often for other clients, so keep their briefs, designs and code out of this build.\nOpen `~/.hostinger/<domain>/` by its exact path; don't list or glob `~/.hostinger/`, and don't open,\ncopy from or diff against another workspace there, in any stage. A run that has no domain yet works\nonly in the one workspace it created itself; when provision moves it to `~/.hostinger/<new-domain>/`,\nthat becomes this site's workspace. One exception: if the user, in this conversation, names another\nof their own sites to reuse, read that one workspace, read-only, and tell them in one line what you\ntook from it.\n\n| File (under `~/.hostinger/<domain>/`) | Written by | Contains |\n|---|---|---|\n| `state.json` | every stage | tool, next stage, Hostinger connector version, timestamps |\n| `connect.json` | connect | tool, account/plan readiness |\n| `brief.md` | refine | refined brief, audience, tone, pages, must-haves |\n| `site.json` | provision | chosen backend, username, install id, all URLs |\n| `content-model.md` | content model | entities, fields, pages, navigation |\n| `design.md` (+ `brand/` logo files) | design | chosen design direction: tokens, type, logo, components, per-page composition, accessibility |\n| `src/` | frontend | the website source implementing `design.md`; framework choice is recorded in `state.json → frontend` |\n| `deploy.json` | deploy | build id, deployed at, live URLs |\n| `events.jsonl` | every stage, via `run-log.mjs` | append-only log of issues, user feedback, skill gaps and wins |\n| `reports/<run>.json` + `.html`, `report.html` | done (or any early stop), via `run-log.mjs` | the build report for each run; `report.html` is the latest |\n\n`state.json` is the minimum you must keep current. **`stage` is the *next* stage to run** (not the\nlast completed one), and it does **not** store the domain — the directory is the key. Because\nprovision runs in the background, its status lives in its own `provision` field rather than in\n`stage`, so prepare work can advance `stage` while provision is still settling:\n\n```json\n{\n \"tool\": \"claude-code\",\n \"stage\": \"content_model\",\n \"provision\": {\n \"status\": \"in_progress\",\n \"domain\": \"example.com\",\n \"started_at\": \"2026-10-08T10:00:00Z\"\n },\n \"updated_at\": \"2026-10-08T10:00:00Z\"\n}\n```\n\n- `provision` is **absent** until provision fires, `{\"status\":\"in_progress\", …}` while the\n background poll runs, and `{\"status\":\"completed\", …}` once `hosting_websites_list-setups` confirms\n it. `domain` is the poll target.\n- Any **instance-barrier** step (content-model push, frontend data, deploy) must read\n `provision.status == \"completed\"` — and re-verify it against the connector — before it runs. A\n prepare step ignores `provision` entirely.\n- On **resume**, `provision.status` is a hint only: re-poll `hosting_websites_list-setups` to learn\n the truth, then update the field.\n- `run` (the current run), `history` (one entry per stage or phase, with real start and end\n times), and `reports_sent` / `reports_rejected` (which copy of each report reached Hostinger) are\n written **only** by `run-log.mjs`. Leave them alone when you update `state.json` by hand.\n- The **onboarding slug** is how hPanel names the coding tool: `claude` for Claude Code, else\n `cursor`, `codex`, `vscode`, `antigravity`, `devin`. Map it from the `<tool>` you pass to\n `run-start --tool` (`state.json → run.tool`, e.g. `claude-code` → `claude`).\n\n---\n\n## Run log and build report\n\n`run-log.mjs` (next to this file) records the run while it happens and turns it into a report at the\nend. Timestamps come from the script, so they're measured, not remembered. It needs only Node.\nEvery command is `node <skill>/run-log.mjs <domain> …`, where `<skill>` is the folder this file is\nin. Write it out in full each time (a shell variable holding the command breaks in zsh):\n\n```bash\nnode <skill>/run-log.mjs <domain> run-start --tool <tool> --model <model> --skill <skill version> # check-state, first thing\nnode <skill>/run-log.mjs <domain> run-update --kind resume # check-state, once the route is agreed (or redesign / iterate)\nnode <skill>/run-log.mjs <domain> stage-start frontend # before a stage\nnode <skill>/run-log.mjs <domain> stage-end frontend ok_after_retries --note \"second framework tried\"\nnode <skill>/run-log.mjs <domain> stage-start frontend --phase shell # split stages log each phase\nnode <skill>/run-log.mjs <domain> event issue '{\"stage\":\"content_model\",\"symptom\":\"…\",\"cause\":\"…\",\"workaround\":\"…\",\"severity\":\"medium\",\"resolved\":true}'\nnode <skill>/run-log.mjs <domain> report --outcome live # done, or wherever the run stops\nnode <skill>/run-log.mjs <domain> report-sent <run> # after the report was sent (see below)\nnode <skill>/run-log.mjs <domain> report-rejected <run> # the send returned 422\nnode <skill>/run-log.mjs <domain> unsent # check-state: reports still to send\nnode <skill>/run-log.mjs <domain> run-resume --reason \"about/contact layout fix\" # follow-up in the same session\n```\n\n**Follow-ups continue the run.** When the user asks for a change after the final report in the\nsame session (a layout fix, a copy tweak, a redeploy), that work belongs to the run it follows:\ncall `run-resume`, log the feedback and the stages it re-runs as usual, then `report` again. That\nrewrites the same `reports/<run>.json` and `.html`, so you get one report that includes the follow-up,\nnot a second report that replaces `report.html`. Use `run-start` only in a new session; check-state\ndoes that.\n\n`stage-end` results: `ok`, `ok_after_retries`, `ok_with_fixups`, `failed`, `skipped`, `stopped`.\nStages: `check_state`, `refine`, `provision`, `content_model`, `design`, `frontend`, `validate`,\n`deploy`, `done` (connect has its own `connect.json`). Split stages use `--phase`: provision `fire` /\n`join`, content model `prepare` / `push`, frontend `shell` / `data`.\n\n**Forgot a `stage-start`?** Log it as soon as you notice, with roughly when the stage began:\n`stage-start frontend --started-ago 6m` (or `--started-at <ISO time>`). The same flags work on a\n`stage-end` that has no start. With neither, the script assumes the stage began when the previous one\nended. Either way the start is marked as estimated in the report. Never log a start and an end\nback to back after the work is done: that records a stage that took no time. If you already did,\ncorrect it with `stage-fix frontend --started-ago 6m`.\n\n**Log an event the moment it happens**, not at the end — details are gone by then:\n\n| Event | Log it when | Fields (required in bold) |\n|---|---|---|\n| `issue` | something fails, misbehaves, or needs a workaround — including your own mistakes | **stage**, **symptom**, cause, workaround, severity (`high`/`medium`/`low`), resolved, user_had_to_intervene |\n| `feedback` | the user corrects you, rejects something, or changes scope | **stage**, **category** (e.g. `design_rejected`, `layout_bug`, `scope_add`, `wrong_assumption`), **paraphrase** |\n| `skill_gap` | a step is dummy, missing, wrong or silent on what you needed | **skill** (path), **gap** |\n| `good` | something worked notably well or fast | **text** |\n\nParaphrase the user; don't paste long quotes, file contents, credentials or personal data. Set\n`user_had_to_intervene` when the user had to point the problem out.\n\n**Tokens** go on `report --tokens '<json>'` only if your tool tells you your own usage directly\n(`{\"source\":\"…\",\"requests\":…,\"input\":…,\"output\":…,\"cache_read\":…,\"cache_write\":…,\"est_cost_usd\":…}`).\nOtherwise leave it out and the report says `{\"source\":\"not reported\"}` (not null: the connector rejects a null `tokens`). Never read session transcripts or log\nfiles to get it.\n\nThe report is saved to `reports/<run>.json` and `.html`, and `report.html` in the workspace is the\nlatest. If a run never reaches its report (the window closed), the next `run-start` closes it as\n`abandoned`, so its log still becomes a report.\n\n**Send every report to Hostinger.** Sending is part of the flow, so don't ask first; tell the user in\none line right after the send (see `09-done`). Right after each\n`report` (follow-ups included), read `reports/<run>.json` and pass its contents, unchanged, as the\narguments of the Hostinger connector tool `hosting_headless_submit-build-report`. Then record the\nresult:\n\n- **Success:** `report-sent <run>`.\n- **422** (the report doesn't match the schema the endpoint holds): `report-rejected <run>`. Don't\n resend it.\n- **Anything else** (network error, 5xx, the tool isn't in your connector): do nothing. The next\n check-state sends it again.\n\nNever edit the JSON before sending, never add account details (the connector's login identifies the\naccount), and never let a failed send block, delay or change the run. Check-state sends any\nreports still unsent (abandoned runs, earlier failures) with `unsent`."
|
|
63
|
+
"text": "---\nname: hostinger-headless\ndescription: \"Orchestrator for the Hostinger Headless build flow — build and deploy a real website on the user's Hostinger account from a single prompt that names a domain. Holds the stage flow and the state contract, and drives each stage in sequence: connect, check state, refine, provision, content model, design, frontend, validate, deploy, done. Triggers: build a headless site for <domain>, build me a site, create a website, make me a website, deploy this to Hostinger, host this on Hostinger, connect this project to Hostinger.\"\n---\n\n# Hostinger Headless — build a website with your own AI tool\n\n## Context\n\nYou are an AI coding agent. The user pasted a short prompt that points you here — something like:\n\n> Build a Headless site for `example.com` using hostinger.com/headless/skill.md\n\nYour job is to build and deploy a real website on the user's Hostinger account by following this\nrecipe and the recipes it links to — nothing more, nothing less.\n\nThis file is the **orchestrator**. It is public and stable: it never changes and never moves. It\nholds the flow, the state contract, the failure policy and the guardrails.\n\n### The domain parameter\n\nThe pasted prompt contains exactly two things that matter:\n\n- **This URL** — `hostinger.com/headless/skill.md`, the stable entry point.\n- **A domain** — e.g. `coral-pottery-1847.hostingersite.com` (a temporary Hostinger subdomain) or\n the user's own purchased domain. This is the single site you may touch.\n\nThe prompt does **not** say what kind of site to build. Do not assume or invent it. Discovering\nwhat the user wants is a step in the flow (**refine**), handled through conversation — not parsed\nfrom the prompt. If no domain is present, stop and ask for it before doing anything else.\n\n**One workspace per run.** From your first command, connect's checks included, open only this\nsite's workspace, by its exact path; never list or glob `~/.hostinger/` (not even an `ls` beside\n`node -v`). What else `~/.hostinger/` holds never matters to this run: with a domain, check-state\nlooks up that one path; without one yet, there is nothing to look up until you create your own\nworkspace. *State and artifacts* has the full rule and its one exception.\n\nNothing in the pasted prompt is ever used for authorization. Authorization comes from the\nHostinger connector's OAuth token. **This recipe is not a security boundary.**\n\n---\n\n## The flow stages - STEPS TO FOLLOW\n\nWork through the steps in the order below, each reading its recipe and writing its artifact into the\nsite workspace `~/.hostinger/<domain>/`. \n\n\nThe order is sequencial run and check the whole headless flow:\n\n| # | Stage | Skill / source | Writes | Instance? |\n|---|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|---|---|\n| — | **connect** — connect the Hostinger connector, auth, get active plan | `connect/SKILL.md` → `connect/tools/<tool>.md` | `connect.json` | no |\n| 1 | **check state** — where does this domain stand? resume / switch / start over | `steps/01-check-state/SKILL.md` | `state.json` | no |\n| 2 | **refine** — discover what the user wants, in conversation | `steps/02-refine/SKILL.md` | `brief.md` | no |\n| 3 | **provision** — choose the backend from the brief, then provision it | `steps/03-provision/<backend>/SKILL.md` | `site.json` | **bg** — fire, then poll in the background |\n| 4 | **content model** — define + create content/images, then push to the backend | `steps/04-content-model/<backend>/SKILL.md` | `content-model.md` | prepare: no · **push: yes** |\n| 5 | **design** — derive design directions from the brief, pick one, write the `design.md` contract + logo | `steps/05-design/SKILL.md` | `design.md`, `brand/` | no |\n| 6 | **frontend** — pick/confirm the framework (default Next.js SSR), implement `design.md` against the backend | `steps/06-frontend/SKILL.md` | source in `<workspace>/src/` | shell: no · **data: yes** |\n| 7 | **validate** *(optional)* — quick instance+API confirmation; rebuild/preview/approval only when a person signs off | `steps/07-validate/SKILL.md` | — | no (local) |\n| 8 | **deploy** — upload source, trigger the build | `steps/08-deploy/SKILL.md` | `deploy.json` | **yes** |\n| 9 | **done** — hand over URLs, dashboard, summary, next steps (the **final report**) | `steps/09-done/SKILL.md` | — | no |\n\n**The most important is that connect stage runs first and always**\nIt's fetched from the public URL and runs *before* the Hostinger MCP Server\nis loaded, the main goal of this is to have the Hostinger MCP Server.\n\nYou can find those connect skills under `connect/`.\nThe main skill lives in `connect/SKILL.md`.\nThere are also specific tools under `connect/tools/`.\n\nThe other 9 numbered steps live under `steps/` and are read from the installed skill folder (this will be guided in connect step).\n\n**One concurrency rule:** two *independent* stages in a single session may run at once, but never\ntwo writers of the same artifact, and never two agents/windows on the same domain — the `.lock`\n(Step 1) guards that. Parallelism is always *optional*: an agent that can't interleave work runs the\nsteps serially and is still correct. Saying this, Always that you consider, you can run the steps\nin parallel using different threads or processes to save time, but always ensure the right output.\n\nSteps can generate **State files** those files are only hints; the truth is what the Hostinger connector reports, so\nverify a \"done\" marker against reality before acting on it. If a step's `SKILL.md` is missing from the installed skill folder, stop and tell\nthe user rather than improvising the stage from its description.\n\nThe **Instance?** column says whether a stage touches the live backend:\n`no` = pure prepare work that can run during the provision wait;\n`push`/`data`/`yes` = a commit phase that joins the instance barrier;\n`bg` = provision itself, the background task.\n\n**Always end with the final report.** Whenever a run ends, give the user the final report defined\nin `steps/09-done/SKILL.md`: an outcome paragraph, a *Steps completed* table (one row per stage that\nran), the links, where state lives, and any gaps. That includes runs that stop early at a dummy or\nmissing step, a failed provision, or a user pause. Every run also saves a **build report**\n(`run-log.mjs report`, see *Run log and build report* below), on those early stops too.\n\n**Log the run as you go.** Wrap every stage (and each phase of a split stage) in\n`run-log.mjs stage-start` / `stage-end`, and log an event the moment something goes wrong, the user\ncorrects you, or a skill is missing or wrong — before you continue. See *Run log and build report*.\n\n**Why this order.** connect first, so account/plan failures surface before the user spends time\ntalking. Check-state right after, so a resumed, duplicate, or brand-new run is sorted out — and the\nuser is offered the choice to continue, switch domains, or start over — before any new work begins.\nRefinement before provisioning, because the backend cannot be chosen until the brief exists.\nValidation before deploy, always — nothing reaches the live domain unseen.\n\n### When you ask the user\n\nA question tool's result is the user's answer only when it carries their choice or their words. When\nit comes back skipped, rejected, empty or as an error (a *skipped* result), nobody has answered yet:\nCursor's `AskQuestion`, for one, returns \"Questions skipped by the user\" when no one touched it. Ask\nthe same questions again as plain text in your reply, end your turn, and continue from what the user\ntypes. One plain-text re-ask is enough: it can't come back skipped. Fill a question you asked with a\ndefault only when the user says so in their own words (\"you decide\", \"just build it\", \"skip that\"),\nor when they said up front that no one will be there to answer; then list those defaults under gaps\nin the final report. Every step that asks the user something follows this.\n\n### Background provision & the instance barrier\n\n**Provision always runs — what it does depends on what the domain already has.** Often onboarding on\nhostinger.com has already created the website before you run, so the domain *has* a headless instance;\nbut an instance existing does **not** mean provision is done or can be skipped. `check_state` works out\nwhich of these cases applies and records it, then provision takes the matching path:\n\n1. **An empty headless shell already exists for the domain (the common onboarding case).** Onboarding\n creates the website first — an **empty headless shell** (`hosting_websites_start-setup { type:\n \"headless_empty\" }`): `website_type` is `headless`, but **no backend is installed** (no WordPress at\n `/cms`, no store). This still goes through provision, which **adopts the shell and swaps it to the\n chosen backend** (the backend is picked in refine first): WordPress is installed into the shell's\n `/cms` with `wordpress_installations_install { type: \"headless\" }`, or the shell is adopted as the\n Ecommerce storefront host. **Never skip provision for a shell, and never create a second site.** See\n the provision router's *Onboarding handoff*.\n2. **No instance exists at all** (the user ran the headless flow without onboarding provisioning\n anything). Provision creates a fresh headless instance with `hosting_websites_start-setup` using the\n chosen backend's own `type`.\n3. **User wants a webapp.** Provision is a plain Node.js app host, not headless — `webapp` ignores\n headless shells entirely and is only reached by experimental actions, never the onboarding flow.\n\nEither way the create/install is asynchronous and returns `queued` immediately, then it's polled\nevery ~15 s for a few minutes — **what you poll depends on the path**: a fresh\n`hosting_websites_start-setup` is polled with `hosting_websites_list-setups` until `completed`, while a\nshell swap (`wordpress_installations_install`) creates no new setup, so it's polled with\n`wordpress_installations_list` until the `/cms` row is `is_valid`. Don't block on that poll. The\nsingle rule that makes the overlap safe:\n\n> **Fire provision as a background task, then do every stage's prepare work while it settles. The\n> moment a step must write to the live instance, it waits for provision to report ready (via the\n> backend step's readiness poll — see above). That wait is the *instance barrier*.**\n\n---\n\n## State and artifacts\n\nEverything for a domain — its state **and** its generated source — lives under the **site workspace\n`~/.hostinger/<domain>/`** (resolve `~` to the home directory: `$HOME` in a shell, `os.homedir()` in\nNode — `~/.hostinger` means `$HOME/.hostinger`). This is a **global, per-user** location, not a\nproject folder, so this domain's build is resumable from **any** directory — the current working\ndirectory does not matter. Create the workspace if it's missing and do all of a stage's work inside\nit (including the frontend `src/`, built and deployed from `~/.hostinger/<domain>/src/`).\n\n**The directory name is the domain**, so the files inside it never repeat it.\n\n**Read and write only this site's workspace.** The other workspaces under `~/.hostinger/` are the\nuser's other sites, often for other clients, so keep their briefs, designs and code out of this build.\nOpen `~/.hostinger/<domain>/` by its exact path; don't list or glob `~/.hostinger/`, and don't open,\ncopy from or diff against another workspace there, in any stage. A run that has no domain yet works\nonly in the one workspace it created itself; when provision moves it to `~/.hostinger/<new-domain>/`,\nthat becomes this site's workspace. One exception: if the user, in this conversation, names another\nof their own sites to reuse, read that one workspace, read-only, and tell them in one line what you\ntook from it.\n\n| File (under `~/.hostinger/<domain>/`) | Written by | Contains |\n|---|---|---|\n| `state.json` | every stage | tool, next stage, Hostinger connector version, timestamps |\n| `connect.json` | connect | tool, account/plan readiness |\n| `brief.md` | refine | refined brief, audience, tone, pages, must-haves |\n| `site.json` | provision | chosen backend, username, install id, all URLs |\n| `content-model.md` | content model | entities, fields, pages, navigation |\n| `design.md` (+ `brand/` logo files) | design | chosen design direction: tokens, type, logo, components, per-page composition, accessibility |\n| `src/` | frontend | the website source implementing `design.md`; framework choice is recorded in `state.json → frontend` |\n| `deploy.json` | deploy | build id, deployed at, live URLs |\n| `events.jsonl` | every stage, via `run-log.mjs` | append-only log of issues, user feedback, skill gaps and wins |\n| `reports/<run>.json` + `.html`, `report.html` | done (or any early stop), via `run-log.mjs` | the build report for each run; `report.html` is the latest |\n\n`state.json` is the minimum you must keep current. **`stage` is the *next* stage to run** (not the\nlast completed one), and it does **not** store the domain — the directory is the key. Because\nprovision runs in the background, its status lives in its own `provision` field rather than in\n`stage`, so prepare work can advance `stage` while provision is still settling:\n\n```json\n{\n \"tool\": \"claude-code\",\n \"stage\": \"content_model\",\n \"provision\": {\n \"status\": \"in_progress\",\n \"domain\": \"example.com\",\n \"started_at\": \"2026-10-08T10:00:00Z\"\n },\n \"updated_at\": \"2026-10-08T10:00:00Z\"\n}\n```\n\n- `provision` is **absent** until provision fires, `{\"status\":\"in_progress\", …}` while the\n background poll runs, and `{\"status\":\"completed\", …}` once `hosting_websites_list-setups` confirms\n it. `domain` is the poll target.\n- Any **instance-barrier** step (content-model push, frontend data, deploy) must read\n `provision.status == \"completed\"` — and re-verify it against the connector — before it runs. A\n prepare step ignores `provision` entirely.\n- On **resume**, `provision.status` is a hint only: re-poll `hosting_websites_list-setups` to learn\n the truth, then update the field.\n- `run` (the current run), `history` (one entry per stage or phase, with real start and end\n times), and `reports_sent` / `reports_rejected` (which copy of each report reached Hostinger) are\n written **only** by `run-log.mjs`. Leave them alone when you update `state.json` by hand.\n- The **onboarding slug** is how hPanel names the coding tool: `claude` for Claude Code, else\n `cursor`, `codex`, `vscode`, `antigravity`, `devin`. Map it from the `<tool>` you pass to\n `run-start --tool` (`state.json → run.tool`, e.g. `claude-code` → `claude`).\n\n---\n\n## Run log and build report\n\n`run-log.mjs` (next to this file) records the run while it happens and turns it into a report at the\nend. Timestamps come from the script, so they're measured, not remembered. It needs only Node.\nEvery command is `node <skill>/run-log.mjs <domain> …`, where `<skill>` is the folder this file is\nin. Write it out in full each time (a shell variable holding the command breaks in zsh):\n\n```bash\nnode <skill>/run-log.mjs <domain> run-start --tool <tool> --model <model> --skill <skill version> # check-state, first thing\nnode <skill>/run-log.mjs <domain> run-update --kind resume # check-state, once the route is agreed (or redesign / iterate)\nnode <skill>/run-log.mjs <domain> stage-start frontend # before a stage\nnode <skill>/run-log.mjs <domain> stage-end frontend ok_after_retries --note \"second framework tried\"\nnode <skill>/run-log.mjs <domain> stage-start frontend --phase shell # split stages log each phase\nnode <skill>/run-log.mjs <domain> event issue '{\"stage\":\"content_model\",\"symptom\":\"…\",\"cause\":\"…\",\"workaround\":\"…\",\"severity\":\"medium\",\"resolved\":true}'\nnode <skill>/run-log.mjs <domain> report --outcome live # done, or wherever the run stops\nnode <skill>/run-log.mjs <domain> report-sent <run> # after the report was sent (see below)\nnode <skill>/run-log.mjs <domain> report-rejected <run> # the send returned 422\nnode <skill>/run-log.mjs <domain> unsent # check-state: reports still to send\nnode <skill>/run-log.mjs <domain> run-resume --reason \"about/contact layout fix\" # follow-up in the same session\n```\n\n**Follow-ups continue the run.** When the user asks for a change after the final report in the\nsame session (a layout fix, a copy tweak, a redeploy), that work belongs to the run it follows:\ncall `run-resume`, log the feedback and the stages it re-runs as usual, then `report` again. That\nrewrites the same `reports/<run>.json` and `.html`, so you get one report that includes the follow-up,\nnot a second report that replaces `report.html`. Use `run-start` only in a new session; check-state\ndoes that.\n\n`stage-end` results: `ok`, `ok_after_retries`, `ok_with_fixups`, `failed`, `skipped`, `stopped`.\nStages: `check_state`, `refine`, `provision`, `content_model`, `design`, `frontend`, `validate`,\n`deploy`, `done` (connect has its own `connect.json`). Split stages use `--phase`: provision `fire` /\n`join`, content model `prepare` / `push`, frontend `shell` / `data`.\n\n**Forgot a `stage-start`?** Log it as soon as you notice, with roughly when the stage began:\n`stage-start frontend --started-ago 6m` (or `--started-at <ISO time>`). The same flags work on a\n`stage-end` that has no start. With neither, the script assumes the stage began when the previous one\nended. Either way the start is marked as estimated in the report. Never log a start and an end\nback to back after the work is done: that records a stage that took no time. If you already did,\ncorrect it with `stage-fix frontend --started-ago 6m`.\n\n**Log an event the moment it happens**, not at the end — details are gone by then:\n\n| Event | Log it when | Fields (required in bold) |\n|---|---|---|\n| `issue` | something fails, misbehaves, or needs a workaround — including your own mistakes | **stage**, **symptom**, cause, workaround, severity (`high`/`medium`/`low`), resolved, user_had_to_intervene |\n| `feedback` | the user corrects you, rejects something, or changes scope | **stage**, **category** (e.g. `design_rejected`, `layout_bug`, `scope_add`, `wrong_assumption`), **paraphrase** |\n| `skill_gap` | a step is dummy, missing, wrong or silent on what you needed | **skill** (path), **gap** |\n| `good` | something worked notably well or fast | **text** |\n\nParaphrase the user; don't paste long quotes, file contents, credentials or personal data. Set\n`user_had_to_intervene` when the user had to point the problem out.\n\n**Tokens** go on `report --tokens '<json>'` only if your tool tells you your own usage directly\n(`{\"source\":\"…\",\"requests\":…,\"input\":…,\"output\":…,\"cache_read\":…,\"cache_write\":…,\"est_cost_usd\":…}`).\nOtherwise leave it out and the report says `{\"source\":\"not reported\"}` (not null: the connector rejects a null `tokens`). Never read session transcripts or log\nfiles to get it.\n\nThe report is saved to `reports/<run>.json` and `.html`, and `report.html` in the workspace is the\nlatest. If a run never reaches its report (the window closed), the next `run-start` closes it as\n`abandoned`, so its log still becomes a report.\n\n**Send every report to Hostinger.** Sending is part of the flow, so don't ask first; tell the user in\none line right after the send (see `09-done`). Right after each\n`report` (follow-ups included), read `reports/<run>.json` and pass its contents, unchanged, as the\narguments of the Hostinger connector tool `hosting_headless_submit-build-report`. Then record the\nresult:\n\n- **Success:** `report-sent <run>`.\n- **422** (the report doesn't match the schema the endpoint holds): `report-rejected <run>`. Don't\n resend it.\n- **Anything else** (network error, 5xx, the tool isn't in your connector): do nothing. The next\n check-state sends it again.\n\nNever edit the JSON before sending, never add account details (the connector's login identifies the\naccount), and never let a failed send block, delay or change the run. Check-state sends any\nreports still unsent (abandoned runs, earlier failures) with `unsent`."
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"uri": "skill://hostinger-headless/connect/SKILL.md",
|
|
@@ -134,17 +134,17 @@ export default [
|
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
"uri": "skill://hostinger-headless/steps/01-check-state/SKILL.md",
|
|
137
|
-
"digest": "sha256:
|
|
138
|
-
"size":
|
|
137
|
+
"digest": "sha256:1c6cd793a02758c65d46fecb8e7d8fe1c50544dcc3bbde23b22786ebd24f754c",
|
|
138
|
+
"size": 13173,
|
|
139
139
|
"mimeType": "text/markdown",
|
|
140
|
-
"text": "---\nname: headless-step-check-state\ndescription: \"First build step of the Hostinger Headless flow, run right after connect. Works out where a given domain's build stands — is a run already in progress, does the site already exist, how far did it get — by reconciling the global workspace state under ~/.hostinger/<domain>/ with what the Hostinger connector reports, then guides the user to continue, switch or buy a domain, or start over. Owns all resume/position logic for the flow.\"\n---\n\n# headless-step-check-state\n\nThe first build step of the Hostinger Headless flow (orchestrated by `hostinger.com/headless/skill.md`),\nrun right after connect. Connect has already connected the Hostinger connector and confirmed the\naccount can build. Your job here is to\n**figure out where this domain stands and agree the route with the user** before any new work begins.\nThis step owns all resume/position logic — no other stage second-guesses it.\n\nNothing in this step creates, provisions, deploys, or deletes anything. It reads and guides. Its\nonly writes to Hostinger are reports: unsent build reports (Step 0) and the connected tool (Step 3).\n\n---\n\n## Where state lives\n\nEverything for a domain lives under the global site workspace **`~/.hostinger/<domain>/`** — one\ndirectory per domain, under the user's home (`~` = `$HOME`), **not** the current working directory.\nThe directory name **is** the domain, so `state.json` inside does not store it. Because it's global,\nthis domain's build is resumable from any directory.\n\n`state.json` shape (`stage` is the *next* stage to run; `provision` is the background task's status,\npresent only once provision has fired):\n\n```json\n{ \"tool\": \"claude-code\", \"stage\": \"content_model\",\n \"provision\": { \"status\": \"in_progress\", \"domain\": \"<domain>\", \"started_at\": \"…\" },\n \"updated_at\": \"…\" }\n```\n\n`provision.status` is `in_progress` or `completed` — but it is a **hint**: always re-verify it against\n`hosting_websites_list-setups` (Step 1) before trusting it to gate an instance-barrier step.\n\n---\n\n## Step 0 — Start the run log\n\nBefore anything else, start a run and the stage (see *Run log and build report* in the top-level\n`SKILL.md`):\n\n```bash\nnode <skill>/run-log.mjs <domain> run-start --tool <tool> --model <model> --skill <skill version>\nnode <skill>/run-log.mjs <domain> stage-start check_state\n```\n\nIf an earlier run on this domain never wrote its report, `run-start` closes it as `abandoned` first.\n\nThen send any build reports that haven't reached Hostinger yet (that abandoned run, or a send that\nfailed last time):\n\n```bash\nnode <skill>/run-log.mjs <domain> unsent\n```\n\nEach line is `<run> <path>`. For each, send the file through `hosting_headless_submit-build-report`\nand record the result, as described in *Run log and build report* in the top-level `SKILL.md`. One\ntry each. Don't ask first; if any went out, say so in one line (e.g. \"Sent 1 earlier build report to\nHostinger.\"). Don't let a failure hold up this step.\nCheck-state runs once per session. A follow-up after the final report, in the same session, doesn't\ncome back here: it calls `run-resume` (see *Run log and build report* in the top-level `SKILL.md`).\n\n## Step 1 — Is a build actively running right now? (check this before anything else)\n\nKeep two things separate, and **never guess from a timestamp.**\n\n- **A platform build actively in progress** — the only *reliable* \"running now\" signal. Check\n `hosting_websites_list-setups { \"domain\": \"<domain>\" }` and exact-match the domain row:\n - `status: running` → provisioning is underway **right now** (a background provision from an earlier\n turn). Don't start a second one — reconcile `state.json`'s `provision` to `in_progress`, and\n either resume prepare work while it finishes or poll it to `completed` at the barrier.\n - `status: failed` → not running; route to provision to self-heal (Step 3).\n - `completed` → provision is done; set `provision.status` to `completed`. Instance-barrier steps\n (content push, frontend data, deploy) are now unblocked.\n - no row → not provisioned yet; carry on.\n\n This check **is** the source of truth for `provision.status`: a stored `in_progress`/`completed` in\n `state.json` is only a hint until this reconciles it.\n\n- **A `.lock`** — `~/.hostinger/<domain>/.lock`. It only tells you *a previous run\n touched this domain*. **Do not judge it by age** — a 2-minute-old lock and a 3-year-old lock get the\n same treatment, and a lock on its own is **not** a reason to interrupt the user. Carry it into Step 5:\n if there's nothing to resume (new build), silently take it over; only if there's real state to resume\n do you also ask *is another Claude window building this domain right now?* (yes → stop; no → take\n over).\n\nOnce you're actively working this domain, **acquire/refresh the lock** (write the current timestamp),\nand **release it on a clean stop** (Step 6). The lock marks an active session, nothing more.\n\n---\n\n## Step 2 — Read the workspace directory\n\nCheck `~/.hostinger/<domain>/` by its exact path (for example `test -d ~/.hostinger/<domain>`), not\nby listing `~/.hostinger/`:\n\n- **Absent** → no history for this domain in the workspace.\n- **Present** → read `state.json` (its `stage`) and whatever artifacts exist (`brief.md`,\n `site.json`, …). Treat these as a hint, not the truth.\n\n---\n\n## Step 3 — Verify against reality (reality wins)\n\nQuery the Hostinger connector — the local files are never trusted on their own:\n\n- **Does the site exist?** `hosting_websites_list { \"domain\": \"<domain>\" }`. ⚠️ This filter is a\n **case-insensitive substring match**, not exact — so `test.com` would also return\n `shop.test.com`. **Compare each returned row's `domain` to your target for an *exact* match**\n (case-insensitive) and ignore the rest. No exact row ⇒ the domain is free. An exact row ⇒ the site\n exists; note its `website_type` (`headless`, `wordpress`, `nodejs`, …) and `username`.\n- **Tell hPanel the tool connected.** As soon as you have the exact row, call\n `hosting_headless_report-tool-connected { \"username\": \"<row username>\", \"domain\": \"<domain>\", \"tool\": \"<slug>\" }`\n once, where `<slug>` is the onboarding slug (see *State and artifacts* in the top-level `SKILL.md`).\n This flips hPanel's onboarding card to \"connected\". Don't ask first and don't mention it. Skip the\n call when there's no exact row or your tool has no onboarding slug. If\n `hosting_headless_report-tool-connected` isn't in your connector, or the call returns an error,\n ignore it: no retry, nothing said to the user, and the run carries on unchanged.\n- **How far did it get?** If the site exists, check its setup via\n `hosting_websites_list-setups { \"domain\": \"<domain>\" }` (exact-match the row the same way):\n - `status: completed` → provisioned; then, depending on backend, check whether a build has succeeded.\n - `status: running` → still provisioning (see Step 1).\n - `status: failed` → provisioning stopped before finishing → route to **provision** to self-heal.\n- **Is it an empty headless shell from onboarding?** A `headless` site whose setup is `completed` is\n **not necessarily finished** — onboarding now creates the website *before* the build runs, as an\n **empty headless shell** (`hosting_websites_start-setup { type: \"headless_empty\" }`): `website_type`\n is `headless` but **no backend is installed** (no WordPress at `/cms`, no store). Tell a shell from a\n real backend the same way provision does: within 24 h the setup `type` is `headless_empty`; otherwise\n `wordpress_installations_list { username, domain, ownership: \"all\" }` returns **no** `/cms` row and\n there is no `site.json` recording a backend. An empty shell means **provision has not chosen or\n installed a backend yet** — this is a **new build** that happens to already own its website, not a\n finished or resumable one. Route it to **refine** (choose the backend), then **provision** adopts the\n shell and swaps it to that backend (see the provision router's *Onboarding handoff*). Never create a\n second site, and never skip provision for it.\n\nReconcile with the local state. **When they disagree, reality wins** and the local `stage` is\ncorrected.\n\n---\n\n## Step 4 — Determine the position\n\nConnect always writes `state.json` (with `stage: check_state`) before this step runs, so local\nstate is never truly empty. **\"Local state: none\" here means only connect artifacts exist** —\n`connect.json` + a `state.json` still pointing at `check_state`/`refine`, and **no build artifacts\nyet** (`brief.md`, `site.json`, …).\n\n| Local state | Reality (domain) | Position |\n|---|---|---|\n| none (connect only) | free | **new build** — start from refine |\n| none (connect only) | empty headless shell (onboarding) | **new build on an existing shell** — start from refine; provision adopts the shell and swaps it to the chosen backend. Never create a second site, never skip provision |\n| none (connect only) | site exists with a real backend | **adopt / resume** — never create a second site; route to the first unfinished stage |\n| build artifacts present, incomplete | matches | **resume** at `state.json`'s `stage` |\n| build artifacts present | disagrees | **reality wins** — recompute the stage from what actually exists |\n| build artifacts present, all done | live site exists | **finished** — this is now an *iterate* request, not a build |\n\n---\n\n## Step 5 — Guide the user\n\nThe question to ask depends on whether there is actually **something to resume** — i.e. **build\nartifacts** (`brief.md`, `site.json`, …) or a **live site** for this domain. A stray `.lock` or\nconnect-only files are *not* something to resume.\n\n- **New build** (no build artifacts and either the domain is free **or** it holds only an empty\n headless shell from onboarding) → just proceed to refine. **Don't ask resume vs start over** —\n there's nothing to resume; both would land in the same place. An empty headless shell is not\n \"something to resume\": it has no backend and no content yet, so it's a fresh build that already owns\n its website — provision will adopt and swap it. Mention it in one line (\"Using the headless site\n onboarding already created for this domain.\"). If a leftover `.lock` is lying around, silently take\n it over and continue (optionally mention it in one line).\n- **Something to resume** (build artifacts exist, or a site exists for the domain **with a real backend\n installed** — WordPress at `/cms`, a store, or a deployed app) → tell the user plainly what's there\n and **ask them to choose**, regardless of how old it is (don't guess from timestamps):\n\n- **Resume** — pick up at the computed stage. (If a `.lock` is present in this case, also confirm no\n other window is actively building this domain right now.)\n- **Start over / redo a step** — only safe **before anything is provisioned**. If the site is already\n provisioned or live, you **cannot** re-create it: that is forward-only *iterate* (change content or\n design and redeploy), not a fresh build. Never re-provision or overwrite a live site; if the user\n insists on a clean slate for a live domain, explain the limitation and point them at hPanel.\n- **Switch domain** — they want a different domain. A new domain is a **new workspace**\n (`~/.hostinger/<other-domain>/`, with its own `src/`). Hand off to start that domain's run.\n- **Buy a domain** — they want a custom domain instead of the temporary one. Route to the domain\n purchase / `connect-domain` flow, then continue with the chosen domain.\n\nWhen the intent is ambiguous, **ask** — don't guess.\n\n---\n\n## Step 6 — Record and route\n\nWrite `~/.hostinger/<domain>/state.json` with the agreed next `stage` and a fresh timestamp. If the\nroute isn't a new build, record it with `run-log.mjs <domain> run-update --kind resume` (or\n`redesign` / `iterate`), then end the stage with `stage-end check_state ok --note \"<the route>\"`.\nThen:\n\n- **Handing straight to the next stage in this same session?** Keep the `.lock` (you are still\n actively running) and continue.\n- **Stopping and returning control to the user?** (e.g. the next stage isn't built yet, or you are\n waiting on them.) **Delete the `.lock`** — the session is no longer active, and a leftover lock just\n forces an unnecessary \"is another window running?\" question on the next run. Always release the lock\n on a clean stop; it exists only to mark an actively-running session. A stop is still the end of a\n run: give the final report and save the build report (`09-done`).\n\n---\n\n## Invariants\n\n- **One directory per domain.** The domain is the key; state files never repeat it.\n- **Only this domain's workspace.** Never list `~/.hostinger/` or open another site's workspace (see\n *State and artifacts* in the top-level `SKILL.md`).\n- **Never create a second site** on a domain that already has one.\n- **Never re-provision or overwrite a live site.** Recovery is forward-only.\n- **Reality beats local state**, always.\n- **When ambiguous, ask.**\n"
|
|
140
|
+
"text": "---\nname: headless-step-check-state\ndescription: \"First build step of the Hostinger Headless flow, run right after connect. Works out where a given domain's build stands — is a run already in progress, does the site already exist, how far did it get — by reconciling the global workspace state under ~/.hostinger/<domain>/ with what the Hostinger connector reports, then guides the user to continue, switch or buy a domain, or start over. Owns all resume/position logic for the flow.\"\n---\n\n# headless-step-check-state\n\nThe first build step of the Hostinger Headless flow (orchestrated by `hostinger.com/headless/skill.md`),\nrun right after connect. Connect has already connected the Hostinger connector and confirmed the\naccount can build. Your job here is to\n**figure out where this domain stands and agree the route with the user** before any new work begins.\nThis step owns all resume/position logic — no other stage second-guesses it.\n\nNothing in this step creates, provisions, deploys, or deletes anything. It reads and guides. Its\nonly writes to Hostinger are reports: unsent build reports (Step 0) and the connected tool (Step 3).\n\n---\n\n## Where state lives\n\nEverything for a domain lives under the global site workspace **`~/.hostinger/<domain>/`** — one\ndirectory per domain, under the user's home (`~` = `$HOME`), **not** the current working directory.\nThe directory name **is** the domain, so `state.json` inside does not store it. Because it's global,\nthis domain's build is resumable from any directory.\n\n`state.json` shape (`stage` is the *next* stage to run; `provision` is the background task's status,\npresent only once provision has fired):\n\n```json\n{ \"tool\": \"claude-code\", \"stage\": \"content_model\",\n \"provision\": { \"status\": \"in_progress\", \"domain\": \"<domain>\", \"started_at\": \"…\" },\n \"updated_at\": \"…\" }\n```\n\n`provision.status` is `in_progress` or `completed` — but it is a **hint**: always re-verify it against\n`hosting_websites_list-setups` (Step 1) before trusting it to gate an instance-barrier step.\n\n---\n\n## Step 0 — Start the run log\n\nBefore anything else, start a run and the stage (see *Run log and build report* in the top-level\n`SKILL.md`):\n\n```bash\nnode <skill>/run-log.mjs <domain> run-start --tool <tool> --model <model> --skill <skill version>\nnode <skill>/run-log.mjs <domain> stage-start check_state\n```\n\nIf an earlier run on this domain never wrote its report, `run-start` closes it as `abandoned` first.\n\nThen send any build reports that haven't reached Hostinger yet (that abandoned run, or a send that\nfailed last time):\n\n```bash\nnode <skill>/run-log.mjs <domain> unsent\n```\n\nEach line is `<run> <path>`. For each, send the file through `hosting_headless_submit-build-report`\nand record the result, as described in *Run log and build report* in the top-level `SKILL.md`. One\ntry each. Don't ask first; if any went out, say so in one line (e.g. \"Sent 1 earlier build report to\nHostinger.\"). Don't let a failure hold up this step.\nCheck-state runs once per session. A follow-up after the final report, in the same session, doesn't\ncome back here: it calls `run-resume` (see *Run log and build report* in the top-level `SKILL.md`).\n\n## Step 1 — Is a build actively running right now? (check this before anything else)\n\nKeep two things separate, and **never guess from a timestamp.**\n\n- **A platform build actively in progress** — the only *reliable* \"running now\" signal. Check\n `hosting_websites_list-setups { \"domain\": \"<domain>\" }` and exact-match the domain row:\n - `status: running` → provisioning is underway **right now** (a background provision from an earlier\n turn). Don't start a second one — reconcile `state.json`'s `provision` to `in_progress`, and\n either resume prepare work while it finishes or poll it to `completed` at the barrier.\n - `status: failed` → not running; route to provision to self-heal (Step 3).\n - `completed` → provision is done; set `provision.status` to `completed`. Instance-barrier steps\n (content push, frontend data, deploy) are now unblocked.\n - no row → not provisioned yet; carry on.\n\n This check **is** the source of truth for `provision.status`: a stored `in_progress`/`completed` in\n `state.json` is only a hint until this reconciles it.\n\n- **A `.lock`** — `~/.hostinger/<domain>/.lock`. It only tells you *a previous run\n touched this domain*. **Do not judge it by age** — a 2-minute-old lock and a 3-year-old lock get the\n same treatment, and a lock on its own is **not** a reason to interrupt the user. Carry it into Step 5:\n if there's nothing to resume (new build), silently take it over; only if there's real state to resume\n do you also ask *is another Claude window building this domain right now?* (yes → stop; no → take\n over).\n\nOnce you're actively working this domain, **acquire/refresh the lock** (write the current timestamp),\nand **release it on a clean stop** (Step 6). The lock marks an active session, nothing more.\n\n---\n\n## Step 2 — Read the workspace directory\n\nCheck `~/.hostinger/<domain>/` by its exact path (for example `test -d ~/.hostinger/<domain>`), not\nby listing `~/.hostinger/`:\n\n- **Absent** → no history for this domain in the workspace.\n- **Present** → read `state.json` (its `stage`) and whatever artifacts exist (`brief.md`,\n `site.json`, …). Treat these as a hint, not the truth.\n\n---\n\n## Step 3 — Verify against reality (reality wins)\n\nQuery the Hostinger connector — the local files are never trusted on their own:\n\n- **Does the site exist?** `hosting_websites_list { \"domain\": \"<domain>\" }`. ⚠️ This filter is a\n **case-insensitive substring match**, not exact — so `test.com` would also return\n `shop.test.com`. **Compare each returned row's `domain` to your target for an *exact* match**\n (case-insensitive) and ignore the rest. No exact row ⇒ the domain is free. An exact row ⇒ the site\n exists; note its `website_type` (`headless`, `wordpress`, `nodejs`, …) and `username`.\n- **Tell hPanel the tool connected.** As soon as you have the exact row, call\n `hosting_headless_report-tool-connected { \"username\": \"<row username>\", \"domain\": \"<domain>\", \"tool\": \"<slug>\" }`\n once, where `<slug>` is the onboarding slug (see *State and artifacts* in the top-level `SKILL.md`).\n This flips hPanel's onboarding card to \"connected\". Don't ask first and don't mention it. Skip the\n call when there's no exact row or your tool has no onboarding slug. If\n `hosting_headless_report-tool-connected` isn't in your connector, or the call returns an error,\n ignore it: no retry, nothing said to the user, and the run carries on unchanged.\n- **How far did it get?** If the site exists, check its setup via\n `hosting_websites_list-setups { \"domain\": \"<domain>\" }` (exact-match the row the same way):\n - `status: completed` → provisioned; then, depending on backend, check whether a build has succeeded.\n - `status: running` → still provisioning (see Step 1).\n - `status: failed` → provisioning stopped before finishing → route to **provision** to self-heal.\n- **Is it an empty headless shell from onboarding?** A `headless` site whose setup is `completed` is\n **not necessarily finished** — onboarding now creates the website *before* the build runs, as an\n **empty headless shell** (`hosting_websites_start-setup { type: \"headless_empty\" }`): `website_type`\n is `headless` but **no backend is installed** (no WordPress at `/cms`, no store). Tell a shell from a\n real backend the same way provision does: within 24 h the setup `type` is `headless_empty`; otherwise\n `wordpress_installations_list { username, domain, ownership: \"all\" }` returns **no** `/cms` row and\n there is no `site.json` recording a backend. An empty shell means **provision has not chosen or\n installed a backend yet** — this is a **new build** that happens to already own its website, not a\n finished or resumable one. Route it to **refine** (choose the backend), then **provision** adopts the\n shell and swaps it to that backend (see the provision router's *Onboarding handoff*). Never create a\n second site, and never skip provision for it.\n\nReconcile with the local state. **When they disagree, reality wins** and the local `stage` is\ncorrected.\n\n---\n\n## Step 4 — Determine the position\n\nConnect always writes `state.json` (with `stage: check_state`) before this step runs, so local\nstate is never truly empty. **\"Local state: none\" here means only connect artifacts exist** —\n`connect.json` + a `state.json` still pointing at `check_state`/`refine`, and **no build artifacts\nyet** (`brief.md`, `site.json`, …).\n\n| Local state | Reality (domain) | Position |\n|---|---|---|\n| none (connect only) | free | **new build** — start from refine |\n| none (connect only) | empty headless shell (onboarding) | **new build on an existing shell** — start from refine; provision adopts the shell and swaps it to the chosen backend. Never create a second site, never skip provision |\n| none (connect only) | site exists with a real backend | **adopt / resume** — never create a second site; route to the first unfinished stage |\n| build artifacts present, incomplete | matches | **resume** at `state.json`'s `stage` |\n| build artifacts present | disagrees | **reality wins** — recompute the stage from what actually exists |\n| build artifacts present, all done | live site exists | **finished** — this is now an *iterate* request, not a build |\n\n---\n\n## Step 5 — Guide the user\n\nThe question to ask depends on whether there is actually **something to resume** — i.e. **build\nartifacts** (`brief.md`, `site.json`, …) or a **live site** for this domain. A stray `.lock` or\nconnect-only files are *not* something to resume.\n\n- **New build** (no build artifacts and either the domain is free **or** it holds only an empty\n headless shell from onboarding) → just proceed to refine. **Don't ask resume vs start over** —\n there's nothing to resume; both would land in the same place. An empty headless shell is not\n \"something to resume\": it has no backend and no content yet, so it's a fresh build that already owns\n its website — provision will adopt and swap it. Mention it in one line (\"Using the headless site\n onboarding already created for this domain.\"). If a leftover `.lock` is lying around, silently take\n it over and continue (optionally mention it in one line).\n- **Something to resume** (build artifacts exist, or a site exists for the domain **with a real backend\n installed** — WordPress at `/cms`, a store, or a deployed app) → tell the user plainly what's there\n and **ask them to choose**, regardless of how old it is (don't guess from timestamps):\n\n- **Resume** — pick up at the computed stage. (If a `.lock` is present in this case, also confirm no\n other window is actively building this domain right now.)\n- **Start over / redo a step** — only safe **before anything is provisioned**. If the site is already\n provisioned or live, you **cannot** re-create it: that is forward-only *iterate* (change content or\n design and redeploy), not a fresh build. Never re-provision or overwrite a live site; if the user\n insists on a clean slate for a live domain, explain the limitation and point them at hPanel.\n- **Switch domain** — they want a different domain. A new domain is a **new workspace**\n (`~/.hostinger/<other-domain>/`, with its own `src/`). Hand off to start that domain's run.\n- **Buy a domain** — they want a custom domain instead of the temporary one. Route to the domain\n purchase / `connect-domain` flow, then continue with the chosen domain.\n\nWhen the intent is ambiguous, **ask** — don't guess. A skipped tool result is ambiguous; see\n*When you ask the user* in the top-level `SKILL.md`.\n\n---\n\n## Step 6 — Record and route\n\nWrite `~/.hostinger/<domain>/state.json` with the agreed next `stage` and a fresh timestamp. If the\nroute isn't a new build, record it with `run-log.mjs <domain> run-update --kind resume` (or\n`redesign` / `iterate`), then end the stage with `stage-end check_state ok --note \"<the route>\"`.\nThen:\n\n- **Handing straight to the next stage in this same session?** Keep the `.lock` (you are still\n actively running) and continue.\n- **Stopping and returning control to the user?** (e.g. the next stage isn't built yet, or you are\n waiting on them.) **Delete the `.lock`** — the session is no longer active, and a leftover lock just\n forces an unnecessary \"is another window running?\" question on the next run. Always release the lock\n on a clean stop; it exists only to mark an actively-running session. A stop is still the end of a\n run: give the final report and save the build report (`09-done`).\n\n---\n\n## Invariants\n\n- **One directory per domain.** The domain is the key; state files never repeat it.\n- **Only this domain's workspace.** Never list `~/.hostinger/` or open another site's workspace (see\n *State and artifacts* in the top-level `SKILL.md`).\n- **Never create a second site** on a domain that already has one.\n- **Never re-provision or overwrite a live site.** Recovery is forward-only.\n- **Reality beats local state**, always.\n- **When ambiguous, ask.**\n"
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
"uri": "skill://hostinger-headless/steps/02-refine/SKILL.md",
|
|
144
|
-
"digest": "sha256:
|
|
145
|
-
"size":
|
|
144
|
+
"digest": "sha256:e186715938d72ad7590ffb6b36ea3a48b1ea64c4f04fe667c3b2d027631e3ad8",
|
|
145
|
+
"size": 18861,
|
|
146
146
|
"mimeType": "text/markdown",
|
|
147
|
-
"text": "---\nname: headless-step-refine\ndescription: \"Step 2 (refine) of the Hostinger Headless flow. Turns the user's seed — often just a domain — into a brief through conversation: asks what they want to build (purpose, audience, tone, pages, products, must-haves; images, local files, links all welcome), then *infers* the backend that fits (WordPress, Ecommerce, WordPress + Ecommerce, WordPress + WooCommerce for a one-site content+store, or Webapp for a custom app/SPA) — it never asks the user to pick a backend or a technology; the user confirms the plan in plain language, then it writes brief.md. Hostinger Ecommerce stays the default for selling; WooCommerce is chosen only when the user explicitly asks for it or wants content and a store on one WordPress site.\"\n---\n\n# headless-step-refine\n\nFind out what the user wants to build, guess which backend fits, and write it down. This is a\n**conversation, not a form**: the pasted prompt names a domain and nothing else, so everything about\nthe site comes from talking to the user.\n\n**Keep it short.** The only thing you must know is *what they are building* — enough to guess the\nbackend. Every other question is skippable: the more the user gives, the better the site, but never\nhold the build hostage to refinement. Fill gaps with sensible defaults and say so in the brief.\n\n> **Never ask the user to choose a backend or a technology.** Don't ask \"WordPress or Ecommerce?\",\n> don't present WordPress/Ecommerce/WooCommerce/Webapp as options, and don't surface the backend in a\n> multiple-choice UI. Open with **\"What do you want to build?\"** and infer the backend yourself in\n> Step 4 from what they describe (and whether they sell). The backend is a technical decision the flow\n> makes *for* the user — they confirm the **plan** (what the site is and does), not the plumbing. The\n> only exception is when the user *volunteers* a backend (\"build it on WordPress\", \"use WooCommerce\"):\n> then honour their choice and note it in the brief.\n\n**In:** the domain (the workspace directory name), plus anything the user already shared.\n**Out:** `~/.hostinger/<domain>/brief.md`.\n\nNothing in this step touches Hostinger. It only asks, decides and writes.\n\n---\n\n## Step 1 — Read the workspace, then the resume check\n\nRead everything already in `~/.hostinger/<domain>/` (`state.json`, `connect.json`, any notes, images\nor files the user dropped there) — it all counts as input.\n\nIf `brief.md` already exists for this domain, **don't overwrite it**. Show the user a three-line\nsummary (what the site is, its pages, the suggested backend) and ask: *keep it, or change something?*\nKeep → go to Step 6. Change → edit only what they asked for, then re-confirm (Step 5).\n\nIf the site is already provisioned (`site.json` exists), the backend is **fixed** — never change the\nSuggested backend on a provisioned site. Explain that switching backends means a new domain.\n\n## Step 2 — Gather what you already have\n\nBefore asking anything, collect what the user has already given you, so you don't ask for it again:\n\n- **Inspect the directory the command was run in** — this is the biggest signal for the build mode\n (Step 3). Look for an existing **frontend / webapp / commerce project**: a `package.json`, a\n framework config (`next.config.*`, `astro.config.*`, `vite.config.*`, `svelte.config.*`, `nuxt.*`),\n a `src/` or `app/` tree, `index.html`, a storefront/cart, etc. A real project here usually means the\n user **already has their own frontend** and mainly needs a Hostinger backend to wire it to — lean\n toward \"bring your own frontend\" in Step 3. If the folder has nothing site-like, assume they don't\n and lean toward \"agent builds it\".\n- Anything in the pasted prompt beyond the domain (a sentence, a link, a screenshot).\n- Files in the current project that look like site material (copy, logos, product lists, images).\n- What the agent already knows about the user from local memory or this conversation (a prior build,\n stack preferences, a frontend they always use).\n- The domain name itself — a hint only (`coral-pottery.com` suggests pottery), never a conclusion.\n\n## Step 3 — Ask\n\nIf Step 2 already answers *what are you building* and *will they sell*, skip the Round 1–3 questions\nbelow, but still ask **Build mode** before moving on.\n\nOtherwise open with one line and **wait for the user's answer**: *\"Tell me what you want to build — a\nsentence is enough. Links, screenshots or files help too. Skip anything you don't want to answer.\"*\n\n> **\"What do you want to build?\" is an OPEN question — ask it in plain prose and wait.** Never turn it\n> into a multiple-choice list of pre-baked site ideas (\"Coffee shop / Travel blog / Portfolio …\"):\n> that puts words in the user's mouth and defeats the point of asking. Let them describe it in their\n> own words. The structured question UI is only for genuinely *choice-shaped* follow-ups (selling\n> yes/no, which pages, tone adjectives, the build mode) — not for the opening question, and never for\n> the backend.\n\nAsk in **short rounds of 2–3 questions**, and adapt each round to the previous answers. If your tool\nhas a structured question UI (multiple choice with an \"other\" field), use it for the choice-shaped\nfollow-up questions (pages, selling yes/no, tone) — **never for the opening \"what do you want to\nbuild?\" and never for the backend** (that's your decision, not theirs). **At most 3 rounds**, and stop early as soon as you can guess the backend and the user\nseems done — \"just build it\", \"you decide\", or a skipped question means *use defaults*, not *ask\nagain*.\n\n**Round 1 — what it is (the only required round):**\n\n1. **What are you building?** The business or project, and what the site is for.\n2. **Will you sell anything on it?** Physical products, digital products, services with checkout —\n or nothing. *This decides the backend.* Skip it only when the answer is already unmistakable\n (\"an online shop for my candles\", \"my personal blog\"); when in doubt, ask.\n3. **Who is it for?** The audience, and how they'll find the site. *(skippable)*\n\n**Round 2 — what goes on it (optional, adapt to Round 1):**\n\n- **Content they'll publish over time** — blog posts, articles, news, recipes, portfolio pieces,\n case studies? How often? Or is it a handful of pages that rarely change?\n- **Pages** — propose a page list from what you know and ask them to add or remove.\n- If they sell: **what and how many** — a rough product count, categories, whether products have\n variants (size, colour), and the shipping regions.\n\n**Round 3 — how it should feel (optional, skip what you already know):**\n\nThis is the round that captures **all design input** — the design step reads it from `brief.md` to derive\nthe design directions, so it is **not** asked again later; gather it here:\n\n- **Reference & look** — a **site they like (URL)** and what to take from it, plus a few adjectives for\n the vibe. Screenshots welcome.\n- **Colours & brand** — brand/accent colours, logo, fonts, existing copy, photos. Note where they live.\n- **Must-haves and no-gos** — contact form, newsletter, bookings, languages, things to avoid.\n\nIf the user asks for something outside the scope (user accounts, member areas, bookings with a\ncalendar backend, user-generated content), say plainly that the headless flow doesn't support it yet\nand record it under **Out of scope** — don't promise it.\n\n> **A contact form *is* supported (WordPress backends).** If the user wants one, keep it in\n> **Must-haves** and name it \"Contact form\". Provision installs **Contact Form 7** on `/cms` and the\n> frontend wires a contact page whose form emails the site owner through WordPress's built-in\n> `wp_mail()` — no third-party service and no API keys. It's **opt-in**: built only when the brief asks\n> for it. (A form that needs accounts, payments or a booking calendar is still out of scope — this is a\n> plain name/email/message enquiry form.)\n\n### Build mode — ask once (this one *is* a user choice)\n\nThe backend is your call, but **how the frontend gets built is the user's** — because most of the time\nthe user already has their own frontend and just needs a Hostinger backend to wire it to. After the\nfolder + memory inspection (Step 2), ask **one** question (a structured choice is ideal here), seeding\nthe default from what you found:\n\n- **Bring your own frontend** *(default when Step 2 found a real project in the folder)* — \"I already\n have a frontend / I'll drive the design and we vibe-code it together.\" The agent **uses the user's\n existing project in the current folder as the source**, collaborates on design/code at the user's\n direction, and wires it to the provisioned backend. There is **no agent-generated design** — the\n design step defers to the user (see the Build mode box at the top of the design step), and the frontend step builds on their project\n rather than scaffolding a new one.\n- **Agent designs and builds it** *(default when the folder has nothing site-like)* — \"Give me ideas\n — you design and code it.\" Today's flow: the design step proposes directions and the frontend step\n scaffolds and implements them.\n\nRecord the answer as the brief's **Build mode**. This is the one frontend/technology question you *do*\nput to the user; everything else about the stack (framework, SSR/SSG) is still inferred. The deliverable\nis the same either way — a working site on the backend; only *who drives the frontend* changes.\n\n## Step 4 — Decide the backend (your call, from the brief — not a question)\n\nFrom what the user described, **you** pick the backend that fits — this is an internal decision, never\na question put back to the user. Pick the backend that fits what the user told you:\n\n| Backend | Choose when | What it gives the owner |\n|---|---|---|\n| **WordPress** | No selling. The owner publishes and edits content: pages, blog, articles, portfolio. | Headless WordPress at `/cms` — write and edit content in wp-admin. |\n| **Ecommerce** | Selling is the point, and the rest is a few static pages (About, Contact, FAQ, policies). No ongoing blog. | Hostinger Ecommerce store — products, collections, checkout, payments, shipping, orders. |\n| **WordPress + Ecommerce** | Selling **and** regular editorial content (a blog, guides, lookbooks, recipes) the owner keeps adding to, run as **two** backends (WordPress site + separate Hostinger Ecommerce store). | Both: WordPress for content, the Ecommerce store for products and checkout. |\n| **WordPress + WooCommerce** | **Only** when the user explicitly names WooCommerce, or explicitly wants content **and** a store on **one** WordPress site (one admin, one domain). The one-site alternative to WordPress + Ecommerce. | A single headless WordPress at `/cms` with the WooCommerce plugin — products, cart, coupons and checkout (Cash on Delivery) over the WooCommerce Store API, plus WP posts/pages on the same site. |\n| **Webapp** | A custom **app, SPA or web application** that brings its own code and data — a dashboard, tool, calculator, booking app, a project the user is deploying — **not** a content site or a store. No CMS and no store engine. | A plain Node.js app host (an empty website, no `/cms`), with a MySQL database when the app needs one. The app owns all its routes and data. |\n\nRules of thumb:\n\n- **Is it an app, or a site?** A custom application the user builds/brings (its own logic, its own data,\n maybe a database) → **Webapp**. A content site or a shop → one of the three above. When the user says\n \"my app / SPA / dashboard / tool\", or hands you an existing app project to deploy, that's **Webapp**.\n- **Selling decides first (for sites).** No selling → WordPress. Selling → Ecommerce or\n WordPress + Ecommerce.\n- **Then the content.** Selling plus only a few fixed pages → Ecommerce. Selling plus content the\n owner will keep publishing → WordPress + Ecommerce.\n- **Hostinger Ecommerce is the default for selling — WooCommerce is never the default and is never\n recommended on its own.** Choose **WordPress + WooCommerce** only when the user explicitly names\n WooCommerce, or explicitly wants content **and** a store on one WordPress site (one admin, one\n domain — the one-site alternative to WordPress + Ecommerce). For every other selling case, keep\n Hostinger Ecommerce. If the user wants both content and a store but hasn't said \"one site\" or\n \"WooCommerce\", offer it as an option — don't switch to it silently.\n- **Don't add WordPress \"just in case\".** A second backend is a second admin to learn. Recommend the\n combination only when the user described ongoing content.\n- Selling a single service by enquiry (no checkout, no cart) is **not** commerce → WordPress with a\n contact page.\n- **Webapp never uses WordPress or a store.** If the user wants a custom app *and* a CMS/shop, that's two\n different sites — build the app as a Webapp and tell them the content/commerce backend is a separate\n domain.\n\n**Then set rendering** (write it in the brief's **Rendering** section): **Ecommerce → SSR required**\n(carts/search need per-request rendering — any SSR framework works; Next.js is the default, not a\nrequirement); **WordPress + WooCommerce → SSR required** (same reason — cart/checkout need per-request\nrendering); **WordPress → SSG (Astro)** if the site is mostly static and the owner is fine rebuilding\nto publish, otherwise **SSR**; **Webapp → whatever the app is** (SSR for a server app, SPA for a\nclient-only app) — it deploys as a Node.js app either way. When unsure, default to **Next.js SSR**.\n\n## Step 5 — Confirm the plan in one message\n\nSend **one** message that confirms **what you'll build** — the plan, not the plumbing. Lead with the\nsite, not the backend:\n\n- **The plan** — what it is, audience, pages, products (if any), tone. Mark anything you defaulted\n (*\"assumed: warm, minimal look\"*) so the user sees what they skipped.\n- **How it's built** — state the backend as a *consequence* in one plain line, focused on what it\n means for them day to day (\"I'll build this on WordPress, so you edit pages and posts in a normal\n WordPress admin\"), **not** as a choice you're asking them to make. Don't ask \"is WordPress OK?\" or\n offer alternatives unless it was a genuine close call *and* the difference matters to the user.\n\nEnd with *\"Good to go, or anything to change?\"* A yes (or silence-equivalent like \"go\") → write\n`brief.md`. Changes → apply them and write it; don't loop back into questions. If the user\n*volunteers* a different backend, use theirs and note it in the brief.\n\n## Step 6 — Write `brief.md` and hand off\n\nWrite `brief.md` from the template below. Fill every section from the conversation, defaulting what\nthe user skipped and tagging it `(assumed)`; leave out **Products** when nothing is sold. Use the\nuser's own words where you can. Then update `state.json`:\n`\"stage\": \"provision\"`, fresh `updated_at`.\n\nThe **Suggested backend** section is the contract with `03-provision/SKILL.md`, which routes on it.\nIts first line must be exactly one of `**WordPress**`, `**Ecommerce**`, `**WordPress + Ecommerce**`,\n`**WordPress + WooCommerce**` or `**Webapp**`, followed by an em dash and the one-line reason.\n`**WordPress + WooCommerce**` is the WordPress backend with the WooCommerce store turned on — provision\nroutes it to the `wordpress` subfolder with the store enabled (it is **not** a separate backend).\n\n## `brief.md` template\n\n```markdown\n# Brief — <domain>\n\n> Brief agreed with the user on <today>.\n\n## Purpose\n<what the site is, who runs it, what it's for — 2–3 sentences>\n\n## Audience\n<who visits, how they arrive, what device they mostly use>\n\n## Tone\n<voice in a sentence or two>\n\n## Design direction\n<look & feel in a few adjectives; **reference site(s)** the user named (URLs) and what to take from them\n(layout, structure, mood); colours / brand palette; any screenshots. This is where ALL design questions\nland — the frontend step reads this and does not re-ask. Write \"Agent's choice: <short style>\" when the\nuser didn't specify. Record in the user's own words a request to keep their current look (\"Keep the look\nwe have: <site URL | screenshots | brand guide>\") and any logo idea they describe; the design step acts\non both.>\n\n## Pages\n- **<Page>** — <one-line purpose>\n- …\n\n## Products\n<only when selling: what is sold, rough count, categories, variants, shipping regions, payments>\n\n## Content\n<what the owner will publish over time and how often — or \"A few fixed pages; no ongoing content.\">\n\n## Must-haves\n- …\n\n## Out of scope\n- …\n\n## Assets\n<logo, colours, photos, copy the user provided and where they are; \"None yet\" if none>\n\n## Suggested backend\n**<WordPress | Ecommerce | WordPress + Ecommerce | WordPress + WooCommerce | Webapp>** — <one-line reason>.\n<\"Confirmed by the user.\" or \"User chose this over the recommended <X> because <reason>.\">\n\n## Build mode\n**<Bring your own frontend | Agent designs and builds>** — <one line>. <When \"Bring your own frontend\":\nthe path to the user's existing project in the current folder (the frontend source), and how they want\nto drive design/code. When \"Agent designs and builds\": leave the design + frontend steps to run as\nnormal.> The design step and the frontend step both read this: **Bring your own frontend** → design\ndefers to the user and frontend builds on their project; **Agent designs and builds** → both run as normal.\n\n## Rendering\n**<SSR | SSG>** — <reason>. Rules: **Ecommerce requires SSR** and **WordPress + WooCommerce requires\nSSR** (carts/checkout need per-request rendering — any SSR framework works; Next.js is the default, not\na requirement); **WordPress may be SSG (Astro)** when the site is mostly static and rarely changes,\notherwise **SSR**. Default when unsure: **Next.js SSR**.\n```\n\n## Example — WordPress + Ecommerce\n\n> A ceramicist sells around 30 handmade pieces and writes a monthly studio journal.\n\n```markdown\n## Suggested backend\n**WordPress + Ecommerce** — sells ~30 products with checkout, and the owner publishes a monthly\nstudio journal they want to edit themselves.\nConfirmed by the user.\n```\n"
|
|
147
|
+
"text": "---\nname: headless-step-refine\ndescription: \"Step 2 (refine) of the Hostinger Headless flow. Turns the user's seed — often just a domain — into a brief through conversation: asks what they want to build (purpose, audience, tone, pages, products, must-haves; images, local files, links all welcome), then *infers* the backend that fits (WordPress, Ecommerce, WordPress + Ecommerce, WordPress + WooCommerce for a one-site content+store, or Webapp for a custom app/SPA) — it never asks the user to pick a backend or a technology; the user confirms the plan in plain language, then it writes brief.md. Hostinger Ecommerce stays the default for selling; WooCommerce is chosen only when the user explicitly asks for it or wants content and a store on one WordPress site.\"\n---\n\n# headless-step-refine\n\nFind out what the user wants to build, guess which backend fits, and write it down. This is a\n**conversation, not a form**: the pasted prompt names a domain and nothing else, so everything about\nthe site comes from talking to the user.\n\n**Keep it short.** The only thing you must know is *what they are building* — enough to guess the\nbackend. Every other question is optional: the more the user gives, the better the site, but never\nhold the build hostage to refinement. Fill gaps with sensible defaults and say so in the brief. A\nskipped question tool is not the user passing on a question; see *When you ask the user* in the\ntop-level `SKILL.md`.\n\n> **Never ask the user to choose a backend or a technology.** Don't ask \"WordPress or Ecommerce?\",\n> don't present WordPress/Ecommerce/WooCommerce/Webapp as options, and don't surface the backend in a\n> multiple-choice UI. Open with **\"What do you want to build?\"** and infer the backend yourself in\n> Step 4 from what they describe (and whether they sell). The backend is a technical decision the flow\n> makes *for* the user — they confirm the **plan** (what the site is and does), not the plumbing. The\n> only exception is when the user *volunteers* a backend (\"build it on WordPress\", \"use WooCommerce\"):\n> then honour their choice and note it in the brief.\n\n**In:** the domain (the workspace directory name), plus anything the user already shared.\n**Out:** `~/.hostinger/<domain>/brief.md`.\n\nNothing in this step touches Hostinger. It only asks, decides and writes.\n\n---\n\n## Step 1 — Read the workspace, then the resume check\n\nRead everything already in `~/.hostinger/<domain>/` (`state.json`, `connect.json`, any notes, images\nor files the user dropped there) — it all counts as input.\n\nIf `brief.md` already exists for this domain, **don't overwrite it**. Show the user a three-line\nsummary (what the site is, its pages, the suggested backend) and ask: *keep it, or change\nsomething?* Keep → go to Step 6. Change → edit only what they asked for, then re-confirm (Step 5). A\nskipped tool result is not Keep; see *When you ask the user* in the top-level `SKILL.md`.\n\nIf the site is already provisioned (`site.json` exists), the backend is **fixed** — never change the\nSuggested backend on a provisioned site. Explain that switching backends means a new domain.\n\n## Step 2 — Gather what you already have\n\nBefore asking anything, collect what the user has already given you, so you don't ask for it again:\n\n- **Inspect the directory the command was run in** — this is the biggest signal for the build mode\n (Step 3). Look for an existing **frontend / webapp / commerce project**: a `package.json`, a\n framework config (`next.config.*`, `astro.config.*`, `vite.config.*`, `svelte.config.*`, `nuxt.*`),\n a `src/` or `app/` tree, `index.html`, a storefront/cart, etc. A real project here usually means the\n user **already has their own frontend** and mainly needs a Hostinger backend to wire it to — lean\n toward \"bring your own frontend\" in Step 3. If the folder has nothing site-like, assume they don't\n and lean toward \"agent builds it\".\n- Anything in the pasted prompt beyond the domain (a sentence, a link, a screenshot).\n- Files in the current project that look like site material (copy, logos, product lists, images).\n- What the agent already knows about the user from local memory or this conversation (a prior build,\n stack preferences, a frontend they always use).\n- The domain name itself — a hint only (`coral-pottery.com` suggests pottery), never a conclusion.\n\n## Step 3 — Ask\n\nIf Step 2 already answers *what are you building* and *will they sell*, skip the Round 1–3 questions\nbelow, but still ask **Build mode** before moving on.\n\nOtherwise open with one line and **wait for the user's answer**: *\"Tell me what you want to build — a\nsentence is enough. Links, screenshots or files help too. Skip anything you don't want to answer.\"*\n\n> **\"What do you want to build?\" is an OPEN question — ask it in plain prose and wait.** Never turn it\n> into a multiple-choice list of pre-baked site ideas (\"Coffee shop / Travel blog / Portfolio …\"):\n> that puts words in the user's mouth and defeats the point of asking. Let them describe it in their\n> own words. The structured question UI is only for genuinely *choice-shaped* follow-ups (selling\n> yes/no, which pages, tone adjectives, the build mode) — not for the opening question, and never for\n> the backend.\n\nAsk in **short rounds of 2–3 questions**, and adapt each round to the previous answers. If your tool\nhas a structured question UI (multiple choice with an \"other\" field), use it for the choice-shaped\nfollow-up questions (pages, selling yes/no, tone) — **never for the opening \"what do you want to\nbuild?\" and never for the backend** (that's your decision, not theirs). **At most 3 rounds**, and\nstop early as soon as you can guess the backend and the user seems done — \"just build it\" or \"you\ndecide\" means *use defaults*, not *ask again*. A skipped tool result is not their answer: re-ask\nthat round in plain text, as *When you ask the user* in the top-level `SKILL.md` says. The re-ask\nrepeats the round; it doesn't count toward the 3.\n\n**Round 1 — what it is (the only required round):**\n\n1. **What are you building?** The business or project, and what the site is for.\n2. **Will you sell anything on it?** Physical products, digital products, services with checkout —\n or nothing. *This decides the backend.* Skip it only when the answer is already unmistakable\n (\"an online shop for my candles\", \"my personal blog\"); when in doubt, ask.\n3. **Who is it for?** The audience, and how they'll find the site. *(optional)*\n\n**Round 2 — what goes on it (optional, adapt to Round 1):**\n\n- **Content they'll publish over time** — blog posts, articles, news, recipes, portfolio pieces,\n case studies? How often? Or is it a handful of pages that rarely change?\n- **Pages** — propose a page list from what you know and ask them to add or remove.\n- If they sell: **what and how many** — a rough product count, categories, whether products have\n variants (size, colour), and the shipping regions.\n\n**Round 3 — how it should feel (optional, skip what you already know):**\n\nThis is the round that captures **all design input** — the design step reads it from `brief.md` to derive\nthe design directions, so it is **not** asked again later; gather it here:\n\n- **Reference & look** — a **site they like (URL)** and what to take from it, plus a few adjectives for\n the vibe. Screenshots welcome.\n- **Colours & brand** — brand/accent colours, logo, fonts, existing copy, photos. Note where they live.\n- **Must-haves and no-gos** — contact form, newsletter, bookings, languages, things to avoid.\n\nIf the user asks for something outside the scope (user accounts, member areas, bookings with a\ncalendar backend, user-generated content), say plainly that the headless flow doesn't support it yet\nand record it under **Out of scope** — don't promise it.\n\n> **A contact form *is* supported (WordPress backends).** If the user wants one, keep it in\n> **Must-haves** and name it \"Contact form\". Provision installs **Contact Form 7** on `/cms` and the\n> frontend wires a contact page whose form emails the site owner through WordPress's built-in\n> `wp_mail()` — no third-party service and no API keys. It's **opt-in**: built only when the brief asks\n> for it. (A form that needs accounts, payments or a booking calendar is still out of scope — this is a\n> plain name/email/message enquiry form.)\n\n### Build mode — ask once (this one *is* a user choice)\n\nThe backend is your call, but **how the frontend gets built is the user's** — because most of the time\nthe user already has their own frontend and just needs a Hostinger backend to wire it to. After the\nfolder + memory inspection (Step 2), ask **one** question (a structured choice is ideal here), seeding\nthe default from what you found:\n\n- **Bring your own frontend** *(default when Step 2 found a real project in the folder)* — \"I already\n have a frontend / I'll drive the design and we vibe-code it together.\" The agent **uses the user's\n existing project in the current folder as the source**, collaborates on design/code at the user's\n direction, and wires it to the provisioned backend. There is **no agent-generated design** — the\n design step defers to the user (see the Build mode box at the top of the design step), and the frontend step builds on their project\n rather than scaffolding a new one.\n- **Agent designs and builds it** *(default when the folder has nothing site-like)* — \"Give me ideas\n — you design and code it.\" Today's flow: the design step proposes directions and the frontend step\n scaffolds and implements them.\n\nA skipped tool result is not a pick: re-ask in plain text, and use the seeded default only when the\nuser says so (see *When you ask the user* in the top-level `SKILL.md`).\n\nRecord the answer as the brief's **Build mode**. This is the one frontend/technology question you *do*\nput to the user; everything else about the stack (framework, SSR/SSG) is still inferred. The deliverable\nis the same either way — a working site on the backend; only *who drives the frontend* changes.\n\n## Step 4 — Decide the backend (your call, from the brief — not a question)\n\nFrom what the user described, **you** pick the backend that fits — this is an internal decision, never\na question put back to the user. Pick the backend that fits what the user told you:\n\n| Backend | Choose when | What it gives the owner |\n|---|---|---|\n| **WordPress** | No selling. The owner publishes and edits content: pages, blog, articles, portfolio. | Headless WordPress at `/cms` — write and edit content in wp-admin. |\n| **Ecommerce** | Selling is the point, and the rest is a few static pages (About, Contact, FAQ, policies). No ongoing blog. | Hostinger Ecommerce store — products, collections, checkout, payments, shipping, orders. |\n| **WordPress + Ecommerce** | Selling **and** regular editorial content (a blog, guides, lookbooks, recipes) the owner keeps adding to, run as **two** backends (WordPress site + separate Hostinger Ecommerce store). | Both: WordPress for content, the Ecommerce store for products and checkout. |\n| **WordPress + WooCommerce** | **Only** when the user explicitly names WooCommerce, or explicitly wants content **and** a store on **one** WordPress site (one admin, one domain). The one-site alternative to WordPress + Ecommerce. | A single headless WordPress at `/cms` with the WooCommerce plugin — products, cart, coupons and checkout (Cash on Delivery) over the WooCommerce Store API, plus WP posts/pages on the same site. |\n| **Webapp** | A custom **app, SPA or web application** that brings its own code and data — a dashboard, tool, calculator, booking app, a project the user is deploying — **not** a content site or a store. No CMS and no store engine. | A plain Node.js app host (an empty website, no `/cms`), with a MySQL database when the app needs one. The app owns all its routes and data. |\n\nRules of thumb:\n\n- **Is it an app, or a site?** A custom application the user builds/brings (its own logic, its own data,\n maybe a database) → **Webapp**. A content site or a shop → one of the three above. When the user says\n \"my app / SPA / dashboard / tool\", or hands you an existing app project to deploy, that's **Webapp**.\n- **Selling decides first (for sites).** No selling → WordPress. Selling → Ecommerce or\n WordPress + Ecommerce.\n- **Then the content.** Selling plus only a few fixed pages → Ecommerce. Selling plus content the\n owner will keep publishing → WordPress + Ecommerce.\n- **Hostinger Ecommerce is the default for selling — WooCommerce is never the default and is never\n recommended on its own.** Choose **WordPress + WooCommerce** only when the user explicitly names\n WooCommerce, or explicitly wants content **and** a store on one WordPress site (one admin, one\n domain — the one-site alternative to WordPress + Ecommerce). For every other selling case, keep\n Hostinger Ecommerce. If the user wants both content and a store but hasn't said \"one site\" or\n \"WooCommerce\", offer it as an option — don't switch to it silently.\n- **Don't add WordPress \"just in case\".** A second backend is a second admin to learn. Recommend the\n combination only when the user described ongoing content.\n- Selling a single service by enquiry (no checkout, no cart) is **not** commerce → WordPress with a\n contact page.\n- **Webapp never uses WordPress or a store.** If the user wants a custom app *and* a CMS/shop, that's two\n different sites — build the app as a Webapp and tell them the content/commerce backend is a separate\n domain.\n\n**Then set rendering** (write it in the brief's **Rendering** section): **Ecommerce → SSR required**\n(carts/search need per-request rendering — any SSR framework works; Next.js is the default, not a\nrequirement); **WordPress + WooCommerce → SSR required** (same reason — cart/checkout need per-request\nrendering); **WordPress → SSG (Astro)** if the site is mostly static and the owner is fine rebuilding\nto publish, otherwise **SSR**; **Webapp → whatever the app is** (SSR for a server app, SPA for a\nclient-only app) — it deploys as a Node.js app either way. When unsure, default to **Next.js SSR**.\n\n## Step 5 — Confirm the plan in one message\n\nSend **one** message that confirms **what you'll build** — the plan, not the plumbing. Lead with the\nsite, not the backend:\n\n- **The plan** — what it is, audience, pages, products (if any), tone. Mark anything you defaulted\n (*\"assumed: warm, minimal look\"*) so the user sees what they skipped.\n- **How it's built** — state the backend as a *consequence* in one plain line, focused on what it\n means for them day to day (\"I'll build this on WordPress, so you edit pages and posts in a normal\n WordPress admin\"), **not** as a choice you're asking them to make. Don't ask \"is WordPress OK?\" or\n offer alternatives unless it was a genuine close call *and* the difference matters to the user.\n\nEnd with *\"Good to go, or anything to change?\"* A yes (or \"go\") → write `brief.md`. A skipped tool\nresult is not a yes (see *When you ask the user* in the top-level `SKILL.md`). Changes → apply them\nand write it; don't loop back into questions. If the user *volunteers* a different backend, use\ntheirs and note it in the brief.\n\n## Step 6 — Write `brief.md` and hand off\n\nWrite `brief.md` from the template below. Fill every section from the conversation, defaulting what\nthe user skipped and tagging it `(assumed)`; leave out **Products** when nothing is sold. Use the\nuser's own words where you can. Then update `state.json`:\n`\"stage\": \"provision\"`, fresh `updated_at`.\n\nThe **Suggested backend** section is the contract with `03-provision/SKILL.md`, which routes on it.\nIts first line must be exactly one of `**WordPress**`, `**Ecommerce**`, `**WordPress + Ecommerce**`,\n`**WordPress + WooCommerce**` or `**Webapp**`, followed by an em dash and the one-line reason.\n`**WordPress + WooCommerce**` is the WordPress backend with the WooCommerce store turned on — provision\nroutes it to the `wordpress` subfolder with the store enabled (it is **not** a separate backend).\n\n## `brief.md` template\n\n```markdown\n# Brief — <domain>\n\n> Brief agreed with the user on <today>.\n\n## Purpose\n<what the site is, who runs it, what it's for — 2–3 sentences>\n\n## Audience\n<who visits, how they arrive, what device they mostly use>\n\n## Tone\n<voice in a sentence or two>\n\n## Design direction\n<look & feel in a few adjectives; **reference site(s)** the user named (URLs) and what to take from them\n(layout, structure, mood); colours / brand palette; any screenshots. This is where ALL design questions\nland — the frontend step reads this and does not re-ask. Write \"Agent's choice: <short style>\" when the\nuser didn't specify. Record in the user's own words a request to keep their current look (\"Keep the look\nwe have: <site URL | screenshots | brand guide>\") and any logo idea they describe; the design step acts\non both.>\n\n## Pages\n- **<Page>** — <one-line purpose>\n- …\n\n## Products\n<only when selling: what is sold, rough count, categories, variants, shipping regions, payments>\n\n## Content\n<what the owner will publish over time and how often — or \"A few fixed pages; no ongoing content.\">\n\n## Must-haves\n- …\n\n## Out of scope\n- …\n\n## Assets\n<logo, colours, photos, copy the user provided and where they are; \"None yet\" if none>\n\n## Suggested backend\n**<WordPress | Ecommerce | WordPress + Ecommerce | WordPress + WooCommerce | Webapp>** — <one-line reason>.\n<\"Confirmed by the user.\" or \"User chose this over the recommended <X> because <reason>.\">\n\n## Build mode\n**<Bring your own frontend | Agent designs and builds>** — <one line>. <When \"Bring your own frontend\":\nthe path to the user's existing project in the current folder (the frontend source), and how they want\nto drive design/code. When \"Agent designs and builds\": leave the design + frontend steps to run as\nnormal.> The design step and the frontend step both read this: **Bring your own frontend** → design\ndefers to the user and frontend builds on their project; **Agent designs and builds** → both run as normal.\n\n## Rendering\n**<SSR | SSG>** — <reason>. Rules: **Ecommerce requires SSR** and **WordPress + WooCommerce requires\nSSR** (carts/checkout need per-request rendering — any SSR framework works; Next.js is the default, not\na requirement); **WordPress may be SSG (Astro)** when the site is mostly static and rarely changes,\notherwise **SSR**. Default when unsure: **Next.js SSR**.\n```\n\n## Example — WordPress + Ecommerce\n\n> A ceramicist sells around 30 handmade pieces and writes a monthly studio journal.\n\n```markdown\n## Suggested backend\n**WordPress + Ecommerce** — sells ~30 products with checkout, and the owner publishes a monthly\nstudio journal they want to edit themselves.\nConfirmed by the user.\n```\n"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"uri": "skill://hostinger-headless/steps/03-provision/SKILL.md",
|
|
@@ -176,10 +176,10 @@ export default [
|
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
"uri": "skill://hostinger-headless/steps/04-content-model/SKILL.md",
|
|
179
|
-
"digest": "sha256:
|
|
180
|
-
"size":
|
|
179
|
+
"digest": "sha256:1e053bcf2f4334174de951537cd4bd099b384bc548b2476607e0eb09a20264d5",
|
|
180
|
+
"size": 13878,
|
|
181
181
|
"mimeType": "text/markdown",
|
|
182
|
-
"text": "---\nname: headless-step-content-model\ndescription: \"Step 4 (content model) of the Hostinger Headless flow. Prepare phase: turns brief.md into a content helper (content-model.md) — fixed Title/Description plus a tailored generation prompt per section (Navigation, Pages, Articles) — and generates the drafts/images, no live backend needed. Push phase: routes to the backend subfolder to create it the backend's way once provision is ready.\"\n---\n\n# headless-step-content-model\n\nTwo phases around the **instance barrier** (see the top-level `SKILL.md`):\n\n- **Prepare — no instance.** Build the content helper from `brief.md` and generate the content it\n calls for. Runs *during* the provision wait; needs only the brief.\n- **Push — needs the instance.** Create that content in the backend. Gated on\n `provision.status == \"completed\"`; routes to `wordpress/` or `ecommerce/`.\n\n**In:** `brief.md`. **Out:** `content-model.md` (+ any generated media under\n`~/.hostinger/<domain>/media/`).\n\n> Don't wait for provisioning — the helper comes from the brief alone.\n\n---\n\n## Prepare phase — write the content helper (no instance)\n\n`content-model.md` is a lightweight helper, not a schema. Fill the **fixed values** from the brief and\nwrite a **generation prompt** for each section — tailored to *this* site, not generic. Then run those\nprompts to produce the drafts (and images), so the push phase is just upload.\n\nWrite `content-model.md` from this template, adapting every prompt to the brief's purpose, audience\nand tone:\n\n```markdown\n# Content — <domain>\n\n> Content helper prepared from brief.md on <today>. Fixed values are set; each prompt is run\n> during the provision wait to generate the content and media. Push to the backend happens later.\n\n## Title\n<site name, from the brief>\n\n## Description\n<one–two line site description / default meta, from the brief>\n\n## Navigation\n> Generate the primary menu from the site's pages in reading order (<list the brief's pages>).\n> Short labels; note which items are static pages vs. listing pages. Add a footer menu if the\n> brief implies one (socials, contact).\n\n## Pages\n> Generate each page the brief lists. For every page: title, slug, a short section outline, the\n> copy for static pages (e.g. About, Contact), and SEO title + description.\n> <bullet the brief's pages with their one-line purpose>\n\n## Articles (optional)\n> Generate <N> articles across the brief's categories (<list them>). For each: title, slug,\n> body in the brief's tone, category, any relationship (e.g. destination), published date, SEO,\n> and a featured image (see *Images* below — Hostinger AI generation or Unsplash stock) with\n> alt text. Omit if the brief has no article-like content.\n\n## Products (optional — required for a commerce brief)\n> Skip for a non-commerce brief. For a commerce brief, generate the catalog content here (prepare\n> work, no instance): for each product — name, slug, SKU, short + long description in the brief's\n> tone, price, the collection it belongs to, variants if any, and a product image (see *Images* —\n> Hostinger AI generation or Unsplash stock) with alt text. Group products into the collections\n> the brief implies, and generate any **coupons** the brief calls for (code, type, amount). The\n> `ecommerce` push phase creates these on the store's sales channel; a **WordPress + WooCommerce** brief\n> seeds them into WooCommerce instead (see `wordpress/SKILL.md` + `wordpress/woocommerce.md`) — there\n> the **SKU is the idempotency key** and coupons + product categories are part of the catalog.\n```\n\nGuidance:\n\n- **Fixed vs. prompt.** Title and Description are concrete values. Navigation, Pages and Articles are\n prompts because their volume/wording is generated — keep them as prompts so the push phase (or a\n rerun) can regenerate consistently.\n- **Run the prompts now.** Generate the actual page copy and article drafts during the wait — this is\n the slowest work and the whole reason it runs in parallel. Images depend on the chosen source (see\n *Images* below): Unsplash photos and your own generated files can be produced now; Hostinger AI\n images only generate once the instance exists, so write their brief now and generate at the barrier.\n Put any generated/own media in `~/.hostinger/<domain>/media/` and reference it by filename. Record\n alt text for every image.\n- **Optional sections.** Drop **Articles** if the brief has no article-like content. Drop **Products**\n for a non-commerce brief; for a commerce brief, generate the product catalog here (names, prices,\n descriptions, collections, images) — that's prepare work, and the `ecommerce` push phase then creates\n it on the store.\n- **Don't block on design.** The design step (5) owns the look, and it runs in parallel during the\n provision wait, so never wait for it here. If `design.md` already exists when you source images,\n follow its **Imagery** section; otherwise generate neutral, on-topic imagery from the brief.\n\n### Images — generate with Hostinger AI, or source stock from Unsplash\n\nEvery article (and every product) needs an image. There are two sources, and **you MUST offer the user\nboth and let them choose — never pick silently.** State the default for this backend in one line, name\nthe other option, and proceed on their answer (a skipped answer / \"you decide\" = use the default):\n\n| Option | Tool | Backends | Needs the live instance? |\n|---|---|---|---|\n| **A — Hostinger AI image generation** *(default for WordPress / WooCommerce)* | `wordpress_ai-content_generate-images` | WordPress, WordPress + WooCommerce | **yes** — runs at the instance barrier |\n| **B — Unsplash stock photos** *(default for Ecommerce / Webapp; the fallback everywhere)* | Unsplash API / web search | any | no — pure prepare work |\n\n> **Do not silently fall back to stock on a WordPress site.** On WordPress / WooCommerce the default is\n> **Hostinger AI (Option A)** — on-brand, generated from `design.md`. Using Unsplash on a WordPress build\n> is only correct when **the user chose it**, or when **AI generation actually failed** (and then you say\n> so). Taking stock because it is faster, synchronous, or avoids the async poll is a **defect**, not a\n> shortcut: the async start → poll → upload-before-expiry flow (Option A below) is the expected work, not\n> an optional extra. If you are tempted to skip it \"for speed\", stop and offer the user the choice instead.\n\nIf you can generate images yourself (some agents can), that's a third path: put the files in `media/`\nand record alt text — no tool needed. **Record alt text for every image whichever source you use.**\n\n**During the prepare wait, write an image brief for each image** into `content-model.md` no matter the\noption (subject, `image_type`, aspect, and the alt text you expect). Option B can also *fetch* the\nphoto now (below); Option A only generates once the instance exists, so its briefs wait for the barrier.\n\n#### Option A — Hostinger AI image generation (WordPress backends)\n\n`wordpress_ai-content_generate-images` generates on-brand images for a live WordPress install, so it\nruns **at the instance barrier** (after `provision.status == \"completed\"`), using the install from\n`site.json` (`username`, and `wordpress.install_id` → the `software` path param). Follow `design.md`'s\n**Imagery** section for look and palette. It is **asynchronous**:\n\n1. **Start** one generation per image:\n\n ```json\n { \"operation\": \"wordpress_ai-content_generate-images\",\n \"params\": {\n \"username\": \"<site.json username>\",\n \"software\": \"<site.json wordpress.install_id>\",\n \"prompt\": \"<what the image shows; for a logo, the wordmark + mark to render>\",\n \"image_type\": \"hero|background|inline|gallery|product|logo\",\n \"num_of_images\": 1,\n \"style\": \"<photographic style tokens from design.md; ignored for logo>\",\n \"color_guidance\": \"<logo only — colour direction, not rendered as text>\" } }\n ```\n\n `prompt` (3–2500 chars) and `image_type` are required; `width`/`height` (128–2048) default per type\n and are ignored for `logo`. Returns `202` with a `generation_id` and `status: in-progress`.\n **Only one generation runs per install at a time** — a `409` means one is already running (and it\n returns no id), so wait for it to finish before starting the next. The endpoint allows 10\n requests/minute.\n\n2. **Poll** `wordpress_ai-content_generation-result` with `{ username, software, generation_id }`\n every few seconds until `status` is `completed` (or `failed`). A completed result carries\n `result.images[]` — each with a **temporary** `url`, plus `width`, `height`, `content_type` — and\n `result.expires_at`.\n\n3. **Use the images before they expire.** The URLs are short-lived, so in the push phase hand each\n `url` straight to WordPress with `media-upload-from-url`, which downloads and stores WP's own copy\n (see the wordpress push `SKILL.md`). Don't persist the temporary URL as the live image. There is no\n photographer to credit — leave `caption` empty or a short on-brand one. Record under the\n article/product in `content-model.md`:\n\n ```markdown\n Image: Hostinger AI (image_type: <type>, generation_id: <uuid>)\n Image alt: <what the image shows>\n Image file: <site>-<subject>.jpg\n ```\n\n On `failed`, retry once with a tightened prompt; if it fails again, fall back to Option B and note\n the gap. Never block the whole push on a single image.\n\n#### Option B — stock photos from Unsplash (any backend, no instance)\n\nThis is still prepare work and needs no live backend: you pick the photos and record their URLs here,\nand the push phase uploads them.\n\n1. **Find candidates.** Use one photo per article, matching its subject and destination.\n - With an `UNSPLASH_ACCESS_KEY` in the environment, use the official API:\n `GET https://api.unsplash.com/search/photos?query=<subject>&orientation=landscape` with header\n `Authorization: Client-ID <key>`.\n - Without a key, use web search limited to `unsplash.com` (e.g. \"unsplash photo Lisbon tram\") and\n take the photo id from the result URL: the last segment of `unsplash.com/photos/<slug>-<id>`, or\n the whole segment for `unsplash.com/photos/<id>`.\n - Don't call `unsplash.com/napi/…`. It sits behind a bot challenge and returns 401.\n2. **Resolve a direct file URL.** `GET https://unsplash.com/photos/<id>/download?force=true` (don't\n follow the redirect) answers `302` with `Location: https://images.unsplash.com/photo-<…>?…`. The\n `dl=` parameter in it also carries the photographer's name. Drop the query string and request a\n web size: `https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max`. The upload ability\n needs this direct file URL, which returns 200 with `image/jpeg`. A photo *page* URL won't work.\n3. **Look before you keep it.** Download a small preview (`?w=500`) and view it. Search snippets\n only show captions, and a photo often isn't quite what its title says. Reject anything off-topic.\n If nothing fits exactly, take the closest real photo and note the gap rather than mislabel it.\n4. **Write alt text from what the photo shows**, not from the article's image brief. For example,\n use \"Shopfronts and parked scooters on a Hanoi street\" even if the article is about rain.\n5. **Record it** under each article in `content-model.md`:\n\n ```markdown\n Image: https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max\n Image credit: <Photographer> — https://unsplash.com/photos/<id> (Unsplash License)\n Image alt: <what the photo shows>\n Image file: <site>-<subject>.jpg\n ```\n\nUnsplash photos are free to use under the Unsplash License. Still credit the photographer: the push\nphase stores \"Photo by <Photographer> on Unsplash\" as the image caption.\n\nThen update `state.json`: set `stage` to the next prepare stage still to do, fresh `updated_at`. Do\n**not** advance past the barrier here.\n\n---\n\n## Push phase — create it the backend's way (needs the instance)\n\nOnly once `provision.status == \"completed\"` (re-verified against `hosting_websites_list-setups`),\nroute on the brief's **Suggested backend** and follow that subfolder, which maps the helper onto the\nlive backend — for WordPress, through the per-site WP MCP's ability tools (the installation JWT\nauthenticates that MCP session; `wp/v2` REST is the public read surface, not a write path for the\ntoken) to create the menu, pages, posts, categories and media:\n\n| Suggested backend | Follow |\n|---|---|\n| WordPress | `wordpress/SKILL.md` |\n| WordPress + WooCommerce | `wordpress/SKILL.md` — pushes WP content **and** seeds the WooCommerce catalog (its WooCommerce section + `wordpress/woocommerce.md`) |\n| Ecommerce | `ecommerce/SKILL.md` |\n| Webapp | — (no push; skip to frontend) |\n\n> **Supported backends: `wordpress` (optionally with a WooCommerce store), `ecommerce`, and `webapp`** —\n> where `webapp` is \"supported\" by **skipping this whole stage** (it has no content backend; see the note\n> below). A **WordPress + WooCommerce** brief routes to `wordpress/SKILL.md`, which pushes the WP content\n> **and** seeds the WooCommerce catalog in the same push. PocketBase is not available in this version.\n> Push to whichever single backend provision set up (see the provision router) — don't try to push to two\n> backends here.\n>\n> **Webapp has no content backend.** There is no CMS or store to seed, so a `webapp` brief **skips this\n> whole stage** — both prepare and push. The app owns its own data (its own API or database). Leave\n> `content-model.md` unwritten, mark content-model `skipped` in the run log, and go straight to the\n> frontend step.\n\nAfter the push completes, update `state.json` and continue to the frontend **data** phase.\n"
|
|
182
|
+
"text": "---\nname: headless-step-content-model\ndescription: \"Step 4 (content model) of the Hostinger Headless flow. Prepare phase: turns brief.md into a content helper (content-model.md) — fixed Title/Description plus a tailored generation prompt per section (Navigation, Pages, Articles) — and generates the drafts/images, no live backend needed. Push phase: routes to the backend subfolder to create it the backend's way once provision is ready.\"\n---\n\n# headless-step-content-model\n\nTwo phases around the **instance barrier** (see the top-level `SKILL.md`):\n\n- **Prepare — no instance.** Build the content helper from `brief.md` and generate the content it\n calls for. Runs *during* the provision wait; needs only the brief.\n- **Push — needs the instance.** Create that content in the backend. Gated on\n `provision.status == \"completed\"`; routes to `wordpress/` or `ecommerce/`.\n\n**In:** `brief.md`. **Out:** `content-model.md` (+ any generated media under\n`~/.hostinger/<domain>/media/`).\n\n> Don't wait for provisioning — the helper comes from the brief alone.\n\n---\n\n## Prepare phase — write the content helper (no instance)\n\n`content-model.md` is a lightweight helper, not a schema. Fill the **fixed values** from the brief and\nwrite a **generation prompt** for each section — tailored to *this* site, not generic. Then run those\nprompts to produce the drafts (and images), so the push phase is just upload.\n\nWrite `content-model.md` from this template, adapting every prompt to the brief's purpose, audience\nand tone:\n\n```markdown\n# Content — <domain>\n\n> Content helper prepared from brief.md on <today>. Fixed values are set; each prompt is run\n> during the provision wait to generate the content and media. Push to the backend happens later.\n\n## Title\n<site name, from the brief>\n\n## Description\n<one–two line site description / default meta, from the brief>\n\n## Navigation\n> Generate the primary menu from the site's pages in reading order (<list the brief's pages>).\n> Short labels; note which items are static pages vs. listing pages. Add a footer menu if the\n> brief implies one (socials, contact).\n\n## Pages\n> Generate each page the brief lists. For every page: title, slug, a short section outline, the\n> copy for static pages (e.g. About, Contact), and SEO title + description.\n> <bullet the brief's pages with their one-line purpose>\n\n## Articles (optional)\n> Generate <N> articles across the brief's categories (<list them>). For each: title, slug,\n> body in the brief's tone, category, any relationship (e.g. destination), published date, SEO,\n> and a featured image (see *Images* below — Hostinger AI generation or Unsplash stock) with\n> alt text. Omit if the brief has no article-like content.\n\n## Products (optional — required for a commerce brief)\n> Skip for a non-commerce brief. For a commerce brief, generate the catalog content here (prepare\n> work, no instance): for each product — name, slug, SKU, short + long description in the brief's\n> tone, price, the collection it belongs to, variants if any, and a product image (see *Images* —\n> Hostinger AI generation or Unsplash stock) with alt text. Group products into the collections\n> the brief implies, and generate any **coupons** the brief calls for (code, type, amount). The\n> `ecommerce` push phase creates these on the store's sales channel; a **WordPress + WooCommerce** brief\n> seeds them into WooCommerce instead (see `wordpress/SKILL.md` + `wordpress/woocommerce.md`) — there\n> the **SKU is the idempotency key** and coupons + product categories are part of the catalog.\n```\n\nGuidance:\n\n- **Fixed vs. prompt.** Title and Description are concrete values. Navigation, Pages and Articles are\n prompts because their volume/wording is generated — keep them as prompts so the push phase (or a\n rerun) can regenerate consistently.\n- **Run the prompts now.** Generate the actual page copy and article drafts during the wait — this is\n the slowest work and the whole reason it runs in parallel. Images depend on the chosen source (see\n *Images* below): Unsplash photos and your own generated files can be produced now; Hostinger AI\n images only generate once the instance exists, so write their brief now and generate at the barrier.\n Put any generated/own media in `~/.hostinger/<domain>/media/` and reference it by filename. Record\n alt text for every image.\n- **Optional sections.** Drop **Articles** if the brief has no article-like content. Drop **Products**\n for a non-commerce brief; for a commerce brief, generate the product catalog here (names, prices,\n descriptions, collections, images) — that's prepare work, and the `ecommerce` push phase then creates\n it on the store.\n- **Don't block on design.** The design step (5) owns the look, and it runs in parallel during the\n provision wait, so never wait for it here. If `design.md` already exists when you source images,\n follow its **Imagery** section; otherwise generate neutral, on-topic imagery from the brief.\n\n### Images — generate with Hostinger AI, or source stock from Unsplash\n\nEvery article (and every product) needs an image. There are two sources, and **you MUST offer the user\nboth and let them choose — never pick silently.** State the default for this backend in one line, name\nthe other option, and proceed on their answer (\"you decide\" = use the default; a skipped tool result is\nnot an answer, see *When you ask the user* in the top-level `SKILL.md`):\n\n| Option | Tool | Backends | Needs the live instance? |\n|---|---|---|---|\n| **A — Hostinger AI image generation** *(default for WordPress / WooCommerce)* | `wordpress_ai-content_generate-images` | WordPress, WordPress + WooCommerce | **yes** — runs at the instance barrier |\n| **B — Unsplash stock photos** *(default for Ecommerce / Webapp; the fallback everywhere)* | Unsplash API / web search | any | no — pure prepare work |\n\n> **Do not silently fall back to stock on a WordPress site.** On WordPress / WooCommerce the default is\n> **Hostinger AI (Option A)** — on-brand, generated from `design.md`. Using Unsplash on a WordPress build\n> is only correct when **the user chose it**, or when **AI generation actually failed** (and then you say\n> so). Taking stock because it is faster, synchronous, or avoids the async poll is a **defect**, not a\n> shortcut: the async start → poll → upload-before-expiry flow (Option A below) is the expected work, not\n> an optional extra. If you are tempted to skip it \"for speed\", stop and offer the user the choice instead.\n\nIf you can generate images yourself (some agents can), that's a third path: put the files in `media/`\nand record alt text — no tool needed. **Record alt text for every image whichever source you use.**\n\n**During the prepare wait, write an image brief for each image** into `content-model.md` no matter the\noption (subject, `image_type`, aspect, and the alt text you expect). Option B can also *fetch* the\nphoto now (below); Option A only generates once the instance exists, so its briefs wait for the barrier.\n\n#### Option A — Hostinger AI image generation (WordPress backends)\n\n`wordpress_ai-content_generate-images` generates on-brand images for a live WordPress install, so it\nruns **at the instance barrier** (after `provision.status == \"completed\"`), using the install from\n`site.json` (`username`, and `wordpress.install_id` → the `software` path param). Follow `design.md`'s\n**Imagery** section for look and palette. It is **asynchronous**:\n\n1. **Start** one generation per image:\n\n ```json\n { \"operation\": \"wordpress_ai-content_generate-images\",\n \"params\": {\n \"username\": \"<site.json username>\",\n \"software\": \"<site.json wordpress.install_id>\",\n \"prompt\": \"<what the image shows; for a logo, the wordmark + mark to render>\",\n \"image_type\": \"hero|background|inline|gallery|product|logo\",\n \"num_of_images\": 1,\n \"style\": \"<photographic style tokens from design.md; ignored for logo>\",\n \"color_guidance\": \"<logo only — colour direction, not rendered as text>\" } }\n ```\n\n `prompt` (3–2500 chars) and `image_type` are required; `width`/`height` (128–2048) default per type\n and are ignored for `logo`. Returns `202` with a `generation_id` and `status: in-progress`.\n **Only one generation runs per install at a time** — a `409` means one is already running (and it\n returns no id), so wait for it to finish before starting the next. The endpoint allows 10\n requests/minute.\n\n2. **Poll** `wordpress_ai-content_generation-result` with `{ username, software, generation_id }`\n every few seconds until `status` is `completed` (or `failed`). A completed result carries\n `result.images[]` — each with a **temporary** `url`, plus `width`, `height`, `content_type` — and\n `result.expires_at`.\n\n3. **Use the images before they expire.** The URLs are short-lived, so in the push phase hand each\n `url` straight to WordPress with `media-upload-from-url`, which downloads and stores WP's own copy\n (see the wordpress push `SKILL.md`). Don't persist the temporary URL as the live image. There is no\n photographer to credit — leave `caption` empty or a short on-brand one. Record under the\n article/product in `content-model.md`:\n\n ```markdown\n Image: Hostinger AI (image_type: <type>, generation_id: <uuid>)\n Image alt: <what the image shows>\n Image file: <site>-<subject>.jpg\n ```\n\n On `failed`, retry once with a tightened prompt; if it fails again, fall back to Option B and note\n the gap. Never block the whole push on a single image.\n\n#### Option B — stock photos from Unsplash (any backend, no instance)\n\nThis is still prepare work and needs no live backend: you pick the photos and record their URLs here,\nand the push phase uploads them.\n\n1. **Find candidates.** Use one photo per article, matching its subject and destination.\n - With an `UNSPLASH_ACCESS_KEY` in the environment, use the official API:\n `GET https://api.unsplash.com/search/photos?query=<subject>&orientation=landscape` with header\n `Authorization: Client-ID <key>`.\n - Without a key, use web search limited to `unsplash.com` (e.g. \"unsplash photo Lisbon tram\") and\n take the photo id from the result URL: the last segment of `unsplash.com/photos/<slug>-<id>`, or\n the whole segment for `unsplash.com/photos/<id>`.\n - Don't call `unsplash.com/napi/…`. It sits behind a bot challenge and returns 401.\n2. **Resolve a direct file URL.** `GET https://unsplash.com/photos/<id>/download?force=true` (don't\n follow the redirect) answers `302` with `Location: https://images.unsplash.com/photo-<…>?…`. The\n `dl=` parameter in it also carries the photographer's name. Drop the query string and request a\n web size: `https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max`. The upload ability\n needs this direct file URL, which returns 200 with `image/jpeg`. A photo *page* URL won't work.\n3. **Look before you keep it.** Download a small preview (`?w=500`) and view it. Search snippets\n only show captions, and a photo often isn't quite what its title says. Reject anything off-topic.\n If nothing fits exactly, take the closest real photo and note the gap rather than mislabel it.\n4. **Write alt text from what the photo shows**, not from the article's image brief. For example,\n use \"Shopfronts and parked scooters on a Hanoi street\" even if the article is about rain.\n5. **Record it** under each article in `content-model.md`:\n\n ```markdown\n Image: https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max\n Image credit: <Photographer> — https://unsplash.com/photos/<id> (Unsplash License)\n Image alt: <what the photo shows>\n Image file: <site>-<subject>.jpg\n ```\n\nUnsplash photos are free to use under the Unsplash License. Still credit the photographer: the push\nphase stores \"Photo by <Photographer> on Unsplash\" as the image caption.\n\nThen update `state.json`: set `stage` to the next prepare stage still to do, fresh `updated_at`. Do\n**not** advance past the barrier here.\n\n---\n\n## Push phase — create it the backend's way (needs the instance)\n\nOnly once `provision.status == \"completed\"` (re-verified against `hosting_websites_list-setups`),\nroute on the brief's **Suggested backend** and follow that subfolder, which maps the helper onto the\nlive backend — for WordPress, through the per-site WP MCP's ability tools (the installation JWT\nauthenticates that MCP session; `wp/v2` REST is the public read surface, not a write path for the\ntoken) to create the menu, pages, posts, categories and media:\n\n| Suggested backend | Follow |\n|---|---|\n| WordPress | `wordpress/SKILL.md` |\n| WordPress + WooCommerce | `wordpress/SKILL.md` — pushes WP content **and** seeds the WooCommerce catalog (its WooCommerce section + `wordpress/woocommerce.md`) |\n| Ecommerce | `ecommerce/SKILL.md` |\n| Webapp | — (no push; skip to frontend) |\n\n> **Supported backends: `wordpress` (optionally with a WooCommerce store), `ecommerce`, and `webapp`** —\n> where `webapp` is \"supported\" by **skipping this whole stage** (it has no content backend; see the note\n> below). A **WordPress + WooCommerce** brief routes to `wordpress/SKILL.md`, which pushes the WP content\n> **and** seeds the WooCommerce catalog in the same push. PocketBase is not available in this version.\n> Push to whichever single backend provision set up (see the provision router) — don't try to push to two\n> backends here.\n>\n> **Webapp has no content backend.** There is no CMS or store to seed, so a `webapp` brief **skips this\n> whole stage** — both prepare and push. The app owns its own data (its own API or database). Leave\n> `content-model.md` unwritten, mark content-model `skipped` in the run log, and go straight to the\n> frontend step.\n\nAfter the push completes, update `state.json` and continue to the frontend **data** phase.\n"
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"uri": "skill://hostinger-headless/steps/04-content-model/ecommerce/SKILL.md",
|
|
@@ -218,10 +218,10 @@ export default [
|
|
|
218
218
|
},
|
|
219
219
|
{
|
|
220
220
|
"uri": "skill://hostinger-headless/steps/05-design/SKILL.md",
|
|
221
|
-
"digest": "sha256:
|
|
222
|
-
"size":
|
|
221
|
+
"digest": "sha256:e5f233a5eda8a1d50c5445f0608198df78c91f415d17456179e50592cb39835c",
|
|
222
|
+
"size": 34251,
|
|
223
223
|
"mimeType": "text/markdown",
|
|
224
|
-
"text": "---\nname: headless-step-design\ndescription: \"Step 5 (design) of the Hostinger Headless flow. Derives three distinct design directions from brief.md (plus any reference sites and brand assets), lets the user pick one in a single question, checks the chosen one against the 8-point grid and WCAG 2.2 AA, designs the logo as outlined SVG (wordmark, icon mark, favicon), following any logo wish the owner gave, then writes design.md — tokens, type, logo, components, per-page composition, accessibility and imagery — as the contract the frontend implements (the framework/stack is chosen later by the frontend step). When the brief says to keep an existing look, writes design.md from that look and its own logo instead. Prepare work: runs during the provision wait and never touches the backend.\"\n---\n\n# headless-step-design\n\nTurn the brief into a look the user has chosen, and write it down so the frontend can't drift from\nit. **One question, three options, one spec.** The output is **files** (`design.md` plus the logo),\nnot a conversation: the frontend step implements `design.md`, not the chat.\n\n**In:** `brief.md` (required), `content-model.md` (if it exists yet), reference sites and assets the\nuser gave. **Out:** `~/.hostinger/<domain>/design.md` and the logo files in\n`~/.hostinger/<domain>/brand/`.\n\n> ## ⚠️ Build mode: \"Bring your own frontend\"? This step defers to the user\n> Read `brief.md` → **Build mode** first. When it is **Bring your own frontend**, the user drives the\n> design — **do not generate three directions or write a prescriptive `design.md` over their project.**\n> The frontend already exists (or the user vibe-codes it with you); your job is to follow *their*\n> direction, not impose one. Skip Steps 1–7 below: either write nothing, or capture only what the user\n> explicitly asks for (a short note of their stated colours/vibe) so the frontend step has it. The logo\n> and token work here apply only when **Build mode is \"Agent designs and builds\"**. (This mirrors the\n> frontend step's existing \"existing app the user brought\" handling, now generalised to any backend.)\n\nThis is **prepare work**: it runs while provision settles (see *Background provision & the instance\nbarrier* in the top-level `SKILL.md`). Nothing here calls the Hostinger connector or the backend.\n\n**Keep it short.** Refine already asked about tone and look; don't ask again. The step asks the user\n**one** question: either the keep/change question (Step 1 and *Keep the current look*) or the three\ndirections (Step 4), never both. \"You decide\" is a valid answer.\n\n**Talk like a person, not a designer.** Everything the user reads describes how the site will\n**look and feel**: colours by name (\"hot pink, butter yellow\"), lettering by feel (\"big chunky\nrounded headlines\"), and the details they'll notice (\"photos tilted like stickers on a scrapbook\").\nNo hex codes, font names, or design jargon (palette, accent, tokens, shape language, contrast).\n\n**Two hard rules apply to every direction, however loud or quiet it is:**\n\n- **The 8-point grid.** Every spacing, sizing and layout value is a multiple of 8 px (details in\n 5.2).\n- **WCAG 2.2 Level AA.** Colour, focus, motion, target size and reflow are decided here, so the\n frontend can't build them wrong (details in 5.1 and 5.3).\n\nThese rules are for you, not the user. **Never mention them**: no grid, contrast, WCAG or\naccessibility talk in anything the user reads, and no \"all options follow…\" reassurances. The user\njudges the look in the preview; the rules just make sure it works.\n\n---\n\n## Step 1 — Read the workspace, then the resume check\n\nRead `brief.md`, `state.json`, and `content-model.md` if it exists.\n\n- **`design.md` already exists and this is not a redesign** → don't overwrite it. Describe the\n current look in two or three plain sentences and ask: *keep it, or change something?* That is this\n step's one question.\n - **Keep** → leave the file alone and go straight to the `state.json` update at the end of Step 7.\n - **Change** → edit only what they asked for, re-run the 5.x checks on what changed, then save.\n- **The site is already deployed** (`deploy.json` exists, or `state.json` has `deployed_at`) and the\n user asked for a different look → this is a **redesign**. Tell them plainly: their content stays\n as it is, only the look changes, and they'll see a preview before it goes live. Then run Steps 2–7\n as normal. The current `design.md` becomes one more signal in Step 2, so carry over what the user\n wants to keep and move away from the rest.\n- **The brief says to keep the current look** (*Design direction* says so in the owner's words:\n \"keep the look we have\", \"same look, new platform\") → follow *Keep the current look* below instead\n of Steps 2–6. No three directions and no new logo. A live site, screenshots or a brand guide alone\n are signals for Step 2, not a request to keep.\n\n### Keep the current look\n\n1. **Read the look from what the brief points at.** A live site: use Step 2's reference-site method\n (computed fonts, colours by painted area, the feel), but this site is the owner's own, so take its\n logo file as it is (the header `<img>` or inline `<svg>`, plus its favicon if it has one) and save\n it unchanged in `brand/`. Screenshots or a brand guide: read the colours and lettering from them;\n a logo seen only in a screenshot counts as no logo file, so list \"logo file needed\" under gaps in\n the final report. Files or exact colours and fonts the owner supplied win over anything you read.\n2. **Confirm in one question.** Describe the kept look in two plain sentences and ask: *keep it, or\n change something?* \"Keep the look we have\" still gets this question; skip it only when the brief\n says to change nothing at all (\"keep it exactly as it is\"). Change →\n apply only what they asked for; a request for a new logo runs Step 6 for the logo alone.\n3. **Check it** as in Step 5. The kept colours are fixed like brand colours: a pair that fails gets a\n shade of the same hue in that role only. Spacing moves to the nearest grid value, and a font\n without an `@fontsource` package swaps to the closest one that resolves (5.4).\n4. **Logo.** Reuse the existing logo and favicon unchanged; never redraw or recolour them. With no\n existing logo, the header shows the site name as text in the kept display font, and no logo is\n designed.\n5. **Write `design.md`** (Step 7) from the kept look, then hand off as usual. In **Logo**:\n - *Files* and *Use* name the files as saved (a `.png` stays a `.png`), or say \"No favicon kept\".\n - *Built from* says \"Kept, not rebuilt\". *Measured* still gives the width at `--logo-h-sm`\n (≤ 192 px; else lower `--logo-h-sm` or show the name as text on small screens), and the header\n link has `alt` = the site name and a target of at least 24 px (as in Step 6).\n - With no logo: *Files* says \"None\", *Use* says the header link holds the site name as text in\n `--font-display` and the browser's default favicon stays, and the *Don't* line is dropped.\n\n## Step 2 — Collect the signals\n\nBefore inventing anything, gather what already constrains the design:\n\n| Signal | Where | How it constrains |\n|---|---|---|\n| Tone, purpose, audience | `brief.md` → *Tone*, *Purpose*, *Audience* | Sets the mood all three directions must fit |\n| Brand assets | `brief.md` → *Assets* | Logo, brand colours and fonts are **fixed** in every direction. Never recolour a supplied logo; Step 6 uses it instead of designing one |\n| Pages and entities | `content-model.md` → *Navigation*, *Pages*, *Articles*; if it doesn't exist yet, `brief.md` → *Pages* | Drives the component inventory and page composition |\n| Reference sites | URLs in the brief or the conversation | Cues to borrow (see below) |\n| Images already sourced | `content-model.md` → the image lines under *Articles* (skip if none yet) | Glance at a few at small size; keep accents from clashing with their dominant colours |\n| Current design (redesign only) | existing `design.md` | What to keep, what to move away from |\n\n**Reference sites.** Open each one in a real browser if you have one. Static fetches are often\nblocked or rate-limited; without a browser, fetch the HTML and its linked CSS instead. If neither\nworks, say so in one line and continue from the brief alone. Pull **cues**:\n\n- **Fonts:** the computed `font-family` of `h1`/`h2` and of body `p`.\n- **Colours:** background colours weighted by painted area (element width × height), the top 5–8.\n Skip transparent and semi-transparent backgrounds and overlays (cookie banners, modals, chat\n widgets). Text colours come from headings, paragraphs and links.\n- **Feel:** look at the first screen and one further down (screenshots shown to you, not saved in\n the workspace) and note what creates the feel: type weight, colour blocking, shapes, density.\n\n**Borrow cues, never assets** (the owner's own site under *Keep the current look* is the exception).\nDon't use its logo, copy, images or exact layout. **The brief wins** over the live site when they\ndisagree (the user's words are what they want). Note the mismatch in the direction.\n\n**Brand colours that fail contrast** are still fixed, but only in roles where they pass. A pale brand\nyellow can be a background band or a large decorative shape, never body text. Say so in the\ndirection.\n\n## Step 3 — Derive three directions\n\nWrite three directions that **all fit the brief** but are **genuinely different**. Vary at least two\nof: palette (temperature, saturation, light vs. dark), type personality (geometric, grotesk, serif,\ndisplay), and shape language (sharp vs. round, flat vs. outlined, airy vs. dense). Three shades of\none idea is not a choice. Neither is a wildcard that ignores the brief.\n\nOrder them: **A is the recommendation**, the closest reading of the brief and references. B and C\npush in different directions.\n\nEach direction carries exactly this:\n\n- **Name** — 2–3 evocative words (\"Sticker-book maximal\", \"Quiet gallery\").\n- **Mood** — one line on how the site feels.\n- **Palette** — 5–7 hex colours with roles (base, text, primary accent, secondary accents, surface).\n These are the core colours the user sees; Step 5 may add derived shades (link, muted, error,\n focus) as tokens.\n- **Type** — a display font and a body font, both self-hostable through `@fontsource`.\n- **Shape language** — corners, borders, shadows and density in one phrase (\"chunky 3 px ink\n outlines, hard 8 px offset shadows, tilted cards\").\n- **Signature element** — the one memorable move (a scrolling marquee, stickers, oversized numerals,\n a duotone photo treatment). If it moves on its own, it gets a pause control (5.3).\n- **Logo idea** — one line on how the name becomes a logo, using a move from Step 6 (\"letters\n bounce in different colours and the O is a smiley\"). When the brief carries a logo wish (*Design\n direction* or *Assets*), every direction's logo idea is that wish in its own style. A wish is words\n describing a logo; a logo file in *Assets* is a supplied logo and wins (Step 6).\n\n**Only offer what can pass.** Before asking, run two quick gates on **all three**:\n\n1. **Contrast gate.** Every colour that will carry text (base, surfaces, and any accent used as a\n text block) has a palette text colour reaching **4.5:1** on it. Two text colours are fine (dark on\n light blocks, light on dark ones). Accents that only ever hold large text need 3:1.\n2. **Font gate.** Each font resolves on npm, and the display font also has a static `@fontsource`\n package, because the logo is built from it (5.4).\n\nA direction that fails either gate gets adjusted, not offered as-is. Don't offer a look that only\nworks by breaking the rules, such as body text on a neon accent or a marquee that can't be stopped.\nLoud is fine; inaccessible is not.\n\n**Avoid the template look** unless the brief asks for neutral: Inter or Roboto everywhere, a\nblue-to-purple gradient hero, centred hero with three feature cards, grey cards on white. Every\ndirection should look like *this* site, not a starter theme.\n\n## Step 4 — Ask once\n\nAsk **one** question, with A marked as the recommendation. With a structured question UI (multiple\nchoice plus an \"other\" field), each option's **label** is the direction's name and its\n**description** is one or two plain sentences, under ~160 characters. Without one, write each\noption as one line in the same shape.\n\nThe description says what the site would feel like, the main colours by name, what the lettering\nis like, the logo idea, and the one detail they'll remember. Hex codes and font names stay in your\nnotes. The question may end with one plain clause saying they can add a logo idea to their pick, but\nnever suggest one or give an example: that idea would end up in the logo.\n\n> **Sticker-book maximal** — Loud and playful, like a scrapbook: candy pink, butter yellow and lilac,\n> chunky headlines, tilted sticker photos and a bouncy logo with a smiley O.\n\n> **Midnight disco** — A dark, glowing night-out feel: deep purple with neon pink, yellow and\n> turquoise, heavy rounded headlines and a logo like a neon sign.\n\nThen:\n\n- **Pick** → use it as written, and confirm the look in one line.\n- **\"You decide\" / skipped** → use A, and tell them in one line which look you went with.\n- **A mix** (\"A's colours with C's fonts\") → merge them. Don't ask again.\n- **Something else** → build a direction from what they said, run Step 5, and use it. Don't send\n three new options.\n\n**Logo wish.** An answer can carry a logo idea next to the pick (\"A, and make the logo a little\nteapot\"); it replaces the chosen direction's logo idea, and wins over a wish from the brief.\nWhenever the logo follows a wish, from the answer or the brief, add it once to the line that confirms\nthe pick: *\"The logo follows your idea: a little teapot.\"* If the kit can't draw it as said (a\ndetailed illustration, a photo), build its closest kit version and name what you simplified in that\nsame line. Don't ask a follow-up.\n\nDon't ask about anything else in this step.\n\n## Step 5 — Check the chosen direction\n\nFix any problem found here by adjusting the direction, not by asking the user. Don't report these\nchecks or their fixes; the user sees the result in the preview.\n\n### 5.1 Contrast — WCAG 2.2 SC 1.4.3 and 1.4.11\n\n**Which pairs count:** walk the component inventory you're about to write (Components table). For\neach component, list every foreground/background combination in every state: default, hover,\nfocus, active, error. Include text on coloured bands (footer links on dark, marquee text, chips),\nplaceholder text and photo credits. Compute the WCAG contrast ratio for each:\n\n| Kind | Examples | Minimum |\n|---|---|---|\n| Body text | paragraphs, links, footer text, input text, placeholder text, captions | **4.5:1** |\n| Large text | ≥ 24 px regular, or ≥ 18.66 px (14 pt) bold | **3:1** |\n| Non-text | input and button boundaries (when the outline is the only edge), focus indicator against each adjacent colour, icons that carry meaning, chip outlines | **3:1** |\n\nA pair that fails gets a darker or lighter shade of the **same hue**; never drop a brand colour. A\nnew shade becomes a token (for example `--color-link` or `--color-muted`). Disabled controls and\npure decoration are exempt, but say which elements those are. **Text on photos** needs a scrim or\nsolid backing that gets the text to 4.5:1 against the lightest part of the image. If you can't\nguarantee that, don't put text on photos. Record every pair and its ratio in `design.md`.\n\n```js\n// node -e: relative luminance + contrast ratio, hex in → ratio out\nconst L = h => { const c = h.match(/\\w\\w/g).map(x => parseInt(x, 16) / 255)\n .map(v => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\n return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; };\nconst ratio = (a, b) => { const [x, y] = [L(a), L(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };\n```\n\n### 5.2 The 8-point grid\n\nValues are checked in px at the default 16 px root (`0.5rem` = 8 px).\n\n| What | Rule |\n|---|---|\n| Spacing (padding, margin, gap) | Multiples of 8: `8 16 24 32 48 64 96 128`. **4 px** is the only half-step, and only inside small components (chip padding, icon-to-label gap) |\n| Component sizes | Heights and fixed widths are multiples of 8. Buttons, nav items and inputs are **48 px** tall minimum |\n| Radii | `8 16 24 32`, or `999px` for pills. 4 px only on elements under 32 px tall |\n| Line heights | Fixed-size text: multiples of 8 (16/24, 18/32, 24/32), or of 4 for text ≤ 14 px (12/16, 14/20). Fluid headings (`clamp()` sizes) use a unitless line-height (1.0–1.2) and are exempt, because their line box can't stay on the grid at every size |\n| Fluid spacing | `clamp()` for spacing or sizes uses grid values at both ends. Fluid **font sizes** are exempt |\n| Shadow offsets | 4 or 8 multiples |\n| Layout | Content width, gutters and breakpoints are multiples of 8 (e.g. `1200`, `768`, `1024`, `1280`) |\n| Exempt | Border and outline widths (1–4 px), focus offset, font sizes, the `999px` pill radius, the 4-multiple line heights above, and anything that isn't a length (angles like a `-2deg` tilt, durations, opacity) |\n\nEvery value in the **Tokens** block must pass this table.\n\n### 5.3 WCAG 2.2 AA — what the design must decide\n\nThese criteria are settled at design time. Write each answer into the **Accessibility** section of\n`design.md`.\n\n| Criterion | The design must specify |\n|---|---|\n| 1.4.1 Use of colour | Links in running text are underlined, not colour-only. States (active nav, selected, error) also change shape, weight or an icon, not only colour |\n| 1.4.4 Resize text | Font sizes in `rem`; nothing breaks at 200 % zoom |\n| 1.4.10 Reflow | Works at **320 CSS px** wide with no horizontal scroll. Every component has a stated ≤ 320 px behaviour; decorative pieces never widen the page |\n| 1.4.12 Text spacing | No fixed-height boxes around text, so nothing clips when a user overrides spacing (line-height 1.5, paragraph spacing 2×, letter-spacing 0.12 em, word-spacing 0.16 em). Fixed line-height tokens are fine; clipping is not |\n| 2.2.2 Pause, Stop, Hide | Anything that moves on its own for more than 5 s (marquee, carousel, animated sticker) has a **visible pause control**, and doesn't move at all under `prefers-reduced-motion`. Hover-to-pause alone doesn't count |\n| 2.3.1 Three flashes | Nothing flashes more than 3 times a second |\n| 2.4.7 Focus visible | One focus style (e.g. 3 px outline, 2 px offset) whose colour reaches 3:1 against **every** surface it can appear on. Name a second colour for surfaces where the first fails |\n| 2.4.11 Focus not obscured *(new in 2.2)* | Sticky headers, cookie bars and chat bubbles never cover the focused element. Either the header isn't sticky, or `scroll-padding-top: var(--header-h)` |\n| 2.5.8 Target size *(new in 2.2)* | Interactive targets ≥ 24 × 24 px with spacing. On this grid: **48 px** for buttons, nav and inputs, ≥ 24 px for inline icon buttons |\n| 3.2.6 Consistent help *(new in 2.2)* | The contact or help link sits in the same place on every page (e.g. last nav item and footer) |\n| 3.3.2 Labels | Every form field has a visible label; placeholders are never the only label |\n| 1.1.1 Text alternatives | Content images get alt text describing what they show; purely decorative images and shapes get empty alt or `aria-hidden` |\n\n### 5.4 Fonts resolve\n\nFor each family, prefer the variable package and fall back to the static one:\n`npm view @fontsource-variable/<slug> version`, then `npm view @fontsource/<slug> version`. One must\nanswer with a version. If neither does, swap to the closest family that does. Record the exact\npackage names. The **display** family must also resolve as a static package\n(`npm view @fontsource/<slug> version`): the static packages ship `.woff` files the logo kit can\nread; the variable ones ship only `.woff2`.\n\n## Step 6 — Design the logo\n\n**The user supplied a logo** → copy it into `brand/` unchanged, note it in the **Logo** section, and\nskip the rest of this step. Never redraw or recolour it.\n\nOtherwise, turn the name into a **designed** logo: shaped, coloured and composed. Typing the name in\nthe display font with an emoji or symbol next to it doesn't count. Build the chosen direction's **logo\nidea** (as replaced by any logo wish, Step 4) with one or two of these moves:\n\n| Move | What it is | Suits |\n|---|---|---|\n| **Letter swap** | One letter becomes a simple shape tied to the subject: O → smiley, sun, loaf, plate, eye; I → candle, pencil, brush; A → roof, tent. Swap **one character** only, even if the letter appears twice, and the word must still read | Playful, friendly brands |\n| **Bounce** | Letters tilt ±3–6° and bob off the baseline, each in a palette colour, with an ink outline and hard shadow | Loud, maximal directions |\n| **Stack** | The name split over 2–3 lines on coloured tiles, or set as a tight block | Long names, bold directions |\n| **Tag** | The main word large; a short second word on a pill, ribbon or tab | Two-part names (\"Dopamine Den\") |\n| **Seal** | The name around a ring, with a monogram or symbol in the middle | A secondary mark (footer, hero sticker, social avatar), never the header logo |\n| **Typographic contrast** | Weight, style or case contrast between words (a light italic word next to a heavy upright one), custom tight spacing, one shared stroke or cut between letters | Quiet, refined, editorial directions |\n\nNo emoji, clip-art, gradients, or details that vanish at small sizes. Colours come only from the\npalette (base and surface colours count).\n\n### Build it with the logo kit\n\nThe kit (`logo-kit.mjs`, next to this file) does the error-prone parts: it outlines letters into\nvector paths, sets text on a circle, draws the outline-and-shadow treatment, measures, and validates\nthe output. You write the composition. Copy the kit into `brand/` so the build is self-contained\nand a redesign can re-run it:\n\n```bash\nmkdir -p ~/.hostinger/<domain>/brand && cd ~/.hostinger/<domain>/brand\ncp <the folder this SKILL.md is in>/logo-kit.mjs .\nnpm init -y >/dev/null && npm i opentype.js @fontsource/<display-slug>\n```\n\nWrite `brand/logo-build.mjs`, import from `./logo-kit.mjs`, and always run it from `brand/`\n(packages resolve from the working directory):\n\n| Kit function | Use |\n|---|---|\n| `loadFont(slug, weight, style)` | Loads a static `@fontsource` weight across all its subsets (`latin`, `latin-ext`, …), so accented letters like Ė or Ł resolve; `style` is `'normal'` or `'italic'`. Throws if that weight or style isn't in the package, so pick another |\n| `layout(font, text, size)` | Kerned glyph positions, total width, `capHeight`, `xHeight`; place letters one by one for bounce or swap. Throws if a character isn't in the font |\n| `glyphPath(glyph, x, baselineY, size)` / `textPath(font, text, x, baselineY, size)` | Outlined path data for one letter or a whole word. For typographic contrast, load two weights or styles and set each word with `textPath` |\n| `pathBox(font, text, x, baselineY, size)` | Tight box around a word's outlines; size the viewBox from it plus room for outlines, shadows and tilt |\n| `arcText(font, text, { cx, cy, r, size })` | Letters around a circle, for seals |\n| `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`) |\n| `svgDoc({ width, height, title, body })` | Root SVG with `role=\"img\"` and a `<title>` |\n| `check(svg)` | Throws on `NaN`, live `<text>`, a missing title or viewBox. Run it on every file before writing |\n\nNever call opentype's `path.toPathData()` directly; the kit's serializer exists because opentype.js\n2.0 writes `NaN` into some paths, and browsers silently stop drawing at the first `NaN`.\n\n### Deliver four files\n\n| File | What | Must read clearly at |\n|---|---|---|\n| `brand/logo.svg` | The primary, mostly horizontal logo for the header | Its header display heights (desktop and small screens) |\n| `brand/logo-mark.svg` | A square icon from the same idea (initial, monogram or the swapped symbol) on a solid rounded tile (corner radius ≈ ¼ of the side) in a palette colour | 32 px |\n| `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 |\n| `brand/logo-build.mjs` | The script that built them, with `package.json`, kept so a redesign can adjust it | — |\n\nAdd `brand/logo-seal.svg` only if the direction uses a seal as a secondary mark. Delete\n`brand/node_modules` when you're done; a redesign runs `npm i` in `brand/` first.\n\n**Check the sizes.** Display heights are multiples of 8, with one for desktop and one for small\nscreens (they may be equal). Work them out from the viewBox:\n\n- **Letter size:** letter height on screen = letter height ÷ viewBox height × display height, and it\n must be ≥ 16 px. Use `xHeight` when the logo is mostly lowercase and `capHeight` when it's mostly\n capitals; for mixed fonts, take the smaller word. A two-line lockup usually needs 64 px.\n- **Headroom:** `pathBox` covers the letters, not what you add. Leave room above for accents (Ė, Ž, Å sit above cap height) and below for descenders, plus whatever bounce, tilt, outline and shadow add, so nothing touches the viewBox edge.\n- **Width:** width on screen = viewBox width ÷ viewBox height × display height. At the small-screen\n height it must be **≤ 192 px**, which leaves room for the menu button in a 320 px header. If it\n doesn't fit, lower the small-screen height (letters still ≥ 16 px), simplify the logo, or show\n `logo-mark.svg` with the name beside it on small screens.\n- **Look at it.** If you have a browser, render the files at both header heights, 32 px and 16 px,\n the favicon on a light and a dark background, and fix anything that reads wrong. Without a\n browser, rely on the numbers and list \"logo not visually checked\" under gaps in the final report.\n\nThe logo is exempt from text-contrast minimums, but the header link around it needs `alt` = the site\nname and a target of at least 24 px.\n\nDon't show the logo to the user in this step; they see it in the preview.\n\n## Step 7 — Write `design.md` and hand off\n\nWrite `design.md` from the template below. Fill every section. The **Tokens** block is copied\nverbatim into the frontend's global stylesheet, so it must be valid CSS. Every colour, font, type\nsize, line height, space, radius and shadow used elsewhere in the file must exist in it as a token.\nThe template's token list is the **minimum**: add what the design needs (`--color-error`,\n`--color-on-accent`, `--leading-h3`…) under the same naming scheme.\n**Breakpoints are the one exception**: CSS custom properties don't work inside `@media`, so they are\nlisted in *Grid, shape & motion* as plain px values.\n\nThen update `state.json`: set `\"stage\": \"frontend\"` (also for a redesign: restyle, then validate and\ndeploy) and `updated_at` to the current UTC time (ISO 8601). Don't touch `provision`, `run` or\n`history` (`run-log.mjs` owns those), or add other keys.\n\nIf content prepare hasn't sourced images yet, it now follows `design.md` → **Imagery** (see\n`04-content-model/SKILL.md`).\n\n## `design.md` template\n\n````markdown\n# Design — <domain>\n\n> Direction **<name>**, selected on <YYYY-MM-DD> from three options (others offered: <B name>,\n> <C name>). <\"Picked by the user.\" | \"User said 'you decide'; recommendation A used.\" |\n> \"Merged by the user: <what>.\"> — or, when kept: \"Kept the owner's current look from <site URL |\n> screenshots | brand guide>, confirmed on <YYYY-MM-DD>.\"\n\n## Direction\n<2–3 sentences: the mood, what it borrows from the references (and any brief/reference mismatch),\nthe signature element.>\n\n## Stack\n**<chosen by the frontend step>** — design doesn't decide the stack. The frontend step (06) picks and\nconfirms the framework (default Next.js SSR; ecommerce must be SSR) and records it in\n`state.json → frontend`. Leave this as a placeholder for the frontend step to fill in.\n\n## Tokens\n```css\n:root {\n /* colour — semantic names, never \"--pink-2\" */\n --color-base: #…; --color-text: #…; --color-surface: #…; --color-muted: #…;\n --color-primary: #…; --color-accent-1: #…; --color-accent-2: #…; --color-link: #…;\n --color-focus: #…; --color-focus-alt: #…; /* for surfaces where --color-focus is < 3:1 */\n /* type */\n --font-display: '<Family> Variable', <fallback stack>;\n --font-body: '<Family> Variable', <fallback stack>;\n --text-body: …rem; --leading-body: …rem; /* 8-multiple in px */\n --text-small: …rem; --leading-small: …rem; /* 4-multiple in px */\n --text-h1: clamp(…rem, …, …rem); --text-h2: clamp(…); --text-h3: …rem;\n --leading-display: 1.05; /* unitless; fluid headings are grid-exempt */\n /* 8-point grid */\n --space-half: 4px; --space-1: 8px; --space-2: 16px; --space-3: 24px; --space-4: 32px;\n --space-6: 48px; --space-8: 64px; --space-12: 96px; --space-16: 128px;\n --target-min: 48px;\n /* shape */\n --radius-sm: 8px; --radius-card: …; --radius-pill: 999px;\n --border: …; --shadow: …; --shadow-hover: …;\n --focus-ring: 3px solid var(--color-focus); --focus-offset: 2px;\n /* layout */\n --content-max: …px; /* multiple of 8 */\n --header-h: …px; /* header band height, multiple of 8; scroll-padding-top uses it if sticky */\n --logo-h: …px; /* header logo height on desktop, multiple of 8 */\n --logo-h-sm: …px; /* on small screens; logo ≤ 192 px wide at this height */\n}\n```\n\n## Palette roles\n| Token | Hex | Used for |\n|---|---|---|\n\n## Contrast\n| Foreground | Background | Kind (body text / large text / non-text) | Ratio | Needs | Passes |\n|---|---|---|---|---|---|\n\n## Type\n- **Display:** <family> (`@fontsource-variable/<slug>@<version>`), weights …, tracking …; sizes `--text-h1…h3`, `--leading-display`\n- **Body:** <family> (`@fontsource-variable/<slug>@<version>`); `--text-body` / `--leading-body`, measure ≤ 68ch\n\n## Logo\n- **Concept:** <the move(s), and what any swapped letter or symbol stands for, plus \"From the owner:\n '<their words>'\" only when it follows their wish | \"Supplied by the user\" | \"Kept from <source>\" |\n \"No logo: owner kept the current look\" (header shows the site name as text in `--font-display`)>\n- **Built from:** `@fontsource/<display-slug>@<version>`, weight(s)/style(s) …\n- **Files:** `brand/logo.svg` (header, `--logo-h` desktop / `--logo-h-sm` small screens),\n `brand/logo-mark.svg`, `brand/favicon.svg`<, `brand/logo-seal.svg`>; source `brand/logo-build.mjs`\n- **Measured:** letters … px tall at `--logo-h-sm`; width … px at `--logo-h-sm`\n- **Use:** the frontend copies `brand/*.svg` into `public/`. Header: `<a href=\"/\"><img src=\"/logo.svg\"\n alt=\"<Site name>\" style=\"height: var(--logo-h)\"></a>` (switch to `--logo-h-sm` on small screens).\n Favicon: `<link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\">`\n- **Don't:** recolour, stretch, add effects, or retype the logo as live text.\n\n## Grid, shape & motion\n<section padding desktop/mobile (space tokens), gutters, breakpoints in px (multiples of 8), borders,\nradii, shadows, hover behaviour, every animation and its pause control, and \"no motion under\nprefers-reduced-motion\".>\n\n## Accessibility (WCAG 2.2 AA)\n- **Focus:** <style; which colour on which surface>\n- **Targets:** <48 px controls; inline exceptions>\n- **Links & states:** <underline rule; how active/error states differ beyond colour>\n- **Motion:** <what moves, where its pause control is>\n- **Sticky elements:** <none | header uses --header-h + scroll-padding-top>\n- **Help:** <where the contact link sits on every page>\n- **Text on images:** <none | scrim spec and resulting ratio>\n- **Reflow:** <anything special at 320 px>\n- **Exempt from contrast:** <disabled controls / decorative shapes, named>\n\n## Components\n| Component | Renders (from content model) | Look | ≤ 320 px |\n|---|---|---|---|\n| Header + nav | Navigation items | … | … |\n| Post card | Article: image, category, title, excerpt | … | … |\n| … | | | |\n\n## Page composition\n- **<Page>** — <sections top to bottom, each naming its component and band colour>\n- …\n\n## Imagery\n<subjects, mood and colour fit for photos; search keywords; treatment (frame, crop ratio, radius,\nduotone…); alt text written from what the photo shows.>\n\n## Don'ts\n- <3–6 site-specific rules, e.g. \"no grey cards\", \"never put body text on the primary accent\">\n````\n\n## Invariants\n\n- **One question.** Refine owns discovery; this step only chooses among directions (or keeps the\n existing look).\n- **8-point grid, always.** No spacing, size, radius or breakpoint off the grid except the listed\n exemptions.\n- **WCAG 2.2 AA is the floor.** No direction is offered, chosen or written that fails it. Brand\n colours keep their hue but only appear in roles where they pass.\n- **Brand assets are fixed.** A supplied logo, colour or font appears in every direction, unchanged.\n- **A logo is designed, not typed.** Outlined SVG paths built with the logo kit, never live text,\n emoji or clip-art, and always readable at its smallest use. The one exception: a kept look with no\n logo shows the site name as text and gets no new logo.\n- **Self-hosted fonts only** (`@fontsource`). No Google Fonts or other font CDN links in the output.\n- **The tokens block is the only source of values** (breakpoints excepted). Components and pages\n reference tokens, never raw hex codes, pixel values or font names.\n- **No backend calls.** Design never waits on provision and never writes to the instance.\n- **Never overwrite an existing `design.md` silently.** Keep or change, as in Step 1.\n"
|
|
224
|
+
"text": "---\nname: headless-step-design\ndescription: \"Step 5 (design) of the Hostinger Headless flow. Derives three distinct design directions from brief.md (plus any reference sites and brand assets), lets the user pick one in a single question, checks the chosen one against the 8-point grid and WCAG 2.2 AA, designs the logo as outlined SVG (wordmark, icon mark, favicon), following any logo wish the owner gave, then writes design.md — tokens, type, logo, components, per-page composition, accessibility and imagery — as the contract the frontend implements (the framework/stack is chosen later by the frontend step). When the brief says to keep an existing look, writes design.md from that look and its own logo instead. Prepare work: runs during the provision wait and never touches the backend.\"\n---\n\n# headless-step-design\n\nTurn the brief into a look the user has chosen, and write it down so the frontend can't drift from\nit. **One question, three options, one spec.** The output is **files** (`design.md` plus the logo),\nnot a conversation: the frontend step implements `design.md`, not the chat.\n\n**In:** `brief.md` (required), `content-model.md` (if it exists yet), reference sites and assets the\nuser gave. **Out:** `~/.hostinger/<domain>/design.md` and the logo files in\n`~/.hostinger/<domain>/brand/`.\n\n> ## ⚠️ Build mode: \"Bring your own frontend\"? This step defers to the user\n> Read `brief.md` → **Build mode** first. When it is **Bring your own frontend**, the user drives the\n> design — **do not generate three directions or write a prescriptive `design.md` over their project.**\n> The frontend already exists (or the user vibe-codes it with you); your job is to follow *their*\n> direction, not impose one. Skip Steps 1–7 below: either write nothing, or capture only what the user\n> explicitly asks for (a short note of their stated colours/vibe) so the frontend step has it. The logo\n> and token work here apply only when **Build mode is \"Agent designs and builds\"**. (This mirrors the\n> frontend step's existing \"existing app the user brought\" handling, now generalised to any backend.)\n\nThis is **prepare work**: it runs while provision settles (see *Background provision & the instance\nbarrier* in the top-level `SKILL.md`). Nothing here calls the Hostinger connector or the backend.\n\n**Keep it short.** Refine already asked about tone and look; don't ask again. The step asks the user\n**one** question: either the keep/change question (Step 1 and *Keep the current look*) or the three\ndirections (Step 4), never both. \"You decide\" is a valid answer.\n\n**Talk like a person, not a designer.** Everything the user reads describes how the site will\n**look and feel**: colours by name (\"hot pink, butter yellow\"), lettering by feel (\"big chunky\nrounded headlines\"), and the details they'll notice (\"photos tilted like stickers on a scrapbook\").\nNo hex codes, font names, or design jargon (palette, accent, tokens, shape language, contrast).\n\n**Two hard rules apply to every direction, however loud or quiet it is:**\n\n- **The 8-point grid.** Every spacing, sizing and layout value is a multiple of 8 px (details in\n 5.2).\n- **WCAG 2.2 Level AA.** Colour, focus, motion, target size and reflow are decided here, so the\n frontend can't build them wrong (details in 5.1 and 5.3).\n\nThese rules are for you, not the user. **Never mention them**: no grid, contrast, WCAG or\naccessibility talk in anything the user reads, and no \"all options follow…\" reassurances. The user\njudges the look in the preview; the rules just make sure it works.\n\n---\n\n## Step 1 — Read the workspace, then the resume check\n\nRead `brief.md`, `state.json`, and `content-model.md` if it exists.\n\n- **`design.md` already exists and this is not a redesign** → don't overwrite it. Describe the\n current look in two or three plain sentences and ask: *keep it, or change something?* That is this\n step's one question.\n - **Keep** → leave the file alone and go straight to the `state.json` update at the end of Step 7.\n - **Change** → edit only what they asked for, re-run the 5.x checks on what changed, then save.\n- **The site is already deployed** (`deploy.json` exists, or `state.json` has `deployed_at`) and the\n user asked for a different look → this is a **redesign**. Tell them plainly: their content stays\n as it is, only the look changes, and they'll see a preview before it goes live. Then run Steps 2–7\n as normal. The current `design.md` becomes one more signal in Step 2, so carry over what the user\n wants to keep and move away from the rest.\n- **The brief says to keep the current look** (*Design direction* says so in the owner's words:\n \"keep the look we have\", \"same look, new platform\") → follow *Keep the current look* below instead\n of Steps 2–6. No three directions and no new logo. A live site, screenshots or a brand guide alone\n are signals for Step 2, not a request to keep.\n\n### Keep the current look\n\n1. **Read the look from what the brief points at.** A live site: use Step 2's reference-site method\n (computed fonts, colours by painted area, the feel), but this site is the owner's own, so take its\n logo file as it is (the header `<img>` or inline `<svg>`, plus its favicon if it has one) and save\n it unchanged in `brand/`. Screenshots or a brand guide: read the colours and lettering from them;\n a logo seen only in a screenshot counts as no logo file, so list \"logo file needed\" under gaps in\n the final report. Files or exact colours and fonts the owner supplied win over anything you read.\n2. **Confirm in one question.** Describe the kept look in two plain sentences and ask: *keep it, or\n change something?* \"Keep the look we have\" still gets this question; skip it only when the brief\n says to change nothing at all (\"keep it exactly as it is\"). A skipped tool result is not Keep; see\n *When you ask the user* in the top-level `SKILL.md`. Change →\n apply only what they asked for; a request for a new logo runs Step 6 for the logo alone.\n3. **Check it** as in Step 5. The kept colours are fixed like brand colours: a pair that fails gets a\n shade of the same hue in that role only. Spacing moves to the nearest grid value, and a font\n without an `@fontsource` package swaps to the closest one that resolves (5.4).\n4. **Logo.** Reuse the existing logo and favicon unchanged; never redraw or recolour them. With no\n existing logo, the header shows the site name as text in the kept display font, and no logo is\n designed.\n5. **Write `design.md`** (Step 7) from the kept look, then hand off as usual. In **Logo**:\n - *Files* and *Use* name the files as saved (a `.png` stays a `.png`), or say \"No favicon kept\".\n - *Built from* says \"Kept, not rebuilt\". *Measured* still gives the width at `--logo-h-sm`\n (≤ 192 px; else lower `--logo-h-sm` or show the name as text on small screens), and the header\n link has `alt` = the site name and a target of at least 24 px (as in Step 6).\n - With no logo: *Files* says \"None\", *Use* says the header link holds the site name as text in\n `--font-display` and the browser's default favicon stays, and the *Don't* line is dropped.\n\n## Step 2 — Collect the signals\n\nBefore inventing anything, gather what already constrains the design:\n\n| Signal | Where | How it constrains |\n|---|---|---|\n| Tone, purpose, audience | `brief.md` → *Tone*, *Purpose*, *Audience* | Sets the mood all three directions must fit |\n| Brand assets | `brief.md` → *Assets* | Logo, brand colours and fonts are **fixed** in every direction. Never recolour a supplied logo; Step 6 uses it instead of designing one |\n| Pages and entities | `content-model.md` → *Navigation*, *Pages*, *Articles*; if it doesn't exist yet, `brief.md` → *Pages* | Drives the component inventory and page composition |\n| Reference sites | URLs in the brief or the conversation | Cues to borrow (see below) |\n| Images already sourced | `content-model.md` → the image lines under *Articles* (skip if none yet) | Glance at a few at small size; keep accents from clashing with their dominant colours |\n| Current design (redesign only) | existing `design.md` | What to keep, what to move away from |\n\n**Reference sites.** Open each one in a real browser if you have one. Static fetches are often\nblocked or rate-limited; without a browser, fetch the HTML and its linked CSS instead. If neither\nworks, say so in one line and continue from the brief alone. Pull **cues**:\n\n- **Fonts:** the computed `font-family` of `h1`/`h2` and of body `p`.\n- **Colours:** background colours weighted by painted area (element width × height), the top 5–8.\n Skip transparent and semi-transparent backgrounds and overlays (cookie banners, modals, chat\n widgets). Text colours come from headings, paragraphs and links.\n- **Feel:** look at the first screen and one further down (screenshots shown to you, not saved in\n the workspace) and note what creates the feel: type weight, colour blocking, shapes, density.\n\n**Borrow cues, never assets** (the owner's own site under *Keep the current look* is the exception).\nDon't use its logo, copy, images or exact layout. **The brief wins** over the live site when they\ndisagree (the user's words are what they want). Note the mismatch in the direction.\n\n**Brand colours that fail contrast** are still fixed, but only in roles where they pass. A pale brand\nyellow can be a background band or a large decorative shape, never body text. Say so in the\ndirection.\n\n## Step 3 — Derive three directions\n\nWrite three directions that **all fit the brief** but are **genuinely different**. Vary at least two\nof: palette (temperature, saturation, light vs. dark), type personality (geometric, grotesk, serif,\ndisplay), and shape language (sharp vs. round, flat vs. outlined, airy vs. dense). Three shades of\none idea is not a choice. Neither is a wildcard that ignores the brief.\n\nOrder them: **A is the recommendation**, the closest reading of the brief and references. B and C\npush in different directions.\n\nEach direction carries exactly this:\n\n- **Name** — 2–3 evocative words (\"Sticker-book maximal\", \"Quiet gallery\").\n- **Mood** — one line on how the site feels.\n- **Palette** — 5–7 hex colours with roles (base, text, primary accent, secondary accents, surface).\n These are the core colours the user sees; Step 5 may add derived shades (link, muted, error,\n focus) as tokens.\n- **Type** — a display font and a body font, both self-hostable through `@fontsource`.\n- **Shape language** — corners, borders, shadows and density in one phrase (\"chunky 3 px ink\n outlines, hard 8 px offset shadows, tilted cards\").\n- **Signature element** — the one memorable move (a scrolling marquee, stickers, oversized numerals,\n a duotone photo treatment). If it moves on its own, it gets a pause control (5.3).\n- **Logo idea** — one line on how the name becomes a logo, using a move from Step 6 (\"letters\n bounce in different colours and the O is a smiley\"). When the brief carries a logo wish (*Design\n direction* or *Assets*), every direction's logo idea is that wish in its own style. A wish is words\n describing a logo; a logo file in *Assets* is a supplied logo and wins (Step 6).\n\n**Only offer what can pass.** Before asking, run two quick gates on **all three**:\n\n1. **Contrast gate.** Every colour that will carry text (base, surfaces, and any accent used as a\n text block) has a palette text colour reaching **4.5:1** on it. Two text colours are fine (dark on\n light blocks, light on dark ones). Accents that only ever hold large text need 3:1.\n2. **Font gate.** Each font resolves on npm, and the display font also has a static `@fontsource`\n package, because the logo is built from it (5.4).\n\nA direction that fails either gate gets adjusted, not offered as-is. Don't offer a look that only\nworks by breaking the rules, such as body text on a neon accent or a marquee that can't be stopped.\nLoud is fine; inaccessible is not.\n\n**Avoid the template look** unless the brief asks for neutral: Inter or Roboto everywhere, a\nblue-to-purple gradient hero, centred hero with three feature cards, grey cards on white. Every\ndirection should look like *this* site, not a starter theme.\n\n## Step 4 — Ask once\n\nAsk **one** question, with A marked as the recommendation. With a structured question UI (multiple\nchoice plus an \"other\" field), each option's **label** is the direction's name and its\n**description** is one or two plain sentences, under ~160 characters. Without one, write each\noption as one line in the same shape.\n\nThe description says what the site would feel like, the main colours by name, what the lettering\nis like, the logo idea, and the one detail they'll remember. Hex codes and font names stay in your\nnotes. The question may end with one plain clause saying they can add a logo idea to their pick, but\nnever suggest one or give an example: that idea would end up in the logo.\n\n> **Sticker-book maximal** — Loud and playful, like a scrapbook: candy pink, butter yellow and lilac,\n> chunky headlines, tilted sticker photos and a bouncy logo with a smiley O.\n\n> **Midnight disco** — A dark, glowing night-out feel: deep purple with neon pink, yellow and\n> turquoise, heavy rounded headlines and a logo like a neon sign.\n\nThen:\n\n- **Pick** → use it as written, and confirm the look in one line.\n- **\"You decide\"** → use A, and tell them in one line which look you went with.\n- **Skipped tool result** → not an answer: write the three directions as plain lines\n and wait for the reply (see *When you ask the user* in the top-level `SKILL.md`).\n- **A mix** (\"A's colours with C's fonts\") → merge them. Don't ask again.\n- **Something else** → build a direction from what they said, run Step 5, and use it. Don't send\n three new options.\n\n**Logo wish.** An answer can carry a logo idea next to the pick (\"A, and make the logo a little\nteapot\"); it replaces the chosen direction's logo idea, and wins over a wish from the brief.\nWhenever the logo follows a wish, from the answer or the brief, add it once to the line that confirms\nthe pick: *\"The logo follows your idea: a little teapot.\"* If the kit can't draw it as said (a\ndetailed illustration, a photo), build its closest kit version and name what you simplified in that\nsame line. Don't ask a follow-up.\n\nDon't ask about anything else in this step.\n\n## Step 5 — Check the chosen direction\n\nFix any problem found here by adjusting the direction, not by asking the user. Don't report these\nchecks or their fixes; the user sees the result in the preview.\n\n### 5.1 Contrast — WCAG 2.2 SC 1.4.3 and 1.4.11\n\n**Which pairs count:** walk the component inventory you're about to write (Components table). For\neach component, list every foreground/background combination in every state: default, hover,\nfocus, active, error. Include text on coloured bands (footer links on dark, marquee text, chips),\nplaceholder text and photo credits. Compute the WCAG contrast ratio for each:\n\n| Kind | Examples | Minimum |\n|---|---|---|\n| Body text | paragraphs, links, footer text, input text, placeholder text, captions | **4.5:1** |\n| Large text | ≥ 24 px regular, or ≥ 18.66 px (14 pt) bold | **3:1** |\n| Non-text | input and button boundaries (when the outline is the only edge), focus indicator against each adjacent colour, icons that carry meaning, chip outlines | **3:1** |\n\nA pair that fails gets a darker or lighter shade of the **same hue**; never drop a brand colour. A\nnew shade becomes a token (for example `--color-link` or `--color-muted`). Disabled controls and\npure decoration are exempt, but say which elements those are. **Text on photos** needs a scrim or\nsolid backing that gets the text to 4.5:1 against the lightest part of the image. If you can't\nguarantee that, don't put text on photos. Record every pair and its ratio in `design.md`.\n\n```js\n// node -e: relative luminance + contrast ratio, hex in → ratio out\nconst L = h => { const c = h.match(/\\w\\w/g).map(x => parseInt(x, 16) / 255)\n .map(v => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\n return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; };\nconst ratio = (a, b) => { const [x, y] = [L(a), L(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };\n```\n\n### 5.2 The 8-point grid\n\nValues are checked in px at the default 16 px root (`0.5rem` = 8 px).\n\n| What | Rule |\n|---|---|\n| Spacing (padding, margin, gap) | Multiples of 8: `8 16 24 32 48 64 96 128`. **4 px** is the only half-step, and only inside small components (chip padding, icon-to-label gap) |\n| Component sizes | Heights and fixed widths are multiples of 8. Buttons, nav items and inputs are **48 px** tall minimum |\n| Radii | `8 16 24 32`, or `999px` for pills. 4 px only on elements under 32 px tall |\n| Line heights | Fixed-size text: multiples of 8 (16/24, 18/32, 24/32), or of 4 for text ≤ 14 px (12/16, 14/20). Fluid headings (`clamp()` sizes) use a unitless line-height (1.0–1.2) and are exempt, because their line box can't stay on the grid at every size |\n| Fluid spacing | `clamp()` for spacing or sizes uses grid values at both ends. Fluid **font sizes** are exempt |\n| Shadow offsets | 4 or 8 multiples |\n| Layout | Content width, gutters and breakpoints are multiples of 8 (e.g. `1200`, `768`, `1024`, `1280`) |\n| Exempt | Border and outline widths (1–4 px), focus offset, font sizes, the `999px` pill radius, the 4-multiple line heights above, and anything that isn't a length (angles like a `-2deg` tilt, durations, opacity) |\n\nEvery value in the **Tokens** block must pass this table.\n\n### 5.3 WCAG 2.2 AA — what the design must decide\n\nThese criteria are settled at design time. Write each answer into the **Accessibility** section of\n`design.md`.\n\n| Criterion | The design must specify |\n|---|---|\n| 1.4.1 Use of colour | Links in running text are underlined, not colour-only. States (active nav, selected, error) also change shape, weight or an icon, not only colour |\n| 1.4.4 Resize text | Font sizes in `rem`; nothing breaks at 200 % zoom |\n| 1.4.10 Reflow | Works at **320 CSS px** wide with no horizontal scroll. Every component has a stated ≤ 320 px behaviour; decorative pieces never widen the page |\n| 1.4.12 Text spacing | No fixed-height boxes around text, so nothing clips when a user overrides spacing (line-height 1.5, paragraph spacing 2×, letter-spacing 0.12 em, word-spacing 0.16 em). Fixed line-height tokens are fine; clipping is not |\n| 2.2.2 Pause, Stop, Hide | Anything that moves on its own for more than 5 s (marquee, carousel, animated sticker) has a **visible pause control**, and doesn't move at all under `prefers-reduced-motion`. Hover-to-pause alone doesn't count |\n| 2.3.1 Three flashes | Nothing flashes more than 3 times a second |\n| 2.4.7 Focus visible | One focus style (e.g. 3 px outline, 2 px offset) whose colour reaches 3:1 against **every** surface it can appear on. Name a second colour for surfaces where the first fails |\n| 2.4.11 Focus not obscured *(new in 2.2)* | Sticky headers, cookie bars and chat bubbles never cover the focused element. Either the header isn't sticky, or `scroll-padding-top: var(--header-h)` |\n| 2.5.8 Target size *(new in 2.2)* | Interactive targets ≥ 24 × 24 px with spacing. On this grid: **48 px** for buttons, nav and inputs, ≥ 24 px for inline icon buttons |\n| 3.2.6 Consistent help *(new in 2.2)* | The contact or help link sits in the same place on every page (e.g. last nav item and footer) |\n| 3.3.2 Labels | Every form field has a visible label; placeholders are never the only label |\n| 1.1.1 Text alternatives | Content images get alt text describing what they show; purely decorative images and shapes get empty alt or `aria-hidden` |\n\n### 5.4 Fonts resolve\n\nFor each family, prefer the variable package and fall back to the static one:\n`npm view @fontsource-variable/<slug> version`, then `npm view @fontsource/<slug> version`. One must\nanswer with a version. If neither does, swap to the closest family that does. Record the exact\npackage names. The **display** family must also resolve as a static package\n(`npm view @fontsource/<slug> version`): the static packages ship `.woff` files the logo kit can\nread; the variable ones ship only `.woff2`.\n\n## Step 6 — Design the logo\n\n**The user supplied a logo** → copy it into `brand/` unchanged, note it in the **Logo** section, and\nskip the rest of this step. Never redraw or recolour it.\n\nOtherwise, turn the name into a **designed** logo: shaped, coloured and composed. Typing the name in\nthe display font with an emoji or symbol next to it doesn't count. Build the chosen direction's **logo\nidea** (as replaced by any logo wish, Step 4) with one or two of these moves:\n\n| Move | What it is | Suits |\n|---|---|---|\n| **Letter swap** | One letter becomes a simple shape tied to the subject: O → smiley, sun, loaf, plate, eye; I → candle, pencil, brush; A → roof, tent. Swap **one character** only, even if the letter appears twice, and the word must still read | Playful, friendly brands |\n| **Bounce** | Letters tilt ±3–6° and bob off the baseline, each in a palette colour, with an ink outline and hard shadow | Loud, maximal directions |\n| **Stack** | The name split over 2–3 lines on coloured tiles, or set as a tight block | Long names, bold directions |\n| **Tag** | The main word large; a short second word on a pill, ribbon or tab | Two-part names (\"Dopamine Den\") |\n| **Seal** | The name around a ring, with a monogram or symbol in the middle | A secondary mark (footer, hero sticker, social avatar), never the header logo |\n| **Typographic contrast** | Weight, style or case contrast between words (a light italic word next to a heavy upright one), custom tight spacing, one shared stroke or cut between letters | Quiet, refined, editorial directions |\n\nNo emoji, clip-art, gradients, or details that vanish at small sizes. Colours come only from the\npalette (base and surface colours count).\n\n### Build it with the logo kit\n\nThe kit (`logo-kit.mjs`, next to this file) does the error-prone parts: it outlines letters into\nvector paths, sets text on a circle, draws the outline-and-shadow treatment, measures, and validates\nthe output. You write the composition. Copy the kit into `brand/` so the build is self-contained\nand a redesign can re-run it:\n\n```bash\nmkdir -p ~/.hostinger/<domain>/brand && cd ~/.hostinger/<domain>/brand\ncp <the folder this SKILL.md is in>/logo-kit.mjs .\nnpm init -y >/dev/null && npm i opentype.js @fontsource/<display-slug>\n```\n\nWrite `brand/logo-build.mjs`, import from `./logo-kit.mjs`, and always run it from `brand/`\n(packages resolve from the working directory):\n\n| Kit function | Use |\n|---|---|\n| `loadFont(slug, weight, style)` | Loads a static `@fontsource` weight across all its subsets (`latin`, `latin-ext`, …), so accented letters like Ė or Ł resolve; `style` is `'normal'` or `'italic'`. Throws if that weight or style isn't in the package, so pick another |\n| `layout(font, text, size)` | Kerned glyph positions, total width, `capHeight`, `xHeight`; place letters one by one for bounce or swap. Throws if a character isn't in the font |\n| `glyphPath(glyph, x, baselineY, size)` / `textPath(font, text, x, baselineY, size)` | Outlined path data for one letter or a whole word. For typographic contrast, load two weights or styles and set each word with `textPath` |\n| `pathBox(font, text, x, baselineY, size)` | Tight box around a word's outlines; size the viewBox from it plus room for outlines, shadows and tilt |\n| `arcText(font, text, { cx, cy, r, size })` | Letters around a circle, for seals |\n| `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`) |\n| `svgDoc({ width, height, title, body })` | Root SVG with `role=\"img\"` and a `<title>` |\n| `check(svg)` | Throws on `NaN`, live `<text>`, a missing title or viewBox. Run it on every file before writing |\n\nNever call opentype's `path.toPathData()` directly; the kit's serializer exists because opentype.js\n2.0 writes `NaN` into some paths, and browsers silently stop drawing at the first `NaN`.\n\n### Deliver four files\n\n| File | What | Must read clearly at |\n|---|---|---|\n| `brand/logo.svg` | The primary, mostly horizontal logo for the header | Its header display heights (desktop and small screens) |\n| `brand/logo-mark.svg` | A square icon from the same idea (initial, monogram or the swapped symbol) on a solid rounded tile (corner radius ≈ ¼ of the side) in a palette colour | 32 px |\n| `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 |\n| `brand/logo-build.mjs` | The script that built them, with `package.json`, kept so a redesign can adjust it | — |\n\nAdd `brand/logo-seal.svg` only if the direction uses a seal as a secondary mark. Delete\n`brand/node_modules` when you're done; a redesign runs `npm i` in `brand/` first.\n\n**Check the sizes.** Display heights are multiples of 8, with one for desktop and one for small\nscreens (they may be equal). Work them out from the viewBox:\n\n- **Letter size:** letter height on screen = letter height ÷ viewBox height × display height, and it\n must be ≥ 16 px. Use `xHeight` when the logo is mostly lowercase and `capHeight` when it's mostly\n capitals; for mixed fonts, take the smaller word. A two-line lockup usually needs 64 px.\n- **Headroom:** `pathBox` covers the letters, not what you add. Leave room above for accents (Ė, Ž, Å sit above cap height) and below for descenders, plus whatever bounce, tilt, outline and shadow add, so nothing touches the viewBox edge.\n- **Width:** width on screen = viewBox width ÷ viewBox height × display height. At the small-screen\n height it must be **≤ 192 px**, which leaves room for the menu button in a 320 px header. If it\n doesn't fit, lower the small-screen height (letters still ≥ 16 px), simplify the logo, or show\n `logo-mark.svg` with the name beside it on small screens.\n- **Look at it.** If you have a browser, render the files at both header heights, 32 px and 16 px,\n the favicon on a light and a dark background, and fix anything that reads wrong. Without a\n browser, rely on the numbers and list \"logo not visually checked\" under gaps in the final report.\n\nThe logo is exempt from text-contrast minimums, but the header link around it needs `alt` = the site\nname and a target of at least 24 px.\n\nDon't show the logo to the user in this step; they see it in the preview.\n\n## Step 7 — Write `design.md` and hand off\n\nWrite `design.md` from the template below. Fill every section. The **Tokens** block is copied\nverbatim into the frontend's global stylesheet, so it must be valid CSS. Every colour, font, type\nsize, line height, space, radius and shadow used elsewhere in the file must exist in it as a token.\nThe template's token list is the **minimum**: add what the design needs (`--color-error`,\n`--color-on-accent`, `--leading-h3`…) under the same naming scheme.\n**Breakpoints are the one exception**: CSS custom properties don't work inside `@media`, so they are\nlisted in *Grid, shape & motion* as plain px values.\n\nThen update `state.json`: set `\"stage\": \"frontend\"` (also for a redesign: restyle, then validate and\ndeploy) and `updated_at` to the current UTC time (ISO 8601). Don't touch `provision`, `run` or\n`history` (`run-log.mjs` owns those), or add other keys.\n\nIf content prepare hasn't sourced images yet, it now follows `design.md` → **Imagery** (see\n`04-content-model/SKILL.md`).\n\n## `design.md` template\n\n````markdown\n# Design — <domain>\n\n> Direction **<name>**, selected on <YYYY-MM-DD> from three options (others offered: <B name>,\n> <C name>). <\"Picked by the user.\" | \"User said 'you decide'; recommendation A used.\" |\n> \"Merged by the user: <what>.\"> — or, when kept: \"Kept the owner's current look from <site URL |\n> screenshots | brand guide>, confirmed on <YYYY-MM-DD>.\"\n\n## Direction\n<2–3 sentences: the mood, what it borrows from the references (and any brief/reference mismatch),\nthe signature element.>\n\n## Stack\n**<chosen by the frontend step>** — design doesn't decide the stack. The frontend step (06) picks and\nconfirms the framework (default Next.js SSR; ecommerce must be SSR) and records it in\n`state.json → frontend`. Leave this as a placeholder for the frontend step to fill in.\n\n## Tokens\n```css\n:root {\n /* colour — semantic names, never \"--pink-2\" */\n --color-base: #…; --color-text: #…; --color-surface: #…; --color-muted: #…;\n --color-primary: #…; --color-accent-1: #…; --color-accent-2: #…; --color-link: #…;\n --color-focus: #…; --color-focus-alt: #…; /* for surfaces where --color-focus is < 3:1 */\n /* type */\n --font-display: '<Family> Variable', <fallback stack>;\n --font-body: '<Family> Variable', <fallback stack>;\n --text-body: …rem; --leading-body: …rem; /* 8-multiple in px */\n --text-small: …rem; --leading-small: …rem; /* 4-multiple in px */\n --text-h1: clamp(…rem, …, …rem); --text-h2: clamp(…); --text-h3: …rem;\n --leading-display: 1.05; /* unitless; fluid headings are grid-exempt */\n /* 8-point grid */\n --space-half: 4px; --space-1: 8px; --space-2: 16px; --space-3: 24px; --space-4: 32px;\n --space-6: 48px; --space-8: 64px; --space-12: 96px; --space-16: 128px;\n --target-min: 48px;\n /* shape */\n --radius-sm: 8px; --radius-card: …; --radius-pill: 999px;\n --border: …; --shadow: …; --shadow-hover: …;\n --focus-ring: 3px solid var(--color-focus); --focus-offset: 2px;\n /* layout */\n --content-max: …px; /* multiple of 8 */\n --header-h: …px; /* header band height, multiple of 8; scroll-padding-top uses it if sticky */\n --logo-h: …px; /* header logo height on desktop, multiple of 8 */\n --logo-h-sm: …px; /* on small screens; logo ≤ 192 px wide at this height */\n}\n```\n\n## Palette roles\n| Token | Hex | Used for |\n|---|---|---|\n\n## Contrast\n| Foreground | Background | Kind (body text / large text / non-text) | Ratio | Needs | Passes |\n|---|---|---|---|---|---|\n\n## Type\n- **Display:** <family> (`@fontsource-variable/<slug>@<version>`), weights …, tracking …; sizes `--text-h1…h3`, `--leading-display`\n- **Body:** <family> (`@fontsource-variable/<slug>@<version>`); `--text-body` / `--leading-body`, measure ≤ 68ch\n\n## Logo\n- **Concept:** <the move(s), and what any swapped letter or symbol stands for, plus \"From the owner:\n '<their words>'\" only when it follows their wish | \"Supplied by the user\" | \"Kept from <source>\" |\n \"No logo: owner kept the current look\" (header shows the site name as text in `--font-display`)>\n- **Built from:** `@fontsource/<display-slug>@<version>`, weight(s)/style(s) …\n- **Files:** `brand/logo.svg` (header, `--logo-h` desktop / `--logo-h-sm` small screens),\n `brand/logo-mark.svg`, `brand/favicon.svg`<, `brand/logo-seal.svg`>; source `brand/logo-build.mjs`\n- **Measured:** letters … px tall at `--logo-h-sm`; width … px at `--logo-h-sm`\n- **Use:** the frontend copies `brand/*.svg` into `public/`. Header: `<a href=\"/\"><img src=\"/logo.svg\"\n alt=\"<Site name>\" style=\"height: var(--logo-h)\"></a>` (switch to `--logo-h-sm` on small screens).\n Favicon: `<link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\">`\n- **Don't:** recolour, stretch, add effects, or retype the logo as live text.\n\n## Grid, shape & motion\n<section padding desktop/mobile (space tokens), gutters, breakpoints in px (multiples of 8), borders,\nradii, shadows, hover behaviour, every animation and its pause control, and \"no motion under\nprefers-reduced-motion\".>\n\n## Accessibility (WCAG 2.2 AA)\n- **Focus:** <style; which colour on which surface>\n- **Targets:** <48 px controls; inline exceptions>\n- **Links & states:** <underline rule; how active/error states differ beyond colour>\n- **Motion:** <what moves, where its pause control is>\n- **Sticky elements:** <none | header uses --header-h + scroll-padding-top>\n- **Help:** <where the contact link sits on every page>\n- **Text on images:** <none | scrim spec and resulting ratio>\n- **Reflow:** <anything special at 320 px>\n- **Exempt from contrast:** <disabled controls / decorative shapes, named>\n\n## Components\n| Component | Renders (from content model) | Look | ≤ 320 px |\n|---|---|---|---|\n| Header + nav | Navigation items | … | … |\n| Post card | Article: image, category, title, excerpt | … | … |\n| … | | | |\n\n## Page composition\n- **<Page>** — <sections top to bottom, each naming its component and band colour>\n- …\n\n## Imagery\n<subjects, mood and colour fit for photos; search keywords; treatment (frame, crop ratio, radius,\nduotone…); alt text written from what the photo shows.>\n\n## Don'ts\n- <3–6 site-specific rules, e.g. \"no grey cards\", \"never put body text on the primary accent\">\n````\n\n## Invariants\n\n- **One question.** Refine owns discovery; this step only chooses among directions (or keeps the\n existing look).\n- **8-point grid, always.** No spacing, size, radius or breakpoint off the grid except the listed\n exemptions.\n- **WCAG 2.2 AA is the floor.** No direction is offered, chosen or written that fails it. Brand\n colours keep their hue but only appear in roles where they pass.\n- **Brand assets are fixed.** A supplied logo, colour or font appears in every direction, unchanged.\n- **A logo is designed, not typed.** Outlined SVG paths built with the logo kit, never live text,\n emoji or clip-art, and always readable at its smallest use. The one exception: a kept look with no\n logo shows the site name as text and gets no new logo.\n- **Self-hosted fonts only** (`@fontsource`). No Google Fonts or other font CDN links in the output.\n- **The tokens block is the only source of values** (breakpoints excepted). Components and pages\n reference tokens, never raw hex codes, pixel values or font names.\n- **No backend calls.** Design never waits on provision and never writes to the instance.\n- **Never overwrite an existing `design.md` silently.** Keep or change, as in Step 1.\n"
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
227
|
"uri": "skill://hostinger-headless/steps/05-design/logo-kit.mjs",
|
|
@@ -232,10 +232,10 @@ export default [
|
|
|
232
232
|
},
|
|
233
233
|
{
|
|
234
234
|
"uri": "skill://hostinger-headless/steps/06-frontend/SKILL.md",
|
|
235
|
-
"digest": "sha256:
|
|
236
|
-
"size":
|
|
235
|
+
"digest": "sha256:78a73a17fcff1d5ff8a6476e612b79d42809e9e36009b5db2b21153249904d4c",
|
|
236
|
+
"size": 13998,
|
|
237
237
|
"mimeType": "text/markdown",
|
|
238
|
-
"text": "---\nname: headless-step-frontend\ndescription: \"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.\"\n---\n\n# headless-step-frontend\n\nBuild the site's frontend against the backend from `site.json`, implementing the look the design step\nalready chose and wrote to **`design.md`** (plus the logo in `brand/`). The design is decided; this step\nis where it becomes real code. There's no per-framework recipe — use good judgement, follow `design.md`\nas the contract, and ask only for the clarifications you genuinely need. **Out:** source in the site\nworkspace `~/.hostinger/<domain>/src/`.\n\n> ## ⚠️ Build mode: \"Bring your own frontend\"? Read this first (any backend)\n> Read `brief.md` → **Build mode**. When it is **Bring your own frontend**, the user already has a\n> frontend (the project in the folder the command was run in) and this step **wires it to the backend\n> from `site.json` — it does not scaffold a new project and there is usually no `design.md` to\n> implement** (the design step deferred to the user). **Use their project as the source**: copy or\n> point `~/.hostinger/<domain>/src/` at it, keep its framework and UI, and collaborate on changes at\n> the user's direction (vibe-code with them). Your job is to connect it to the live backend (WordPress\n> `wp/v2` reads, the WooCommerce/Ecommerce Store API, env/keys) and make it deployable (set\n> `engines.node` to `22`, remove any `.htaccess`), then hand off to deploy. This generalises the Webapp\n> \"existing app\" case below to WordPress and Ecommerce too. When **Build mode is \"Agent designs and\n> builds\"**, ignore this box and proceed normally (scaffold + implement `design.md`).\n\n> ## ⚠️ Webapp backend? Read this first\n> A `webapp` brief has **no CMS/store and no `content-model.md`** — the app owns its own data (its own\n> API, or the MySQL database recorded in `site.json → database`). It comes in two shapes, decided back in\n> **refine**:\n>\n> - **An existing app the user brought to deploy** (refine routes \"deploy my app\" here). **Use their\n> project as the source** — copy/point `~/.hostinger/<domain>/src/` at it and keep its own framework and\n> UI. **Do not scaffold a new project, and do not re-implement `design.md` over it** (so **the design\n> step does not apply** to an app that brings its own UI). Your job is only to make it deployable: set\n> `engines.node` to `22`, remove any `.htaccess`, wire its data/env, then hand off to deploy. **Skip the\n> Step 2 design pass**, and ignore the `content-model.md` requirement in Step 3.\n> - **A new app built from the brief.** Scaffold and build it from `brief.md` (and `design.md` if the\n> design step produced one — design *does* run for a from-scratch app). Its data is owned by the app, so\n> there is still **no `content-model.md`** to bind against.\n>\n> Framework choice (Step 1), the build, and hand-off (Step 4) apply to both. The per-backend bullets in\n> Step 3 are **WordPress / WooCommerce / Ecommerce only** — for a webapp follow the Webapp bullet there.\n\n## Step 1 — Pick the framework (guess → default → confirm)\n\n**The rendering is already decided in `brief.md` → Rendering** (SSR or SSG, set during refine). Honour\nit, and the backend constraint:\n\n- **Ecommerce → SSR required** (carts/search need per-request rendering). Any SSR framework works —\n Next.js is the default, not a requirement (Express, Nuxt, SvelteKit, etc. are all fine).\n- **WordPress + WooCommerce → SSR required** (same reason — cart/checkout need per-request rendering).\n- **WordPress → SSG (Astro)** when the brief says SSG, otherwise **SSR (Next.js default)**.\n- **Webapp → the app's own stack.** An **existing app** keeps its current framework (don't impose one); a\n **new app** uses whatever fits the brief (SSR or SPA). Either way it deploys as a Node.js app (Step 8) —\n an SSR app runs a server, a SPA builds to static output.\n- Map rendering → framework: **`ssr` → Next.js**, **`ssg` → Astro** — unless the brief/prompt explicitly\n names another supported framework, in which case use that.\n\nSupported frameworks (what the Hostinger build pipeline can deploy — the `app_type` in Step 8 deploy):\n\n- **Server/SSR-capable:** Next.js, Nuxt.js, Astro, SvelteKit, Express, NestJS, Fastify.\n- **Static / SPA:** React, Vue, Angular, Vite, Parcel, Preact, Svelte, (Astro / SvelteKit static too).\n\nConfirm the framework with the user in one line only if the brief left it open. Record the choice in\n`state.json`:\n\n```json\n{ \"frontend\": { \"framework\": \"nextjs\", \"rendering\": \"ssr\" } }\n```\n\nIf the brief is silent, default to **Next.js SSR**. Ecommerce must be SSR (never SSG) — but the SSR\nframework itself is free to be any supported one, not just Next.js.\n\n## Step 2 — Implement `design.md` (don't re-design)\n\nThe look was already chosen and written by the design step. **Read `~/.hostinger/<domain>/design.md` and\nimplement it faithfully** — tokens (colour, type, spacing), components, per-page composition, imagery and\naccessibility — plus the logo files in `brand/`. Don't re-run design questions or invent a new direction here.\n\n- Treat `design.md` as the contract: the built pages should match its tokens, type scale, components and\n per-page composition, and the 8-point grid / WCAG 2.2 AA rules it carries.\n- Use the **logo** from `brand/` as-is (wordmark, icon mark, favicon); never recolour or redraw it.\n- If `design.md` is **missing** (e.g. a resumed older build), fall back to `brief.md → Design direction`\n (reference site, colours, brand, adjectives) and a tasteful, accessible default — but prefer `design.md`\n whenever it exists.\n- **Don't gate on approval here** — the result is shown at **validate** (Step 7).\n\n**Only ask about the code, and only if it matters** (design decisions belong in the design step):\n- Where the **source should live** — default `~/.hostinger/<domain>/src/`; ask only if they want a\n specific folder or an existing repo.\n- **Git** — initialise / connect / push (default: a local project, no remote).\n- A specific **component library** or technical must-have, if they implied one.\n\n## Step 3 — Build against the backend\n\nTwo phases around the instance barrier (see the top-level `SKILL.md`): scaffold + build the shell while\nprovision settles, then wire live data once `provision.status == \"completed\"`.\n\nRead the backend from `site.json`:\n\n- **WordPress** → `urls.rest_api` (`wp/v2`), public read. Content is in the posts/pages/categories/media\n the content-model push created; nav/plan come from `content-model.md`. Map each item's **SEO** from its\n `yoast_head_json` into the page `<head>` (title, description, Open Graph, JSON-LD schema) — Yoast is\n installed by default at provision (`site.json → seo.enabled: true`). If `seo.enabled` is `false`\n (Yoast absent), fall back to generating per-page title/description/OG tags from each item's content.\n - **Contact form** (`site.json → contact_form.enabled: true`, any WordPress backend) → render the\n contact page's form and submit it **client-side** to Contact Form 7's public feedback endpoint. Put a\n `submitContactForm(data)` helper in the one data module: build a `FormData` with the field names from\n `contact_form.fields` (e.g. `your-name`, `your-email`, `your-subject`, `your-message`) **plus CF7's\n hidden fields** — `_wpcf7` = `contact_form.form_id`, `_wpcf7_version`, `_wpcf7_locale` (e.g. `en_US`),\n `_wpcf7_unit_tag` (any stable string), `_wpcf7_container_post` = `0` — and `POST` it to\n `contact_form.feedback_url` **without setting `Content-Type`** (let the browser add the multipart\n boundary). `/cms` is same-origin, so no CORS and no secret. Branch on the JSON `status`: `mail_sent`\n → success state; `validation_failed` → map `invalid_fields[].field` to per-input errors;\n `spam`/`aborted`/`mail_failed` → a generic \"couldn't send, try again\" message. Include the honeypot\n decoy field named in `contact_form.honeypot_field` (e.g. `your_website`), rendered hidden/off-screen,\n **empty**, `autocomplete=\"off\"`, `tabindex=\"-1\"` — the deployed spam guard rejects it if filled. A user\n validation error is a **normal UI state, not a build error**. When `contact_form.enabled` is `false` or absent, render the contact page with\n plain contact details and **no form** — graceful, like disabled checkout; never half-build. Full\n contract: `../04-content-model/wordpress/contact-form.md` §3.\n- **WordPress + WooCommerce** (`site.json → woocommerce.enabled: true`) → the WordPress bullet above **plus**\n the store. Read the catalog over the **public WooCommerce Store API** (`site.json → urls.store_api`,\n `/cms/wp-json/wc/store/v1`, no auth, open CORS): `GET /products` (prices are **integer minor units** on\n `prices.price` with `prices.currency_minor_unit` — divide to display), `GET /products/categories`. Cart,\n coupons and checkout are **client-side** against the Store API with the `Nonce` + `Cart-Token` headers\n (`GET /cart`, `POST /cart/add-item`, `/cart/apply-coupon`, `POST /checkout`) — see\n `../04-content-model/wordpress/woocommerce.md`. **Never embed a consumer key client-side**; the\n storefront is fully public. Blog/pages on the same site still come from `urls.rest_api` (`wp/v2`) + Yoast.\n - **Honour `site.json → woocommerce.payment_enabled`.** It is `false` until the owner enables a payment\n gateway + shipping in wp-admin (that config isn't reachable headlessly — see the provision note). While\n `false`, build the full catalog, product pages and the client-side cart as normal, but render a\n graceful disabled-checkout state (e.g. a disabled \"Checkout\" with \"Checkout isn't set up yet\"), **don't\n call `/checkout`**, and don't treat it as a build error. When `true`, wire checkout (`payment_method`\n `\"cod\"` works out of the box).\n- **Ecommerce** → the **public Hostinger Storefront Core V2 API** (`https://api-ecommerce.hostinger.com/v2`,\n no auth, open CORS), keyed on the **custom sales channel** (`site.json → ecommerce.sales_channel.id`):\n `GET /channels/<scha_*>/products` (prices are **minor units** on variants), `POST /channels/<scha_*>/checkout`\n for buying. Cart is client-side; never embed a token. Read\n `ecommerce_miscellaneous_custom-storefront-setup-instructions` for the authoritative, current guide.\n - **Honour `site.json → ecommerce.payment_enabled`.** When it is `false` (free account / no paid\n Ecommerce plan — see the provision gate), there is **no checkout**: build the full catalog, product\n pages and the client-side cart as normal, but **do not call the checkout endpoint**. Instead render\n a clear, graceful state at the checkout step — e.g. a disabled \"Checkout\" button with a short note\n like \"Online checkout isn't available on this plan yet\" (and/or a contact/enquiry fallback). A\n missing checkout is **not** a build error here — never leave the storefront half-built, throw, or\n retry checkout because payment is off. When `payment_enabled` is `true`, wire checkout normally.\n- **Webapp** → **no `content-model.md` and no headless backend to read**. The app uses its own data source:\n the MySQL database from `site.json → database` (through the `DB_*` / `DATABASE_URL` env vars — set at\n provision/deploy, see `../03-provision/webapp/SKILL.md` Step 3; `127.0.0.1` only resolves on the server,\n not locally), or whatever external API the app calls. For an existing app, keep its data layer as-is.\n\nGuidelines (framework-agnostic):\n\n- **One data module** is the single place that talks to the backend (e.g. `src/lib/data.ts`) — one file\n to inspect, one base URL to switch. Keep it swappable so the shell can build against stubs first.\n- **Fetch at request time for SSR** (fresh content, no rebuild); a static build fetches at build time.\n- **A failed or incomplete fetch is an error, not a silent empty/placeholder.** Planned content missing\n from the API is a build error too — a site that builds with holes ships with holes. *(WordPress/Ecommerce;\n a webapp has no planned content to check.)*\n- **`content-model.md` is the source for what should exist** (pages, nav, collections mapping) — for\n WordPress/Ecommerce only; a **webapp has no `content-model.md`**.\n- **No `.htaccess`** in the project (deploy would overwrite the live one); don't commit `node_modules`\n or build output.\n- Set `engines.node` to `22` (the deploy builds on Node 22).\n\n## Step 4 — Hand off\n\nConfirm the project builds clean locally against the live backend, no placeholder/fixture content\nremains, and `state.json → frontend` matches what you built. Update `state.json` (`stage: \"validate\"`).\nValidate (Step 7) owns the clean rebuild + user preview; deploy (Step 8) ships it with the `app_type`\nand output directory that match the chosen framework + rendering.\n\n> **Webapp:** just confirm the app builds clean and `state.json → frontend` is right — there's no\n> `content-model.md` or headless backend to check. A DB-backed app can't reach its database locally\n> (`127.0.0.1` is server-only), so its data layer is verified **after** deploy, not here.\n"
|
|
238
|
+
"text": "---\nname: headless-step-frontend\ndescription: \"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.\"\n---\n\n# headless-step-frontend\n\nBuild the site's frontend against the backend from `site.json`, implementing the look the design step\nalready chose and wrote to **`design.md`** (plus the logo in `brand/`). The design is decided; this step\nis where it becomes real code. There's no per-framework recipe — use good judgement, follow `design.md`\nas the contract, and ask only for the clarifications you genuinely need. **Out:** source in the site\nworkspace `~/.hostinger/<domain>/src/`.\n\n> ## ⚠️ Build mode: \"Bring your own frontend\"? Read this first (any backend)\n> Read `brief.md` → **Build mode**. When it is **Bring your own frontend**, the user already has a\n> frontend (the project in the folder the command was run in) and this step **wires it to the backend\n> from `site.json` — it does not scaffold a new project and there is usually no `design.md` to\n> implement** (the design step deferred to the user). **Use their project as the source**: copy or\n> point `~/.hostinger/<domain>/src/` at it, keep its framework and UI, and collaborate on changes at\n> the user's direction (vibe-code with them). Your job is to connect it to the live backend (WordPress\n> `wp/v2` reads, the WooCommerce/Ecommerce Store API, env/keys) and make it deployable (set\n> `engines.node` to `22`, remove any `.htaccess`), then hand off to deploy. This generalises the Webapp\n> \"existing app\" case below to WordPress and Ecommerce too. When **Build mode is \"Agent designs and\n> builds\"**, ignore this box and proceed normally (scaffold + implement `design.md`).\n\n> ## ⚠️ Webapp backend? Read this first\n> A `webapp` brief has **no CMS/store and no `content-model.md`** — the app owns its own data (its own\n> API, or the MySQL database recorded in `site.json → database`). It comes in two shapes, decided back in\n> **refine**:\n>\n> - **An existing app the user brought to deploy** (refine routes \"deploy my app\" here). **Use their\n> project as the source** — copy/point `~/.hostinger/<domain>/src/` at it and keep its own framework and\n> UI. **Do not scaffold a new project, and do not re-implement `design.md` over it** (so **the design\n> step does not apply** to an app that brings its own UI). Your job is only to make it deployable: set\n> `engines.node` to `22`, remove any `.htaccess`, wire its data/env, then hand off to deploy. **Skip the\n> Step 2 design pass**, and ignore the `content-model.md` requirement in Step 3.\n> - **A new app built from the brief.** Scaffold and build it from `brief.md` (and `design.md` if the\n> design step produced one — design *does* run for a from-scratch app). Its data is owned by the app, so\n> there is still **no `content-model.md`** to bind against.\n>\n> Framework choice (Step 1), the build, and hand-off (Step 4) apply to both. The per-backend bullets in\n> Step 3 are **WordPress / WooCommerce / Ecommerce only** — for a webapp follow the Webapp bullet there.\n\n## Step 1 — Pick the framework (guess → default → confirm)\n\n**The rendering is already decided in `brief.md` → Rendering** (SSR or SSG, set during refine). Honour\nit, and the backend constraint:\n\n- **Ecommerce → SSR required** (carts/search need per-request rendering). Any SSR framework works —\n Next.js is the default, not a requirement (Express, Nuxt, SvelteKit, etc. are all fine).\n- **WordPress + WooCommerce → SSR required** (same reason — cart/checkout need per-request rendering).\n- **WordPress → SSG (Astro)** when the brief says SSG, otherwise **SSR (Next.js default)**.\n- **Webapp → the app's own stack.** An **existing app** keeps its current framework (don't impose one); a\n **new app** uses whatever fits the brief (SSR or SPA). Either way it deploys as a Node.js app (Step 8) —\n an SSR app runs a server, a SPA builds to static output.\n- Map rendering → framework: **`ssr` → Next.js**, **`ssg` → Astro** — unless the brief/prompt explicitly\n names another supported framework, in which case use that.\n\nSupported frameworks (what the Hostinger build pipeline can deploy — the `app_type` in Step 8 deploy):\n\n- **Server/SSR-capable:** Next.js, Nuxt.js, Astro, SvelteKit, Express, NestJS, Fastify.\n- **Static / SPA:** React, Vue, Angular, Vite, Parcel, Preact, Svelte, (Astro / SvelteKit static too).\n\nConfirm the framework with the user in one line only if the brief left it open. Record the choice in\n`state.json`:\n\n```json\n{ \"frontend\": { \"framework\": \"nextjs\", \"rendering\": \"ssr\" } }\n```\n\nIf the brief is silent, default to **Next.js SSR**. Ecommerce must be SSR (never SSG) — but the SSR\nframework itself is free to be any supported one, not just Next.js.\n\n## Step 2 — Implement `design.md` (don't re-design)\n\nThe look was already chosen and written by the design step. **Read `~/.hostinger/<domain>/design.md` and\nimplement it faithfully** — tokens (colour, type, spacing), components, per-page composition, imagery and\naccessibility — plus the logo files in `brand/`. Don't re-run design questions or invent a new direction here.\n\n- Treat `design.md` as the contract: the built pages should match its tokens, type scale, components and\n per-page composition, and the 8-point grid / WCAG 2.2 AA rules it carries.\n- Use the **logo** from `brand/` as-is (wordmark, icon mark, favicon); never recolour or redraw it.\n- If `design.md` is **missing** (e.g. a resumed older build), fall back to `brief.md → Design direction`\n (reference site, colours, brand, adjectives) and a tasteful, accessible default — but prefer `design.md`\n whenever it exists.\n- **Don't gate on approval here** — the result is shown at **validate** (Step 7).\n\n**Only ask about the code, and only if it matters** (design decisions belong in the design step):\n- Where the **source should live** — default `~/.hostinger/<domain>/src/`; ask only if they want a\n specific folder or an existing repo.\n- **Git** — initialise / connect / push (default: a local project, no remote).\n- A specific **component library** or technical must-have, if they implied one.\n\n## Step 3 — Build against the backend\n\nTwo phases around the instance barrier (see the top-level `SKILL.md`): scaffold + build the shell while\nprovision settles, then wire live data once `provision.status == \"completed\"`.\n\nRead the backend from `site.json`:\n\n- **WordPress** → `urls.rest_api` (`wp/v2`), public read. Content is in the posts/pages/categories/media\n the content-model push created; nav/plan come from `content-model.md`. Map each item's **SEO** from its\n `yoast_head_json` into the page `<head>` (title, description, Open Graph, JSON-LD schema) — Yoast is\n installed by default at provision (`site.json → seo.enabled: true`). If `seo.enabled` is `false`\n (Yoast absent), fall back to generating per-page title/description/OG tags from each item's content.\n - **Contact form** (`site.json → contact_form.enabled: true`, any WordPress backend) → render the\n contact page's form and submit it **client-side** to Contact Form 7's public feedback endpoint. Put a\n `submitContactForm(data)` helper in the one data module: build a `FormData` with the field names from\n `contact_form.fields` (e.g. `your-name`, `your-email`, `your-subject`, `your-message`) **plus CF7's\n hidden fields** — `_wpcf7` = `contact_form.form_id`, `_wpcf7_version`, `_wpcf7_locale` (e.g. `en_US`),\n `_wpcf7_unit_tag` (any stable string), `_wpcf7_container_post` = `0` — and `POST` it to\n `contact_form.feedback_url` **without setting `Content-Type`** (let the browser add the multipart\n boundary). `/cms` is same-origin, so no CORS and no secret. Branch on the JSON `status`: `mail_sent`\n → success state; `validation_failed` → map `invalid_fields[].field` to per-input errors;\n `spam`/`aborted`/`mail_failed` → a generic \"couldn't send, try again\" message. Include the honeypot\n decoy field named in `contact_form.honeypot_field` (e.g. `your_website`), rendered hidden/off-screen,\n **empty**, `autocomplete=\"off\"`, `tabindex=\"-1\"` — the deployed spam guard rejects it if filled. A user\n validation error is a **normal UI state, not a build error**. When `contact_form.enabled` is `false` or absent, render the contact page with\n plain contact details and **no form** — graceful, like disabled checkout; never half-build. Full\n contract: `../04-content-model/wordpress/contact-form.md` §3.\n- **WordPress + WooCommerce** (`site.json → woocommerce.enabled: true`) → the WordPress bullet above **plus**\n the store. Read the catalog over the **public WooCommerce Store API** (`site.json → urls.store_api`,\n `/cms/wp-json/wc/store/v1`, no auth, open CORS): `GET /products` (prices are **integer minor units** on\n `prices.price` with `prices.currency_minor_unit` — divide to display), `GET /products/categories`. Cart,\n coupons and checkout are **client-side** against the Store API with the `Nonce` + `Cart-Token` headers\n (`GET /cart`, `POST /cart/add-item`, `/cart/apply-coupon`, `POST /checkout`) — see\n `../04-content-model/wordpress/woocommerce.md`. **Never embed a consumer key client-side**; the\n storefront is fully public. Blog/pages on the same site still come from `urls.rest_api` (`wp/v2`) + Yoast.\n - **Honour `site.json → woocommerce.payment_enabled`.** It is `false` until the owner enables a payment\n gateway + shipping in wp-admin (that config isn't reachable headlessly — see the provision note). While\n `false`, build the full catalog, product pages and the client-side cart as normal, but render a\n graceful disabled-checkout state (e.g. a disabled \"Checkout\" with \"Checkout isn't set up yet\"), **don't\n call `/checkout`**, and don't treat it as a build error. When `true`, wire checkout (`payment_method`\n `\"cod\"` works out of the box).\n- **Ecommerce** → the **public Hostinger Storefront Core V2 API** (`https://api-ecommerce.hostinger.com/v2`,\n no auth, open CORS), keyed on the **custom sales channel** (`site.json → ecommerce.sales_channel.id`):\n `GET /channels/<scha_*>/products` (prices are **minor units** on variants), `POST /channels/<scha_*>/checkout`\n for buying. Cart is client-side; never embed a token. Read\n `ecommerce_miscellaneous_custom-storefront-setup-instructions` for the authoritative, current guide.\n - **Honour `site.json → ecommerce.payment_enabled`.** When it is `false` (free account / no paid\n Ecommerce plan — see the provision gate), there is **no checkout**: build the full catalog, product\n pages and the client-side cart as normal, but **do not call the checkout endpoint**. Instead render\n a clear, graceful state at the checkout step — e.g. a disabled \"Checkout\" button with a short note\n like \"Online checkout isn't available on this plan yet\" (and/or a contact/enquiry fallback). A\n missing checkout is **not** a build error here — never leave the storefront half-built, throw, or\n retry checkout because payment is off. When `payment_enabled` is `true`, wire checkout normally.\n- **Webapp** → **no `content-model.md` and no headless backend to read**. The app uses its own data source:\n the MySQL database from `site.json → database` (through the `DB_*` / `DATABASE_URL` env vars — set at\n provision/deploy, see `../03-provision/webapp/SKILL.md` Step 3; `127.0.0.1` only resolves on the server,\n not locally), or whatever external API the app calls. For an existing app, keep its data layer as-is.\n\nGuidelines (framework-agnostic):\n\n- **One data module** is the single place that talks to the backend (e.g. `src/lib/data.ts`) — one file\n to inspect, one base URL to switch. Keep it swappable so the shell can build against stubs first.\n- **Fetch at request time for SSR** (fresh content, no rebuild); a static build fetches at build time.\n- **A failed or incomplete fetch is an error, not a silent empty/placeholder.** Planned content missing\n from the API is a build error too — a site that builds with holes ships with holes. *(WordPress/Ecommerce;\n a webapp has no planned content to check.)*\n- **`content-model.md` is the source for what should exist** (pages, nav, collections mapping) — for\n WordPress/Ecommerce only; a **webapp has no `content-model.md`**.\n- **No `.htaccess`** in the project (deploy would overwrite the live one); don't commit `node_modules`\n or build output.\n- Set `engines.node` to `22` (the deploy builds on Node 22).\n- **Next.js 16+ only:** set the `build` script to `next build --webpack`, not plain `next build`. Next.js\n 16 defaults `next build` to **Turbopack**, whose native compiler needs a newer glibc than the Hostinger\n build server has, so a Turbopack build fails on deploy and won't fall back to webpack. Webpack builds\n clean there. The `--webpack` flag exists only on Next.js 16+; on **Next.js 15.x** leave the build script\n as plain `next build` (it already defaults to webpack). (Alternative: pin Next.js to `15.x`, keeping\n plain `next build`.)\n\n## Step 4 — Hand off\n\nConfirm the project builds clean locally against the live backend, no placeholder/fixture content\nremains, and `state.json → frontend` matches what you built. Update `state.json` (`stage: \"validate\"`).\nValidate (Step 7) owns the clean rebuild + user preview; deploy (Step 8) ships it with the `app_type`\nand output directory that match the chosen framework + rendering.\n\n> **Webapp:** just confirm the app builds clean and `state.json → frontend` is right — there's no\n> `content-model.md` or headless backend to check. A DB-backed app can't reach its database locally\n> (`127.0.0.1` is server-only), so its data layer is verified **after** deploy, not here.\n"
|
|
239
239
|
},
|
|
240
240
|
{
|
|
241
241
|
"uri": "skill://hostinger-headless/steps/07-validate/SKILL.md",
|
|
@@ -246,10 +246,10 @@ export default [
|
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
248
|
"uri": "skill://hostinger-headless/steps/08-deploy/SKILL.md",
|
|
249
|
-
"digest": "sha256:
|
|
250
|
-
"size":
|
|
249
|
+
"digest": "sha256:c6f2ef2d62ae4ba0ab7686148e1898585c8c93bcb22e91c2bcf25b80f7bd987d",
|
|
250
|
+
"size": 12010,
|
|
251
251
|
"mimeType": "text/markdown",
|
|
252
|
-
"text": "---\nname: headless-step-deploy\ndescription: \"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.\"\n---\n\n# headless-step-deploy\n\nShip the frontend to the live domain through the Hostinger connector. You hand the connector a zip of\nthe project **source** and it uploads it, runs `npm install` + the build script on the server, and\nserves the result. **Out:** `deploy.json`.\n\nThis is an **instance-barrier** step: run it only after `provision.status == \"completed\"` and validate\n(if run) has passed. All operations go through the connector (`execute`).\n\n**In:** the project in `~/.hostinger/<domain>/src/`, `site.json` (`username`, `urls`), `state.json`\n(`frontend.framework`, `frontend.rendering`).\n\n> ## ⚠️ The one rule that makes this work: pass build settings explicitly\n> If you let `hosting_deploy` **auto-detect** build settings, for a headless site that detection returns\n> **null** (`appType:null`, `outputDirectory:null`, Node 20) — the build \"completes\" but the **root\n> serves 403** (no `appType`/output → the vhost serves a directory with no entry). This is reproducible.\n> **Always pass `appType`, `outputDirectory`, `nodeVersion`, `buildScript`, `packageManager` and\n> `rootDirectory` on the `hosting_deploy` call** with the framework's values; the deploy then resolves\n> them and serves correctly, and `/cms` is preserved.\n\n## Build configurations\n\nMap `frontend.framework` (from `state.json`) to its `appType` + build output directory, and pass those\non the `hosting_deploy` call. `appType` is the framework's value, e.g. `next`, `nuxt`, `astro`,\n`svelte-kit`, `nest`, `express`, `fastify`, `react`, `create-react-app`, `gatsby`, `vite`, `vue`,\n`angular`, `parcel`, `svelte`, `other`.\n\nCommon cases (all `nodeVersion: 22`, `buildScript: \"build\"`, `packageManager: \"npm\"`, `rootDirectory: \".\"`):\n\n| Framework / mode | Local config | `appType` | `outputDirectory` |\n|---|---|---|---|\n| **Next.js — SSR** *(default)* | `output: 'standalone'` | `next` | `.next` |\n| **Nuxt — SSR** | default | `nuxt` | `.output` |\n| **SvelteKit — SSR** | node adapter | `svelte-kit` | `build` |\n| **Astro — static** | `output: 'static'` | `astro` | `dist` |\n| **Vite / React / Vue — static (SPA)** | — | `vite` (or `react`/`vue`) | `dist` |\n| **Express / NestJS / Fastify — server** | — | `express`/`nest`/`fastify` | build output dir |\n\nSSR/server frameworks run a persistent Node process (root sends `x-powered-by` / `cache-control: no-store`);\nstatic frameworks serve their output dir as files. If unsure of a framework's output dir, check the\nframework's own build output, but **always pass `appType`/`outputDirectory` on the deploy call with the\nframework's correct values** — don't rely on auto-detection (it returns null → 403).\n\n## Step 1 — Pre-flight\n\n1. **Instance live.** `provision.status == \"completed\"`, re-verified with\n `hosting_websites_list-setups { \"domain\": \"<domain>\" }`.\n2. **Build output exists locally** (the framework's output dir — e.g. `.next/`, `dist/`, `.output/`) and\n the project builds clean (frontend/validate already did this). If not, go back.\n3. **Env vars (only if used).** Build-time vars (e.g. `NEXT_PUBLIC_*`) must be set **before** the build\n with `hosting_nodejs_replace-environment-variables` (full replace). Never put secrets in the repo or\n the archive. **Webapp with a database:** that full replace wipes the `DB_*`/`DATABASE_URL` that\n `hosting_databases_setup-website` injected (the generated password reads back masked), so set all app\n vars in the same replace and, if you can't, first reset the password with\n `hosting_databases_change-password` and resend every `DB_*`/`DATABASE_URL` — see\n `../03-provision/webapp/SKILL.md` Step 3.\n\n## Step 2 — Archive the source\n\nZip the project **source** with `package.json` at the archive root. **Include** the app/source,\n`public/`, the framework config (`next.config.mjs` / `astro.config.mjs`), `tsconfig.json`,\n`package.json`, `package-lock.json`, `.nvmrc`. **Exclude** `node_modules/`, the build output\n(`.next/`, `dist/`, `out/`), `.git/`, and any `.htaccess` — the server installs and builds from source.\n\n```bash\ncd ~/.hostinger/<domain>/src\nzip -r -X ../app.zip . -x 'node_modules/*' '.next/*' 'dist/*' 'out/*' '.git/*' '**/.htaccess'\n```\n\n## Step 3 — Deploy (one shot: settings + upload + build)\n\nPass the framework's build settings **inline** on the deploy call — there is no separate settings step.\n`path` is the archive from Step 2 (or the source directory itself; `hosting_deploy` archives a directory\nfor you, skipping `node_modules/`, `.git/` and `.env`, but it does **not** read `.gitignore`, so the\nbuild output must already be absent).\n\n```json\n{ \"operation\": \"hosting_deploy\",\n \"params\": {\n \"domain\": \"<domain>\",\n \"path\": \"/absolute/path/to/.hostinger/<domain>/app.zip\",\n \"type\": \"nodejs\",\n \"appType\": \"<framework appType, e.g. next>\",\n \"outputDirectory\": \"<framework output dir, e.g. .next>\",\n \"nodeVersion\": 22,\n \"buildScript\": \"build\",\n \"packageManager\": \"npm\",\n \"rootDirectory\": \".\" } }\n```\n\n`hosting_deploy` uploads the source and triggers `npm install` + the build **end-to-end** — do not call\nany upload or build operation yourself. It returns a build `buildUuid` at once. This **overwrites the\nwebsite root** (expected) and keeps `/cms` intact. Passing `appType`/`outputDirectory` explicitly is what\nkeeps the root from serving 403 (see the rule above).\n\n## Step 4 — Poll to completion\n\nPoll `hosting_get-deployment { \"domain\": \"<domain>\", \"buildUuid\": \"<buildUuid>\" }` with backoff until\n`build.state` is `completed`. On `failed`, the same response carries the end of the build log and the\nfailure analysis — read it, fix locally, re-archive and redeploy (pass `fromLine`, the line count from\nthe previous call, to read only new log output). `hosting_get-deployment` also returns the build settings\nstored for the site: `appType`/`outputDirectory`/`nodeVersion` should echo what you passed; if they read\n`null`, the settings didn't take — fix the deploy call before continuing. For an SSR app that built but\nhangs, `hosting_nodejs_restart-application { username, domain }` recovers the server without a rebuild.\n\n## Step 5 — Verify the live site (all must pass)\n\n| Check | How | Expect |\n|---|---|---|\n| Root serves | `GET https://<domain>/` | **200** (not 403 / 404) |\n| SSR is live *(server frameworks)* | response headers | `x-powered-by` present, `cache-control: no-store` |\n| Static serves *(static frameworks)* | `GET https://<domain>/` | 200 HTML with real home content |\n| Key routes | a couple of planned routes + one deep route | 200, real content |\n| **Backend survived** *(WordPress / WooCommerce)* | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200 |\n\n> **Only WordPress (including WordPress + WooCommerce) has a `/cms` on the domain**, so the *Backend\n> survived* check is WordPress-only.\n> - **WordPress + WooCommerce:** also confirm the store survived the deploy —\n> `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` → 200. There is nothing external to\n> link (no sales-channel step — that's Ecommerce only). **Checkout** stays owner-completed: when\n> `site.json → woocommerce.payment_enabled` is `false`, don't test `/checkout`; a catalog that serves 200\n> is a passing, complete deploy.\n> - **Ecommerce:** the backend is the **Hostinger Ecommerce store**, read over the external Storefront API\n> (`api-ecommerce.hostinger.com`) — nothing a deploy touches lives on this domain. There is no `/cms`;\n> confirm the storefront still fetches products as a frontend check if you like. **Only check that\n> checkout can be reached when `site.json → ecommerce.payment_enabled` is `true`** — on a free account\n> (`payment_enabled: false`) there is no checkout, so **skip that check entirely**; a catalog that serves\n> 200 is a passing, complete deploy. Never fail or hold the deploy because checkout is unavailable.\n> - **Webapp:** no content backend at all — the whole root is the app. Skip the check; `/cms` won't resolve.\n\nA `completed` build whose root 403s almost always means the build settings weren't passed to\n`hosting_deploy` (null settings) — pass them and redeploy. Caching can briefly serve a stale root;\nre-check after ~30 s.\n\n## Step 6 — Record `deploy.json`\n\n```json\n{\n \"build_uuid\": \"…\",\n \"stack\": \"nextjs\",\n \"rendering\": \"ssr\",\n \"app_type\": \"next\",\n \"output_directory\": \".next\",\n \"node_version\": 22,\n \"method\": \"hosting_deploy (type: nodejs, settings passed inline)\",\n \"deployed_at\": \"…\",\n \"verified_at\": \"…\",\n \"urls\": { \"live\": \"https://<domain>\", \"cms\": \"https://<domain>/cms\" },\n \"checks\": { \"root\": 200, \"ssr_headers\": true, \"backend_preserved\": true }\n}\n```\n\nNo secrets in `deploy.json`. The `urls.cms` field and the `backend_preserved` check above are\n**WordPress-only** — adjust the record to the backend:\n\n- **WordPress:** keep `urls.cms` and `checks.backend_preserved` as shown.\n- **WordPress + WooCommerce:** keep `urls.cms` + `checks.backend_preserved` (it *is* WordPress), and add\n `urls.store_api` from `site.json`; no external link step.\n- **Ecommerce:** omit `urls.cms`/`backend_preserved`; record the store URLs instead (`urls.storefront_api`,\n `urls.store_admin` from `site.json`) and link the custom sales channel to the live domain —\n `ecommerce_sales-channels_update { store_id, sales_channel_id, url }` (see the ecommerce provision step).\n- **Webapp:** omit `urls.cms` and `backend_preserved` entirely — `urls` is just `{ \"live\": \"https://<domain>\" }`.\n\nUpdate `state.json` (`stage` → `done`), then continue to Step 9 (done).\n\n---\n\n## Note — one deploy path, and never `type: \"static\"`\n\n`hosting_deploy` is the only path: give its `path` either the source **directory** (it archives it for\nyou) or a local **archive** (`zip`, `tar`, `tar.gz`, `tgz`, `7z`) — there is no separate upload or build\nstep to fall back to. Either way pass the explicit `appType`/`outputDirectory`/`nodeVersion`/\n`buildScript`/`packageManager`/`rootDirectory` from the table. Never deploy a headless site with\n`type: \"static\"` — it serves the directory as plain files and can overwrite `/cms`.\n"
|
|
252
|
+
"text": "---\nname: headless-step-deploy\ndescription: \"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.\"\n---\n\n# headless-step-deploy\n\nShip the frontend to the live domain through the Hostinger connector. You hand the connector a zip of\nthe project **source** and it uploads it, runs `npm install` + the build script on the server, and\nserves the result. **Out:** `deploy.json`.\n\nThis is an **instance-barrier** step: run it only after `provision.status == \"completed\"` and validate\n(if run) has passed. All operations go through the connector (`execute`).\n\n**In:** the project in `~/.hostinger/<domain>/src/`, `site.json` (`username`, `urls`), `state.json`\n(`frontend.framework`, `frontend.rendering`).\n\n> ## ⚠️ The one rule that makes this work: pass build settings explicitly\n> If you let `hosting_deploy` **auto-detect** build settings, for a headless site that detection returns\n> **null** (`appType:null`, `outputDirectory:null`, Node 20) — the build \"completes\" but the **root\n> serves 403** (no `appType`/output → the vhost serves a directory with no entry). This is reproducible.\n> **Always pass `appType`, `outputDirectory`, `nodeVersion`, `buildScript`, `packageManager` and\n> `rootDirectory` on the `hosting_deploy` call** with the framework's values; the deploy then resolves\n> them and serves correctly, and `/cms` is preserved.\n\n## Build configurations\n\nMap `frontend.framework` (from `state.json`) to its `appType` + build output directory, and pass those\non the `hosting_deploy` call. `appType` is the framework's value, e.g. `next`, `nuxt`, `astro`,\n`svelte-kit`, `nest`, `express`, `fastify`, `react`, `create-react-app`, `gatsby`, `vite`, `vue`,\n`angular`, `parcel`, `svelte`, `other`.\n\nCommon cases (all `nodeVersion: 22`, `buildScript: \"build\"`, `packageManager: \"npm\"`, `rootDirectory: \".\"`):\n\n| Framework / mode | Local config | `appType` | `outputDirectory` |\n|---|---|---|---|\n| **Next.js — SSR** *(default)* | `output: 'standalone'` | `next` | `.next` |\n| **Nuxt — SSR** | default | `nuxt` | `.output` |\n| **SvelteKit — SSR** | node adapter | `svelte-kit` | `build` |\n| **Astro — static** | `output: 'static'` | `astro` | `dist` |\n| **Vite / React / Vue — static (SPA)** | — | `vite` (or `react`/`vue`) | `dist` |\n| **Express / NestJS / Fastify — server** | — | `express`/`nest`/`fastify` | build output dir |\n\nSSR/server frameworks run a persistent Node process (root sends `x-powered-by` / `cache-control: no-store`);\nstatic frameworks serve their output dir as files. If unsure of a framework's output dir, check the\nframework's own build output, but **always pass `appType`/`outputDirectory` on the deploy call with the\nframework's correct values** — don't rely on auto-detection (it returns null → 403).\n\n> ## ⚠️ Next.js 16+: build with webpack, not Turbopack (recurring deploy failure)\n> Next.js 16 makes **Turbopack** the default bundler for `next build`. Turbopack's native (Rust)\n> compiler needs a newer **glibc** than the Hostinger build server ships, so the server build aborts\n> with a \"glibc too old\" error — and Turbopack **does not fall back** to webpack on its own, so the\n> deploy fails every time. **Fix: build with webpack.** On **Next.js 16+** the project's `build` script\n> in `package.json` must be `next build --webpack` (not plain `next build`); `buildScript` on the deploy\n> call stays `\"build\"` — that's what runs it. The `--webpack` flag exists only on Next.js 16+; on\n> **Next.js 15.x** leave the build script as plain `next build` (it already defaults to webpack).\n> **Confirm this in `package.json` before deploying** (the frontend step scaffolds it this way — see\n> `../06-frontend`). Alternative, only if you must avoid the `--webpack` flag: pin Next.js to `15.x`\n> (keeping plain `next build`), whose `next build` still defaults to webpack. Next.js only — other\n> frameworks are unaffected.\n\n## Step 1 — Pre-flight\n\n1. **Instance live.** `provision.status == \"completed\"`, re-verified with\n `hosting_websites_list-setups { \"domain\": \"<domain>\" }`.\n2. **Build output exists locally** (the framework's output dir — e.g. `.next/`, `dist/`, `.output/`) and\n the project builds clean (frontend/validate already did this). If not, go back.\n - **Next.js 16+:** `package.json`'s `build` script is `next build --webpack`, not plain `next build` —\n the server's glibc can't run Turbopack's native compiler (see the box above). On Next.js 15.x leave it\n as plain `next build`. Fix it here if it isn't.\n3. **Env vars (only if used).** Build-time vars (e.g. `NEXT_PUBLIC_*`) must be set **before** the build\n with `hosting_nodejs_replace-environment-variables` (full replace). Never put secrets in the repo or\n the archive. **Webapp with a database:** that full replace wipes the `DB_*`/`DATABASE_URL` that\n `hosting_databases_setup-website` injected (the generated password reads back masked), so set all app\n vars in the same replace and, if you can't, first reset the password with\n `hosting_databases_change-password` and resend every `DB_*`/`DATABASE_URL` — see\n `../03-provision/webapp/SKILL.md` Step 3.\n\n## Step 2 — Archive the source\n\nZip the project **source** with `package.json` at the archive root. **Include** the app/source,\n`public/`, the framework config (`next.config.mjs` / `astro.config.mjs`), `tsconfig.json`,\n`package.json`, `package-lock.json`, `.nvmrc`. **Exclude** `node_modules/`, the build output\n(`.next/`, `dist/`, `out/`), `.git/`, and any `.htaccess` — the server installs and builds from source.\n\n```bash\ncd ~/.hostinger/<domain>/src\nzip -r -X ../app.zip . -x 'node_modules/*' '.next/*' 'dist/*' 'out/*' '.git/*' '**/.htaccess'\n```\n\n## Step 3 — Deploy (one shot: settings + upload + build)\n\nPass the framework's build settings **inline** on the deploy call — there is no separate settings step.\n`path` is the archive from Step 2 (or the source directory itself; `hosting_deploy` archives a directory\nfor you, skipping `node_modules/`, `.git/` and `.env`, but it does **not** read `.gitignore`, so the\nbuild output must already be absent).\n\n```json\n{ \"operation\": \"hosting_deploy\",\n \"params\": {\n \"domain\": \"<domain>\",\n \"path\": \"/absolute/path/to/.hostinger/<domain>/app.zip\",\n \"type\": \"nodejs\",\n \"appType\": \"<framework appType, e.g. next>\",\n \"outputDirectory\": \"<framework output dir, e.g. .next>\",\n \"nodeVersion\": 22,\n \"buildScript\": \"build\",\n \"packageManager\": \"npm\",\n \"rootDirectory\": \".\" } }\n```\n\n`hosting_deploy` uploads the source and triggers `npm install` + the build **end-to-end** — do not call\nany upload or build operation yourself. It returns a build `buildUuid` at once. This **overwrites the\nwebsite root** (expected) and keeps `/cms` intact. Passing `appType`/`outputDirectory` explicitly is what\nkeeps the root from serving 403 (see the rule above).\n\n## Step 4 — Poll to completion\n\nPoll `hosting_get-deployment { \"domain\": \"<domain>\", \"buildUuid\": \"<buildUuid>\" }` with backoff until\n`build.state` is `completed`. On `failed`, the same response carries the end of the build log and the\nfailure analysis — read it, fix locally, re-archive and redeploy (pass `fromLine`, the line count from\nthe previous call, to read only new log output). `hosting_get-deployment` also returns the build settings\nstored for the site: `appType`/`outputDirectory`/`nodeVersion` should echo what you passed; if they read\n`null`, the settings didn't take — fix the deploy call before continuing. For an SSR app that built but\nhangs, `hosting_nodejs_restart-application { username, domain }` recovers the server without a rebuild.\n\n## Step 5 — Verify the live site (all must pass)\n\n| Check | How | Expect |\n|---|---|---|\n| Root serves | `GET https://<domain>/` | **200** (not 403 / 404) |\n| SSR is live *(server frameworks)* | response headers | `x-powered-by` present, `cache-control: no-store` |\n| Static serves *(static frameworks)* | `GET https://<domain>/` | 200 HTML with real home content |\n| Key routes | a couple of planned routes + one deep route | 200, real content |\n| **Backend survived** *(WordPress / WooCommerce)* | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200 |\n\n> **Only WordPress (including WordPress + WooCommerce) has a `/cms` on the domain**, so the *Backend\n> survived* check is WordPress-only.\n> - **WordPress + WooCommerce:** also confirm the store survived the deploy —\n> `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` → 200. There is nothing external to\n> link (no sales-channel step — that's Ecommerce only). **Checkout** stays owner-completed: when\n> `site.json → woocommerce.payment_enabled` is `false`, don't test `/checkout`; a catalog that serves 200\n> is a passing, complete deploy.\n> - **Ecommerce:** the backend is the **Hostinger Ecommerce store**, read over the external Storefront API\n> (`api-ecommerce.hostinger.com`) — nothing a deploy touches lives on this domain. There is no `/cms`;\n> confirm the storefront still fetches products as a frontend check if you like. **Only check that\n> checkout can be reached when `site.json → ecommerce.payment_enabled` is `true`** — on a free account\n> (`payment_enabled: false`) there is no checkout, so **skip that check entirely**; a catalog that serves\n> 200 is a passing, complete deploy. Never fail or hold the deploy because checkout is unavailable.\n> - **Webapp:** no content backend at all — the whole root is the app. Skip the check; `/cms` won't resolve.\n\nA `completed` build whose root 403s almost always means the build settings weren't passed to\n`hosting_deploy` (null settings) — pass them and redeploy. Caching can briefly serve a stale root;\nre-check after ~30 s.\n\n## Step 6 — Record `deploy.json`\n\n```json\n{\n \"build_uuid\": \"…\",\n \"stack\": \"nextjs\",\n \"rendering\": \"ssr\",\n \"app_type\": \"next\",\n \"output_directory\": \".next\",\n \"node_version\": 22,\n \"method\": \"hosting_deploy (type: nodejs, settings passed inline)\",\n \"deployed_at\": \"…\",\n \"verified_at\": \"…\",\n \"urls\": { \"live\": \"https://<domain>\", \"cms\": \"https://<domain>/cms\" },\n \"checks\": { \"root\": 200, \"ssr_headers\": true, \"backend_preserved\": true }\n}\n```\n\nNo secrets in `deploy.json`. The `urls.cms` field and the `backend_preserved` check above are\n**WordPress-only** — adjust the record to the backend:\n\n- **WordPress:** keep `urls.cms` and `checks.backend_preserved` as shown.\n- **WordPress + WooCommerce:** keep `urls.cms` + `checks.backend_preserved` (it *is* WordPress), and add\n `urls.store_api` from `site.json`; no external link step.\n- **Ecommerce:** omit `urls.cms`/`backend_preserved`; record the store URLs instead (`urls.storefront_api`,\n `urls.store_admin` from `site.json`) and link the custom sales channel to the live domain —\n `ecommerce_sales-channels_update { store_id, sales_channel_id, url }` (see the ecommerce provision step).\n- **Webapp:** omit `urls.cms` and `backend_preserved` entirely — `urls` is just `{ \"live\": \"https://<domain>\" }`.\n\nUpdate `state.json` (`stage` → `done`), then continue to Step 9 (done).\n\n---\n\n## Note — one deploy path, and never `type: \"static\"`\n\n`hosting_deploy` is the only path: give its `path` either the source **directory** (it archives it for\nyou) or a local **archive** (`zip`, `tar`, `tar.gz`, `tgz`, `7z`) — there is no separate upload or build\nstep to fall back to. Either way pass the explicit `appType`/`outputDirectory`/`nodeVersion`/\n`buildScript`/`packageManager`/`rootDirectory` from the table. Never deploy a headless site with\n`type: \"static\"` — it serves the directory as plain files and can overwrite `/cms`.\n"
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
255
|
"uri": "skill://hostinger-headless/steps/09-done/SKILL.md",
|