@typeroll/mcp-server 0.45.42 → 0.45.44
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/AGENTS.md +17 -16
- package/README.md +12 -5
- package/dist/bundled-content.js +11 -10
- package/dist/server.js +15 -1
- package/dist/tools/content-types.js +2 -2
- package/dist/tools/forms.js +16 -3
- package/dist/tools/helpers.js +7 -0
- package/dist/tools/page-blocks.js +6 -20
- package/dist/tools/pages.js +11 -4
- package/dist/tools/partials.js +3 -2
- package/dist/tools/preview.js +6 -0
- package/dist/tools/reusable-blocks.js +98 -0
- package/dist/tools/settings.js +3 -2
- package/dist/tools/styles.js +108 -0
- package/dist/tools/working-copy.js +5 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/README.md +1 -0
- package/skills/tr-brand.md +10 -0
- package/skills/tr-content-write.md +17 -0
- package/skills/tr-forms.md +20 -6
- package/skills/tr-import-url.md +10 -0
- package/skills/tr-migrate-wp.md +7 -2
- package/skills/tr-new-site.md +29 -7
- package/skills/tr-page-template.md +1 -1
- package/skills/tr-upgrade-rendering.md +71 -0
package/dist/server.js
CHANGED
|
@@ -16,6 +16,8 @@ import { contentTypeTools } from './tools/content-types.js';
|
|
|
16
16
|
import { pageTemplateTools } from './tools/page-templates.js';
|
|
17
17
|
import { mediaTools } from './tools/media.js';
|
|
18
18
|
import { redirectTools } from './tools/redirects.js';
|
|
19
|
+
import { styleTools } from './tools/styles.js';
|
|
20
|
+
import { reusableBlockTools } from './tools/reusable-blocks.js';
|
|
19
21
|
import { migrationTools } from './tools/migration.js';
|
|
20
22
|
import { formTools } from './tools/forms.js';
|
|
21
23
|
import { searchTools } from './tools/search.js';
|
|
@@ -113,7 +115,17 @@ The full playbook ships with this server — use it:
|
|
|
113
115
|
5. Pages default to block mode. Build with add_block/update_block; make layouts
|
|
114
116
|
responsive per breakpoint with set_block_responsive (grid columns, icon-box
|
|
115
117
|
layout, … take { mobile, tablet, laptop, desktop, wide } values).
|
|
116
|
-
6.
|
|
118
|
+
6. Appearance lives in named styles: list_styles before styling, reuse a
|
|
119
|
+
style through a block's style_id, and create_style for any look that could
|
|
120
|
+
recur. Use one-off block values only for something clearly unique, never
|
|
121
|
+
classed raw HTML. Text must meet WCAG AA contrast. A heading's small label
|
|
122
|
+
is its eyebrow field. For what styles cannot express, use page or site
|
|
123
|
+
custom_css on s-<style> or block custom_class selectors, not [data-block].
|
|
124
|
+
7. Reuse: content that must stay identical on many pages is a global block
|
|
125
|
+
(make_block_global; core/global_block references it). A section people
|
|
126
|
+
start from and then adapt is a block template (list_block_templates,
|
|
127
|
+
insert_block_template copies it). Never duplicate shared content by hand.
|
|
128
|
+
8. No site yet? With an org-scoped key, create_site bootstraps one.
|
|
117
129
|
|
|
118
130
|
If anything here conflicts with what a tool returns, trust the tool. Every
|
|
119
131
|
tool's own description carries its specifics.
|
|
@@ -142,6 +154,8 @@ export function buildServer(options) {
|
|
|
142
154
|
...pageTemplateTools,
|
|
143
155
|
...mediaTools,
|
|
144
156
|
...redirectTools,
|
|
157
|
+
...styleTools,
|
|
158
|
+
...reusableBlockTools,
|
|
145
159
|
...migrationTools,
|
|
146
160
|
...formTools,
|
|
147
161
|
...settingsTools,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
2
|
+
import { ok, withErrorBoundary, versionParam, writeAuthority } from './helpers.js';
|
|
3
3
|
const schema = z.array(z.object({ name: z.string(), label: z.string(), type: z.enum(['text', 'textarea', 'richtext', 'image', 'file', 'color', 'select', 'multiselect', 'boolean', 'number', 'url', 'email', 'date', 'datetime', 'list', 'list_simple', 'array', 'object', 'page_ref', 'page_ref_list']) }).passthrough());
|
|
4
4
|
const fields = {
|
|
5
5
|
label_singular: z.string().optional(), label_plural: z.string().optional(), fields: schema.optional(),
|
|
@@ -14,7 +14,7 @@ const fields = {
|
|
|
14
14
|
export const contentTypeTools = [
|
|
15
15
|
{ name: 'page_completeness', description: 'Report missing, unverified and stale fields for Pages of one content type, with per-field counts and worst-first Page IDs. Read-only; agent-writable fields only by default.', inputSchema: { content_type: z.string(), limit: z.number().int().min(1).max(200).optional(), stale_after_days: z.number().min(0).optional(), agent_writable_only: z.boolean().optional(), version: versionParam },
|
|
16
16
|
handler: withErrorBoundary(async (args, { client, siteId }) => { const { content_type, ...query } = args; return ok(await client.get(siteId, `content-types/${encodeURIComponent(content_type)}/completeness`, query)); }) },
|
|
17
|
-
{ name: 'change_page_content_type', description: 'Change a saved Page to another content type. Preserves its identity, block body, status and existing URL. Save or discard its working copy first. If supplied, fields replaces the complete custom fields object; omitted fields are kept and must be valid for the new type. Snapshots a revision before changing.', inputSchema: { page_id: z.string(), content_type: z.string(), fields: z.record(z.unknown()).optional(), version: versionParam },
|
|
17
|
+
{ name: 'change_page_content_type', description: 'Change a saved Page to another content type. Preserves its identity, block body, status and existing URL. Save or discard its working copy first. If supplied, fields replaces the complete custom fields object; omitted fields are kept and must be valid for the new type. Snapshots a revision before changing.', inputSchema: { page_id: z.string(), content_type: z.string(), fields: z.record(z.unknown()).optional(), ...writeAuthority, version: versionParam },
|
|
18
18
|
handler: withErrorBoundary(async (args, { client, siteId }) => { const { page_id, version, ...body } = args; return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/content-type`, body, { version })); }) },
|
|
19
19
|
{ name: 'list_content_types', description: 'List the field schemas, URL patterns, sorting, allowed templates and default templates for Pages. All content is a Page; use list_pages with content_type to filter.', inputSchema: { version: versionParam },
|
|
20
20
|
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'content-types', { version: args.version }))) },
|
package/dist/tools/forms.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Forms — agents can create/update/delete forms,
|
|
1
|
+
// Forms — agents can create/update/delete forms, manage their email
|
|
2
|
+
// notifications and webhooks, and read submissions, like the portal.
|
|
2
3
|
// The customer-facing submit endpoint is HMAC-signed and lives at
|
|
3
4
|
// /api/forms/submit. Agents place forms by reference: core/form in block mode,
|
|
4
5
|
// or <x-form id="…" /> in HTML mode. Preview/build expands either reference
|
|
@@ -33,6 +34,16 @@ const targetSchema = z.object({
|
|
|
33
34
|
context_params: z.array(z.string()).max(8).optional().describe('Explicit URL query keys forwarded to the app hydration endpoint; reserved security keys are rejected.'),
|
|
34
35
|
installation_id: z.string(), path: z.string(), hydrate: z.boolean().optional(), session_param: z.string().optional(),
|
|
35
36
|
}).optional().describe('Admin-only: bind to a declared POST route of an enabled app installation. Read its authenticated guide first.');
|
|
37
|
+
// Actions run after a successful submission: email notifications, signed
|
|
38
|
+
// webhooks and app-provided action types. Same validation as the portal's
|
|
39
|
+
// Forms editor; admin permission required, as in the portal.
|
|
40
|
+
const actionSchema = z.object({
|
|
41
|
+
id: z.string().optional().describe('Keep an existing action\'s id to update it in place (webhooks keep their stored secret).'),
|
|
42
|
+
type: z.string().describe('"email", "webhook", or an action type provided by an installed app.'),
|
|
43
|
+
config: z.record(z.string(), z.unknown()).describe('email: { to, subject, body, cc?, bcc?, reply_to?, include_all?, format?: "html"|"text" } — to/subject/body may use {{field}} placeholders, e.g. to: "{{email}}" for a confirmation to the visitor. ' +
|
|
44
|
+
'webhook: { url (https), fields: ["name","email"] (only these values are sent), secret (signing secret; send "••••••••" to keep the stored one) }.'),
|
|
45
|
+
});
|
|
46
|
+
const actionsDescription = 'The complete list of actions after a submission (replaces the current list): email notifications, webhooks, app actions. Read the form first and send back the actions you keep. Requires admin permission, like the portal.';
|
|
36
47
|
export const formTools = [
|
|
37
48
|
{
|
|
38
49
|
name: 'list_forms',
|
|
@@ -44,7 +55,7 @@ export const formTools = [
|
|
|
44
55
|
},
|
|
45
56
|
{
|
|
46
57
|
name: 'read_form',
|
|
47
|
-
description: 'Read one form by id, including fields/steps, submit_text, and
|
|
58
|
+
description: 'Read one form by id, including fields/steps, submit_text, success_message and, for admins, its actions (email notifications and webhooks; secrets masked). Place it with a `core/form` block using data.form_id on a block-mode page, or `<x-form id="…" />` in HTML mode. Both references are expanded server-side with validation, signed token, initial state, and the shared runtime.',
|
|
48
59
|
inputSchema: { form_id: z.string() },
|
|
49
60
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
50
61
|
const res = await client.get(siteId, `forms/${encodeURIComponent(args.form_id)}`);
|
|
@@ -62,6 +73,7 @@ export const formTools = [
|
|
|
62
73
|
submit_text: z.string().optional(),
|
|
63
74
|
success_message: z.string().optional().describe('Shown after the final step. May contain basic HTML (links, emphasis); it is sanitized.'),
|
|
64
75
|
success_redirect_url: z.string().optional().describe('After the final step, send the visitor to this absolute http(s) URL or root-relative path (for example a booking or thank-you page) instead of showing success_message. Pass an empty string to clear.'),
|
|
76
|
+
actions: z.array(actionSchema).optional().describe(actionsDescription),
|
|
65
77
|
target: targetSchema,
|
|
66
78
|
},
|
|
67
79
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -74,7 +86,7 @@ export const formTools = [
|
|
|
74
86
|
},
|
|
75
87
|
{
|
|
76
88
|
name: 'update_form',
|
|
77
|
-
description: 'Patch a form. Provide only what you want to change. Passing `steps` replaces the whole step list; passing `fields` (sugar for simple forms) replaces the whole step list with ONE static step built from them. The existing form_id is immutable.',
|
|
89
|
+
description: 'Patch a form. Provide only what you want to change. Passing `steps` replaces the whole step list; passing `fields` (sugar for simple forms) replaces the whole step list with ONE static step built from them. Passing `actions` replaces the email notifications and webhooks (admin). The existing form_id is immutable.',
|
|
78
90
|
inputSchema: {
|
|
79
91
|
form_id: z.string(),
|
|
80
92
|
patch: z.object({
|
|
@@ -84,6 +96,7 @@ export const formTools = [
|
|
|
84
96
|
submit_text: z.string().optional(),
|
|
85
97
|
success_message: z.string().optional().describe('Shown after the final step. May contain basic HTML (links, emphasis); it is sanitized.'),
|
|
86
98
|
success_redirect_url: z.string().optional().describe('After the final step, send the visitor to this absolute http(s) URL or root-relative path (for example a booking or thank-you page) instead of showing success_message. Pass an empty string to clear.'),
|
|
99
|
+
actions: z.array(actionSchema).optional().describe(actionsDescription),
|
|
87
100
|
target: targetSchema,
|
|
88
101
|
}),
|
|
89
102
|
},
|
package/dist/tools/helpers.js
CHANGED
|
@@ -53,6 +53,13 @@ export function withErrorBoundary(handler) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
/** Shared zod fragment for the optional `version` parameter. */
|
|
56
|
+
/**
|
|
57
|
+
* MCP writes Page field values with the same authority as an editor in the
|
|
58
|
+
* portal. Only a value the listed business set itself needs a stated reason.
|
|
59
|
+
*/
|
|
60
|
+
export const writeAuthority = {
|
|
61
|
+
override_reason: z.string().max(500).optional().describe('Why a value the listed business set itself (through its edit link) is being replaced. Required only for those values, as in the portal; the write otherwise returns 409 naming the field.'),
|
|
62
|
+
};
|
|
56
63
|
export const versionParam = z
|
|
57
64
|
.string()
|
|
58
65
|
.optional()
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// Page-block mutation tools
|
|
2
|
-
//
|
|
3
|
-
// (override with `version`).
|
|
1
|
+
// Page-block mutation tools: get/add/update/move/remove and friends. All
|
|
2
|
+
// target the active version (override with `version`).
|
|
4
3
|
//
|
|
5
|
-
// The AI agent uses these to author block-mode pages directly.
|
|
6
|
-
// HTML
|
|
7
|
-
// It never writes. A person accepts that preview in the page editor.
|
|
4
|
+
// The AI agent uses these to author block-mode pages directly. Typeroll does
|
|
5
|
+
// not convert HTML into blocks; block pages are built with blocks.
|
|
8
6
|
import { z } from 'zod';
|
|
9
7
|
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
10
8
|
const blockSchema = z.object({
|
|
@@ -89,7 +87,7 @@ export const pageBlockTools = [
|
|
|
89
87
|
},
|
|
90
88
|
{
|
|
91
89
|
name: 'update_block',
|
|
92
|
-
description: 'Update a block\'s data fields (shallow merge), style overrides, or responsive settings. Works on pages, partials, and page templates — use `target` to address non-page containers. Does not modify children or slots; use move/add/remove for structural changes.',
|
|
90
|
+
description: 'Update a block\'s data fields (shallow merge), style overrides, or responsive settings. Works on pages, partials, and page templates — use `target` to address non-page containers. Does not modify children or slots; use move/add/remove for structural changes. For appearance, prefer a named style (`data.style_id`, see list_styles) over custom_class or custom_css. From render version 2, custom_class lands on the heading or button link element itself instead of the block wrapper.',
|
|
93
91
|
inputSchema: {
|
|
94
92
|
target: targetSchema.optional(),
|
|
95
93
|
page_id: z.string().optional(),
|
|
@@ -193,7 +191,7 @@ export const pageBlockTools = [
|
|
|
193
191
|
},
|
|
194
192
|
{
|
|
195
193
|
name: 'set_page_mode',
|
|
196
|
-
description: 'Safely switch a page\'s content_mode between "blocks" and "html". Always snapshots a revision before the flip so the previous state is restorable. HTML → blocks does not convert or rewrite the body. Blocks → HTML drops the block tree (the revision retains it). Prefer this over `update_page patch={content_mode}` because that bypasses snapshotting.
|
|
194
|
+
description: 'Safely switch a page\'s content_mode between "blocks" and "html". Always snapshots a revision before the flip so the previous state is restorable. HTML → blocks does not convert or rewrite the body. Blocks → HTML drops the block tree (the revision retains it). Prefer this over `update_page patch={content_mode}` because that bypasses snapshotting. Typeroll does not convert HTML into blocks: after switching to blocks, build the content with blocks.',
|
|
197
195
|
inputSchema: {
|
|
198
196
|
page_id: z.string(),
|
|
199
197
|
to: z.enum(['blocks', 'html']).describe('Target mode.'),
|
|
@@ -204,16 +202,4 @@ export const pageBlockTools = [
|
|
|
204
202
|
return ok(res);
|
|
205
203
|
}),
|
|
206
204
|
},
|
|
207
|
-
{
|
|
208
|
-
name: 'convert_page_to_blocks',
|
|
209
|
-
description: 'Preview a heuristic conversion of a page\'s html_content into a block tree. Always returns the proposal, including unconverted text, classes and markup, and does not write. A person accepts the preview in the page editor. This tool cannot change content_mode or replace the body.',
|
|
210
|
-
inputSchema: {
|
|
211
|
-
page_id: z.string(),
|
|
212
|
-
version: versionParam,
|
|
213
|
-
},
|
|
214
|
-
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
215
|
-
const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/blocks/convert`, { dry_run: true }, v(args.version));
|
|
216
|
-
return ok(res);
|
|
217
|
-
}),
|
|
218
|
-
},
|
|
219
205
|
];
|
package/dist/tools/pages.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Pages tools. Read + write + batch + blocks-view + preview.
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
3
|
+
import { ok, withErrorBoundary, versionParam, writeAuthority } from './helpers.js';
|
|
4
4
|
const answerSources = z.record(z.object({ source_url: z.string().url().optional(), import_run_id: z.string().max(200).optional() }).strict()).optional().describe('Evidence per schema leaf path. Actor and timestamp are always assigned by the server; unchanged answers are not confirmed.');
|
|
5
|
+
function authorityBody(args) {
|
|
6
|
+
return args.override_reason ? { override_reason: args.override_reason } : {};
|
|
7
|
+
}
|
|
5
8
|
function v(version) {
|
|
6
9
|
return version ? { version } : undefined;
|
|
7
10
|
}
|
|
@@ -107,7 +110,8 @@ export const pageTools = [
|
|
|
107
110
|
sort_order: z.number().finite().optional().describe('Manual Page order; lower numbers come first in ascending lists. Content types and listings choose how to sort.'),
|
|
108
111
|
template: z.string().nullable().optional().describe('Allowed Page template ID; null uses the content type default.'),
|
|
109
112
|
image_sizes_default: z.string().optional().describe('Per-page default `sizes` for responsive images (e.g. "(max-width: 640px) 360px, 560px"). Overrides the site setting; a per-<img> `sizes` attr still wins. Set when this page\'s images render narrower than the generic default so the browser stops over-fetching.'),
|
|
110
|
-
custom_css: z.string().optional().describe('Per-page CSS, injected into <head> as a <style> AFTER the site-level custom_css (so it overrides site styling). This is the RIGHT home for page-specific styling — page metadata, not content. Put a page\'s <style> here instead of stuffing it into a core/html block (which is opaque and un-editable in the visual editor).'),
|
|
113
|
+
custom_css: z.string().optional().describe('Per-page CSS, injected into <head> as a <style> AFTER the site-level custom_css (so it overrides site styling). This is the RIGHT home for page-specific styling — page metadata, not content. Put a page\'s <style> here instead of stuffing it into a core/html block (which is opaque and un-editable in the visual editor). Syntax errors refuse the write; target s-<style> classes or block custom_class rather than [data-block]/.block-* platform markup.'),
|
|
114
|
+
...writeAuthority,
|
|
111
115
|
version: versionParam,
|
|
112
116
|
},
|
|
113
117
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -168,11 +172,12 @@ export const pageTools = [
|
|
|
168
172
|
})
|
|
169
173
|
.passthrough(),
|
|
170
174
|
answer_sources: answerSources,
|
|
175
|
+
...writeAuthority,
|
|
171
176
|
save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call — use for pre-approved or batch changes. Without it, changes stay in the unsaved draft until commit_working_copy.'),
|
|
172
177
|
version: versionParam,
|
|
173
178
|
},
|
|
174
179
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
175
|
-
const res = await client.patch(siteId, `pages/${encodeURIComponent(args.page_id)}`, { ...args.patch, ...(args.answer_sources ? { answer_sources: args.answer_sources } : {}), ...(args.save ? { save: true } : {}) }, v(args.version));
|
|
180
|
+
const res = await client.patch(siteId, `pages/${encodeURIComponent(args.page_id)}`, { ...args.patch, ...(args.answer_sources ? { answer_sources: args.answer_sources } : {}), ...authorityBody(args), ...(args.save ? { save: true } : {}) }, v(args.version));
|
|
176
181
|
return ok(res);
|
|
177
182
|
}),
|
|
178
183
|
},
|
|
@@ -183,11 +188,12 @@ export const pageTools = [
|
|
|
183
188
|
page_id: z.string(),
|
|
184
189
|
page: z.object({ title: z.string().min(1) }).passthrough(),
|
|
185
190
|
answer_sources: answerSources,
|
|
191
|
+
...writeAuthority,
|
|
186
192
|
save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call — use for pre-approved or batch changes. Without it, changes stay in the unsaved draft until commit_working_copy.'),
|
|
187
193
|
version: versionParam,
|
|
188
194
|
},
|
|
189
195
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
190
|
-
const res = await client.put(siteId, `pages/${encodeURIComponent(args.page_id)}`, { ...args.page, ...(args.answer_sources ? { answer_sources: args.answer_sources } : {}), ...(args.save ? { save: true } : {}) }, v(args.version));
|
|
196
|
+
const res = await client.put(siteId, `pages/${encodeURIComponent(args.page_id)}`, { ...args.page, ...(args.answer_sources ? { answer_sources: args.answer_sources } : {}), ...authorityBody(args), ...(args.save ? { save: true } : {}) }, v(args.version));
|
|
191
197
|
return ok(res);
|
|
192
198
|
}),
|
|
193
199
|
},
|
|
@@ -201,6 +207,7 @@ export const pageTools = [
|
|
|
201
207
|
patch: z.record(z.unknown()),
|
|
202
208
|
save: z.boolean().optional(),
|
|
203
209
|
answer_sources: answerSources,
|
|
210
|
+
...writeAuthority,
|
|
204
211
|
}))
|
|
205
212
|
.min(1)
|
|
206
213
|
.max(200),
|
package/dist/tools/partials.js
CHANGED
|
@@ -89,11 +89,12 @@ export const partialTools = [
|
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
name: 'create_free_block',
|
|
92
|
-
description: 'Create a new
|
|
92
|
+
description: 'Create a new global block: one shared source that every page using it shows, so an edit updates them all. Pass `blocks` for a block global block, referenced from block pages with a `core/global_block` block (data: { global_block_id }); or `html_content` for HTML pages, embedded with <x-include name="block-id" />. To turn an existing page block into a global block, use make_block_global. For a starter that is copied and then edited per page, use save_block_template instead.',
|
|
93
93
|
inputSchema: {
|
|
94
94
|
id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/),
|
|
95
95
|
name: z.string().optional(),
|
|
96
|
-
|
|
96
|
+
blocks: z.array(z.record(z.unknown())).optional().describe('Block tree for a block global block.'),
|
|
97
|
+
html_content: z.string().optional().describe('HTML for <x-include> in HTML pages. Send blocks or html_content.'),
|
|
97
98
|
status: z.enum(['draft', 'published']).optional(),
|
|
98
99
|
version: versionParam,
|
|
99
100
|
},
|
package/dist/tools/preview.js
CHANGED
|
@@ -26,6 +26,12 @@ export const previewTools = [
|
|
|
26
26
|
.boolean()
|
|
27
27
|
.optional()
|
|
28
28
|
.describe('Render unsaved drafts (working copies) too — yours AND the editor\'s. Off by default (saved content only). Signed into the token, so a draft link needs its own mint. Use this for your own iteration loop; use a plain link when the user wants to see exactly what a deploy would ship.'),
|
|
29
|
+
render_version: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(1)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Render with this platform render version instead of the site\'s own, to preview an upgrade before applying it (read_site_settings returns render.version, render.latest and what each upgrade changes). Signed into the token. Compare against a plain link to see exactly what would change; apply with update_site_settings render_version.'),
|
|
29
35
|
version: versionParam,
|
|
30
36
|
},
|
|
31
37
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Reusable blocks: global blocks (one shared source, referenced) and block
|
|
2
|
+
// templates (saved starters, copied in). Global blocks themselves are
|
|
3
|
+
// partials; see partials.ts for list/read/update/delete.
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
6
|
+
function v(version) {
|
|
7
|
+
return version ? { version } : undefined;
|
|
8
|
+
}
|
|
9
|
+
export const reusableBlockTools = [
|
|
10
|
+
{
|
|
11
|
+
name: 'make_block_global',
|
|
12
|
+
description: 'Turn a block on a page (with everything inside it) into a global block and put a reference in its place. Other pages then add the same block with add_block type "core/global_block" data { global_block_id }. Editing the global block (update_partial / update_block with target { kind: "partial", id }) changes every page that uses it. Use for content that must stay identical everywhere, such as a shared call to action. Writes the page draft.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
page_id: z.string(),
|
|
15
|
+
block_id: z.string(),
|
|
16
|
+
name: z.string().optional().describe('Name editors see; defaults to the block name.'),
|
|
17
|
+
global_block_id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/).optional().describe('Id for the new global block; derived from the name when omitted.'),
|
|
18
|
+
version: versionParam,
|
|
19
|
+
},
|
|
20
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
21
|
+
const { page_id, version, ...body } = args;
|
|
22
|
+
return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/blocks/make-global`, body, v(version)));
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'detach_global_block',
|
|
27
|
+
description: 'Replace a core/global_block reference on a page with an editable copy of the global block\'s blocks. Later edits to the global block no longer reach this page. Writes the page draft.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
page_id: z.string(),
|
|
30
|
+
block_id: z.string().describe('The id of the core/global_block reference block.'),
|
|
31
|
+
version: versionParam,
|
|
32
|
+
},
|
|
33
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
34
|
+
const { page_id, version, ...body } = args;
|
|
35
|
+
return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/blocks/detach`, body, v(version)));
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'list_block_templates',
|
|
40
|
+
description: 'List the site\'s block templates: saved section starters (name, description, block count). Inserting one copies its blocks into a page, where they are then edited independently. Check here before building a common section from scratch.',
|
|
41
|
+
inputSchema: {},
|
|
42
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'block-templates'))),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'read_block_template',
|
|
46
|
+
description: 'Read one block template, including its block tree.',
|
|
47
|
+
inputSchema: { template_id: z.string() },
|
|
48
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, `block-templates/${encodeURIComponent(args.template_id)}`))),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'save_block_template',
|
|
52
|
+
description: 'Save a block template: a section starter that editors and agents insert as an independent copy. Pass `from: { page_id, block_id }` to save a block from a page (its draft), or `blocks`. Prefer templates that use named styles (style_id) over one-off values, so copies stay consistent. Templates belong to the site, not a branch.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
name: z.string(),
|
|
55
|
+
description: z.string().optional().describe('When to use it.'),
|
|
56
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/).optional(),
|
|
57
|
+
from: z.object({ page_id: z.string(), block_id: z.string() }).optional(),
|
|
58
|
+
blocks: z.array(z.record(z.unknown())).optional(),
|
|
59
|
+
},
|
|
60
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'block-templates', args))),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'update_block_template',
|
|
64
|
+
description: 'Rename a block template, change its description, or replace its blocks. Pages that already inserted it do not change.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
template_id: z.string(),
|
|
67
|
+
name: z.string().optional(),
|
|
68
|
+
description: z.string().optional(),
|
|
69
|
+
blocks: z.array(z.record(z.unknown())).optional(),
|
|
70
|
+
},
|
|
71
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
72
|
+
const { template_id, ...body } = args;
|
|
73
|
+
return ok(await client.patch(siteId, `block-templates/${encodeURIComponent(template_id)}`, body));
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'delete_block_template',
|
|
78
|
+
description: 'Delete a block template. Pages that already inserted it keep their copies.',
|
|
79
|
+
inputSchema: { template_id: z.string() },
|
|
80
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.del(siteId, `block-templates/${encodeURIComponent(args.template_id)}`))),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'insert_block_template',
|
|
84
|
+
description: 'Insert a copy of a block template into a page (new block ids), at the top level or inside parent_id (slot_index for column slots), at `position` (default: end). Returns added_ids. Writes the page draft.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
page_id: z.string(),
|
|
87
|
+
template_id: z.string(),
|
|
88
|
+
parent_id: z.string().optional().nullable(),
|
|
89
|
+
slot_index: z.number().int().min(0).optional(),
|
|
90
|
+
position: z.number().int().min(0).optional(),
|
|
91
|
+
version: versionParam,
|
|
92
|
+
},
|
|
93
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
94
|
+
const { page_id, version, ...body } = args;
|
|
95
|
+
return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/blocks/insert-template`, body, v(version)));
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
];
|
package/dist/tools/settings.js
CHANGED
|
@@ -13,7 +13,7 @@ function v(version) {
|
|
|
13
13
|
export const settingsTools = [
|
|
14
14
|
{
|
|
15
15
|
name: 'read_site_settings',
|
|
16
|
-
description: "Read every site setting: name, tagline, logo, favicon/app icons, colors, fonts, contact info, social links, URL trailing-slash policy, iframe host allowlist, default SEO suffix/description, language, robots_txt, image_sizes_default, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css. Pass `version` to read a branch's settings (with copy-on-write chain-fallback to main for fields the branch hasn't overridden).",
|
|
16
|
+
description: "Read every site setting: name, tagline, logo, favicon/app icons, colors, fonts, contact info, social links, URL trailing-slash policy, iframe host allowlist, default SEO suffix/description, language, robots_txt, image_sizes_default, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css, and `render` (the site's platform render version, the latest version and what upgrading would change). Pass `version` to read a branch's settings (with copy-on-write chain-fallback to main for fields the branch hasn't overridden).",
|
|
17
17
|
inputSchema: {
|
|
18
18
|
version: versionParam,
|
|
19
19
|
},
|
|
@@ -53,7 +53,8 @@ export const settingsTools = [
|
|
|
53
53
|
// Scriptable surfaces. Trusted because the caller has an API key.
|
|
54
54
|
scripts_head: z.string().optional().describe('Raw HTML injected into <head> on every page. Use for analytics, fonts, third-party CSS links.'),
|
|
55
55
|
scripts_body_end: z.string().optional().describe('Raw HTML injected just before </body> on every page. Use for chat widgets, deferred analytics.'),
|
|
56
|
-
custom_css: z.string().optional().describe('Global CSS in a <style> in <head>, after block CSS and before the page\'s own custom CSS and the template base stylesheet (reset + global). Base rules for :root tokens (spacing, radius, container widths), body font and line-height load after it, so override those with a more specific selector (for example html:root or body.page) rather than a bare :root/body rule. Preview uses the same order. Lets you define site-wide design tokens (CSS variables, @media queries, :hover states) without inlining on every element.'),
|
|
56
|
+
custom_css: z.string().optional().describe('Global CSS in a <style> in <head>, after block CSS and before the page\'s own custom CSS and the template base stylesheet (reset + global). Base rules for :root tokens (spacing, radius, container widths), body font and line-height load after it, so override those with a more specific selector (for example html:root or body.page) rather than a bare :root/body rule. Preview uses the same order. Lets you define site-wide design tokens (CSS variables, @media queries, :hover states) without inlining on every element. Prefer named styles (create_style) for anything a style can express. Syntax errors (unbalanced braces, unclosed comments or strings, expression()/javascript:) refuse the write; selectors on platform markup ([data-block], .block-*) come back as warnings because they can change between render versions — target s-<style> classes or block custom_class instead.'),
|
|
57
|
+
render_version: z.number().int().min(1).optional().describe('Platform render version. Platform output changes ship as new versions so existing sites never change look on their own. Upgrade only after previewing (get_preview_link render_version) and with the user\'s approval; the tr-upgrade-rendering skill compares every page. Lowering it returns to an earlier version.'),
|
|
57
58
|
colors: z.record(z.string()).optional(),
|
|
58
59
|
fonts: z.record(z.unknown()).optional(),
|
|
59
60
|
contact: z
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Style library tools. A site's look is a library of NAMED styles
|
|
2
|
+
// (SiteSettings.styles): element roles (body, h1–h6, link) style every
|
|
3
|
+
// matching element, and every other style is chosen per block through the
|
|
4
|
+
// block's `style_id` field. Reuse beats one-off values.
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
7
|
+
function v(version) {
|
|
8
|
+
return version ? { version } : undefined;
|
|
9
|
+
}
|
|
10
|
+
const COLOR_HELP = 'A palette token (primary, secondary, accent, background, surface, text, text_light), a derived readable text colour for a palette background (on_primary, on_secondary, on_accent), or a hex/rgb/hsl colour. Prefer tokens so a palette change flows through.';
|
|
11
|
+
const length = z.string().describe('CSS length: px, rem, em, %, vw, ch, or clamp()/min()/max()');
|
|
12
|
+
const color = z.string().describe(COLOR_HELP);
|
|
13
|
+
const propsSchema = z.object({
|
|
14
|
+
font: z.enum(['heading', 'body', 'mono', 'inherit']).optional(),
|
|
15
|
+
size: length.optional(),
|
|
16
|
+
weight: z.number().int().min(100).max(900).optional(),
|
|
17
|
+
line_height: z.string().optional().describe('Unitless number such as "1.5", or a length'),
|
|
18
|
+
letter_spacing: z.string().optional().describe('e.g. "0.08em" or "normal"'),
|
|
19
|
+
transform: z.enum(['none', 'uppercase', 'lowercase', 'capitalize']).optional(),
|
|
20
|
+
italic: z.boolean().optional(),
|
|
21
|
+
color: color.optional(),
|
|
22
|
+
background: color.optional(),
|
|
23
|
+
align: z.enum(['left', 'center', 'right', 'start', 'end', 'justify']).optional(),
|
|
24
|
+
decoration: z.enum(['none', 'underline']).optional(),
|
|
25
|
+
space_before: length.optional(),
|
|
26
|
+
space_after: length.optional(),
|
|
27
|
+
padding: z.string().optional().describe('One to four CSS lengths'),
|
|
28
|
+
max_width: length.optional(),
|
|
29
|
+
radius: length.optional(),
|
|
30
|
+
border: z.object({
|
|
31
|
+
width: length.optional(),
|
|
32
|
+
style: z.enum(['solid', 'dashed', 'dotted', 'none']).optional(),
|
|
33
|
+
color: color.optional(),
|
|
34
|
+
sides: z.enum(['all', 'top', 'right', 'bottom', 'left', 'block', 'inline']).optional(),
|
|
35
|
+
}).optional(),
|
|
36
|
+
}).describe('Style properties. Mobile-first: `base` applies everywhere, `at.<breakpoint>` from that width up.');
|
|
37
|
+
const atSchema = z.object({
|
|
38
|
+
tablet: propsSchema.optional(),
|
|
39
|
+
laptop: propsSchema.optional(),
|
|
40
|
+
desktop: propsSchema.optional(),
|
|
41
|
+
wide: propsSchema.optional(),
|
|
42
|
+
}).describe('Overrides from each site breakpoint upward (tablet, laptop, desktop, wide).');
|
|
43
|
+
const styleFields = {
|
|
44
|
+
name: z.string().describe('Author-facing name, in the site\'s language, e.g. "Överrubrik" or "Hero title".'),
|
|
45
|
+
role: z.enum(['body', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'link', 'lead', 'eyebrow', 'small', 'quote', 'button', 'button_secondary', 'section']).optional()
|
|
46
|
+
.describe('Standard role. body/h1–h6/link style every matching element on the site; the others mark the standard style for that purpose. One style per role.'),
|
|
47
|
+
targets: z.array(z.enum(['text', 'heading', 'list', 'button', 'container'])).optional()
|
|
48
|
+
.describe('Block kinds that can select this style via style_id (text = core/prose, heading = core/heading and core/rich_heading, list, button, container = core/section and core/container).'),
|
|
49
|
+
description: z.string().optional().describe('When to use it; shown to editors and agents.'),
|
|
50
|
+
base: propsSchema.optional(),
|
|
51
|
+
at: atSchema.optional(),
|
|
52
|
+
hover: z.object({ color: color.optional(), background: color.optional(), decoration: z.enum(['none', 'underline']).optional() }).optional(),
|
|
53
|
+
css: z.string().optional().describe('Advanced CSS scoped to the style: bare declarations, or rules where `&` is the style selector (e.g. "&::before { content: \'—\' }"). Prefer structured properties.'),
|
|
54
|
+
};
|
|
55
|
+
export const styleTools = [
|
|
56
|
+
{
|
|
57
|
+
name: 'list_styles',
|
|
58
|
+
description: 'List the site\'s named style library: element roles (body, h1–h6, link) that style every matching element, and the styles blocks select with `style_id`. Also returns missing_standard_roles and contrast/library problems. READ THIS BEFORE STYLING ANY PAGE: reuse an existing style for a recurring look (eyebrows, leads, card titles, buttons, section spacing) instead of per-block values, raw HTML classes or page CSS.',
|
|
59
|
+
inputSchema: { version: versionParam },
|
|
60
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'styles', v(args.version)))),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'create_style',
|
|
64
|
+
description: 'Create a named style. Do this whenever a look could recur — an eyebrow, a lead paragraph, a card title, a quote, a button variant, a section rhythm — then apply it with the block\'s `style_id` field. Only a clearly unique one-off may use block settings instead. Set values for phones in `base` and grow them in `at.tablet`/`at.desktop`. Text must meet WCAG AA contrast (4.5:1, 3:1 for ≥24px or bold ≥18.7px) against its background; a failing style is refused. Never light grey on white.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
id: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/).describe('Stable slug; the CSS class becomes s-<id>. Cannot change later.'),
|
|
67
|
+
...styleFields,
|
|
68
|
+
name: styleFields.name,
|
|
69
|
+
version: versionParam,
|
|
70
|
+
},
|
|
71
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
72
|
+
const { version, ...style } = args;
|
|
73
|
+
return ok(await client.post(siteId, 'styles', { targets: [], base: {}, ...style }, v(version)));
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'update_style',
|
|
78
|
+
description: 'Update a style. `base`, `hover` and each breakpoint in `at` merge property by property (pass null to remove one); name, targets, description and css replace. Every page using the style changes at once — tell the user which pages that affects when it matters.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
style_id: z.string(),
|
|
81
|
+
name: styleFields.name.optional(),
|
|
82
|
+
role: styleFields.role,
|
|
83
|
+
targets: styleFields.targets,
|
|
84
|
+
description: styleFields.description,
|
|
85
|
+
base: z.record(z.unknown()).optional().describe('Property patch for base; null removes a property.'),
|
|
86
|
+
at: z.record(z.unknown()).optional().describe('Per-breakpoint property patches; null removes a breakpoint or property.'),
|
|
87
|
+
hover: z.record(z.unknown()).optional(),
|
|
88
|
+
css: z.string().optional(),
|
|
89
|
+
version: versionParam,
|
|
90
|
+
},
|
|
91
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
92
|
+
const { version, style_id, ...patch } = args;
|
|
93
|
+
return ok(await client.patch(siteId, `styles/${encodeURIComponent(style_id)}`, patch, v(version)));
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'delete_style',
|
|
98
|
+
description: 'Delete a style. Blocks that still reference it fall back to their default look; reassign them first.',
|
|
99
|
+
inputSchema: { style_id: z.string(), version: versionParam },
|
|
100
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.del(siteId, `styles/${encodeURIComponent(args.style_id)}`, v(args.version)))),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'apply_standard_styles',
|
|
104
|
+
description: 'Add the standard styles the site is missing (body, H1–H6, link, lead, eyebrow, small text, quote, primary/secondary button, section), with considered sizes and spacing for phones, tablets and desktops. The natural first step when building a new site, before pages: then adjust them to the brand (fonts, colours, scale) with update_style. New sites already have them. overwrite:true resets the standard roles to the defaults and changes the site\'s look — ask first.',
|
|
105
|
+
inputSchema: { overwrite: z.boolean().optional(), version: versionParam },
|
|
106
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'styles/standard', { overwrite: args.overwrite === true }, v(args.version)))),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// The portal editor shows any agent-written draft as "Unsaved changes", so
|
|
14
14
|
// the human can also Save or Discard it from the UI.
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
-
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
16
|
+
import { ok, withErrorBoundary, versionParam, writeAuthority } from './helpers.js';
|
|
17
17
|
function v(version) {
|
|
18
18
|
return version ? { version } : undefined;
|
|
19
19
|
}
|
|
@@ -42,10 +42,11 @@ export const workingCopyTools = [
|
|
|
42
42
|
description: 'SAVE a doc\'s unsaved draft: promote it onto the saved doc through the canonical write path (revision snapshot, SEO transform, redirect hygiene) and delete the draft. Identical to the Save button in the portal editor. Returns { committed, seo_warnings, auto_redirects, retired_redirects }. Never changes publish status. Deploys only ship saved content, so commit before trigger_deploy. Equivalent shortcut: save:true on the write tools.',
|
|
43
43
|
inputSchema: {
|
|
44
44
|
target: wcTargetSchema,
|
|
45
|
+
...writeAuthority,
|
|
45
46
|
version: versionParam,
|
|
46
47
|
},
|
|
47
48
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
48
|
-
const res = await client.post(siteId, wcPath(args.target), {}, v(args.version));
|
|
49
|
+
const res = await client.post(siteId, wcPath(args.target), { ...(args.override_reason ? { override_reason: args.override_reason } : {}) }, v(args.version));
|
|
49
50
|
return ok(res);
|
|
50
51
|
}),
|
|
51
52
|
},
|
|
@@ -109,10 +110,11 @@ export const workingCopyTools = [
|
|
|
109
110
|
page_id: z.string(),
|
|
110
111
|
revision_id: z.string(),
|
|
111
112
|
save: z.boolean().optional().describe('Save immediately (default false: leave it as an unsaved draft to preview).'),
|
|
113
|
+
...writeAuthority,
|
|
112
114
|
version: versionParam,
|
|
113
115
|
},
|
|
114
116
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
115
|
-
const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/revisions/${encodeURIComponent(args.revision_id)}/restore`, { save: args.save === true }, v(args.version));
|
|
117
|
+
const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/revisions/${encodeURIComponent(args.revision_id)}/restore`, { save: args.save === true, ...(args.override_reason ? { override_reason: args.override_reason } : {}) }, v(args.version));
|
|
116
118
|
return ok(res);
|
|
117
119
|
}),
|
|
118
120
|
},
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
package/skills/README.md
CHANGED
|
@@ -33,6 +33,7 @@ instructions. Read the relevant recipe when the task calls for it.
|
|
|
33
33
|
| `tr-new-site.md` | "create a new site", "bootstrap a site for…" | Settings → header/footer partials → homepage → inner pages → deploy. Full setup recipe. |
|
|
34
34
|
| `tr-brand.md` | "create a brand", "choose colors", "design the look"| Palette recipes by mood, typography pairings, CSS variable setup, preview. |
|
|
35
35
|
| `tr-redesign-branch.md`| "redesign", "modernize", anything site-wide-design | Branch-isolated work with preview links, merge when approved. |
|
|
36
|
+
| `tr-upgrade-rendering.md` | "upgrade rendering", render version behind latest | Screenshot-compares every page at the current and target render version, reports differences, upgrades with approval. |
|
|
36
37
|
| `tr-content-write.md` | "write a page about…", "draft copy for…" | Discovery first (settings + sample pages), then drafts in the site's voice, previews, iterates. |
|
|
37
38
|
| `tr-images.md` | "make an image / hero / illustration", media uploads | Generates locally → signed upload URL → metadata patch → embed. |
|
|
38
39
|
|
package/skills/tr-brand.md
CHANGED
|
@@ -159,6 +159,16 @@ replace_partial partial_id="header" html_content="<updated HTML>"
|
|
|
159
159
|
|
|
160
160
|
Same for footer.
|
|
161
161
|
|
|
162
|
+
## Step 5b — Express the typography as styles
|
|
163
|
+
|
|
164
|
+
Colours and fonts are only half the brand. Put the type scale, weights,
|
|
165
|
+
spacing and recurring treatments into the site's named styles (`list_styles`,
|
|
166
|
+
`update_style`, `create_style`): body text and H1–H6 apply everywhere; the
|
|
167
|
+
eyebrow, lead, quote, buttons and section rhythm are chosen per block with
|
|
168
|
+
`style_id`. Set phone values in `base` and grow them in `at.tablet` and
|
|
169
|
+
`at.desktop`. Prefer this to custom CSS: editors can see and change styles,
|
|
170
|
+
and every page follows.
|
|
171
|
+
|
|
162
172
|
## Step 6 — Custom CSS for advanced tokens (optional)
|
|
163
173
|
|
|
164
174
|
If the brand needs things beyond the 7 base tokens — e.g. a gradient,
|
|
@@ -37,6 +37,23 @@ Read the actual HTML of an existing page. Note:
|
|
|
37
37
|
- Length conventions (do existing pages run 200 words or 2000?)
|
|
38
38
|
- Whether internal links use absolute or relative URLs
|
|
39
39
|
|
|
40
|
+
`list_styles` too: use the site's named styles (`style_id` on heading, text,
|
|
41
|
+
list, button and section blocks) for eyebrows, leads, quotes and buttons. If a
|
|
42
|
+
treatment you need could recur and no style fits, create one rather than
|
|
43
|
+
setting one-off values; ask first when it changes the site's look.
|
|
44
|
+
|
|
45
|
+
A small label above a heading is the heading block's `eyebrow`, and a line
|
|
46
|
+
below it is its `subtitle` (render version 2), each with its own style field.
|
|
47
|
+
Never write them as separate text blocks with classed HTML. In text blocks,
|
|
48
|
+
style whole paragraphs with site text styles (`<p class="s-lead">`). Content
|
|
49
|
+
that needs headings with parts, images or buttons belongs in their own blocks,
|
|
50
|
+
not in a text block's HTML.
|
|
51
|
+
|
|
52
|
+
`list_block_templates` and `list_partials` too. Start a common section from a
|
|
53
|
+
block template (`insert_block_template`, then edit the copy). Use a global
|
|
54
|
+
block (`core/global_block`) for content that must stay identical on every
|
|
55
|
+
page, such as a shared call to action; don't paste copies of it.
|
|
56
|
+
|
|
40
57
|
### 2. Ask for the brief
|
|
41
58
|
|
|
42
59
|
If the user hasn't told you, ask:
|