@typeroll/mcp-server 0.7.7 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -13
- package/dist/index.js +15 -65
- package/dist/server.js +129 -0
- package/package.json +7 -1
- package/skills/README.md +9 -6
- package/skills/tr-blog.md +91 -97
- package/skills/tr-collection-template.md +262 -0
- package/skills/tr-migrate-astro.md +278 -0
- package/skills/tr-page-template.md +168 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tr-migrate-astro
|
|
3
|
+
description: Use when the user wants to migrate an Astro site — particularly an Astro Content Collections-backed site — to Typeroll. Walks `src/content/<collection>/*.md(x)`, lifts frontmatter into Typeroll collection schemas, converts markdown bodies into richtext fields, batch-imports items, then maps `src/pages/*` into Typeroll pages/partials. Triggers on "migrate an Astro site", "import from src/content", "convert content collections", or when the user names a local Astro repo as the source.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Migrate an Astro site to Typeroll
|
|
7
|
+
|
|
8
|
+
Astro's [Content Collections](https://docs.astro.build/en/guides/content-collections/) and Typeroll's `Collection` + `CollectionItem` map one-to-one. A collection in Astro is a directory of frontmatter-bearing files under `src/content/<name>/`; in Typeroll it's a schema + items doc set under `organizations/{org}/sites/{site}/collections/{name}/`. The Astro schema (`zod.object(...)` in `src/content/config.ts`) is your field list. The frontmatter values are field values. The markdown bodies are richtext fields.
|
|
9
|
+
|
|
10
|
+
This skill walks the migration from a checked-out Astro repo on the user's machine to a target Typeroll site. You run it locally — the source repo is on disk, the MCP just receives the final shape.
|
|
11
|
+
|
|
12
|
+
## Preconditions
|
|
13
|
+
|
|
14
|
+
- The Astro repo is checked out locally and `npm install`d (so we can read `src/content/config.ts` to extract the schemas).
|
|
15
|
+
- `@typeroll/mcp-server` configured with a valid `TYPEROLL_API_KEY` pointing at the target site (or org-scoped key + a `site_id` argument per call).
|
|
16
|
+
- Target Typeroll site exists. **Empty starter site is best.** If non-empty, treat existing pages and collections as off-limits unless the user explicitly says otherwise.
|
|
17
|
+
- The Astro design / theme is **not** being migrated — Typeroll has its own design layer. We migrate content; the user re-skins on the Typeroll side.
|
|
18
|
+
|
|
19
|
+
## Recipe
|
|
20
|
+
|
|
21
|
+
### 1. Map the source
|
|
22
|
+
|
|
23
|
+
From the Astro repo root:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ls src/content/ # which collections exist?
|
|
27
|
+
cat src/content/config.ts # collection schemas (zod)
|
|
28
|
+
ls src/pages/ # standalone pages
|
|
29
|
+
ls public/ # static assets and images
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Build a working manifest:
|
|
33
|
+
|
|
34
|
+
| Astro source | Typeroll target |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `src/content/blog/*.md` | Collection `blog` with items |
|
|
37
|
+
| `src/content/projects/*.mdx` | Collection `projects` with items |
|
|
38
|
+
| `src/pages/about.astro` | Page with slug `about` |
|
|
39
|
+
| `src/pages/services/[slug].astro` | If dynamic from a collection → that collection's `route_template`. If genuinely per-page → individual Typeroll pages. |
|
|
40
|
+
| `public/og/*.png`, `public/images/*` | Upload to Typeroll media via `upload_media_from_url` (after staging them on a temporary public URL) or via local upload if the MCP supports it. |
|
|
41
|
+
| `src/layouts/*.astro` | Header / footer / shared chunks → Typeroll partials. The rest of the layout is the site design, owned by the target site. |
|
|
42
|
+
| `src/components/*.astro` | Either become partials (if reused across pages) or get inlined into the page that uses them. |
|
|
43
|
+
|
|
44
|
+
### 2. Learn the target's design (don't skip)
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
get_site
|
|
48
|
+
read_site_settings # colours, fonts, voice
|
|
49
|
+
list_partials
|
|
50
|
+
read_partial partial_id="header"
|
|
51
|
+
list_pages limit=5
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Same rule as in `tr-migrate-wp`: you're moving content into the *target's* visual language, not preserving the source's. Note the existing fonts, colour vars, header structure.
|
|
55
|
+
|
|
56
|
+
### 3. Translate one collection schema
|
|
57
|
+
|
|
58
|
+
Pick the most representative collection first (usually `blog`). Read its zod schema:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// src/content/config.ts
|
|
62
|
+
const blog = defineCollection({
|
|
63
|
+
type: 'content',
|
|
64
|
+
schema: z.object({
|
|
65
|
+
title: z.string(),
|
|
66
|
+
description: z.string(),
|
|
67
|
+
pubDate: z.coerce.date(),
|
|
68
|
+
updatedDate: z.coerce.date().optional(),
|
|
69
|
+
heroImage: z.string().optional(),
|
|
70
|
+
author: z.string().default('Editorial'),
|
|
71
|
+
tags: z.array(z.string()).default([]),
|
|
72
|
+
draft: z.boolean().default(false),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Map zod types to Typeroll field types:
|
|
78
|
+
|
|
79
|
+
| Astro zod | Typeroll field type |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `z.string()` | `text` (or `textarea` if it's a description/excerpt — judge by typical length) |
|
|
82
|
+
| `z.string().long()` / a description field | `textarea` |
|
|
83
|
+
| `z.coerce.date()` / `z.date()` | `date` |
|
|
84
|
+
| `z.number()` | `number` |
|
|
85
|
+
| `z.boolean()` | `boolean` |
|
|
86
|
+
| `z.string()` with image path / `image()` helper | `image` |
|
|
87
|
+
| `z.array(z.string())` | `text` (comma-joined) or `tags` if you have a tags field type |
|
|
88
|
+
| `z.enum([...])` | `text` with a comment about the allowed values; the model writes the listing logic |
|
|
89
|
+
| `z.object({...})` (nested) | Flatten into prefixed fields, or pre-render into a `*_html` field (see `tr-collection-template`) |
|
|
90
|
+
| Markdown body | `body: richtext` (the markdown content of the file, converted to HTML — see §4) |
|
|
91
|
+
|
|
92
|
+
Create the collection with the design template baked in (this is the new pattern — read `tr-blog` if you haven't yet):
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
create_collection {
|
|
96
|
+
"name": "blog",
|
|
97
|
+
"label_singular": "Article",
|
|
98
|
+
"label_plural": "Articles",
|
|
99
|
+
"slug_field": "slug",
|
|
100
|
+
"sort_field": "date",
|
|
101
|
+
"sort_dir": "desc",
|
|
102
|
+
"route_template": "/blog/{slug}",
|
|
103
|
+
"fields": [
|
|
104
|
+
{"name":"title", "type":"text", "required":true},
|
|
105
|
+
{"name":"slug", "type":"text", "required":true},
|
|
106
|
+
{"name":"description", "type":"textarea"},
|
|
107
|
+
{"name":"date", "type":"date", "required":true},
|
|
108
|
+
{"name":"updated_date","type":"date"},
|
|
109
|
+
{"name":"hero_image", "type":"image"},
|
|
110
|
+
{"name":"author", "type":"text"},
|
|
111
|
+
{"name":"tags", "type":"text"},
|
|
112
|
+
{"name":"body", "type":"richtext"}
|
|
113
|
+
],
|
|
114
|
+
"item_template_html": "<article class=\"post\">...</article>"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Field-name rules: ASCII, lowercase, `[a-z][a-z0-9_-]*`. The Astro source `pubDate` → Typeroll `date`. `updatedDate` → `updated_date` (snake_case is fine; camelCase isn't).
|
|
119
|
+
|
|
120
|
+
### 4. Convert markdown bodies → richtext
|
|
121
|
+
|
|
122
|
+
Astro stores the markdown body as the file content after the `---` frontmatter fence. Typeroll's `body: richtext` wants HTML. Pick one of:
|
|
123
|
+
|
|
124
|
+
**Option A — use Astro's own markdown renderer (preferred when the repo already builds):**
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { unified } from 'unified';
|
|
128
|
+
import remarkParse from 'remark-parse';
|
|
129
|
+
import remarkRehype from 'remark-rehype';
|
|
130
|
+
import rehypeStringify from 'rehype-stringify';
|
|
131
|
+
|
|
132
|
+
const md2html = async (md) => {
|
|
133
|
+
const file = await unified()
|
|
134
|
+
.use(remarkParse)
|
|
135
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
136
|
+
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
137
|
+
.process(md);
|
|
138
|
+
return String(file);
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Run this on the body of every collection file. Image references inside the markdown (``) need their `src` rewritten to the uploaded Typeroll CDN URLs after step 6.
|
|
143
|
+
|
|
144
|
+
**Option B — convert ad-hoc:** `marked`, `markdown-it`, or any other parser. Just stay consistent across files so the HTML output looks uniform.
|
|
145
|
+
|
|
146
|
+
Typeroll's sanitiser will strip `<script>` and event handlers from the output regardless. If the markdown carried embedded raw HTML you want to keep (iframes for video, etc.), check the sanitiser config in `packages/site-template/src/lib/sanitize.ts` for the whitelist.
|
|
147
|
+
|
|
148
|
+
### 5. Resolve and upload images
|
|
149
|
+
|
|
150
|
+
For every image referenced by an item (`heroImage`, images inside the markdown body):
|
|
151
|
+
|
|
152
|
+
1. Stage the local file at a temporary public URL (or use a local-upload MCP tool if one is configured).
|
|
153
|
+
2. `upload_media_from_url url=<staged-url> alt=<from frontmatter or filename>` — record the returned CDN URL.
|
|
154
|
+
3. Substitute the CDN URL into both the `hero_image` field value AND the markdown-converted HTML body (regex replace `src=` references).
|
|
155
|
+
|
|
156
|
+
Keep a local map (`./astro-migration-state.json`):
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"media": {
|
|
161
|
+
"./hero.png": "https://cdn.typeroll.com/<orgId>/<siteId>/abc123.png"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
So a partial run is resumable and you don't re-upload the same image twice.
|
|
167
|
+
|
|
168
|
+
### 6. Batch-import items
|
|
169
|
+
|
|
170
|
+
For each file in `src/content/<collection>/`:
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import fs from 'node:fs';
|
|
174
|
+
import path from 'node:path';
|
|
175
|
+
import matter from 'gray-matter';
|
|
176
|
+
|
|
177
|
+
const files = fs.readdirSync('src/content/blog').filter(f => /\.mdx?$/.test(f));
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const raw = fs.readFileSync(`src/content/blog/${file}`, 'utf8');
|
|
180
|
+
const { data, content } = matter(raw);
|
|
181
|
+
const slug = file.replace(/\.mdx?$/, '');
|
|
182
|
+
const body = await md2html(content);
|
|
183
|
+
|
|
184
|
+
// create_collection_item via the MCP
|
|
185
|
+
await mcp.callTool('create_collection_item', {
|
|
186
|
+
collection: 'blog',
|
|
187
|
+
status: data.draft ? 'draft' : 'published',
|
|
188
|
+
fields: {
|
|
189
|
+
title: data.title,
|
|
190
|
+
slug,
|
|
191
|
+
description: data.description,
|
|
192
|
+
date: data.pubDate ? new Date(data.pubDate).toISOString().slice(0, 10) : null,
|
|
193
|
+
updated_date: data.updatedDate ? new Date(data.updatedDate).toISOString().slice(0, 10) : null,
|
|
194
|
+
hero_image: mediaMap[data.heroImage] || data.heroImage,
|
|
195
|
+
author: data.author,
|
|
196
|
+
tags: (data.tags || []).join(', '),
|
|
197
|
+
body,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Rate-limit awareness: spawn at most 5 parallel `create_collection_item` calls; the API caps at ~60 writes/minute and responds with 429 + `Retry-After` if exceeded.
|
|
204
|
+
|
|
205
|
+
Set `status: 'draft'` (or honour Astro's `draft: true` frontmatter) for items the user should review before publishing. Only published items get static pages.
|
|
206
|
+
|
|
207
|
+
### 7. Build the listing page
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
create_page title="Blog" slug="blog" status="published" content_mode="html"
|
|
211
|
+
html_content="<section><h1>Blog</h1>
|
|
212
|
+
<!-- typeroll:listing:blog -->
|
|
213
|
+
<!-- /typeroll:listing:blog -->
|
|
214
|
+
</section>"
|
|
215
|
+
|
|
216
|
+
regenerate_collection_listing
|
|
217
|
+
collection="blog"
|
|
218
|
+
page_id="blog"
|
|
219
|
+
item_template="<article class=\"blog-card\"><a href=\"{{url}}\"><h2>{{title}}</h2><p>{{description}}</p><time>{{date}}</time></a></article>"
|
|
220
|
+
wrap_open="<div class=\"blog-grid\">"
|
|
221
|
+
wrap_close="</div>"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
See `tr-blog` for full styling and post-import update flow.
|
|
225
|
+
|
|
226
|
+
### 8. Translate standalone pages
|
|
227
|
+
|
|
228
|
+
For each non-dynamic `.astro` page under `src/pages/`:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
read_partial partial_id="header" # learn target's nav style
|
|
232
|
+
# Re-skin the page content using target site's CSS variables and partials.
|
|
233
|
+
create_page title="About" slug="about" status="draft" content_mode="html"
|
|
234
|
+
html_content="..."
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Default to **draft** — the user reviews each page before publishing.
|
|
238
|
+
|
|
239
|
+
For dynamic Astro pages (`[slug].astro` that consume a collection), you're already done: the matching Typeroll collection's `route_template` produces the same URLs at build time.
|
|
240
|
+
|
|
241
|
+
### 9. Add redirects for URL changes
|
|
242
|
+
|
|
243
|
+
If Astro's slugs differed from what you derived for Typeroll (e.g. Astro had `/posts/my-article` but you want `/blog/my-article`), bulk-add redirects:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
add_redirect from="/posts/my-article" to="/blog/my-article" status=301
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Or build a redirect map from the `astro-migration-state.json` and apply it in one pass.
|
|
250
|
+
|
|
251
|
+
### 10. Deploy
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
trigger_deploy
|
|
255
|
+
get_deploy_status job_id=<id>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Browse the resulting site, compare against the Astro source, surface anything that drifted.
|
|
259
|
+
|
|
260
|
+
## Astro-specific gotchas
|
|
261
|
+
|
|
262
|
+
- **`.mdx` files with custom components.** Components inside MDX (`<MyCustom prop="..." />`) will not render in Typeroll because the component definitions don't migrate. Two options: (1) replace each component with its rendered HTML output (run the Astro build, scrape the rendered HTML, use that as `body`); (2) if the component is a reusable visual element used across many items, factor it into a Typeroll partial and replace MDX usages with `<x-include name="..." />` calls. See `tr-page-template` for the partial-include pattern.
|
|
263
|
+
- **`src/content/config.ts` typed `image()` helper.** Astro resolves `image()` fields at build time to optimised assets. After migration the Typeroll `hero_image` field carries the original source URL — re-upload via step 5 to get it onto the Typeroll CDN.
|
|
264
|
+
- **`getCollection` filters at runtime.** If `src/pages/blog/index.astro` does `getCollection('blog', ({ data }) => !data.draft)`, Typeroll handles this via `status` on the item. Don't translate the filter — set `status: 'draft'` on items where `data.draft === true`.
|
|
265
|
+
- **Per-tag pages (`/blog/tag/[tag].astro`).** Typeroll doesn't auto-generate these. Either (a) drop tag pages and use a client-side filter in the blog listing JS, (b) generate them by enumerating unique tags and calling `create_page` per tag with a server-side-pre-filtered listing. (a) is preferable for ≤dozens of tags.
|
|
266
|
+
- **`rehype-pretty-code` / shiki / fenced code with syntax highlighting.** The HTML output of these contains inline styles that the Typeroll sanitiser preserves. The colours match the Astro site's theme at import time; redoing the target design later means re-running the highlighter against the same markdown source. Keep a copy of the raw markdown if you might.
|
|
267
|
+
- **`og:image` per page from `astro-og-image` style plugins.** Typeroll has its own SEO surface (`seo_title`, `seo_description`, `seo_og_image`). Set them explicitly on each page during step 8.
|
|
268
|
+
- **i18n via `src/content/<lang>/<collection>/`.** Typeroll's site-level `default_language` + per-page `language` field cover this (capabilities: `supports_language_per_page: true`). Map each language directory to per-item `language: 'sv-SE' | 'en-US' | …` instead of separate collections.
|
|
269
|
+
|
|
270
|
+
## Mixing imported + new content
|
|
271
|
+
|
|
272
|
+
Half-migrate, leave drafts, let the user inspect, iterate. You can:
|
|
273
|
+
|
|
274
|
+
- Import only collections, skip standalone pages, let the user rebuild those from scratch.
|
|
275
|
+
- Import everything as `status: 'draft'`, treat publishing as a per-item human review pass.
|
|
276
|
+
- Mix sources: pull article bodies from Astro markdown, use Claude to generate fresh excerpts/SEO metadata before writing the item.
|
|
277
|
+
|
|
278
|
+
Keep the local `astro-migration-state.json` honest — it's the only way to make a partial run resumable when the API rate-limits or a markdown parser trips on an edge case file.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tr-page-template
|
|
3
|
+
description: Use when several pages on a Typeroll site share the same outer structure — category pages, service-detail pages, landing-page variants — and the user wants to edit the shared bits in one place. Covers two flows: the HTML-mode pattern with partials + `<x-include>` (works everywhere, recommended for Phase 1), and the formal block-mode PageTemplate (when the site is in block mode).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Share structure across pages
|
|
7
|
+
|
|
8
|
+
When you have N pages that follow the same skeleton — say 7 category landing pages, each with `Hero → Intro → Features grid → CTA banner` — you don't want to edit 7 HTML bodies whenever the design changes. There are two ways to share structure in Typeroll, and the right pick depends on the site's content mode.
|
|
9
|
+
|
|
10
|
+
## Decide which pattern fits
|
|
11
|
+
|
|
12
|
+
| Site is mostly in… | Use |
|
|
13
|
+
|---|---|
|
|
14
|
+
| **HTML mode** (the Phase 1 default — most sites) | Partials + `<x-include>` — pattern A below |
|
|
15
|
+
| **Block mode** (`supports_blocks_mode=true` and the page's `content_mode='blocks'`) | PageTemplate via `set_page_template` — pattern B below |
|
|
16
|
+
|
|
17
|
+
You can check the site's mode by reading any existing page — `content_mode` is a top-level field on the page doc.
|
|
18
|
+
|
|
19
|
+
If unsure, default to **pattern A**. It works on every Typeroll site and the migration to block-mode templates later is mechanical.
|
|
20
|
+
|
|
21
|
+
## Pattern A — Partials + `<x-include>` (HTML mode)
|
|
22
|
+
|
|
23
|
+
A partial is a named HTML fragment. Putting `<x-include name="my-partial" />` anywhere in a page body inlines that fragment at build time. Editing the partial updates every page that references it on the next deploy.
|
|
24
|
+
|
|
25
|
+
### Recipe for "7 category pages with shared structure"
|
|
26
|
+
|
|
27
|
+
#### 1. Identify the shared chunks
|
|
28
|
+
|
|
29
|
+
Walk through one category page and mark which sections are the same across all 7:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
[Hero: title + tagline + image] ← varies per category
|
|
33
|
+
[Intro paragraph] ← varies per category
|
|
34
|
+
[Common: "Why choose us" three-tile band] ← identical across 7
|
|
35
|
+
[Common: CTA banner with newsletter form] ← identical across 7
|
|
36
|
+
[Common: footer testimonials] ← identical across 7
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Three reusable bits: `why-choose-us`, `cta-newsletter`, `footer-testimonials`.
|
|
40
|
+
|
|
41
|
+
#### 2. Create the partials
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
create_partial partial_id="why-choose-us" html_content="<section class=\"why\">
|
|
45
|
+
<div class=\"container\">
|
|
46
|
+
<h2>Varför oss</h2>
|
|
47
|
+
<div class=\"why__grid\">
|
|
48
|
+
<div class=\"why__tile\"><h3>Erfarenhet</h3><p>20 år i branschen.</p></div>
|
|
49
|
+
<div class=\"why__tile\"><h3>Kvalitet</h3><p>Vi mäter på allt.</p></div>
|
|
50
|
+
<div class=\"why__tile\"><h3>Närhet</h3><p>Lokala kontor i tre städer.</p></div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
<style>
|
|
55
|
+
.why{padding:4rem 0;background:var(--color-surface)}
|
|
56
|
+
.why__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:2rem;margin-top:2rem}
|
|
57
|
+
.why__tile h3{font-family:var(--font-heading);margin-bottom:0.5rem}
|
|
58
|
+
</style>"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Same for `cta-newsletter` and `footer-testimonials`.
|
|
62
|
+
|
|
63
|
+
The partial-id is what `<x-include>` references — keep it kebab-case and descriptive.
|
|
64
|
+
|
|
65
|
+
#### 3. Build each category page using the partials
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
create_page title="Tjänster för bostadsrätter" slug="brf" content_mode="html"
|
|
69
|
+
html_content="<section class=\"hero hero--brf\">
|
|
70
|
+
<div class=\"container\">
|
|
71
|
+
<h1>Tjänster för bostadsrättsföreningar</h1>
|
|
72
|
+
<p class=\"hero__tagline\">Trygg förvaltning, helt utan överraskningar.</p>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<section class=\"intro\">
|
|
77
|
+
<div class=\"container\">
|
|
78
|
+
<p>Vi har förvaltat över 200 BRF:er i Stockholmsområdet sedan 2005.</p>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<x-include name=\"why-choose-us\" />
|
|
83
|
+
<x-include name=\"cta-newsletter\" />
|
|
84
|
+
<x-include name=\"footer-testimonials\" />"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Repeat for the other 6 categories, varying only the `hero` + `intro` sections.
|
|
88
|
+
|
|
89
|
+
#### 4. Edit once, propagate everywhere
|
|
90
|
+
|
|
91
|
+
Adding a fourth "Why us" tile? Edit `why-choose-us` once:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
replace_partial partial_id="why-choose-us" html_content="<section class=\"why\">
|
|
95
|
+
...four tiles instead of three...
|
|
96
|
+
</section>"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
All 7 pages pick up the change on the next deploy. No multi-page diff.
|
|
100
|
+
|
|
101
|
+
### Edge cases
|
|
102
|
+
|
|
103
|
+
- **`<x-include>` self-closes or has an explicit close.** Both forms work: `<x-include name="x" />` and `<x-include name="x"></x-include>`.
|
|
104
|
+
- **Nested includes are NOT supported.** If `partial-a` references `<x-include name="partial-b" />`, the inner reference is not expanded. Flatten the hierarchy at design time.
|
|
105
|
+
- **Unknown partial → silently empty.** A typo in the `name` attribute removes the tag at expand time with no warning. Verify the partial id matches one returned by `list_partials`.
|
|
106
|
+
- **Partial content is sanitised on save.** `<script>` tags get stripped at partial creation, then the inlined HTML is sanitised again when the page renders. Two passes; both intentional.
|
|
107
|
+
|
|
108
|
+
## Pattern B — Formal PageTemplate (block mode)
|
|
109
|
+
|
|
110
|
+
Available when the site is in block mode (`supports_blocks_mode=true` and the page's `content_mode='blocks'`). A PageTemplate is a `Block[]` tree with one or more `template_content_slot` blocks marking where the page's own blocks go.
|
|
111
|
+
|
|
112
|
+
### Recipe
|
|
113
|
+
|
|
114
|
+
#### 1. List existing templates
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
list_page_templates
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Returns templates already defined on the site.
|
|
121
|
+
|
|
122
|
+
#### 2. Create / pick a template
|
|
123
|
+
|
|
124
|
+
Templates live as `Block[]` trees under `paths.pageTemplate(orgId, siteId, templateId)`. They're created via the portal UI for now (no dedicated MCP `create_page_template` tool yet — the chat-tool surface in `anthropic.ts` exposes list + set, not create).
|
|
125
|
+
|
|
126
|
+
If the template you want doesn't exist, ask the user to create it via **/app/sites/{id}/templates** in the portal, or use the block-mode-aware AI chat in the portal which knows how to write template trees.
|
|
127
|
+
|
|
128
|
+
#### 3. Assign it to a page
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
set_page_template page_id="brf" template_id="category-landing"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The renderer composes the template's blocks with the page's blocks at build time via `composePageWithTemplate` (replacing each `template_content_slot` with the page's own block tree). The page's CSS / theme / SEO settings are unchanged.
|
|
135
|
+
|
|
136
|
+
#### 4. Editing the template propagates to every page
|
|
137
|
+
|
|
138
|
+
The same template id can be set on multiple pages. Editing the template updates them all on next deploy.
|
|
139
|
+
|
|
140
|
+
### When B beats A
|
|
141
|
+
|
|
142
|
+
- Block-mode editor surfaces template assignment as a dropdown in the page settings.
|
|
143
|
+
- Templates compose with the block tree, so the page author can drop content blocks into named slots rather than writing HTML.
|
|
144
|
+
- The renderer is aware of block CSS / JS bundling — assets per block type are aggregated automatically.
|
|
145
|
+
|
|
146
|
+
### When A still beats B even in block mode
|
|
147
|
+
|
|
148
|
+
- Need a small reusable HTML chunk in the middle of a block-mode page → still use `<x-include>` (block-mode bodies can contain free HTML blocks that include partials).
|
|
149
|
+
- One-page change wanted without affecting siblings → just edit the page's blocks; templates are all-or-nothing.
|
|
150
|
+
|
|
151
|
+
## Refactor: 7 already-existing pages into a shared partial
|
|
152
|
+
|
|
153
|
+
If the 7 pages already exist as separate HTML bodies with duplicated chunks:
|
|
154
|
+
|
|
155
|
+
1. `read_page page_id=<one of them>` and identify the shared HTML literally — character-for-character chunks that repeat across all 7.
|
|
156
|
+
2. `create_partial partial_id=<descriptive-name> html_content="<the shared chunk>"`.
|
|
157
|
+
3. For each of the 7 pages, `update_page` with the shared chunk replaced by `<x-include name="<name>" />`.
|
|
158
|
+
4. Deploy. The rendered output should be byte-identical to before; only the source pages got shorter.
|
|
159
|
+
|
|
160
|
+
Don't try to abstract first time and hand-write the partials. Refactor from real, working duplication.
|
|
161
|
+
|
|
162
|
+
## Pitfalls
|
|
163
|
+
|
|
164
|
+
- **Don't put hero / page-specific content into a shared partial.** A partial is for things that are *truly identical* across uses. The moment you want it to vary by page, lift the differing parts back into the page body.
|
|
165
|
+
- **Header and footer already use partials.** Don't recreate them inside a category-page shared partial — they're injected by the layout, not by the page body.
|
|
166
|
+
- **CSS scoping.** Partials carrying `<style>` blocks merge into every page that includes them. If two partials define `.tile` differently, the last one wins. Either namespace classes per partial (`.why__tile`, `.feature__tile`) or move shared styles into the site's global CSS via `update_site_settings` → `custom_css`.
|
|
167
|
+
- **Don't migrate to block-mode templates just because you can.** Phase 1 sites are HTML-mode by design; partials + includes give you 90% of the value with zero risk.
|
|
168
|
+
- **`<x-include>` in a partial body referencing another partial.** Won't expand (see edge cases). If you find yourself wanting this, you're building a layout system inside the partial system — at that point the site probably wants block mode.
|