@craftware/crafty-sdk
官方JavaScript SDK用于Crafty公共API(当前本地实现)。
AI代理技能
这个包裹运送一个代理 技能 它教Claude, Cursor和其他代理如何通过其MCP服务器配置Crafty站点,并使用此SDK构建它。它包含在已发布的包中 skills/crafty-cms/.
最快捷的安装方式,加上CLI的 crafty-deploy 技能,是 npx @craftware/crafty-cli skills install (到 .claude/skills/; --global 用于 ~/.claude/skills/, --dir=<path> 其他代理商)。这些技能也可以作为压缩文件下载给克劳德。我和克劳德 https://docs.crafty.do/skills/crafty-cms.zip.
或者在安装SDK后将其复制到项目的技能目录中:
cp -r node_modules/@craftware/crafty-sdk/skills/crafty-cms .claude/skills/
(使用等效技能目录为您的代理,例如: .cursor/skills/)。技能是自成一体的——它包括 SKILL.md 加上 references/mcp-configuration.md 和 references/sdk-development.md.
要求
- 节点
>=20(包引擎) - 或者任何运行时
fetch可用(浏览器、Bun、Deno等)
创建客户端
import { createCraftyClient } from "@craftware/crafty-sdk";
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: "ENVIRONMENT_API_TOKEN",
timeoutMs: 10_000
});
客户端配置
baseUrl(必选):租户基础URL。附加SDK/api/v1如果丢失自动。apiKey(可选):发送的环境API令牌X-API-Key.- 当提供时,API从这个令牌解析活动站点。
pages.*请求自动使用此站点上下文(您未通过)siteSlug)。
siteSlug(可选):用于公共表单提交的网站代码(forms.submit())。fetch(可选):自定义获取实现。headers(可选):应用于所有请求的默认标头。timeoutMs(可选):所有请求的默认超时时间(ms)。仅当值大于时才应用超时0.preview(可选):视觉编辑器预览令牌作为X-Crafty-Preview. 在浏览器中忽略它(SDK读取)?crafty_preview=把它藏在里面sessionStorage); 在服务器通道上previewFromRequest(req);通过false永远不发送。onPreviewInvalid(可选):当API应答时调用X-Crafty-Preview-Status: invalid(过期或外来令牌;响应是公共内容)。locale(可选):为的默认区域设置entries.*和pages.*(如。"en","pt-BR"). 看到 本地化内容.
可用的资源
collections
crafty.collections.list(options?)crafty.collections.get(slug, options?)
entries
crafty.entries.list(collectionSlug, query?, options?)crafty.entries.get(collectionSlug, id, options?)
只返回已发布的条目(每个条目都带有 status 和 published_at; sort_by: "published_at" (按公布日期计算)。草稿只包含在预览模式中。
支持常见的 query 参数:
sort_by(任何可排序字段,包括集合字段,如title)sort_order(asc|desc)q(在集合的string,text和longText字段;2至100个字符)per_pagepage- 作为键/值对的附加过滤器(传递给API)
pages
crafty.pages.list(query?, options?)crafty.pages.get(pageSlug, options?)
pages.* 端点的作用域由解析的环境决定 apiKey.
pages.get() 返回一个包含块帮助器的页面响应:
page.data=>的rawCraftyPagepage.blocks=> all blocks (CraftyBlock[];一个非数组data.blocks被强迫[])page.block(type)=>该类型的第一个块或nullpage.blocksOf(type)=>该类型的所有块([]如果没有一个)page.hasBlock(type)= >布尔page.requireBlock(type)=>第一个块,或抛出一个plainError(不是CraftyError)page.byType.<type>=>该类型的块数组page.firstByType.<type>=>该类型或的第一个块null
本地化内容
当站点在Crafty中配置了区域设置时,可以在其中任何一个区域中请求条目和页面。通过 locale 给客户,或者每个电话 options:
const crafty = createCraftyClient({ baseUrl, apiKey, locale: "en" });
const posts = await crafty.entries.list("posts"); // English
const home = await crafty.pages.get("home", { locale: "pt-BR" }); // overrides the client locale
每个字段都有回退:任何未翻译的内容都会返回到站点的默认区域设置中,因此部分翻译不会留下空字段。不支持的区域设置会退回到最近的区域设置(en-US → en)或默认值。反应进行 meta.locale (服务地点), meta.default_locale 和 meta.locales; get() 反应增加 meta.translations项中包含有用内容的区域设置 hreflang). 没有区域设置的站点会忽略 locale 不返回locale meta.
forms
crafty.forms.list(options?)-列出所有表单(需要API密钥)crafty.forms.get(slug, options?)-获得一个单一的形式(需要API密钥)crafty.forms.submit(formSlug, data, options?)-提交一个公共表单到API密钥的环境
这个 submit() 方法使用不需要身份验证的公共端点。当客户端有 apiKey,它被发送,以便提交到达该键的环境(staging键写入staging);没有一个,它就会投入生产。你必须提供 siteSlug 在提交到工作调用的客户端配置中 submit() 否则会抛出一个同步 CraftyError 使用代码 BAD_REQUEST:
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: "ENVIRONMENT_API_TOKEN",
siteSlug: "my-site",
});
// List forms (authenticated)
const forms = await crafty.forms.list();
// Get form details (authenticated)
const contactForm = await crafty.forms.get("contact");
// Submit a form into the API key's environment
const result = await crafty.forms.submit("contact", {
name: "Jane Doe",
email: "[email protected]",
message: "Hello!",
});
// result => { id, form_id, submitted_at }
mediaCollections
crafty.mediaCollections.list(query?, options?)
支持 query.include 价值观:
"stats""directories"- 支持数组形式,重复数据删除,并序列化为逗号分隔的值;原始字符串将不变地传递
mediaFiles
crafty.mediaFiles.list(query?, options?)
支持常见的 query 参数:
collectiondirectory_idttl_minutesper_pagepage
每个文件都有 url, is_public, expires_at 和 image_transform_base (见 图片)。
bookings
网上预订(需要) siteSlug;网站的“Citas”标签必须打开):
crafty.bookings.config(options?)-服务(包括时间和价格)、专业人员、工时、规则、时区、货币和付款方式;enabled: false当预订取消时。crafty.bookings.availability({ service, professional?, from?, to? }, options?)-本地每日免费时段:{ days: [{ date, closed, slots: [{ start, time, professionals }] }] }. 在浏览器中调用它,不要在构建时调用。crafty.checkout.start({ booking: { service, professional?, start } }, { returnUrl?, cancelUrl?, fields? })-保持插槽并将访问者发送到托管结帐(checkout.create返回会话而不重定向)。与此同时,一个插槽被占用SLOT_UNAVAILABLE.crafty.bookings.get(number, token, options?)-客户看到的预订(来自他们链接的令牌)。
导游: skills/crafty-cms/references/online-bookings.md.
图片
crafty.image(media, options?) 返回调整大小、重新编码的图像的URL; crafty.srcset(media, widths, options?) 返回一个 srcset 值,每个宽度一个URL。两者也都导出为 imageUrl 和 imageSrcSet. media 是一个 CraftyMediaFile,一个 <key>_media 对象从块或条目,或URL字符串。
const photo = block.data.image_media;
<img
src={crafty.image(photo, { width: 800, format: "auto" })}
srcSet={crafty.srcset(photo, [400, 800, 1200, 1600], { format: "auto" })}
sizes="(min-width: 768px) 50vw, 100vw"
width={800}
alt=""
/>
| 选项 | 值 |
|---|---|
width, height | 整数像素,固定到1-4096 |
fit | "scale-down", "contain", "cover", "crop", "pad" |
quality | 1 - 100 |
format | "auto" (浏览器接受时为AVIF/WebP), "webp", "avif", "jpeg", "png" |
未知的选项和值将被删除。url有这样的形式 <image_transform_base>/cdn-cgi/image/width=800,format=auto,onerror=redirect/<source> 并由Cloudflare Image transforms提供服务;Crafty本身从不调整图像的大小。
API返回 image_transform_base 只对 公共 栅格图像,并且仅当Crafty服务器启用图像转换时。否则(带有签名url的私有媒体、svg、pdf、关闭转换) image() 返回原始的 url 不变, srcset() 返回 "",因此浏览器退回到 src. 打电话总是安全的。
llms.txt
狡猾的生成 llms.txt 和 llms-full.txt 从你发布的内容。标题、摘要、语言,以及哪些没有路由的集合被选择加入,这些都来自于admin的“llms.txt”设置屏幕 configure_llms MCP工具;url从可视化编辑器路由中解析。默认情况下,没有路由模式的集合将被排除在外——选择它in,让它的条目出现在索引和in中的“## Optional”下 llms-full.txt.
由Crafty托管的站点将这两个文件注入到站点的根目录 下一个部署;在开始使用此特性之前已经进行的部署不会进行追溯性更新。该网站自己的构建版本已经提供了一个文件 llms.txt / llms-full.txt 已经在部署输出中)总是获胜—Crafty只在文件丢失时添加文件。
从你自己的领域为他们服务:
// Next.js — app/llms.txt/route.ts
export const GET = crafty.llms.handler();
// Next.js — app/llms-full.txt/route.ts
export const GET = crafty.llms.handler({ full: true });
// Astro — src/pages/llms.txt.ts
export const GET = crafty.llms.handler();
// Nuxt — server/routes/llms.txt.ts
export default defineEventHandler(() => crafty.llms.handler()());
对于静态构建,将文件写入 dist/ 自己在构建时,而不是依赖于部署时注入:
// e.g. a postbuild script, run after `vite build` / `astro build`
import { writeFile } from "node:fs/promises";
await writeFile("dist/llms.txt", await crafty.llms.index());
await writeFile("dist/llms-full.txt", await crafty.llms.full());
在网上做广告 <head> 与 crafty.llms.linkTags() (HTML字符串)或 crafty.llms.linkTagAttrs() (对象 useHead / <Head>)。
分析
谷歌标签管理器,GA4,元像素和TikTok像素id在“Analítica”管理屏幕上或与 configure_analytics MCP的工具。不要在站点中硬编码他们的代码片段。
- 狡猾的托管: 部署将加载器注入到每个HTML页面中。它还替换在构建时呈现的SDK的任何块,因此部署设置总是占上风。不需要其他任何东西。
- 任何主机上的SSR: 在服务器上渲染现成的HTML,以便爬虫看到它并保留第一个页面视图:
const analytics = await crafty.analytics.get();
// analytics.head → before </head>
// analytics.body → right after <body> (<noscript> fallbacks)
- 任何主机上的客户端渲染站点: 呼叫
load()在浏览器中。它返回"already-loaded"当页面已经带有Crafty analytics (window.__CRAFTY_ANALYTICS__是由部署注入的块或之前的加载设置的),所以在Crafty Hosting上,部署获胜:
await crafty.analytics.load(); // "loaded" | "already-loaded" | "not-configured" | "unavailable"
客户端加载错过了不运行JavaScript的爬虫。在管理中所做的更改将显示在下一个请求中 get()/load();在Crafty Hosting上,它们需要重新部署。
结构化数据(JSON-LD)
Crafty将内容转换为schema.org JSON-LD,因此谷歌可以显示丰富的结果(小时数,地址,价格,常见问题)和人工智能搜索引擎获得可靠的事实。不要手写JSON-LD:
- 网站背后的业务(类型,地址,工作时间,电话,社交资料)是在“Datos estructurados”管理屏幕上设置的
configure_structured_dataMCP的工具。主页(pages.get("home")),crafty.site.get()把它带进去structured_data. - 集合可以映射到
Product,Article,BlogPosting,FAQPage,Event或者Service(集合编辑或structured_data在update_collection). 然后,每个发布的条目都携带其JSON-LDstructured_data.
crafty.structuredData(...sources) 返回 <script type="application/ld+json"> 标签用于任何混合的页面、条目、列表响应、站点或JSON-LD对象("" 当没有人有时)。FAQPage对象被合并为一个,因为谷歌期望每个页面都有一个FAQPage:
const page = await crafty.pages.get("home");
const faqs = await crafty.entries.list("faq");
head += crafty.structuredData(page, faqs); // LocalBusiness + one FAQPage with every question
对于那些写在纸上的问题, crafty.faqPage(block.data.items) 构建FAQPage { question, answer } 物品。 crafty.jsonLd(...sources) 返回对象而不是HTML useHead / <Head> 组件。
在Crafty Hosting上,部署还将业务JSON-LD添加到主页(index.html),当页面还没有呈现它,所以一个网站得到LocalBusiness,即使没有帮助。
请求选项(每个呼叫)
每个方法都接受 options? 作为最后一个论点:
signal:AbortSignalheaders:特定于请求的报头(与客户端报头合并)timeoutMs:覆盖该调用的客户端超时
const posts = await crafty.entries.list(
"posts",
{ per_page: 15, page: 1, sort_by: "updated_at", sort_order: "desc" },
{ timeoutMs: 5_000 }
);
响应(当前形状)
collections.list()= >{ data: CraftyCollection[] }collections.get()= >CraftyCollectionentries.list()= >分页{ data, links, meta }entries.get()= >{ data: CraftyEntry }pages.list()= >分页{ data, links, meta }pages.get()= >{ data: CraftyPage }加上上面列出的块帮助器mediaCollections.list()= >{ data, meta: { includes } }mediaFiles.list()= >{ data, links, meta }(meta包括ttl_minutes)forms.list()= >{ data: CraftyForm[] }forms.get()= >{ data: CraftyForm }forms.submit()= >{ id, form_id, submitted_at }
错误
SDK抛出 CraftyError HTTP、超时、中止、网络和配置失败(例如no fetch 可在构建时实现)。
import { CraftyError } from "@craftware/crafty-sdk";
try {
await crafty.pages.get("home");
} catch (error) {
if (error instanceof CraftyError) {
console.error(error.code, error.status, error.message);
console.error(error.requestId); // if API sent x-request-id
}
}
当前错误码(及其http状态映射):
NETWORK_ERROR-网络故障,或否fetch施工时可用TIMEOUT-请求超出timeoutMsBAD_REQUEST—HTTP 400或422UNAUTHORIZED- HTTP 401FORBIDDEN- HTTP 403NOT_FOUND- HTTP 404RATE_LIMITED- HTTP 429SLOT_UNAVAILABLE-预订结帐时的HTTP 409:插槽同时被占用SERVER_ERROR- HTTP 5xxUNKNOWN_ERROR-任何其他/未映射状态
的端到端示例
import { createCraftyClient } from "@craftware/crafty-sdk";
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: process.env.CRAFTY_API_KEY,
siteSlug: "my-site",
timeoutMs: 10_000
});
const collections = await crafty.collections.list();
const posts = await crafty.entries.list("posts", { per_page: 15 });
const mediaCollections = await crafty.mediaCollections.list({ include: ["stats", "directories"] });
const mediaFiles = await crafty.mediaFiles.list({ collection: "library", per_page: 24 });
const homePage = await crafty.pages.get("home");
const forms = await crafty.forms.list();
const submission = await crafty.forms.submit("contact", { name: "Jane", email: "[email protected]" });
页面范围(通过API密钥的站点+环境)
pages 请求不再需要 siteSlug 参数。从服务器端推断出该站点
X-API-Key 报头(set from) apiKey 在 createCraftyClient())。
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: "ENVIRONMENT_API_TOKEN",
});
await crafty.pages.list({ per_page: 12 });
const landing = await crafty.pages.get("landing");
const processBlock = landing.block("process");
const allProcessBlocks = landing.blocksOf("process");
const processFirst = landing.firstByType.process;
const processList = landing.byType.process ?? [];
可视化编辑器
Crafty的可视化编辑器在iframe中加载您的网站,并允许编辑器单击文本或图像来更改它。三个部分使网站可编辑:
1. 标记可编辑元素 与 crafty.attrs(target, field, { label }) (也输出为 attrs)。 target 是来自API的块或条目(或 { kind: "block" | "entry", id }); field 是字段键还是JSON路径 json 字段。
<h1 {...crafty.attrs(block, "title")}>{block.data.title}</h1>
<h2 {...crafty.attrs(entry, `sections[${i}].title`, { label: "Título de la sección" })}>{section.title}</h2>
按钮和链接:通过 hrefField (可选 hrefLabel),因此编辑器将目的地与文本一起编辑。单击链接上的任何位置(填充、图标、键盘激活)将选择其标记的标签,而不是跟随它。真正的 <button>, <summary> 和 <label> 元素保持它们自己的行为(菜单,手风琴),除非点击落在被标记的元素上:
<a href={cta.href} className="btn">
<span {...crafty.attrs(entry, "hero.cta.label", { label: "Texto del botón", hrefField: "hero.cta.href", hrefLabel: "Enlace del botón" })}>{cta.label}</span>
<ArrowIcon />
</a>
一个链接,其唯一可编辑的值是它的目标(A) url 场地)被标记在 <a> 本身: <a {...crafty.attrs(entry, "maps_url", { label: "Enlace del mapa" })} href={entry.maps_url}>Ver mapa</a>. 链接目的地永远不会被实时写入页面;他们在保存草稿后显示。
通过 hrefField 只有当内容存储目的地时:如果网站在缺少它时退回到硬编码的href,则离开 hrefField 出(编辑器不能在属性中创建缺失的键) json 字段)。 hrefField 从与标签相同的目标中读取;当标签和URL位于不同的块或项中时,分别标记它们。硬编码链接文本永远不可编辑:标记 <a> 与 url 字段,或者先将文本移动到Crafty。
字符串模板的使用 attrsHtml(target, field, options),返回转义后的 data-crafty-*="…" 对。属性总是被发出,所以静态构建会保留它们。
2. 携带预览令牌。 在浏览器中什么都不需要。使用服务器渲染,每个请求读取它:
import { createCraftyClient, previewFromRequest } from "@craftware/crafty-sdk";
const crafty = createCraftyClient({ baseUrl, apiKey, preview: previewFromRequest(req) });
previewFromRequest 接受URL字符串, URLWeb Request,节点请求(req.url)或特快专递要求(req.query)。
3. 启用网桥 一次,在浏览器中:
import { enableVisualEditing } from "@craftware/crafty-sdk/visual-editing";
enableVisualEditing({ onRefresh: () => reloadContent() });
它不做任何事情,除非页面在Crafty管理iframe中带有预览令牌,所以它在生产中是安全的。即使如此,它仍然保持惰性,直到后端确认令牌(使用的响应) X-Crafty-Preview-Status: active) -通常是网站自己在页面加载期间的第一个Crafty请求。没有浏览器端请求携带令牌的站点(纯SSR,内容只在服务器上获取)必须通过 client 因此,桥梁可以通过一个轻量级请求(pages.list({ per_page: 1 })):
enableVisualEditing({ client: crafty });
如果API回答 invalid (过期或外来令牌),桥将保持惰性,并告诉管理员而不是激活。选项: adminOrigins (默认为 https://*.crafty.do), onRefresh (重新获取而不是完全重新加载), page / entries (服务器渲染页面显示的内容), touchHighlightMs, client (验证令牌,而不是等待站点自己的请求)。
当桥处于活动状态时,在站点内按下的Escape将被转发给管理员(SDK 0.4.1+),因此即使焦点在iframe中,编辑器的底部表单和对话框也会关闭。网站自己处理的Escape(关闭自己的菜单或模式)必须调用 event.preventDefault();然后桥就离开了。
自定义获取层。 如果一个站点用SDK客户端以外的东西获取Crafty(所以既不是站点自己的请求也不是 client 一直贯穿始终 HttpClient),报告 X-Crafty-Preview-Status 响应头你自己,从 @craftware/crafty-sdk/visual-editing:
import { getPreviewStatus, notifyPreviewActive, notifyPreviewInvalid } from "@craftware/crafty-sdk/visual-editing";
const status = response.headers.get("X-Crafty-Preview-Status");
if (status === "active") notifyPreviewActive();
if (status === "invalid") notifyPreviewInvalid();
getPreviewStatus() 读取以这种方式(或SDK自己的客户端)报告的最后状态; PREVIEW_ACTIVE_EVENT / PREVIEW_INVALID_EVENT 挂起的网桥等待的事件名称。
没有捆绑者:
<script src="https://cdn.jsdelivr.net/npm/@craftware/[email protected]/dist/crafty-visual-editing.global.js"></script>
<script>CraftyVisualEditing.enableVisualEditing();</script>
对于生产,固定一个确切的版本(@0.4.1),并加上 integrity="sha384-…" crossorigin="anonymous" 使用jsdeliver为该文件显示的散列。
网站不托管在狡猾的托管必须允许管理员框架他们: Content-Security-Policy: frame-ancestors 'self' https://<tenant>.crafty.do.
笔记
- SDK标准化
baseUrl避免重复/api/v1. Accept: application/json自动设置,除非被覆盖。src/types/generated.ts仍然是一个占位符,直到OpenAPI编码被连接。- 公共
blockTypes端点尚未公开;与块相关的类型目前是手工建模的。