Migrating an existing site to Crafty
MCP prompt:
migrate-site. It runs this whole runbook for you and stops at every checkpoint for you to confirm. Arguments:site_id(the target site),source(the old site's URL, itssitemap.xmlor a description of the export) and optionallyenvironment(defaultproduction) andresearch(defaultyes: it also researches the brand on the web and proposes content the old site lacks;nofor a strict 1:1 move). Needs a token withcrafty:write.In Claude Code, type
/and pickmcp__<server>__migrate-site, where<server>is the name you gave the Crafty server; other clients list it in their prompt menu. If your client does not show MCP prompts, ask the agent to callget_guide migrateand follow it.Redesign instead of a 1:1 move? MCP prompt:
redesign-site. It studies the client's current site, social media and what the rest of the web says about them, sums up their brand, voice and content, and proposes a new site (site map, sections built from your block types, draft copy, visual direction) before it creates anything. Once you approve the proposal it builds it as drafts and never publishes. Arguments:website(the current site's URL) and optionallysocial(profile URLs or handles),goalsandsite_id(leave it out to make the proposal before the client has a Crafty site). Needs a token withcrafty:write. Social networks often block automated reading, so be ready to paste the bio and a few posts or share screenshots.
Runbook for moving a site that already exists (WordPress, Webflow, Wix, a static site, another headless CMS) into Crafty with an agent and the MCP server, without breaking its URLs. Work in this order; each phase has a checkpoint where a human confirms before the next one.
Tools used: bulk_upsert_entries, import_media_from_urls, upsert_redirects (plus the usual create_block_type, create_collection, create_page, publish_*). Token scopes: crafty:read + crafty:write; add crafty:publish only for the final phase.
Golden rules
- Everything lands as a draft. Never pass
publish: trueuntil a human has reviewed the content in the panel. dry_run: truefirst on everybulk_upsert_entriesandupsert_redirectsbatch; fix thefaileditems, then run it for real.- Use
external_id(the old CMS post/page ID, or the old URL when there is no ID) on every entry. Re-running an import then updates instead of duplicating. - Batches: up to 100 entries, 50 media URLs, 500 redirects per call. Keep a local manifest (JSON/CSV) of what each batch sent and what came back (
entry_id,media_id), so a failed run can resume. - Media quota: imports count against the tenant's media quota. Import only what the pages actually use, skip thumbnails and resized copies (WordPress
-300x200.jpgvariants), and ask before importing more than a few hundred files. - Work against the
productionenvironment of the target site unless the user asks for another (tools takeenvironment).
Phase 1 — Crawl
Build an inventory of every URL of the old site:
- Sources, best first: the CMS export or API (WordPress REST
/wp-json/wp/v2/posts?per_page=100&page=N,/pages,/media; or the WXR export),sitemap.xml(and sitemap indexes), then a crawl of internal links from the home page. - For each URL record: old path, title, type guess, language, last modified, the main content (HTML), the images and files it uses, and the old ID if known.
- Also collect the URLs that already redirect (HTTP 301/302) and anything in the old site's redirect plugin or
.htaccess: those must be carried over too. - Research the brand beyond its site: search the web for its name (Google Business profile and reviews, social media, press, directories and marketplaces, award or partner pages). List what the old site lacks as proposed additions: reviews and testimonials, FAQs drawn from customer questions, opening hours, locations, services or products listed elsewhere, awards, press mentions, social links. Give each one its source URL and how sure you are, flag information that contradicts the site or looks outdated, and never invent facts. Keep the additions apart from the inventory: they are proposals, not migrated content. Skip this step for a strict 1:1 move (the
migrate-siteprompt takesresearch: "no").
Checkpoint: show the user the counts per type, any URL you could not fetch and the proposed additions. Only the additions the user picks are added, in phase 5, as new drafts.
Phase 2 — Classify
Group the URLs into:
- Singleton pages (home, about, contact, landing pages) → Crafty pages built from block types.
- Repeating content (posts, news, products, team members, case studies, events) → one collection per type, one entry per item.
- Archives and taxonomy listings (
/category/x,/tag/y,/page/2, author pages, date archives) → usually not migrated as content: the new frontend generates them; they only need redirects. - Junk (search results, feeds, attachment pages,
?replytocom=…) → drop; redirect to the closest useful page when they had traffic.
Decide the new URL scheme with the user (e.g. /blog/2019/05/hello-world → /blog/hello-world), and write the old → new mapping into the inventory now.
Checkpoint: the user approves the content types and the URL scheme.
Phase 3 — Model
Follow "Model content with block types first" in SKILL.md:
- Inspect the current schemas:
list_block_types,list_collections. - For singleton pages,
create_block_typeper recurring section (hero, text + image, cards →repeater, FAQ →repeater, CTA). Reuse existing block types when they fit. - For each repeating type,
create_collectionwith the fields the content really has:title(string),excerpt(text),body(longText, HTML),cover(reference,model: "Media"),gallery(multiReference),published_on(datetime),tags(stringList),category(select), SEO fields (seo_title,seo_description). Nojsonfields for content editors maintain. - Mark the repository the images will go to as public first if the site needs permanent URLs:
manage_media_collectionwithis_public: true.
Checkpoint: the user reviews the schemas in the panel before any import.
Phase 4 — Media
- From the inventory, list the distinct image and file URLs the migrated content uses (full-size originals only).
import_media_from_urlsin batches of up to 50:{ site_id, urls, collection_name }. Each result hasstatus(imported,existingwhen the URL was already imported,duplicatewithin the batch,failed) andmedia_id/original_url.- Save the old URL →
media_id/original_urlmap. Retryfaileditems once; report the rest (private hosts, redirects, too large or unsupported types are refused on purpose — download those locally and userequest_media_upload).
Phase 5 — Content (as drafts)
- Transform each item to the collection schema: clean the HTML (drop builder shortcodes, inline styles, tracking pixels), rewrite internal links to the new paths, and replace image URLs in the HTML with the
original_urlvalues from phase 4. Put media references (cover,gallery) asmedia_idvalues. bulk_upsert_entrieswith{ site_id, collection_slug, entries: [{ slug, external_id, data }], dry_run: true }. Fix everyfaileditem (the errors name the field, e.g.data.published_on: …) and checkwarnings(dropped keys = a field missing from the schema).- Run the same batch without
dry_run. Entries are created as drafts; existing ones (sameexternal_idorslug) are updated with a revision. - Write the additions the user picked in phase 1 as new drafts (no
external_idfrom the old site) and list them, with their sources, in the review report. - Singleton pages:
create_pagewithblocksin the default locale; they start as drafts too. Other languages of the old site go in withconfigure_localesandupsert_translation, not as extra pages.
Phase 6 — Redirects
- From the inventory, one redirect per old URL whose path changes:
{ source: "/old/path", destination: "/new/path", status_code: 301 }. Use 301 for permanent moves; 302/307 only for temporary ones. - Collapse patterns into dynamic rules when possible (
/category/*→/blog,/blog/:year/:month/:slug→/blog/:slug): Crafty hosting allows 2000 static and 100 dynamic rules per deploy, including the site's own_redirects. sourcemust be a path (no domain, no?query, no#fragment);destinationa path or an absolute http(s) URL. Don't redirect a URL to itself or create chains (A → B → C: point A straight at C).upsert_redirectswithdry_run: true, fixfailed, then for real (500 per call). Check withlist_redirects.- Hosted on Crafty: they apply on the next deploy. Custom frontend: read
GET /api/v1/redirectsin the middleware (Next.jsmiddleware.ts, Nuxt server middleware) and answer with thestatus_codeandLocationheader.
Phase 7 — Human review
Give the user a short report: counts created/updated/failed per type, media imported/skipped, redirects created, and the list of items that need a human (failed transformations, missing images, pages that did not map to a block type). Ask them to review the drafts in the panel (and in the visual editor if it is configured). Apply their corrections with update_entry / update_page, or re-run the import batch (it updates by external_id).
Phase 8 — Publish and switch over
Only after the user's explicit go-ahead:
- Publish:
bulk_upsert_entriesagain withpublish: true(token withcrafty:publish) orpublish_entry/publish_pageper item; orschedule_publicationfor a set launch time. - Deploy the new frontend, point the domain at it, then spot-check a sample of old URLs (they must answer 301 to the new ones) and the sitemap.
- Keep the redirects: removing them later breaks links and search rankings.
Not covered yet
There is no WordPress WXR importer, sitemap importer or broken-link report in the panel: the agent does those steps itself with the tools above.