@typeroll/mcp-server 0.42.2 → 0.43.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/AGENTS.md +25 -4
- package/README.md +14 -7
- package/dist/bundled-content.js +7 -7
- package/dist/tools/collections.js +6 -1
- package/dist/tools/migration.js +19 -2
- package/dist/tools/pages.js +2 -3
- package/dist/tools/partials.js +17 -2
- package/dist/tools/settings.js +8 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/tr-blog.md +36 -70
- package/skills/tr-collection-template.md +40 -3
- package/skills/tr-header-footer.md +100 -169
- package/skills/tr-migrate-wp.md +8 -0
- package/skills/tr-new-site.md +19 -37
|
@@ -13,7 +13,7 @@ function existingCollectionArg(args) {
|
|
|
13
13
|
export const collectionTools = [
|
|
14
14
|
{
|
|
15
15
|
name: 'create_collection',
|
|
16
|
-
description: 'Create a new content collection with a field schema and (optionally) per-item URLs. Pass `route_template` (e.g. "/restaurants/{slug}") to give each published item its own URL — omit to default to "/{name}/{slug}", set to "" to disable per-item URLs entirely.',
|
|
16
|
+
description: 'Create a new content collection with a field schema and (optionally) per-item URLs. Pass `route_template` (e.g. "/restaurants/{slug}") to give each published item its own URL — omit to default to "/{name}/{slug}", set to "" to disable per-item URLs entirely. Pass template_kind for a polished native item template, or item_template_blocks for a custom block tree.',
|
|
17
17
|
inputSchema: {
|
|
18
18
|
name: z.string().regex(/^[a-z][a-z0-9_-]{0,62}$/),
|
|
19
19
|
label_singular: z.string().min(1),
|
|
@@ -34,6 +34,11 @@ export const collectionTools = [
|
|
|
34
34
|
sort_dir: z.enum(['asc', 'desc']).optional(),
|
|
35
35
|
route_template: z.string().optional(),
|
|
36
36
|
item_template_html: z.string().optional(),
|
|
37
|
+
template_kind: z
|
|
38
|
+
.enum(['blog', 'article', 'checklist', 'team', 'events', 'products', 'custom'])
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Native starter for item pages. article includes breadcrumbs + body outline; checklist includes optional PDF CTA + explicit previous/next fields.'),
|
|
41
|
+
item_template_blocks: z.array(z.any()).optional().describe('Explicit item-template block tree. Overrides template_kind.'),
|
|
37
42
|
version: versionParam,
|
|
38
43
|
},
|
|
39
44
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
package/dist/tools/migration.js
CHANGED
|
@@ -32,15 +32,32 @@ export const migrationTools = [
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: 'get_migration_readiness',
|
|
35
|
-
description: "Preflight for an import: is this site actually ready to receive a migration? CALL THIS FIRST, before moving any content. Every check exists because its failure is INVISIBLE afterwards — the pages import, the previews render, the customer signs off, and something is quietly wrong.
|
|
35
|
+
description: "Preflight for an import: is this site actually ready to receive a migration? CALL THIS FIRST, before moving any content. Every check exists because its failure is INVISIBLE afterwards — the pages import, the previews render, the customer signs off, and something is quietly wrong. Pass proposed compositions to also inventory their block, field, and native-feature dependencies before implementation. A generic custom block, raw-HTML fallback, per-instance CSS workaround, missing block type, or missing declared field returns waiting_for_native_support and makes ready=false. Do not implement those workarounds; report the gap and wait for Core support. Business-specific custom blocks are allowed only when explicitly listed. Infrastructure warnings cover the pre-cutover verification URL, AI reconstruction, form notification email and whether the target has a design to rebuild INTO.",
|
|
36
36
|
inputSchema: {
|
|
37
37
|
source_url: z
|
|
38
38
|
.string()
|
|
39
39
|
.optional()
|
|
40
40
|
.describe('The site you are migrating FROM, e.g. "https://oldsite.com". When given, the source is probed too: unreachable or bot-blocked (403/429) is a BLOCKER because an import from a host that refuses our requests produces empty pages, and whether /wp-json answers is reported as a warning (without it the importer must scrape HTML and loses ACF/custom fields).'),
|
|
41
|
+
compositions: z
|
|
42
|
+
.array(z.object({
|
|
43
|
+
id: z.string().optional(),
|
|
44
|
+
name: z.string().min(1),
|
|
45
|
+
fields: z.array(z.object({ name: z.string().min(1), type: z.string().optional() })).optional(),
|
|
46
|
+
blocks: z.array(z.any()),
|
|
47
|
+
business_specific_block_types: z.array(z.string()).optional(),
|
|
48
|
+
}))
|
|
49
|
+
.min(1)
|
|
50
|
+
.max(50)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Proposed block compositions to review before build. Include fields when you want missing schema bindings detected. Explicitly list only genuinely business-specific custom block types.'),
|
|
41
53
|
},
|
|
42
54
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
43
|
-
const res =
|
|
55
|
+
const res = args.compositions
|
|
56
|
+
? await client.post(siteId, 'migration-preflight', {
|
|
57
|
+
...(args.source_url ? { source_url: args.source_url } : {}),
|
|
58
|
+
compositions: args.compositions,
|
|
59
|
+
})
|
|
60
|
+
: await client.get(siteId, 'migration-preflight', args.source_url ? { source_url: args.source_url } : undefined);
|
|
44
61
|
return ok(res);
|
|
45
62
|
}),
|
|
46
63
|
},
|
package/dist/tools/pages.js
CHANGED
|
@@ -107,14 +107,13 @@ export const pageTools = [
|
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
name: 'update_page',
|
|
110
|
-
description: 'Shallow-merge update on a page (only the fields you pass change). BUFFER MODEL: content fields land in the page\'s unsaved DRAFT (working copy) — invisible to deploys and default previews until saved; `status`/`date_published` apply immediately. Pass save:true to commit in the same call, or commit_working_copy later after the user approves. Returns the draft view of the page. For "replace this page entirely", use replace_page.
|
|
110
|
+
description: 'Shallow-merge update on a page (only the fields you pass change). BUFFER MODEL: content fields land in the page\'s unsaved DRAFT (working copy) — invisible to deploys and default previews until saved; `status`/`date_published` apply immediately. Pass save:true to commit in the same call, or commit_working_copy later after the user approves. Returns the draft view of the page. For "replace this page entirely", use replace_page. This tool does not accept content_mode; switch modes with `set_page_mode`.',
|
|
111
111
|
inputSchema: {
|
|
112
112
|
page_id: z.string(),
|
|
113
113
|
patch: z
|
|
114
114
|
.object({
|
|
115
115
|
title: z.string().optional(),
|
|
116
116
|
slug: z.string().optional(),
|
|
117
|
-
content_mode: z.enum(['blocks', 'html']).optional().describe('Changing this raw skips revision snapshotting — for safe switches use set_page_mode.'),
|
|
118
117
|
html_content: z.string().optional(),
|
|
119
118
|
blocks: z.array(z.any()).optional().describe('Block tree, only used when content_mode="blocks".'),
|
|
120
119
|
status: z.enum(['draft', 'review', 'unlisted', 'published']).optional(),
|
|
@@ -177,7 +176,7 @@ export const pageTools = [
|
|
|
177
176
|
},
|
|
178
177
|
{
|
|
179
178
|
name: 'batch_update_pages',
|
|
180
|
-
description: 'Apply per-page patches in one call (up to 200 entries). Each entry is { page_id, patch, save? }; failures are reported per-row, the rest still apply. BUFFER MODEL: content patches land in each page\'s unsaved draft; per-entry save:true (or the top-level save flag) commits — typical for a user-approved sweep.',
|
|
179
|
+
description: 'Apply per-page patches in one call (up to 200 entries). Each entry is { page_id, patch, save? }; failures are reported per-row, the rest still apply. BUFFER MODEL: content patches land in each page\'s unsaved draft; per-entry save:true (or the top-level save flag) commits — typical for a user-approved sweep. content_mode is rejected per-row; switch each page with set_page_mode.',
|
|
181
180
|
inputSchema: {
|
|
182
181
|
updates: z
|
|
183
182
|
.array(z.object({
|
package/dist/tools/partials.js
CHANGED
|
@@ -22,7 +22,7 @@ export const partialTools = [
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
name: 'read_partial',
|
|
25
|
-
description: 'Fetch one global block including its
|
|
25
|
+
description: 'Fetch one global block including its content_mode and the active HTML or block tree.',
|
|
26
26
|
inputSchema: {
|
|
27
27
|
partial_id: z.string(),
|
|
28
28
|
version: versionParam,
|
|
@@ -34,13 +34,14 @@ export const partialTools = [
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
name: 'update_partial',
|
|
37
|
-
description: 'Shallow-merge update on a global block. Use partial_id "header" or "footer" for the auto-injected layout blocks, or a kebab-case id for a free block. HTML is sanitized server-side. BUFFER MODEL: content lands in the partial\'s unsaved DRAFT (status applies immediately); pass save:true to commit in the same call, or commit_working_copy later.',
|
|
37
|
+
description: 'Shallow-merge update on a global block. Use partial_id "header" or "footer" for the auto-injected layout blocks, or a kebab-case id for a free block. HTML is sanitized server-side. This tool does not accept content_mode; switch it with set_partial_mode. BUFFER MODEL: content lands in the partial\'s unsaved DRAFT (status applies immediately); pass save:true to commit in the same call, or commit_working_copy later.',
|
|
38
38
|
inputSchema: {
|
|
39
39
|
partial_id: z.string(),
|
|
40
40
|
patch: z
|
|
41
41
|
.object({
|
|
42
42
|
name: z.string().optional(),
|
|
43
43
|
html_content: z.string().optional(),
|
|
44
|
+
blocks: z.array(z.any()).optional().describe('Block tree to stage before switching the partial to blocks mode.'),
|
|
44
45
|
status: z.enum(['draft', 'published']).optional(),
|
|
45
46
|
kind: z.enum(['header', 'footer', 'free']).optional(),
|
|
46
47
|
})
|
|
@@ -53,6 +54,20 @@ export const partialTools = [
|
|
|
53
54
|
return ok(res);
|
|
54
55
|
}),
|
|
55
56
|
},
|
|
57
|
+
{
|
|
58
|
+
name: 'set_partial_mode',
|
|
59
|
+
description: 'Switch a header, footer, or free partial between HTML and native blocks through the revision-safe mode endpoint. To author a native partial deterministically: update_partial with blocks + save:true, then set_partial_mode to="blocks", then read_partial and verify the returned mode/tree. Set convert:true only for heuristic HTML-to-block conversion.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
partial_id: z.string(),
|
|
62
|
+
to: z.enum(['blocks', 'html']),
|
|
63
|
+
convert: z.boolean().optional(),
|
|
64
|
+
version: versionParam,
|
|
65
|
+
},
|
|
66
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
67
|
+
const res = await client.post(siteId, `partials/${encodeURIComponent(args.partial_id)}/mode`, { to: args.to, convert: args.convert ?? false }, v(args.version));
|
|
68
|
+
return ok(res);
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
56
71
|
{
|
|
57
72
|
name: 'replace_partial',
|
|
58
73
|
description: 'Full replace of a global block (PUT). Pass html_content (and optionally name/status) directly — no wrapper object needed. ' +
|
package/dist/tools/settings.js
CHANGED
|
@@ -74,6 +74,14 @@ export const settingsTools = [
|
|
|
74
74
|
.passthrough()
|
|
75
75
|
.optional(),
|
|
76
76
|
social: z.record(z.string()).optional(),
|
|
77
|
+
cookie_consent: z.object({
|
|
78
|
+
enabled: z.boolean().optional(),
|
|
79
|
+
text: z.string().optional().describe('Localized consent copy. May include a privacy-policy link.'),
|
|
80
|
+
privacy_policy_url: z.string().optional(),
|
|
81
|
+
scripts_necessary: z.string().optional().describe('Trusted script markup that runs before consent.'),
|
|
82
|
+
scripts_optional: z.string().optional().describe('Trusted script markup activated only after full consent.'),
|
|
83
|
+
reload_after_consent: z.boolean().optional(),
|
|
84
|
+
}).optional().describe('Native consent banner configuration. Partial updates preserve omitted fields. Script fields execute in visitor browsers and are accepted under the API key authority.'),
|
|
77
85
|
},
|
|
78
86
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
79
87
|
const { version, ...body } = args;
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Model Context Protocol server for the Typeroll public API. Use with Claude Code or any MCP-compatible client to manage a Typeroll site.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/skills/tr-blog.md
CHANGED
|
@@ -13,7 +13,11 @@ description: Use when the user wants to set up a blog, news section, podcast fee
|
|
|
13
13
|
> `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
A blog in Typeroll is a **collection with `
|
|
16
|
+
A blog in Typeroll is a **collection with `item_template_blocks` +
|
|
17
|
+
`route_template`**. Every published item materialises as its own static page at
|
|
18
|
+
build time — there is **no need to call `create_page` per article**. Start with
|
|
19
|
+
the native `article` preset; the listing is a block-mode page with
|
|
20
|
+
`core/collection_list` and updates at build time.
|
|
17
21
|
|
|
18
22
|
If you find yourself about to create 20 pages for 20 articles, stop — you're using the old pattern. The recipe below is the right one.
|
|
19
23
|
|
|
@@ -25,7 +29,7 @@ If you find yourself about to create 20 pages for 20 articles, stop — you're u
|
|
|
25
29
|
|
|
26
30
|
## Recipe
|
|
27
31
|
|
|
28
|
-
### 1. Create the collection with
|
|
32
|
+
### 1. Create the collection with a native item composition
|
|
29
33
|
|
|
30
34
|
```
|
|
31
35
|
create_collection {
|
|
@@ -34,27 +38,25 @@ create_collection {
|
|
|
34
38
|
"label_plural": "Artiklar",
|
|
35
39
|
"icon": "📝",
|
|
36
40
|
"slug_field": "slug",
|
|
37
|
-
"sort_field": "
|
|
41
|
+
"sort_field": "published_at",
|
|
38
42
|
"sort_dir": "desc",
|
|
39
43
|
"route_template": "/blog/{slug}",
|
|
40
|
-
"
|
|
44
|
+
"template_kind": "article",
|
|
41
45
|
"fields": [
|
|
42
46
|
{"name": "title", "type": "text", "label": "Rubrik", "required": true},
|
|
43
47
|
{"name": "slug", "type": "text", "label": "URL-slug", "required": true},
|
|
44
|
-
{"name": "
|
|
48
|
+
{"name": "published_at", "type": "date", "label": "Datum", "required": true},
|
|
45
49
|
{"name": "author", "type": "text", "label": "Författare"},
|
|
46
50
|
{"name": "excerpt", "type": "textarea", "label": "Ingress"},
|
|
47
51
|
{"name": "body", "type": "richtext", "label": "Brödtext"},
|
|
48
|
-
{"name": "
|
|
52
|
+
{"name": "featured_image", "type": "image", "label": "Omslagsbild"}
|
|
49
53
|
]
|
|
50
54
|
}
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- `{{#field}}...{{/field}}` is a conditional — render the block only if the field is truthy. Useful for optional images, authors, etc.
|
|
57
|
-
- **No loops, no nested conditionals.** If you need either, pre-render the HTML in a field on the item itself (see tr-collection-template for patterns).
|
|
57
|
+
The preset writes an ordinary, editable `item_template_blocks` tree with
|
|
58
|
+
breadcrumbs, title/date, body, and a server-rendered outline. Localize labels
|
|
59
|
+
or change field mappings in the collection template editor.
|
|
58
60
|
|
|
59
61
|
**Field name rule:** ASCII only, lowercase, `[a-z][a-z0-9_-]*`. `ä→a`, `ö→o`, `å→a` for the `name`; the `label` can be anything.
|
|
60
62
|
|
|
@@ -64,84 +66,46 @@ create_collection {
|
|
|
64
66
|
create_collection_item collection="blog" status="published" fields={
|
|
65
67
|
"title": "Vår designfilosofi",
|
|
66
68
|
"slug": "var-designfilosofi",
|
|
67
|
-
"
|
|
69
|
+
"published_at": "2025-05-15",
|
|
68
70
|
"author": "Anna Lindström",
|
|
69
71
|
"excerpt": "Vi tror på enkelhet med syfte — varje beslut ska kunna motiveras.",
|
|
70
72
|
"body": "<p>Lång brödtext här...</p><h2>En underrubrik</h2><p>Mer text...</p>",
|
|
71
|
-
"
|
|
73
|
+
"featured_image": "https://cdn.typeroll.com/..."
|
|
72
74
|
}
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
If `
|
|
77
|
+
If `featured_image` is a URL from elsewhere, upload it first via `upload_media_from_url` and use the returned CDN URL.
|
|
76
78
|
|
|
77
79
|
Each published item with this collection's `route_template` automatically becomes `/blog/{slug}` at deploy time — you do **not** need to call `create_page`.
|
|
78
80
|
|
|
79
|
-
### 3. Build the listing page
|
|
80
|
-
|
|
81
|
-
Create a single page that hosts the listing. The HTML between the `typeroll:listing` markers gets regenerated whenever the collection changes:
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
create_page title="Artiklar" slug="blog" status="published" content_mode="html"
|
|
85
|
-
html_content="<section class=\"blog-listing\">
|
|
86
|
-
<div class=\"container\">
|
|
87
|
-
<h1 class=\"section-title\">Artiklar</h1>
|
|
88
|
-
<!-- typeroll:listing:blog -->
|
|
89
|
-
<!-- /typeroll:listing:blog -->
|
|
90
|
-
</div>
|
|
91
|
-
</section>
|
|
92
|
-
<style>
|
|
93
|
-
.blog-listing{padding:4rem 0}
|
|
94
|
-
.blog-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:2rem;margin-top:2rem}
|
|
95
|
-
.blog-card{border:1px solid var(--color-surface);border-radius:0.5rem;overflow:hidden}
|
|
96
|
-
.blog-card a{text-decoration:none;display:block;color:var(--color-text)}
|
|
97
|
-
.blog-card img{width:100%;aspect-ratio:16/9;object-fit:cover}
|
|
98
|
-
.blog-card__body{padding:1.5rem}
|
|
99
|
-
.blog-card__date{font-size:0.8rem;color:var(--color-text-light);display:block;margin-bottom:0.5rem}
|
|
100
|
-
.blog-card__title{font-family:var(--font-heading);font-size:1.25rem;margin-bottom:0.5rem}
|
|
101
|
-
.blog-card__excerpt{color:var(--color-text-light);font-size:0.9rem;margin-bottom:1rem}
|
|
102
|
-
.blog-card__cta{color:var(--color-accent);font-size:0.85rem;font-weight:600}
|
|
103
|
-
</style>"
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### 4. Populate the listing (and re-run after every change)
|
|
81
|
+
### 3. Build the native listing page once
|
|
107
82
|
|
|
108
83
|
```
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<a href=\"{{url}}\">
|
|
114
|
-
{{#image}}<img src=\"{{image}}\" alt=\"{{title}}\">{{/image}}
|
|
115
|
-
<div class=\"blog-card__body\">
|
|
116
|
-
<time class=\"blog-card__date\">{{date}}</time>
|
|
117
|
-
<h2 class=\"blog-card__title\">{{title}}</h2>
|
|
118
|
-
<p class=\"blog-card__excerpt\">{{excerpt}}</p>
|
|
119
|
-
<span class=\"blog-card__cta\">Läs mer →</span>
|
|
120
|
-
</div>
|
|
121
|
-
</a>
|
|
122
|
-
</article>"
|
|
123
|
-
wrap_open="<div class=\"blog-grid\">"
|
|
124
|
-
wrap_close="</div>"
|
|
84
|
+
create_page title="Artiklar" slug="blog" status="published" content_mode="blocks" blocks=[
|
|
85
|
+
{"id":"articles-heading","type":"core/heading","data":{"text":"Artiklar","level":"h1","size":"auto","align":"left"}},
|
|
86
|
+
{"id":"articles-list","type":"core/collection_list","data":{"collection":"blog","sort_by":"published_at","sort_order":"desc","item_block":"core/post_card","layout":"grid","cols":3,"gap":"md"}}
|
|
87
|
+
]
|
|
125
88
|
```
|
|
126
89
|
|
|
127
|
-
|
|
90
|
+
The list resolves from current collection data on every preview/build. No
|
|
91
|
+
generated HTML marker or regeneration call is needed.
|
|
128
92
|
|
|
129
|
-
###
|
|
93
|
+
### 4. Update the header partial to link to the listing
|
|
130
94
|
|
|
131
95
|
```
|
|
132
96
|
read_partial partial_id="header"
|
|
133
97
|
replace_partial partial_id="header" html_content="<updated with /blog link>"
|
|
134
98
|
```
|
|
135
99
|
|
|
136
|
-
###
|
|
100
|
+
### 5. Preview a single article
|
|
137
101
|
|
|
138
102
|
```
|
|
139
103
|
get_preview_link collection_name="blog" item_id="<id>"
|
|
140
104
|
```
|
|
141
105
|
|
|
142
|
-
The returned URL renders the item
|
|
106
|
+
The returned URL renders the native item block tree from current database content.
|
|
143
107
|
|
|
144
|
-
###
|
|
108
|
+
### 6. Deploy
|
|
145
109
|
|
|
146
110
|
```
|
|
147
111
|
trigger_deploy
|
|
@@ -154,20 +118,22 @@ The build produces one HTML file per published article at `/blog/<slug>` plus th
|
|
|
154
118
|
|
|
155
119
|
```
|
|
156
120
|
create_collection_item collection="blog" status="published" fields={ ... }
|
|
157
|
-
regenerate_collection_listing collection="blog" page_id="blog" item_template="..." wrap_open="..." wrap_close="..."
|
|
158
121
|
trigger_deploy
|
|
159
122
|
```
|
|
160
123
|
|
|
161
|
-
|
|
124
|
+
Two calls. No per-article `create_page`, listing regeneration, or HTML diffing.
|
|
162
125
|
|
|
163
126
|
## Pitfalls
|
|
164
127
|
|
|
165
|
-
- **Don't fall back to "one page per article".**
|
|
166
|
-
|
|
128
|
+
- **Don't fall back to "one page per article".** Collection item routes keep
|
|
129
|
+
design, sitemap, preview, and nested URL handling centralized. `create_page`
|
|
130
|
+
rejects slashes in `slug`; use the collection's `route_template`.
|
|
131
|
+
- **Slugs must be unique within the collection.** An item whose route tokens
|
|
132
|
+
are missing cannot receive a static detail URL.
|
|
167
133
|
- **Don't use non-ASCII field names.** `datum` not `Datum`; `forfattare` not `författare` in the `name`. The `label` is free-form.
|
|
168
|
-
- **
|
|
169
|
-
|
|
170
|
-
-
|
|
134
|
+
- **Template too clever.** First compose current native blocks and run the
|
|
135
|
+
composition preflight. Use custom/HTML-backed behavior only when it is
|
|
136
|
+
genuinely business-specific, not to repair a generic Core gap.
|
|
171
137
|
|
|
172
138
|
## When you want a page that ISN'T a collection item
|
|
173
139
|
|
|
@@ -7,9 +7,46 @@ description: Use when building a rich per-item detail page for a Typeroll collec
|
|
|
7
7
|
|
|
8
8
|
Prefer `item_template_blocks` when the design fits the block system. It can
|
|
9
9
|
include `template/item_navigation`, whose previous/next URLs and titles are
|
|
10
|
-
derived from the collection's `sort_field` and `sort_dir
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
derived from the collection's `sort_field` and `sort_dir`. When imported
|
|
11
|
+
navigation deliberately differs, bind its explicit URL/title fields instead.
|
|
12
|
+
The native `article` and `checklist` `template_kind` presets already compose
|
|
13
|
+
breadcrumbs, selected rich text, server-rendered outlines, optional PDF, and
|
|
14
|
+
navigation. Start there.
|
|
15
|
+
|
|
16
|
+
For an archive page, place `core/collection_list` and map its card contract
|
|
17
|
+
through `item_overrides` instead of pre-rendering cards:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"type": "core/collection_list",
|
|
22
|
+
"data": {
|
|
23
|
+
"collection": "checklists",
|
|
24
|
+
"layout": "grid",
|
|
25
|
+
"cols": { "mobile": 1, "tablet": 2, "desktop": 3 },
|
|
26
|
+
"item_overrides": {
|
|
27
|
+
"title_field": "title",
|
|
28
|
+
"excerpt_field": "excerpt",
|
|
29
|
+
"image_field": "image",
|
|
30
|
+
"image_alt_field": "image_alt",
|
|
31
|
+
"href_field": "url",
|
|
32
|
+
"heading_level": "h2",
|
|
33
|
+
"download_url_field": "pdf_url",
|
|
34
|
+
"download_label": "Download PDF"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`core/post_card` omits its image when the mapped image field is empty and
|
|
41
|
+
keeps the title and download as separate links. A missing PDF similarly emits
|
|
42
|
+
no download action. Use `item_overrides.show_image: false` when the archive
|
|
43
|
+
design has no media at all.
|
|
44
|
+
|
|
45
|
+
Before using the HTML patterns below, submit the proposed composition through
|
|
46
|
+
`get_migration_readiness compositions=[…]`. If it reports a generic platform
|
|
47
|
+
gap, wait for native support rather than shipping a generic replacement block,
|
|
48
|
+
raw page HTML, or tenant CSS. The patterns below are for genuinely
|
|
49
|
+
business-specific structured behavior.
|
|
13
50
|
|
|
14
51
|
`item_template_html` uses lightweight Mustache substitution:
|
|
15
52
|
|