@craftware/crafty-sdk 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/cart/index.d.ts +39 -0
- package/dist/cart/index.d.ts.map +1 -0
- package/dist/cart/index.js +131 -0
- package/dist/cart/index.js.map +1 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -1
- package/dist/client.js.map +1 -1
- package/dist/crafty-visual-editing.global.js +1 -1
- package/dist/crafty-visual-editing.global.js.map +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/resources/captcha.d.ts +20 -0
- package/dist/resources/captcha.d.ts.map +1 -0
- package/dist/resources/captcha.js +100 -0
- package/dist/resources/captcha.js.map +1 -0
- package/dist/resources/forms.d.ts +11 -2
- package/dist/resources/forms.d.ts.map +1 -1
- package/dist/resources/forms.js +20 -1
- package/dist/resources/forms.js.map +1 -1
- package/dist/resources/site.d.ts +12 -0
- package/dist/resources/site.d.ts.map +1 -0
- package/dist/resources/site.js +15 -0
- package/dist/resources/site.js.map +1 -0
- package/dist/resources/store.d.ts +35 -0
- package/dist/resources/store.d.ts.map +1 -0
- package/dist/resources/store.js +76 -0
- package/dist/resources/store.js.map +1 -0
- package/dist/structured-data.d.ts +41 -0
- package/dist/structured-data.d.ts.map +1 -0
- package/dist/structured-data.js +91 -0
- package/dist/structured-data.js.map +1 -0
- package/dist/types/public.d.ts +162 -0
- package/dist/types/public.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +10 -1
- package/skills/crafty-cms/SKILL.md +13 -2
- package/skills/crafty-cms/references/mcp-configuration.md +9 -1
- package/skills/crafty-cms/references/migrate-to-crafty.md +10 -2
- package/skills/crafty-cms/references/online-store.md +138 -0
- package/skills/crafty-cms/references/sdk-development.md +48 -0
|
@@ -8,6 +8,12 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
# Crafty CMS (MCP + JavaScript SDK)
|
|
10
10
|
|
|
11
|
+
> **MCP prompt: `build-site`.** It takes a site from a business description to a published website: content model, pages, frontend with the SDK, preview and deploy, confirming the plan with you before it writes. Arguments: `description` (the business and what the site needs) and optionally `site_id` (an existing site). Needs a token with `crafty:write`.
|
|
12
|
+
>
|
|
13
|
+
> In Claude Code, type `/` and pick `mcp__<server>__build-site`, where `<server>` is the name you gave the Crafty server; other clients list it in their prompt menu. If your client does not show MCP prompts, ask the agent to call `get_guide getting-started` and follow it.
|
|
14
|
+
>
|
|
15
|
+
> Redesigning a client's existing site? The `redesign-site` prompt reads their current site and social media and proposes the new one before building it (see `get_guide migrate`).
|
|
16
|
+
|
|
11
17
|
Crafty is a headless, multi-tenant CMS. There are two integration surfaces, and they are used at different stages:
|
|
12
18
|
|
|
13
19
|
| Surface | Purpose | Auth | Use it to |
|
|
@@ -34,13 +40,13 @@ npm create crafty@latest mi-sitio -- --url=https://<tenant>.crafty.do
|
|
|
34
40
|
# same flow: npx @craftware/crafty-cli create mi-sitio --url=https://<tenant>.crafty.do
|
|
35
41
|
```
|
|
36
42
|
|
|
37
|
-
It asks for the framework and template (today: Astro, `astro-services` = "sitio de servicios"; Next.js and Nuxt are coming), logs in through the browser (device flow; the user approves, an admin account is needed), creates the site with the starter's block types, collections, form and demo content, configures the visual editor routes, and writes `.env` (`CRAFTY_API_URL`, `CRAFTY_API_KEY`, `CRAFTY_SITE`), `crafty.json` and `src/crafty.types.ts`. Then:
|
|
43
|
+
It asks for the framework and template (today: Astro, `astro-services` = "sitio de servicios", `astro-store` = "tienda" or `astro-docs` = "documentación"; Next.js and Nuxt are coming), logs in through the browser (device flow; the user approves, an admin account is needed), creates the site with the starter's block types, collections, form and demo content, configures the visual editor routes, and writes `.env` (`CRAFTY_API_URL`, `CRAFTY_API_KEY`, `CRAFTY_SITE`), `crafty.json` and `src/crafty.types.ts`. Then:
|
|
38
44
|
|
|
39
45
|
```bash
|
|
40
46
|
cd mi-sitio && npm install && npm run dev # npm run deploy → Crafty Hosting
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
- Flags (after `--`): `--url`, `--framework=astro`, `--template=astro-services`, `--name`, `--site=<slug>`, `--draft` (keep demo content as drafts; by default it is published so the static build shows it), `--yes` (no prompts). Agents should pass `--url` and `--yes` so nothing blocks on a prompt; the browser approval still needs the user.
|
|
49
|
+
- Flags (after `--`): `--url`, `--framework=astro`, `--template=astro-services|astro-store|astro-docs`, `--name`, `--site=<slug>`, `--draft` (keep demo content as drafts; by default it is published so the static build shows it), `--yes` (no prompts). Agents should pass `--url` and `--yes` so nothing blocks on a prompt; the browser approval still needs the user.
|
|
44
50
|
- The command is interactive only for the login: tell the user to open the printed URL and approve the code.
|
|
45
51
|
- After it finishes, **read the project's `AGENTS.md`** and follow its conventions (one renderer per block type in `src/render/blocks/`, routes in `src/lib/routes.ts`, `npm run types` after changing the content model) instead of the generic steps in Phase 2.
|
|
46
52
|
- If the site slug already exists it fails before creating anything; if provisioning fails midway it names the half-created site: delete it in the panel or rerun with another `--site`.
|
|
@@ -163,6 +169,10 @@ Serving `llms.txt` / `llms-full.txt` for AI agents and crawlers: `crafty.llms.ha
|
|
|
163
169
|
|
|
164
170
|
Analytics and ad pixels (Google Tag Manager, GA4, Meta Pixel, TikTok Pixel): do **not** hard-code their snippets into the site. Set the IDs on the "Analítica" admin screen or with the `configure_analytics` MCP tool; Crafty hosting injects the official loaders into every HTML page on the next deploy. If the site already loads GA4 or pixels through GTM, set only the GTM container. Hosted elsewhere: render `(await crafty.analytics.get()).head` before `</head>` in SSR, or call `crafty.analytics.load()` once in the browser (it skips itself when the deploy already injected them). **Details: `references/sdk-development.md`.**
|
|
165
171
|
|
|
172
|
+
Structured data (schema.org JSON-LD for Google rich results and AI search): do **not** write JSON-LD by hand. Fill the business data (type, address, hours, phone, social profiles) with the `configure_structured_data` MCP tool, map collections to Product / Article / BlogPosting / FAQPage / Event / Service with `structured_data` on `create_collection` / `update_collection`, and render `crafty.structuredData(page, entries)` in `<head>`: the home page carries the business JSON-LD, mapped entries their own. For FAQ blocks, `crafty.structuredData(crafty.faqPage(block.data.items))`. Crafty Hosting adds the business JSON-LD to the home page on deploy when the site does not render it. **Details: `references/sdk-development.md`.**
|
|
173
|
+
|
|
174
|
+
Online store (when the site's "Tienda" tab is on): products are entries of `productos`; keep the cart with `createCart()` from `@craftware/crafty-sdk/cart`, show totals from `crafty.orders.quote(cart.items())` and send the visitor to Crafty's hosted checkout with `crafty.checkout.start(cart.items())`. Never compute prices or taxes in the frontend. **Details: `references/online-store.md`.**
|
|
175
|
+
|
|
166
176
|
## Phase 3 — Deploy a static site with the CLI
|
|
167
177
|
|
|
168
178
|
```bash
|
|
@@ -230,5 +240,6 @@ These are two different credentials — do not mix them:
|
|
|
230
240
|
|
|
231
241
|
- `references/mcp-configuration.md` — connecting every client, scopes, and the full MCP tool catalog with workflows.
|
|
232
242
|
- `references/sdk-development.md` — the complete SDK surface, block helpers, error handling, and examples.
|
|
243
|
+
- `references/online-store.md` — the online store: switching it on (`configure_store`), the products collection, cart, quotes, hosted checkout, orders and webhooks.
|
|
233
244
|
- `references/migrate-to-crafty.md` — migrating an existing site (WordPress or any other) into Crafty: crawl, classify, model, bulk import as drafts, media, redirects, human review, publish.
|
|
234
245
|
- `references/visual-editor.md` — preparing a site for the visual editor: the mandatory question, marking, preview tokens, bridge, headers, `configure_visual_editor`, verification.
|
|
@@ -112,6 +112,8 @@ All tools return a JSON text response. IDs are integers; slugs are strings. Pagi
|
|
|
112
112
|
| `list_forms` | — | `site_id`, `status`, `per_page` | paginated forms, ordered by name |
|
|
113
113
|
| `get_form` | `form_id` | — | one form with its field definitions |
|
|
114
114
|
| `list_redirects` | `site_id` | `environment` (default `production`) | the environment's redirects `{ source, destination, status_code }` |
|
|
115
|
+
| `get_assistant_settings` | `site_id` | `environment` (default `production`) | the site assistant's settings, `status` (`mode` assistant/whatsapp/off, `blocked_reason` server/plan/disabled/quota, this month's `usage`), `answers_count` and the allowed `options` |
|
|
116
|
+
| `list_assistant_answers` | `site_id` | `environment` (default `production`) | the assistant's private fixed answers `{ id, question, answer, author, updated_at }`, newest first |
|
|
115
117
|
| `list_webhooks` | `site_id` | `environment` (default `production`) | content webhooks (admins only) with their events and last deliveries; `upsert_webhook` / `delete_webhook` manage them (`crafty:write`) |
|
|
116
118
|
|
|
117
119
|
### Submissions (`crafty:submissions`)
|
|
@@ -147,14 +149,20 @@ Form submissions may contain personal data, so they require their own scope.
|
|
|
147
149
|
| `manage_media_collection` | `site_id`, `key` | `name`, `description`, `is_public` | creates or updates a media repository; `is_public: true` gives permanent public URLs (existing files are moved in the background); new repositories are private |
|
|
148
150
|
| `update_media` | `site_id`, `media_id`, `alt` | — | sets the alt text (max 500 chars) of one media file; `null` or `""` clears it |
|
|
149
151
|
| `delete_media` | `site_id`, `media_id`, `confirm: true` | — | **permanently** deletes one media file (ids from `list_media`) |
|
|
150
|
-
| `create_form` | `site_id`, `name`, `slug` | `description`, `fields` | `fields: [{ key, type, label, required? }]`; starts as `draft` |
|
|
152
|
+
| `create_form` | `site_id`, `name`, `slug` | `description`, `fields`, `captcha` | `fields: [{ key, type, label, required? }]`; starts as `draft`; protected by a captcha by default when Crafty has one configured (`captcha: false` to opt out), and the frontend must then render it with `crafty.forms.captcha()` |
|
|
151
153
|
| `update_form` | `form_id` | `name`, `slug`, `description`, `fields`, `notification_emails` | only supplied fields change; `fields`/`notification_emails` replace |
|
|
152
154
|
| `configure_visual_editor` | `site_id` | `environment` (default `production`), `frontend_url`, `routes` (`page_pattern`, `home_slug`, `collections`, `fixed`), `render_mode` (`ssr`/`hybrid`/`static`/`unknown`), `upload_media_collection` | visual editor settings of one environment; only sent keys change, `null` resets to the fallback; with only `site_id` it just reads; returns the effective settings plus `llms_warnings` (the `llms.txt` generator's warnings, since routes affect it too) |
|
|
153
155
|
| `configure_llms` | `site_id` | `environment` (default `production`), `title`, `summary`, `language`, `excluded_collections` (routed collections to drop), `included_collections` (routeless collections to opt in), `collection_languages` | admins only; same read/partial-update semantics as `configure_visual_editor`; only sent keys change, `null` resets to the fallback; with only `site_id` it just reads; returns the effective settings, the configuration warnings, and the first 40 lines of the generated `llms.txt` |
|
|
154
156
|
|
|
157
|
+
| `configure_assistant` | `site_id` and at least one setting | `environment` (default `production`), `enabled`, `whatsapp_enabled`, `whatsapp_number`, `whatsapp_label`, `contact_form`, `hours` (`weekdays`/`saturday`/`sunday` → `{ open, close }` or `null`), `timezone`, `greeting`, `suggestions` (max 3), `tone` (`friendly`/`formal`/`technical`), `guidelines`, `privacy_url`, `appearance` (`name`, `color`, `font`, `corners`, `position`, `avatar_on_launcher`) | only sent keys change, `null` resets to the default; the avatar is uploaded in the panel; returns the effective settings and `status` |
|
|
158
|
+
| `configure_store` | `site_id` | `environment` (default `production`), `enabled`, `currency`, `order_prefix`, `prices_include_tax`, `taxes`, `default_tax`, `delivery` (`enabled`, `fee`, `free_from`, `tax`, `zone`), `pickup` (`enabled`, `address`), `payment_methods` (`cash_on_delivery`, `pay_at_pickup`, `transfer`), `transfer_accounts`, `notify_emails`, `customer_emails`, `return_url`, `color`, `captcha` | admins only; `enabled: true` needs the store in the plan and creates (or adopts) the `productos` collection and the checkout URL; only sent keys change; with only `site_id` it just reads; returns the settings, `open`, the products summary and `checkout_url`. See `online-store.md` |
|
|
159
|
+
| `upsert_assistant_answer` | `site_id`, `question`, `answer` | `environment` (default `production`), `answer_id` | adds a fixed answer, or edits the one with `answer_id`; the assistant uses it from the next visitor message |
|
|
160
|
+
| `delete_assistant_answer` | `site_id`, `answer_id` | `environment` (default `production`) | deletes one fixed answer |
|
|
155
161
|
| `upsert_redirects` | `site_id`, `redirects` | `environment` (default `production`), `dry_run` | up to 500 `{ source, destination, status_code? }` per call, matched by `source`; `source` is a path starting with `/` (no spaces, `?` or `#`; Cloudflare Pages `*` splats and `:placeholders` allowed), `destination` a path or absolute http(s) URL, `status_code` 301 (default) / 302 / 307 / 308; invalid items are listed in `failed` and skipped. Crafty hosting writes them to `_redirects` on the next deploy (after the site's own rules; limit 2000 static + 100 dynamic); custom frontends read `GET /api/v1/redirects` |
|
|
156
162
|
| `delete_redirect` | `site_id`, `source` | `environment` (default `production`) | deletes one redirect |
|
|
157
163
|
|
|
164
|
+
Assistant tools follow the panel's **Asistente** section: admins always, editors with read access can read and editors with write access can change.
|
|
165
|
+
|
|
158
166
|
Redirect tools follow the **Pages** permission: reading needs access to pages, changing needs write access.
|
|
159
167
|
|
|
160
168
|
### Publish (`crafty:publish`)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Migrating an existing site to Crafty
|
|
2
2
|
|
|
3
|
+
> **MCP prompt: `migrate-site`.** It runs this whole runbook for you and stops at every checkpoint for you to confirm. Arguments: `site_id` (the target site), `source` (the old site's URL, its `sitemap.xml` or a description of the export) and optionally `environment` (default `production`) and `research` (default `yes`: it also researches the brand on the web and proposes content the old site lacks; `no` for a strict 1:1 move). Needs a token with `crafty:write`.
|
|
4
|
+
>
|
|
5
|
+
> In Claude Code, type `/` and pick `mcp__<server>__migrate-site`, where `<server>` is the name you gave the Crafty server; other clients list it in their prompt menu. If your client does not show MCP prompts, ask the agent to call `get_guide migrate` and follow it.
|
|
6
|
+
>
|
|
7
|
+
> **Redesign instead of a 1:1 move? MCP prompt: `redesign-site`.** It studies the client's current site, social media and what the rest of the web says about them, sums up their brand, voice and content, and proposes a new site (site map, sections built from your block types, draft copy, visual direction) before it creates anything. Once you approve the proposal it builds it as drafts and never publishes. Arguments: `website` (the current site's URL) and optionally `social` (profile URLs or handles), `goals` and `site_id` (leave it out to make the proposal before the client has a Crafty site). Needs a token with `crafty:write`. Social networks often block automated reading, so be ready to paste the bio and a few posts or share screenshots.
|
|
8
|
+
|
|
3
9
|
Runbook for moving a site that already exists (WordPress, Webflow, Wix, a static site, another headless CMS) into Crafty with an agent and the MCP server, without breaking its URLs. Work in this order; each phase has a checkpoint where a human confirms before the next one.
|
|
4
10
|
|
|
5
11
|
Tools used: `bulk_upsert_entries`, `import_media_from_urls`, `upsert_redirects` (plus the usual `create_block_type`, `create_collection`, `create_page`, `publish_*`). Token scopes: `crafty:read` + `crafty:write`; add `crafty:publish` only for the final phase.
|
|
@@ -20,8 +26,9 @@ Build an inventory of every URL of the old site:
|
|
|
20
26
|
1. Sources, best first: the CMS export or API (WordPress REST `/wp-json/wp/v2/posts?per_page=100&page=N`, `/pages`, `/media`; or the WXR export), `sitemap.xml` (and sitemap indexes), then a crawl of internal links from the home page.
|
|
21
27
|
2. For each URL record: old path, title, type guess, language, last modified, the main content (HTML), the images and files it uses, and the old ID if known.
|
|
22
28
|
3. Also collect the URLs that already redirect (HTTP 301/302) and anything in the old site's redirect plugin or `.htaccess`: those must be carried over too.
|
|
29
|
+
4. Research the brand beyond its site: search the web for its name (Google Business profile and reviews, social media, press, directories and marketplaces, award or partner pages). List what the old site lacks as **proposed additions**: reviews and testimonials, FAQs drawn from customer questions, opening hours, locations, services or products listed elsewhere, awards, press mentions, social links. Give each one its source URL and how sure you are, flag information that contradicts the site or looks outdated, and never invent facts. Keep the additions apart from the inventory: they are proposals, not migrated content. Skip this step for a strict 1:1 move (the `migrate-site` prompt takes `research: "no"`).
|
|
23
30
|
|
|
24
|
-
**Checkpoint:** show the user the counts per type
|
|
31
|
+
**Checkpoint:** show the user the counts per type, any URL you could not fetch and the proposed additions. Only the additions the user picks are added, in phase 5, as new drafts.
|
|
25
32
|
|
|
26
33
|
## Phase 2 — Classify
|
|
27
34
|
|
|
@@ -58,7 +65,8 @@ Follow "Model content with block types first" in `SKILL.md`:
|
|
|
58
65
|
1. Transform each item to the collection schema: clean the HTML (drop builder shortcodes, inline styles, tracking pixels), rewrite internal links to the **new** paths, and replace image URLs in the HTML with the `original_url` values from phase 4. Put media references (`cover`, `gallery`) as `media_id` values.
|
|
59
66
|
2. `bulk_upsert_entries` with `{ site_id, collection_slug, entries: [{ slug, external_id, data }], dry_run: true }`. Fix every `failed` item (the errors name the field, e.g. `data.published_on: …`) and check `warnings` (dropped keys = a field missing from the schema).
|
|
60
67
|
3. Run the same batch without `dry_run`. Entries are created as **drafts**; existing ones (same `external_id` or `slug`) are updated with a revision.
|
|
61
|
-
4.
|
|
68
|
+
4. Write the additions the user picked in phase 1 as new drafts (no `external_id` from the old site) and list them, with their sources, in the review report.
|
|
69
|
+
5. Singleton pages: `create_page` with `blocks` in the default locale; they start as drafts too. Other languages of the old site go in with `configure_locales` and `upsert_translation`, not as extra pages.
|
|
62
70
|
|
|
63
71
|
## Phase 6 — Redirects
|
|
64
72
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Online store
|
|
2
|
+
|
|
3
|
+
Crafty can sell for any site without a payment gateway: the merchant takes **cash on delivery**, **payment at pickup** or a **bank transfer**, and confirms the payment in the panel. Card payments through the merchant's own gateway (CardNET, AZUL) come in a later stage and plug into the same orders.
|
|
4
|
+
|
|
5
|
+
The frontend never computes prices and never touches payment data. It shows products, keeps a cart of slugs and quantities, asks Crafty for the totals, and sends the visitor to Crafty's **hosted checkout** (`checkout.crafty.do/{store}`), where they enter their details, pick delivery or pickup and a payment method, and get an order page and an email.
|
|
6
|
+
|
|
7
|
+
## How it fits together
|
|
8
|
+
|
|
9
|
+
| Piece | Where | What it does |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Plan | Crafty central (`store_enabled`) | The workspace's plan must include the online store. |
|
|
12
|
+
| Store settings | Panel → site → **Tienda** tab, or the `configure_store` MCP tool (admins) | Switch the store on per environment; currency, taxes, delivery, pickup, payment methods, order emails, return URL, checkout color, captcha. |
|
|
13
|
+
| Products | The `productos` collection (Panel → Tienda → **Productos**, or entries tools) | One published entry per product. |
|
|
14
|
+
| Cart and totals | Your frontend, with the SDK | `createCart()` keeps the cart; `crafty.orders.quote()` prices it. |
|
|
15
|
+
| Checkout | `checkout.crafty.do/{store}/c/{session}` (hosted by Crafty) | Customer details, delivery or pickup, payment method, captcha; places the order. |
|
|
16
|
+
| Order page | `checkout.crafty.do/{store}/o/{number}?t={token}` | Status, lines, totals and payment instructions; linked from the emails. |
|
|
17
|
+
| Orders | Panel → Tienda → **Pedidos** | Confirm, mark ready, delivered or cancelled, mark paid, internal note, WhatsApp the customer. |
|
|
18
|
+
| Integrations | Content webhooks `order.created`, `order.status_changed`, `order.paid` | Notify other systems (ERP, digital delivery, Slack). |
|
|
19
|
+
|
|
20
|
+
## 1. Switch the store on
|
|
21
|
+
|
|
22
|
+
In the panel: site → **Tienda** → choose the environment → **Activar tienda**. Through MCP (admins):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
configure_store {
|
|
26
|
+
"site_id": 3,
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"currency": "DOP",
|
|
29
|
+
"pickup": { "enabled": true, "address": "Av. Sarasota 55, Santo Domingo" },
|
|
30
|
+
"delivery": { "enabled": true, "fee": "250", "free_from": "4000", "tax": "itbis-18", "zone": "Gran Santo Domingo" },
|
|
31
|
+
"payment_methods": { "cash_on_delivery": true, "pay_at_pickup": true, "transfer": true },
|
|
32
|
+
"transfer_accounts": "Banco Popular · Cuenta corriente 812-345678-9 · Café Monteverde SRL",
|
|
33
|
+
"notify_emails": ["pedidos@monteverde.do"],
|
|
34
|
+
"return_url": "https://monteverde.do/tienda"
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Switching it on:
|
|
39
|
+
|
|
40
|
+
- creates the `productos` collection, or adopts an existing `productos` collection when its store fields have the types below (otherwise it reports the conflicting fields);
|
|
41
|
+
- gives the environment a store handle and its checkout URL (`checkout.crafty.do/<site-slug>` for production, `<site-slug>-<environment>` for others);
|
|
42
|
+
- sets an order prefix from the site's initials (`CM-` → `CM-1001`) and the Dominican tax template (ITBIS 18 %, ITBIS 16 %, Exento).
|
|
43
|
+
|
|
44
|
+
Switching it off stops new orders; products and orders stay. Send `configure_store` with only `site_id` to read the current settings, the checkout URL and how many products have no price.
|
|
45
|
+
|
|
46
|
+
## 2. Products
|
|
47
|
+
|
|
48
|
+
Products are entries of `productos`. While the store is on, these fields can be relabeled but not removed or retyped (pricing reads them), and the collection can't be deleted. Add any other fields you need: they never change the price.
|
|
49
|
+
|
|
50
|
+
| Field | Type | Notes |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `nombre` | string, required | Product name (also the entry title and slug source). |
|
|
53
|
+
| `precio` | decimal, required | Price in the store currency, e.g. `1450` or `1450.50`. Taxes included unless the store says otherwise. |
|
|
54
|
+
| `disponible` | select, required | `disponible`, `pocas` (few left) or `agotado` (sold out: can't be bought). |
|
|
55
|
+
| `variantes` | repeater | Each variant: `nombre` (required), `precio` (only when it changes the price), `sku`. A product with variants must be bought with one. |
|
|
56
|
+
| `imagenes` | multiReference → Media | The first image appears on the checkout and the order. |
|
|
57
|
+
| `sku` | string | |
|
|
58
|
+
| `impuesto` | string | A tax key of the store (`itbis-18`, `itbis-16`, `exento`). Empty uses the store's default tax. |
|
|
59
|
+
|
|
60
|
+
Only **published** products can be bought. Translations of `nombre` (via `upsert_translation` / `translate_content`) show on the checkout in the visitor's language.
|
|
61
|
+
|
|
62
|
+
## 3. The cart and totals in the frontend
|
|
63
|
+
|
|
64
|
+
Store calls need `siteSlug` in the client config. The API key, when sent, picks its environment; without it the store uses production.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createCraftyClient } from "@craftware/crafty-sdk";
|
|
68
|
+
import { createCart } from "@craftware/crafty-sdk/cart";
|
|
69
|
+
|
|
70
|
+
const crafty = createCraftyClient({ baseUrl, apiKey, siteSlug: "monteverde" });
|
|
71
|
+
const cart = createCart({ key: "monteverde:cart" });
|
|
72
|
+
|
|
73
|
+
const store = await crafty.store.get();
|
|
74
|
+
// { enabled, currency, prices_include_tax, taxes, delivery, pickup, payment_methods, checkout_url, … }
|
|
75
|
+
|
|
76
|
+
const { data: products } = await crafty.entries.list("productos", { per_page: 100 });
|
|
77
|
+
|
|
78
|
+
cart.add("geisha-lavado", { variant: "Molido para filtro", quantity: 2 });
|
|
79
|
+
cart.subscribe((items) => renderCartBadge(items));
|
|
80
|
+
|
|
81
|
+
const quote = await crafty.orders.quote(cart.items(), { fulfillment: "delivery" });
|
|
82
|
+
// quote.items, quote.subtotal, quote.delivery_fee, quote.taxes, quote.total (decimal strings)
|
|
83
|
+
// quote.unavailable: lines Crafty left out, with reason not_found | sold_out | variant_not_found | variant_required | no_price
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Show `quote.total`, not a total you add up: Crafty applies variant prices, the free-delivery threshold and rounds each tax once per order, exactly as the order will be charged. `crafty.store.get()` is cached for a minute; `quote()` is always fresh.
|
|
87
|
+
|
|
88
|
+
## 4. Checkout
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
await crafty.checkout.start(cart.items(), {
|
|
92
|
+
returnUrl: "https://monteverde.do/tienda", // optional; defaults to the store's return URL
|
|
93
|
+
fields: { regalo: "Envolver para regalo" }, // optional notes shown to the merchant
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`start()` creates a checkout session (valid 24 hours) and sends the browser to the hosted checkout. `checkout.create()` returns `{ id, url, expires_at }` without redirecting, for SSR or links. The checkout re-prices the cart; if a price changed since the page loaded it shows the new total and asks the customer to confirm.
|
|
98
|
+
|
|
99
|
+
After the order, **Volver a la tienda** leads to the return URL with `?pedido=<number>`. On that page call `cart.clearOnReturn()`: it empties the cart and returns the order number so you can thank the customer.
|
|
100
|
+
|
|
101
|
+
An order can be read back with its token, which is in the order page link (`?t=`):
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const order = await crafty.orders.get("CM-1001", token);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 5. Orders and notifications
|
|
108
|
+
|
|
109
|
+
- Each order gets a number per environment (`<prefix>1001`, `1002`, …), status **Nuevo**, payment **Pendiente**.
|
|
110
|
+
- Status moves forward only: new → confirmed → ready → delivered; any open order can be cancelled.
|
|
111
|
+
- **Marcar pagado** records the payment (with an optional reference, e.g. the transfer number).
|
|
112
|
+
- The customer gets an email on each step (from the store's name, replies go to the merchant); the `notify_emails` get every new order. Turn customer emails off with `customer_emails: false`.
|
|
113
|
+
- Editors see **Pedidos** only with the `orders` section permission (hidden, read or edit), set per user in Settings → Users.
|
|
114
|
+
|
|
115
|
+
## REST endpoints
|
|
116
|
+
|
|
117
|
+
No API key is required; the site is in the URL. Errors use the usual `{ error, message }` shape.
|
|
118
|
+
|
|
119
|
+
| Method | Path | Notes |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| GET | `/api/v1/sites/{site}/store` | Public settings. |
|
|
122
|
+
| POST | `/api/v1/sites/{site}/orders/quote` | Body `{ items: [{ slug, variant?, quantity }], fulfillment?: "delivery" \| "pickup", locale? }`. |
|
|
123
|
+
| POST | `/api/v1/sites/{site}/checkout` | Body `{ items, return_url?, fields?, locale? }` → 201 `{ data: { id, url, expires_at } }`. |
|
|
124
|
+
| GET | `/api/v1/sites/{site}/orders/{number}` | Token in `?token=` or `X-Order-Token`. |
|
|
125
|
+
|
|
126
|
+
A closed store answers `404 STORE_DISABLED`. The quote and checkout endpoints are rate limited per visitor.
|
|
127
|
+
|
|
128
|
+
## Starter
|
|
129
|
+
|
|
130
|
+
`npm create crafty -- --template=astro-store` creates a store site in Astro: catalog, product pages with variants, a cart page with live totals and the hosted checkout. It creates the `productos` collection and demo products, and switches the store on when the workspace plan includes it.
|
|
131
|
+
|
|
132
|
+
## Common pitfalls
|
|
133
|
+
|
|
134
|
+
- **Prices or taxes computed in the frontend.** Always show `quote()` totals; the checkout charges Crafty's numbers.
|
|
135
|
+
- **Products as drafts.** Only published products can be bought; a static build lists only published entries too.
|
|
136
|
+
- **A product with variants added without one.** It comes back in `unavailable` with `variant_required`.
|
|
137
|
+
- **Checkout 404.** The store is off, the plan doesn't include it, or the session expired (24 h): start a new checkout.
|
|
138
|
+
- **Moving the site to another workspace.** Orders and the checkout handle move with it.
|
|
@@ -187,6 +187,7 @@ Types depend on the environment (generate with the key of the environment the co
|
|
|
187
187
|
crafty.forms.list(options?) // => { data: CraftyForm[] } (requires apiKey)
|
|
188
188
|
crafty.forms.get(slug, options?) // => { data: CraftyForm } (requires apiKey)
|
|
189
189
|
crafty.forms.submit(formSlug, data, options?) // => { id, form_id, submitted_at } (public; apiKey optional, picks the environment)
|
|
190
|
+
crafty.forms.captcha(form, container) // => CraftyFormCaptcha | null (browser; renders the form's captcha widget)
|
|
190
191
|
```
|
|
191
192
|
|
|
192
193
|
`forms.submit()` uses a **public** endpoint: the `X-API-Key`, when configured, only selects the environment (a staging key submits to staging, no key submits to production), but it **requires `siteSlug` in the client config** — calling it without one throws a synchronous `CraftyError` with code `BAD_REQUEST`.
|
|
@@ -205,6 +206,19 @@ const result = await crafty.forms.submit("contact", {
|
|
|
205
206
|
// => { id, form_id, submitted_at }
|
|
206
207
|
```
|
|
207
208
|
|
|
209
|
+
Forms protected by a captcha return `captcha: { provider, site_key, token_field }` (absent otherwise), and their submissions are rejected with 422 without the widget token. In the browser, render the widget next to the form and pass it to `submit()`:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const { data: form } = await crafty.forms.get("contact");
|
|
213
|
+
const captcha = await crafty.forms.captcha(form, document.querySelector("#captcha")!); // null when not needed
|
|
214
|
+
|
|
215
|
+
await crafty.forms.submit("contact", values, { captcha }); // adds the token, then resets the widget
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The provider is set by Crafty, so read it from `captcha.provider` instead of assuming one: Turnstile runs invisibly and only shows a challenge to suspicious visitors, hCaptcha shows a checkbox the visitor must tick, so leave room for it in the layout. Without the SDK, load the provider script yourself and post the token in `captcha.token_field`.
|
|
219
|
+
|
|
220
|
+
New forms get the captcha by default once Crafty has captcha keys configured; it is the **Proteger con captcha** box in the form's settings in the panel, or `captcha: false` on `create_form`. Turning it on for a form on a site that does not render the widget makes every submission fail with 422, so update the frontend first.
|
|
221
|
+
|
|
208
222
|
> Note the asymmetry: `collections.get()` returns the bare object, while `forms.get()` and `entries.get()` are wrapped in `{ data }`.
|
|
209
223
|
|
|
210
224
|
### `mediaCollections`
|
|
@@ -281,6 +295,40 @@ crafty.analytics.load(options?) // browser only => "loaded" | "already-loaded"
|
|
|
281
295
|
|
|
282
296
|
Never hard-code GTM / GA4 / pixel snippets in the site, and never add GA4 both here and inside GTM (page views are counted twice).
|
|
283
297
|
|
|
298
|
+
## Structured data
|
|
299
|
+
|
|
300
|
+
The public API serves schema.org JSON-LD in `structured_data`: on the home page (`pages.get(homeSlug)`, the business set with `configure_structured_data`), on `crafty.site.get()`, and on each published entry of a collection with a structured data mapping (`structured_data` on `create_collection` / `update_collection`). It is `null` when nothing is configured or a required property is empty.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
crafty.site.get(options?) // => { name, slug, environment, default_locale, locales, structured_data }
|
|
304
|
+
crafty.structuredData(...sources) // => '<script type="application/ld+json">…</script>' tags, "" when none
|
|
305
|
+
crafty.jsonLd(...sources) // => the JSON-LD objects, for useHead / <Head>
|
|
306
|
+
crafty.faqPage([{ question, answer }]) // => FAQPage JSON-LD for a FAQ block, or null
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Sources can be pages, entries, their API responses, list responses, the site or JSON-LD objects; FAQPage objects are merged into one per page. The script content escapes `<`, so it is safe to inject as raw HTML (`set:html`, `v-html`, `dangerouslySetInnerHTML`). Validate the result at https://search.google.com/test/rich-results. On Crafty Hosting the deploy also adds the business JSON-LD to the home page (`index.html`) when the page does not render it already, so a site gets LocalBusiness even without the helper.
|
|
310
|
+
|
|
311
|
+
## Online store
|
|
312
|
+
|
|
313
|
+
When the store is on (the site's "Tienda" tab), products are entries of the `productos` collection: list them with `entries.list("productos")` (fields `nombre`, `precio`, `disponible`, `variantes`, `imagenes`, `sku`, `impuesto`). Never compute totals in the frontend: the cart holds slugs, variants and quantities only, Crafty prices it, and the visitor finishes on Crafty's hosted checkout (delivery or pickup, cash, pay at pickup or bank transfer). Store calls need `siteSlug` in the client config; the API key, when sent, picks its environment.
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import { createCart } from "@craftware/crafty-sdk/cart";
|
|
317
|
+
|
|
318
|
+
crafty.store.get() // => { enabled, currency, taxes, delivery, pickup, payment_methods, checkout_url, … }
|
|
319
|
+
crafty.orders.quote(cart.items(), { fulfillment }) // => { items, unavailable, subtotal, delivery_fee, taxes, total } as decimal strings
|
|
320
|
+
crafty.checkout.start(cart.items(), { returnUrl }) // creates the session and sends the browser to the hosted checkout
|
|
321
|
+
crafty.checkout.create(cart.items(), options) // => { id, url, expires_at } without redirecting
|
|
322
|
+
crafty.orders.get(number, token) // => the order, with the token from its page link (?t=)
|
|
323
|
+
|
|
324
|
+
const cart = createCart(); // localStorage, key "crafty:cart" (pass { key } per site)
|
|
325
|
+
cart.add("geisha", { variant: "Molido", quantity: 2 });
|
|
326
|
+
cart.update("geisha", 3, "Molido"); cart.remove("taza"); cart.count(); cart.subscribe(render);
|
|
327
|
+
cart.clearOnReturn(); // on the return page: empties the cart when the URL has ?pedido=, returns the number
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`quote().unavailable` lists lines Crafty left out (`not_found`, `sold_out`, `variant_not_found`, `variant_required`, `no_price`). A closed store answers 404 `STORE_DISABLED`. After the order, "Volver a la tienda" leads to `returnUrl` (or the store's setting) with `?pedido=<number>`.
|
|
331
|
+
|
|
284
332
|
## Error handling
|
|
285
333
|
|
|
286
334
|
The SDK throws `CraftyError` for HTTP, timeout, abort, network, and config failures.
|