Documentation menu

migrate

Moving a blog in from another system

Bring an existing blog across without breaking it: the same URLs, the original publish dates, drafts kept as drafts, and images moved onto your Writavo media library.

What stays the same#

  • Every URL. You send each article's existing slug, and nothing is renamed for you.
  • Every publish date. An imported article's first publish carries its original published_at, so feed order, structured data and sitemap dates are what they were.
  • Drafts stay drafts. Only the articles you mark as published go live.
  • Images. They are copied into your media library and the article bodies are rewritten to point at the copies, so the old host can be switched off later.
  • Your source system. An import only reads what you export from it. Nothing is changed there.

The three things that make an import faithful#

  1. 1

    external_id

    Your old system's id for each article. Look it up with GET /articles?external_id= before writing, and update what you find instead of creating a second copy. That is what makes an import safe to run twice, or to resume after it stops.

  2. 2

    slug

    The last segment of the article's URL, sent exactly as it is today. It must match ^[a-z0-9]+(?:-[a-z0-9]+)*$. There are no automatic redirects, so a slug that has to change is a decision to make before you import, not after.

  3. 3

    published_at on the first publish

    POST /articles/{id}/publish accepts the original published_at, and optionally content_updated_at, in its body. Both must be in the past. They are honoured on an article's first publish only; after that the date is fixed, and resending the same value is a harmless no-op.

The easy way: an AI assistant and the MCP server#

The MCP server does the whole import for you from a single file, and handles the parts that are easy to get wrong: matching on external_id, re-hosting images, pacing requests under the rate limits, and resuming after an interruption.

  1. 1

    Connect

    Add the server to your client, for example claude mcp add writavo -- npx -y @writavo/mcp-server. No key goes in the config.

  2. 2

    Sign in

    Ask the assistant to log in. The login tool gives you a link and a short code. Open it, sign in or create your account, and approve access for your Site. If you are new, choose Bring my existing articles during onboarding: the AI pipeline then stays off until you switch it on.

  3. 3

    Export

    Have the assistant read your old system, with your own access to it, and write a Writavo import file (below). Only the published articles and the drafts you want; not scrape records, revisions or anything the old system keeps for itself.

  4. 4

    Dry run

    import_content with dry_run: true validates every article and reports each problem by external_id: slugs, lengths, references, dates, conflicts with what is already on the Site, and how many images will be copied. It writes nothing.

  5. 5

    Import

    Run it again with dry_run: false and confirm: true. It works in batches and keeps its progress next to your file, so call it until it reports done.

  6. 6

    Check

    Compare counts, slugs and dates with the old system before you point any traffic at Writavo.

The import file#

One JSON document. Authors, categories and tags are declared once and referenced from each article; they are matched to what already exists on your Site (categories and tags by slug, authors by name) and created when missing.

writavo import format, version 1
{
  "format": "writavo-import",
  "version": 1,
  "source": {
    "name": "Old blog",
    "url": "https://example.com"
  },
  "authors": [
    {
      "ref": "jane",
      "name": "Jane Doe",
      "bio": "Writes about search.",
      "avatar_url": "https://example.com/jane.png",
      "is_ai_generated": false
    }
  ],
  "categories": [
    {
      "slug": "guides",
      "name": "Guides"
    }
  ],
  "tags": [
    {
      "slug": "seo",
      "name": "SEO"
    }
  ],
  "articles": [
    {
      "external_id": "oldcms:1042",
      "status": "published",
      "title": "How to plan a content calendar",
      "slug": "how-to-plan-a-content-calendar",
      "content": "## Start with the goal\n\n![Calendar](https://example.com/images/calendar.png)\n\n...",
      "excerpt": "A calendar that survives contact with a real team.",
      "seo_title": "How to plan a content calendar",
      "seo_description": "A practical way to plan a quarter of posts.",
      "featured_image": {
        "url": "https://example.com/images/hero.png",
        "alt": "A wall calendar"
      },
      "author": "jane",
      "category": "guides",
      "tags": [
        "seo"
      ],
      "published_at": "2024-03-18T09:00:00Z",
      "content_updated_at": "2025-01-07T14:30:00Z"
    },
    {
      "external_id": "oldcms:1107",
      "status": "draft",
      "title": "Working title: editorial style guide",
      "content": "Notes so far..."
    }
  ]
}
  • external_id and status are required on every article. status is published or draft.
  • A published article also needs its title, slug, content and original published_at. content_updated_at is optional and may not be earlier than published_at.
  • content is markdown and is stored exactly as sent, apart from image URLs, which are rewritten to the copies in your media library.
  • faqs, key_takeaways, howto_steps and comparison use the same shapes as the Article resource.
  • format is optional and names one of your Site's content types by key (see GET /content-types).
  • The JSON Schema is served at https://writavo.com/schemas/import-v1.json, and by the MCP server as the writavo://import-format resource.

With the API directly#

Everything the MCP tool does is ordinary API calls, so you can write your own importer. For each article:

  1. 1

    Find it

    GET /articles?external_id=<id>. If it exists, PATCH it; if not, POST /articles with an Idempotency-Key and the external_id. A new article is always a draft.

  2. 2

    Move its images

    Upload each image with the three step media flow and replace the old URLs in content and featured_image_url with the returned ones.

  3. 3

    Publish it with its original date

    Only for articles that were public in the old system. Skip this call for an article that is already published.

publish with the original date
curl -X POST https://api.writavo.com/v1/articles/$ARTICLE_ID/publish \
  -H "Authorization: Bearer $WRITAVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"published_at": "2024-03-18T09:00:00Z", "content_updated_at": "2025-01-07T14:30:00Z"}'

What does not move#

There are no redirects. If an article's URL has to change, add the redirect on your own site before you switch over, or keep the old slug.
  • Revision history. Each article arrives as its current version.
  • Comments, view counts and other engagement data.
  • Author details beyond name, bio and avatar, and descriptions on categories and tags.
  • A per-article canonical URL. Canonicals are derived from how your blog is delivered.

Limits and cost#

  • Importing is part of the CMS, which is pay-as-you-go and the same on every plan: 10,000 documents and 25 GB of media are included. A plan is only needed for AI generation.
  • Writes are limited to 120 a minute per key and media reservations to 60. The MCP tool paces itself; your own importer should honour Retry-After.
  • A request body may be up to 1 MiB and an image up to 10 MB.
  • Webhooks fire for imported articles like any others. Register your endpoints after the import if you do not want an event per article.

After the import#

Your content is in Writavo, and your old blog is still serving its own copy. Switching your live site over is a separate step: read the articles from the API on your own server, keeping your current URLs, and verify the two side by side before you change anything a reader or a search engine can see.