@typeroll/mcp-server 0.7.4 → 0.7.7

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.
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: tr-brand
3
+ description: Use when the user asks to create a brand identity, design system, or visual style for a site. Triggers on "create a brand", "design the look", "choose colors", "pick fonts", "make it look like [reference]", or "rebrand the site". Produces a cohesive palette, typography scale, and CSS custom properties applied to an existing site.
4
+ ---
5
+
6
+ # Design a brand identity for a Typeroll site
7
+
8
+ This skill turns a brief (or a reference URL/screenshot) into a complete
9
+ visual design system applied to the site's settings and partials.
10
+
11
+ ## Preconditions
12
+
13
+ - Site exists and MCP is configured.
14
+ - You have at least one of: industry, mood words, reference URL, existing
15
+ logo colors, or competitor sites to contrast with.
16
+
17
+ ## Step 1 — Gather context
18
+
19
+ Ask (or infer from the brief):
20
+
21
+ 1. **Industry + audience.** Law firm → formal, trust. Café → warm, approachable.
22
+ Tech startup → clean, modern. Interior design → refined, editorial.
23
+ 2. **Mood words.** 3–5 adjectives the brand should feel: "minimal, Nordic,
24
+ calm" or "bold, energetic, playful".
25
+ 3. **Reference.** A URL, a screenshot, or a competitor they like (and what
26
+ they want to be different from it).
27
+ 4. **Must-keep.** Existing logo color? Legal industry color conventions?
28
+
29
+ If the user provided a URL, fetch it and note the dominant colors,
30
+ typeface categories, and layout density.
31
+
32
+ ## Step 2 — Build the palette
33
+
34
+ A Typeroll site uses 7 color tokens:
35
+
36
+ | Token | Role | Design rule |
37
+ |---|---|---|
38
+ | `primary` | Brand identity. CTA buttons, active nav, links. | High contrast on `background`. |
39
+ | `secondary` | Header, footer, darker sections. | Darker or more neutral than primary. |
40
+ | `accent` | Highlights, price tags, badges, hover states. | High-energy complement. |
41
+ | `background` | Page background. | Near-white for light themes, near-black for dark. |
42
+ | `surface` | Cards, input boxes, code blocks. | Slightly off from `background`. |
43
+ | `text` | Body copy. | ≥4.5:1 contrast ratio on `background`. |
44
+ | `text_light` | Secondary labels, captions, placeholders. | ≥3:1 on `background`. |
45
+
46
+ **Palette recipes by mood:**
47
+
48
+ *Nordic / minimal:*
49
+ ```
50
+ primary: #1f2a30 secondary: #142027 accent: #c9b89a
51
+ background: #faf8f4 surface: #f2ede5 text: #1f2a30 text_light: #7a7265
52
+ ```
53
+
54
+ *Warm / artisan:*
55
+ ```
56
+ primary: #3d2b1f secondary: #2a1d14 accent: #c8860a
57
+ background: #fdf6ee surface: #f7ede0 text: #1a1008 text_light: #8a7060
58
+ ```
59
+
60
+ *Modern / tech:*
61
+ ```
62
+ primary: #2563eb secondary: #1e293b accent: #f59e0b
63
+ background: #ffffff surface: #f8fafc text: #0f172a text_light: #64748b
64
+ ```
65
+
66
+ *Editorial / dark:*
67
+ ```
68
+ primary: #e2c08d secondary: #0f0f0f accent: #e2c08d
69
+ background: #0f0f0f surface: #1a1a1a text: #f5f5f0 text_light: #a0a090
70
+ ```
71
+
72
+ Check WCAG contrast ratios mentally: text on background must be ≥4.5:1.
73
+ The online tool `https://webaim.org/resources/contrastchecker/` is useful
74
+ but not accessible during a tool call — reason about perceived contrast
75
+ instead (light grey on white = bad; dark grey on white = fine).
76
+
77
+ ## Step 3 — Choose typefaces
78
+
79
+ Pick from high-quality Google Fonts pairings:
80
+
81
+ | Heading | Body | Mood |
82
+ |---|---|---|
83
+ | Cormorant Garamond | Raleway | Luxury, editorial |
84
+ | Playfair Display | Source Sans 3 | Classic, readable |
85
+ | DM Serif Display | DM Sans | Contemporary, clean |
86
+ | Fraunces | Mulish | Artisan, craft |
87
+ | Syne | Inter | Bold, modern |
88
+ | Plus Jakarta Sans | Plus Jakarta Sans | Clean, versatile |
89
+ | Libre Baskerville | Libre Franklin | Traditional, trustworthy |
90
+
91
+ Same font for heading and body is fine if it has enough weight variation
92
+ (Inter at 700 + 400 works well).
93
+
94
+ `size_base` should be 16 for most sites; 17–18 for text-heavy editorial
95
+ sites; 15 for dense dashboards.
96
+
97
+ ## Step 4 — Apply to the site
98
+
99
+ One call sets everything:
100
+
101
+ ```
102
+ update_site_settings {
103
+ "colors": { ...all 7 tokens },
104
+ "fonts": { "heading": "...", "body": "...", "size_base": 16 },
105
+ "custom_css": "/* optional: utility classes or @keyframes */"
106
+ }
107
+ ```
108
+
109
+ Read back to confirm: `read_site_settings`.
110
+
111
+ ## Step 5 — Update partials to use the new palette
112
+
113
+ Partials that hardcoded hex colors need updating. Fetch the header:
114
+
115
+ ```
116
+ read_partial partial_id="header"
117
+ ```
118
+
119
+ If it has hardcoded colors, replace them with CSS variable references
120
+ (`var(--color-primary)`) and call `replace_partial`:
121
+
122
+ ```
123
+ replace_partial partial_id="header" html_content="<updated HTML>"
124
+ ```
125
+
126
+ Same for footer.
127
+
128
+ ## Step 6 — Custom CSS for advanced tokens (optional)
129
+
130
+ If the brand needs things beyond the 7 base tokens — e.g. a gradient,
131
+ a special border radius, or a branded highlight color — add them via
132
+ `custom_css`:
133
+
134
+ ```css
135
+ :root {
136
+ --brand-gradient: linear-gradient(135deg, var(--color-primary), var(--color-accent));
137
+ --radius-brand: 2px; /* sharp corners for formal brands */
138
+ --letter-spacing-display: -0.03em; /* tight tracking for display headings */
139
+ }
140
+ ```
141
+
142
+ Then reference `var(--brand-gradient)` etc. in page HTML and partials.
143
+
144
+ ## Step 7 — Preview
145
+
146
+ ```
147
+ get_preview_link
148
+ ```
149
+
150
+ Open in browser. Check:
151
+ - Colors render as intended (not "undefined" or missing)
152
+ - Fonts load (Google Fonts link is in `<head>`)
153
+ - Nav text is readable against header background
154
+ - Body text has sufficient contrast
155
+
156
+ ## Pitfalls
157
+
158
+ - **Don't set colors without checking the header contrast.** If `primary`
159
+ is light, white nav text becomes unreadable. Either darken `primary` or
160
+ make the header use `secondary`.
161
+ - **Custom_css is global.** Rules here apply to every page. Keep it to
162
+ `:root {}` token additions and truly global utilities. Page-specific
163
+ styles go in the page's HTML `<style>` block.
164
+ - **Google Fonts load time.** Two different font families is fine; three
165
+ adds measurable LCP impact. Stick to two families with variable-font
166
+ versions when possible.
167
+ - **Dark themes need dark surface too.** Setting `background: #0f0f0f`
168
+ but leaving `surface: #f8fafc` (white) breaks every card/input. Always
169
+ update all 7 tokens as a set.
@@ -0,0 +1,243 @@
1
+ ---
2
+ name: tr-forms
3
+ description: Use when the user wants to add a contact form, booking form, or any web form to their Typeroll site. Triggers on "lägg till formulär", "contact form", "add a form", "let visitors message us", "booking form", "formulär", or similar. Covers both creating the form definition and embedding the HTML widget on a page.
4
+ ---
5
+
6
+ # Add a form to a Typeroll site
7
+
8
+ Typeroll forms are server-backed: submissions go to
9
+ `/api/forms/submit` (HMAC-signed, rate-limited, honeypot-protected)
10
+ and are stored in Firestore. The user sees them in the portal's
11
+ Submissions inbox. No third-party service needed.
12
+
13
+ ## Preconditions
14
+
15
+ - Site exists and MCP is configured.
16
+ - You know what fields the form needs.
17
+
18
+ ## Recipe
19
+
20
+ ### 1. Create the form definition
21
+
22
+ ```
23
+ create_form {
24
+ "id": "kontakt",
25
+ "name": "Kontaktformulär",
26
+ "fields": [
27
+ {"name":"name", "label":"Namn", "type":"text", "required":true},
28
+ {"name":"email", "label":"E-post", "type":"email", "required":true},
29
+ {"name":"phone", "label":"Telefon", "type":"text"},
30
+ {"name":"message", "label":"Meddelande", "type":"textarea", "required":true},
31
+ {"name":"subject", "label":"Ämne", "type":"select",
32
+ "options":["Prisförfrågan","Samarbete","Övrigt"]}
33
+ ],
34
+ "success_message": "Tack! Vi återkommer inom 24 timmar.",
35
+ "recipient_email": "hej@acme.se"
36
+ }
37
+ ```
38
+
39
+ **Field types:** `text`, `email`, `tel`, `url`, `number`, `textarea`,
40
+ `select`, `radio`, `checkbox`.
41
+
42
+ **Field name rules:** Lowercase ASCII only: `[a-z][a-z0-9_-]*`.
43
+ - `besokt` not `besökt` (`ö→o`)
44
+ - `foretag` not `företag` (`ö→o`, `ä→a`)
45
+ - `meddelande` not `Meddelande` (the `name` must be lowercase; `label` can be anything)
46
+
47
+ ### 2. Get the signed embed token
48
+
49
+ The form's submit button needs a signed token that proves it was
50
+ generated by the platform. Fetch it:
51
+
52
+ ```
53
+ read_form form_id="kontakt"
54
+ ```
55
+
56
+ The response includes `submit_token` (a short-lived HMAC). Use this in
57
+ the HTML.
58
+
59
+ Actually, the embed HTML is best generated by reading the form and
60
+ building it manually — the form's `id` is all you need for the action URL.
61
+
62
+ ### 3. Embed the form on a page
63
+
64
+ Typeroll forms submit to the platform's endpoint. The HTML:
65
+
66
+ ```html
67
+ <section class="contact-section">
68
+ <div class="contact-container">
69
+ <h2>Kontakta oss</h2>
70
+ <p>Fyll i formuläret så återkommer vi inom 24 timmar.</p>
71
+
72
+ <form class="contact-form"
73
+ action="https://app.typeroll.com/api/forms/submit"
74
+ method="POST">
75
+ <!-- Hidden fields — required by the platform -->
76
+ <input type="hidden" name="_form_id" value="kontakt">
77
+ <input type="hidden" name="_site_id" value="YOUR_SITE_ID">
78
+ <input type="hidden" name="_token" value="SIGNED_TOKEN">
79
+ <!-- Honeypot — must stay empty, bots fill it -->
80
+ <input type="text" name="_hp" style="display:none" tabindex="-1" autocomplete="off">
81
+
82
+ <div class="form-group">
83
+ <label for="name">Namn *</label>
84
+ <input type="text" id="name" name="name" required>
85
+ </div>
86
+
87
+ <div class="form-group">
88
+ <label for="email">E-post *</label>
89
+ <input type="email" id="email" name="email" required>
90
+ </div>
91
+
92
+ <div class="form-group">
93
+ <label for="message">Meddelande *</label>
94
+ <textarea id="message" name="message" rows="5" required></textarea>
95
+ </div>
96
+
97
+ <div class="form-group">
98
+ <label for="subject">Ämne</label>
99
+ <select id="subject" name="subject">
100
+ <option value="Prisförfrågan">Prisförfrågan</option>
101
+ <option value="Samarbete">Samarbete</option>
102
+ <option value="Övrigt">Övrigt</option>
103
+ </select>
104
+ </div>
105
+
106
+ <button type="submit" class="btn-primary">Skicka meddelande</button>
107
+ </form>
108
+ </div>
109
+ </section>
110
+
111
+ <style>
112
+ .contact-section{padding:4rem 2rem}
113
+ .contact-container{max-width:600px;margin:0 auto}
114
+ .form-group{margin-bottom:1.5rem}
115
+ .form-group label{display:block;font-weight:600;margin-bottom:0.4rem;font-size:0.9rem}
116
+ .form-group input,.form-group textarea,.form-group select{
117
+ width:100%;padding:0.75rem 1rem;border:1px solid var(--color-surface);
118
+ border-radius:0.375rem;font-family:inherit;font-size:1rem;
119
+ background:var(--color-surface);color:var(--color-text)
120
+ }
121
+ .form-group textarea{resize:vertical}
122
+ .btn-primary{
123
+ background:var(--color-primary);color:#fff;border:none;
124
+ padding:0.875rem 2rem;border-radius:0.375rem;font-size:1rem;
125
+ font-weight:600;cursor:pointer;width:100%
126
+ }
127
+ .btn-primary:hover{opacity:0.9}
128
+ </style>
129
+ ```
130
+
131
+ **Replace:**
132
+ - `YOUR_SITE_ID` → the site's actual id (from `get_site`)
133
+ - `SIGNED_TOKEN` → fetch via `read_form form_id="kontakt"` → `submit_token`
134
+
135
+ ### 4. Add success/error handling (optional JS)
136
+
137
+ To show inline feedback without a page reload:
138
+
139
+ ```html
140
+ <script>
141
+ (function(){
142
+ const form = document.querySelector('.contact-form');
143
+ if(!form) return;
144
+ form.addEventListener('submit', async(e) => {
145
+ e.preventDefault();
146
+ const btn = form.querySelector('[type=submit]');
147
+ btn.disabled = true;
148
+ btn.textContent = 'Skickar…';
149
+ try {
150
+ const res = await fetch(form.action, {method:'POST', body: new FormData(form)});
151
+ const data = await res.json();
152
+ if(data.ok) {
153
+ form.innerHTML = '<p class="form-success">Tack! Vi återkommer inom 24 timmar.</p>';
154
+ } else {
155
+ btn.disabled = false;
156
+ btn.textContent = 'Skicka meddelande';
157
+ alert('Något gick fel: ' + (data.error || 'okänt fel'));
158
+ }
159
+ } catch {
160
+ btn.disabled = false;
161
+ btn.textContent = 'Skicka meddelande';
162
+ alert('Nätverksfel — försök igen.');
163
+ }
164
+ });
165
+ })();
166
+ </script>
167
+ ```
168
+
169
+ ### 5. Update the contact page with the form HTML
170
+
171
+ ```
172
+ update_page page_id="kontakt" patch={
173
+ "html_content": "<full page HTML including the form section>"
174
+ }
175
+ ```
176
+
177
+ ### 6. Verify
178
+
179
+ ```
180
+ read_form form_id="kontakt"
181
+ list_forms
182
+ ```
183
+
184
+ Confirm the form appears and fields match what you embedded.
185
+
186
+ ### 7. Deploy
187
+
188
+ ```
189
+ trigger_deploy
190
+ get_deploy_status job_id=<id>
191
+ ```
192
+
193
+ After deploy, test by submitting the live form. Submissions appear in the
194
+ portal at `/app/sites/{siteId}/forms/kontakt/submissions`.
195
+
196
+ ## Common form patterns
197
+
198
+ ### Booking / appointment request
199
+ ```json
200
+ {"fields": [
201
+ {"name":"name", "type":"text", "label":"Namn", "required":true},
202
+ {"name":"email", "type":"email", "label":"E-post", "required":true},
203
+ {"name":"date", "type":"text", "label":"Önskat datum (YYYY-MM-DD)"},
204
+ {"name":"time", "type":"select","label":"Tid", "options":["09:00","10:00","11:00","14:00","15:00"]},
205
+ {"name":"notes", "type":"textarea","label":"Kommentar"}
206
+ ]}
207
+ ```
208
+
209
+ ### Newsletter signup (minimal)
210
+ ```json
211
+ {"fields": [
212
+ {"name":"email", "type":"email", "label":"E-postadress", "required":true}
213
+ ]}
214
+ ```
215
+
216
+ ### Job application
217
+ ```json
218
+ {"fields": [
219
+ {"name":"name", "type":"text", "label":"Namn", "required":true},
220
+ {"name":"email", "type":"email", "label":"E-post", "required":true},
221
+ {"name":"role", "type":"select","label":"Roll", "options":["Designer","Projektledare","Övrigt"]},
222
+ {"name":"experience", "type":"textarea","label":"Berätta om dig själv"},
223
+ {"name":"portfolio", "type":"url", "label":"Portfolio-URL"}
224
+ ]}
225
+ ```
226
+
227
+ ## Pitfalls
228
+
229
+ - **Tokens expire.** The `submit_token` in the form embed is short-lived
230
+ (24h by default). For forms on long-cached static pages, fetch a fresh
231
+ token via `read_form` and rebuild the page HTML, then redeploy.
232
+ Alternatively, fetch the token client-side via a small `<script>` that
233
+ calls the portal's token endpoint before submit.
234
+ - **Field names must be ASCII.** `message` not `meddelande`... wait,
235
+ `meddelande` is ASCII. `foretag` not `företag`. Only Swedish/Nordic
236
+ special characters (å, ä, ö) cause problems.
237
+ - **Don't use the same form_id on two different forms.** IDs must be
238
+ unique per site — use descriptive names: `kontakt`, `boka`, `nyhetsbrev`.
239
+ - **Honeypot must be invisible.** `_hp` field must have `display:none`.
240
+ If it's visible and a real user fills it, their submission is rejected.
241
+ - **Recipient email is display-only in phase 1.** The portal stores
242
+ submissions; email delivery to `recipient_email` depends on the platform's
243
+ email configuration. Confirm with the customer that they'll check the inbox.
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: tr-import-url
3
+ description: Use when the user wants to import or migrate content from a non-WordPress website — a Squarespace site, a Wix site, a static HTML site, a Webflow export, or any URL the user points at. Also triggers on "copy content from", "rebuild this site", "import from Squarespace/Wix/Webflow", or "make it look like this site". For WordPress sources use tr-migrate-wp instead.
4
+ ---
5
+
6
+ # Import content from a non-WordPress site
7
+
8
+ ## When to use this vs tr-migrate-wp
9
+
10
+ | Source | Use |
11
+ |---|---|
12
+ | WordPress with `/wp-json` accessible | `tr-migrate-wp` |
13
+ | WordPress with REST disabled | This skill (scrape HTML) |
14
+ | Squarespace, Wix, Webflow, static HTML | This skill |
15
+ | CSV / spreadsheet data | This skill (skip scraping, just parse) |
16
+ | Any URL the user points at | This skill |
17
+
18
+ ## Preconditions
19
+
20
+ - Target Typeroll site exists with working header/footer.
21
+ - Source URL(s) accessible (check with a quick `fetch`; if blocked, mention
22
+ it and ask the user for an HTML export or screenshot).
23
+
24
+ ## Recipe
25
+
26
+ ### 1. Inventory the source site
27
+
28
+ Fetch the homepage and build a URL list:
29
+
30
+ ```
31
+ fetch <source-url> # root HTML
32
+ fetch <source-url>/sitemap.xml # XML sitemap if it exists
33
+ ```
34
+
35
+ Parse `<a href>` links to discover internal pages. Build a list:
36
+ - Homepage
37
+ - Top-level pages (About, Services, Contact, etc.)
38
+ - Any sub-pages that look important
39
+
40
+ Avoid: pagination URLs, session URLs, `/wp-admin`, `/cdn-cgi/`, query strings.
41
+
42
+ ### 2. Learn the target's design
43
+
44
+ ```
45
+ read_site_settings
46
+ read_partial partial_id="header"
47
+ list_pages limit=5
48
+ ```
49
+
50
+ The goal is to understand what CSS variables, class names, and structural
51
+ conventions the target site uses so the imported content looks native.
52
+
53
+ ### 3. Fetch and clean each source page
54
+
55
+ For each URL:
56
+
57
+ **a. Fetch the HTML.**
58
+ ```
59
+ fetch <page-url>
60
+ ```
61
+
62
+ If the site returns a bot-block (Cloudflare, 403, or clearly JS-only
63
+ SPA output), note it. Tell the user: "This page blocked direct fetching.
64
+ Can you provide the page source or an HTML export?"
65
+
66
+ **b. Extract the main content.**
67
+
68
+ Discard: nav, header, footer, cookie banners, chat widgets, scripts.
69
+ Keep: `<main>`, `<article>`, the largest content region.
70
+
71
+ Clean the HTML:
72
+ - Strip platform-specific classes: `sqsrte-*`, `wf-*`, `et_*`,
73
+ `elementor-*`, `fl-*`, `divi-*`, `vc_*`
74
+ - Remove empty `<div>`, `<span>`, `<section>` wrappers (no class, no content)
75
+ - Unwrap redundant nesting: `<div><p>text</p></div>` → `<p>text</p>`
76
+ - Keep: `<h1>`–`<h6>`, `<p>`, `<ul>`, `<ol>`, `<img>`, `<a>`, `<table>`,
77
+ `<blockquote>`, `<figure>`, `<figcaption>`, `<strong>`, `<em>`
78
+ - Fix headings: ensure exactly one `<h1>` per page (the page title)
79
+
80
+ **c. Transfer images.** For each `<img src>`:
81
+ ```
82
+ upload_media_from_url url="<source-img-url>" alt="..."
83
+ ```
84
+ Replace the src with the returned CDN URL. Skip tracking pixels
85
+ (1×1 images), decorative SVGs that are just icons, and anything
86
+ that 404s.
87
+
88
+ **d. Adapt to the target's design.**
89
+ Replace source-specific CSS classes with target conventions.
90
+ Use `var(--color-*)` for colors, `var(--font-*)` for type.
91
+
92
+ ### 4. Create pages as drafts
93
+
94
+ ```
95
+ create_page title="Om oss" slug="om-oss"
96
+ html_content="<cleaned, adapted HTML>"
97
+ content_mode="html" status="draft"
98
+ seo_title="Om oss — Acme"
99
+ seo_description="..."
100
+ ```
101
+
102
+ Always draft first. The user signs off before publishing.
103
+
104
+ ### 5. Handle redirects
105
+
106
+ If the source URLs differ from the target slugs, create redirects:
107
+
108
+ ```
109
+ create_redirect from_path="/about" to_path="/om-oss"
110
+ create_redirect from_path="/services.html" to_path="/tjanster"
111
+ ```
112
+
113
+ ### 6. Preview with the user
114
+
115
+ ```
116
+ get_preview_link
117
+ ```
118
+
119
+ Walk through every imported page with the user. Common issues:
120
+ - Heading hierarchy wrong (two H1s, or H3 used where H2 belongs)
121
+ - Images missing alt text
122
+ - Squarespace column layouts that don't work without their grid system
123
+ - Embedded forms or maps that need re-setup
124
+
125
+ ### 7. Publish + deploy
126
+
127
+ After approval:
128
+ ```
129
+ batch_update_pages updates=[
130
+ {page_id: "om-oss", patch: {status: "published"}},
131
+ {page_id: "tjanster", patch: {status: "published"}}
132
+ ]
133
+ trigger_deploy
134
+ get_deploy_status job_id=<id>
135
+ ```
136
+
137
+ ## Platform-specific notes
138
+
139
+ ### Squarespace
140
+ - Main content is inside `.content-wrapper` or `[data-section-theme]` blocks
141
+ - Portfolio images are usually high-resolution originals in `/universal/images/`
142
+ - JSON-LD is Squarespace's own schema — strip it
143
+ - Gallery blocks → convert to CSS grid with inline `<img>` tags
144
+
145
+ ### Wix
146
+ - Wix sites are React SPAs — `fetch` returns an empty shell
147
+ - Ask the user for the Wix site's "Export to HTML" (available in some plans)
148
+ or take screenshots for reference
149
+ - Best path: get content from the user (text + image files), rebuild clean
150
+
151
+ ### Webflow
152
+ - Usually fetchable; clean output
153
+ - Classes like `w-container`, `w-row`, `w-col-*` can be stripped
154
+ - Webflow CMS items are server-rendered — they appear in the HTML
155
+
156
+ ### Static HTML / old sites
157
+ - Often the cleanest import. Fetch, strip nav/footer, keep body.
158
+ - Watch for table-based layouts (pre-2010 sites) — convert to CSS grid
159
+
160
+ ## Pitfalls
161
+
162
+ - **Don't import `<style>` blocks from the source site.** They reference
163
+ external fonts, resets, and classes that don't exist in the target.
164
+ Strip all `<style>` tags from source HTML and rewrite styles in the
165
+ target's conventions.
166
+ - **Don't break the single-H1 rule.** Many source sites have no H1 or
167
+ several. Fix it.
168
+ - **Squarespace/Wix forms.** They won't work after import — the backend
169
+ is vendor-locked. Create a Typeroll form instead: `create_form`.
170
+ - **Analytics/tracking code.** If the source has GA4 or similar, don't
171
+ copy it into pages. Set it via `update_site_settings scripts_head="..."`.
172
+ - **Videos.** YouTube/Vimeo embeds are fine (`<iframe>` is allowed).
173
+ Hosted MP4s need re-uploading if the source URL won't persist.