@typeroll/mcp-server 0.44.74 → 0.45.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.
@@ -1,305 +0,0 @@
1
- ---
2
- name: tr-collection-template
3
- description: Use when building a rich per-item detail page for a Typeroll collection — podcast episodes with audio players and chapter timestamps, case studies with guest cards and metric tiles, products with image galleries and spec tables, anything where the detail template would normally want loops or nested data. Covers the "pre-render into a field" pattern that gets you past the template's no-loops-no-conditionals limit.
4
- ---
5
-
6
- # Rich detail templates for collections
7
-
8
- Prefer `item_template_blocks` when the design fits the block system. It can
9
- include `template/item_navigation`, whose previous/next URLs and titles are
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.
50
-
51
- `item_template_html` uses lightweight Mustache substitution:
52
-
53
- - `{{field}}` — HTML-escaped value
54
- - `{{{field}}}` — raw value (for richtext / pre-rendered HTML)
55
- - `{{#field}}…{{/field}}` — conditional, render block when field is truthy
56
- - `{{url}}` — only meaningful in `regenerate_collection_listing`'s `item_template` (resolves through `route_template`)
57
-
58
- **No loops, no nested field access, no arithmetic.** `{{chapters[0].title}}` doesn't work. `{{#chapters}}{{title}}{{/chapters}}` doesn't either — the section syntax is truthiness-only, not iteration.
59
-
60
- The pattern that gets you everywhere: **pre-render the HTML into a single string field on the item itself.** The agent (you) does the loop in JavaScript/Python during data prep, then writes the resulting HTML into a richtext field like `chapters_html` or `gallery_html`. The template renders it raw with `{{{chapters_html}}}`.
61
-
62
- This skill catalogues the patterns we hit most often, with copy-paste recipes.
63
-
64
- ## Pattern 1 — Audio player + chapter list (podcast episodes)
65
-
66
- **Data shape going in:**
67
-
68
- ```json
69
- {
70
- "title": "Avsnitt 17 — Designsystem på riktigt",
71
- "slug": "17-designsystem-pa-riktigt",
72
- "date": "2025-05-15",
73
- "audio_url": "https://cdn.example.com/avsnitt-17.mp3",
74
- "duration_min": 42,
75
- "chapters": [
76
- { "time_seconds": 0, "title": "Intro" },
77
- { "time_seconds": 132, "title": "Vad är ett designsystem?" },
78
- { "time_seconds": 845, "title": "Tokens vs. komponenter" },
79
- { "time_seconds": 1820, "title": "Vanliga fällor" }
80
- ]
81
- }
82
- ```
83
-
84
- **Pre-render `chapters_html` before calling `create_collection_item`:**
85
-
86
- ```js
87
- const formatTime = (s) =>
88
- s < 3600
89
- ? `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`
90
- : `${Math.floor(s/3600)}:${String(Math.floor(s%3600/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
91
-
92
- const chaptersHtml = `
93
- <ol class="chapters">
94
- ${item.chapters.map(c => `
95
- <li>
96
- <button type="button" data-jump-to="${c.time_seconds}">
97
- <span class="chapters__time">${formatTime(c.time_seconds)}</span>
98
- <span class="chapters__title">${escapeHtml(c.title)}</span>
99
- </button>
100
- </li>
101
- `).join('')}
102
- </ol>`;
103
- ```
104
-
105
- **Schema (note `chapters_html: richtext` — it carries HTML):**
106
-
107
- ```
108
- create_collection {
109
- "name": "avsnitt",
110
- "label_singular": "Avsnitt",
111
- "label_plural": "Avsnitt",
112
- "slug_field": "slug",
113
- "sort_field": "date",
114
- "sort_dir": "desc",
115
- "route_template": "/podd/{slug}",
116
- "fields": [
117
- {"name":"title", "type":"text", "required":true},
118
- {"name":"slug", "type":"text", "required":true},
119
- {"name":"date", "type":"date", "required":true},
120
- {"name":"audio_url", "type":"text", "required":true},
121
- {"name":"duration_min", "type":"number"},
122
- {"name":"excerpt", "type":"textarea"},
123
- {"name":"body", "type":"richtext"},
124
- {"name":"chapters_html", "type":"richtext", "label":"Kapitellista (genereras)"}
125
- ],
126
- "item_template_html": "<article class=\"episode\">\n <header class=\"episode__hero\">\n <p class=\"episode__date\">{{date}} · {{duration_min}} min</p>\n <h1>{{title}}</h1>\n <p class=\"episode__excerpt\">{{excerpt}}</p>\n </header>\n <div class=\"episode__player\">\n <audio controls preload=\"metadata\" src=\"{{audio_url}}\"></audio>\n </div>\n {{#chapters_html}}<section class=\"episode__chapters\"><h2>Kapitel</h2>{{{chapters_html}}}</section>{{/chapters_html}}\n <section class=\"episode__notes\">{{{body}}}</section>\n <script>\n document.querySelectorAll('[data-jump-to]').forEach(b => {\n b.addEventListener('click', () => {\n const a = document.querySelector('audio');\n if (a) { a.currentTime = Number(b.dataset.jumpTo); a.play(); }\n });\n });\n </script>\n <style>\n .episode{max-width:42rem;margin:3rem auto;padding:0 1rem}\n .episode__hero{background:linear-gradient(135deg,var(--color-primary),var(--color-accent));color:#fff;padding:3rem 2rem;border-radius:1rem;margin-bottom:2rem}\n .episode__date{opacity:0.85;font-size:0.85rem}\n .episode__hero h1{font-family:var(--font-heading);font-size:2rem;margin:0.5rem 0}\n .episode__player audio{width:100%}\n .chapters{list-style:none;padding:0;margin:1.5rem 0}\n .chapters li{margin:0.25rem 0}\n .chapters button{display:flex;gap:1rem;width:100%;background:transparent;border:0;padding:0.5rem 0.75rem;cursor:pointer;text-align:left;border-radius:0.375rem;font:inherit;color:inherit}\n .chapters button:hover{background:var(--color-surface)}\n .chapters__time{font-variant-numeric:tabular-nums;color:var(--color-text-light);min-width:4ch}\n </style>\n</article>"
127
- }
128
- ```
129
-
130
- **Create the item with both raw chapters AND the pre-rendered HTML:**
131
-
132
- ```
133
- create_collection_item collection="avsnitt" status="published" fields={
134
- "title": "Avsnitt 17 — Designsystem på riktigt",
135
- "slug": "17-designsystem-pa-riktigt",
136
- "date": "2025-05-15",
137
- "audio_url": "https://cdn.example.com/avsnitt-17.mp3",
138
- "duration_min": 42,
139
- "excerpt": "Vi pratar med...",
140
- "body": "<p>...</p>",
141
- "chapters_html": "<ol class=\"chapters\">...</ol>"
142
- }
143
- ```
144
-
145
- The raw `chapters` array doesn't need to be stored unless you have a use for it (e.g. regenerating the HTML later from a structured source). If you do want it for round-trip editing, add a `chapters_json: textarea` field and stringify the array into it.
146
-
147
- ## Pattern 2 — Guest card with nested fields
148
-
149
- Each episode features a guest with a name, role, photo, and external links. Mustache can't reach into nested objects, so flatten OR pre-render.
150
-
151
- **Option A: Flatten into prefixed fields (preferable when there's ≤1 guest):**
152
-
153
- ```
154
- fields: [
155
- ...,
156
- {"name":"guest_name", "type":"text"},
157
- {"name":"guest_role", "type":"text"},
158
- {"name":"guest_photo", "type":"image"},
159
- {"name":"guest_bio", "type":"textarea"},
160
- {"name":"guest_linkedin", "type":"text"},
161
- {"name":"guest_website", "type":"text"}
162
- ]
163
- ```
164
-
165
- In the template:
166
-
167
- ```html
168
- {{#guest_name}}
169
- <aside class="guest">
170
- {{#guest_photo}}<img src="{{guest_photo}}" alt="{{guest_name}}">{{/guest_photo}}
171
- <div>
172
- <h3>{{guest_name}}</h3>
173
- <p class="guest__role">{{guest_role}}</p>
174
- <p>{{guest_bio}}</p>
175
- <p class="guest__links">
176
- {{#guest_linkedin}}<a href="{{guest_linkedin}}">LinkedIn</a>{{/guest_linkedin}}
177
- {{#guest_website}}<a href="{{guest_website}}">Webbplats</a>{{/guest_website}}
178
- </p>
179
- </div>
180
- </aside>
181
- {{/guest_name}}
182
- ```
183
-
184
- **Option B: Pre-render `guest_html` (when there are multiple guests or arbitrary depth):**
185
-
186
- ```js
187
- const guestHtml = item.guests.map(g => `
188
- <article class="guest">
189
- ${g.photo ? `<img src="${escapeHtml(g.photo)}" alt="${escapeHtml(g.name)}">` : ''}
190
- <div>
191
- <h3>${escapeHtml(g.name)}</h3>
192
- <p class="guest__role">${escapeHtml(g.role)}</p>
193
- ${g.links.map(l => `<a href="${escapeHtml(l.url)}">${escapeHtml(l.label)}</a>`).join(' · ')}
194
- </div>
195
- </article>
196
- `).join('');
197
- ```
198
-
199
- Then `{{{guests_html}}}` in the template.
200
-
201
- ## Pattern 3 — Gradient hero with computed colours
202
-
203
- The hero needs a colour pair derived from a single brand colour the user picked per item. The template can't compute — pre-compute and pass as fields:
204
-
205
- ```js
206
- function shade(hex, amount) { /* lighten/darken */ }
207
-
208
- const item = {
209
- ...,
210
- hero_from: rawColor,
211
- hero_to: shade(rawColor, -0.2),
212
- };
213
- ```
214
-
215
- Template:
216
-
217
- ```html
218
- <header class="hero" style="background:linear-gradient(135deg, {{hero_from}}, {{hero_to}})">
219
- <h1>{{title}}</h1>
220
- </header>
221
- ```
222
-
223
- Inline style with two substituted hex strings — works because the `{{}}` substitutions sit inside a CSS value, not as a CSS variable name. (Don't do this with user-supplied colours that haven't been validated — a malicious item could break out of the style attribute. For agent-curated colours this is fine.)
224
-
225
- ## Pattern 4 — Image gallery with thumbnails
226
-
227
- Same drill. The agent renders the gallery HTML when shaping the item:
228
-
229
- ```js
230
- const galleryHtml = `
231
- <div class="gallery">
232
- ${item.images.map((img, i) => `
233
- <a href="${escapeHtml(img.full)}" class="gallery__item">
234
- <img src="${escapeHtml(img.thumb)}" alt="${escapeHtml(img.alt || `Bild ${i+1}`)}" loading="lazy">
235
- </a>
236
- `).join('')}
237
- </div>`;
238
- ```
239
-
240
- Schema gains `gallery_html: richtext`. Template renders `{{{gallery_html}}}`.
241
-
242
- For a lightbox you can either inline a tiny vanilla JS handler in the item_template_html (works once per page load) or `tr-images` the gallery into a reusable partial.
243
-
244
- ## Pattern 5 — Spec table (for products, services, etc.)
245
-
246
- For a fixed set of spec fields (price, dimensions, in-stock, lead time), just add the fields explicitly:
247
-
248
- ```
249
- fields: [
250
- ...,
251
- {"name":"price_sek", "type":"number"},
252
- {"name":"weight_g", "type":"number"},
253
- {"name":"in_stock", "type":"boolean"},
254
- {"name":"lead_days", "type":"number"}
255
- ]
256
- ```
257
-
258
- ```html
259
- <dl class="specs">
260
- {{#price_sek}}<dt>Pris</dt><dd>{{price_sek}} kr</dd>{{/price_sek}}
261
- {{#weight_g}}<dt>Vikt</dt><dd>{{weight_g}} g</dd>{{/weight_g}}
262
- <dt>Lagerstatus</dt><dd>{{#in_stock}}I lager{{/in_stock}}{{^in_stock}}Slut{{/in_stock}}</dd>
263
- {{#lead_days}}<dt>Leveranstid</dt><dd>{{lead_days}} dagar</dd>{{/lead_days}}
264
- </dl>
265
- ```
266
-
267
- (`{{^field}}…{{/field}}` is the inverse of `{{#field}}` — render when falsy.)
268
-
269
- For variable specs (different products have different attributes), fall back to a pre-rendered `specs_html` field.
270
-
271
- ## Pre-rendering helpers — minimum viable
272
-
273
- Every pre-render needs `escapeHtml`. Put this at the top of your data-prep script:
274
-
275
- ```js
276
- const escapeHtml = (s) =>
277
- String(s ?? '')
278
- .replace(/&/g, '&amp;')
279
- .replace(/</g, '&lt;')
280
- .replace(/>/g, '&gt;')
281
- .replace(/"/g, '&quot;')
282
- .replace(/'/g, '&#39;');
283
- ```
284
-
285
- Skip it only when you're certain the value can't carry user-supplied content (your own constants are fine; anything from a scrape, the user, or a model output goes through `escapeHtml`).
286
-
287
- ## When to flatten vs pre-render
288
-
289
- Rough guideline:
290
-
291
- | Situation | Approach |
292
- |---|---|
293
- | 1–N optional related fields, fixed shape | Flatten into prefixed fields (`guest_name`, `guest_role`, …) and use `{{#field}}` conditionals |
294
- | List of items with internal structure (chapters, gallery, related-links) | Pre-render to a single `*_html` field |
295
- | Computed values (formatted dates, derived colours, totals) | Pre-compute as a sibling field, substitute with `{{}}` |
296
- | Conditional sections based on multiple fields ("show this when status=published AND has_video") | Pre-compute a boolean field; conditional in the template |
297
- | Genuinely dynamic content that changes per visitor | Doesn't fit — the static template renders once at build. Move to client-side JS in the template body, or rethink the page. |
298
-
299
- ## Pitfalls
300
-
301
- - **Forgetting `{{{ }}}` for pre-rendered HTML.** `{{chapters_html}}` (double braces) HTML-escapes the angle brackets and shows source code. Must be triple braces.
302
- - **Mutating a published item's schema.** Removing or renaming a field that the template references silently produces empty sections. Keep templates in sync with schema changes.
303
- - **Pre-rendered HTML drifts when you change the visual design.** The HTML for `chapters_html` was generated against the design as it was on import day. If you redo the look later, you need to re-prep + re-write every item's pre-rendered field, not just the template. Consider keeping the raw data (`chapters_json: textarea` with the original array stringified) so you can regenerate.
304
- - **Inline `<script>` in `item_template_html` runs once per page.** That's fine for self-contained per-page widgets (the audio chapter-jumper above). If two collections need the same widget, factor it into a partial that both `item_template_html`s `<x-include>`.
305
- - **Don't put credentials in pre-rendered HTML.** API keys, signed tokens — they go into the build output and end up on the public web. Run any prep step you wouldn't paste into a public Gist with that in mind.