---
title: Migrate from repo files
status: current
phase: 4
order: 11
---

# Migrate from repo files

This guide is for your **agent**. It's the "match": turning the content that lives today in repo files (JSON, markdown with frontmatter, config) into Pluma content types and entries, without losing anything and without breaking the site.

## 1. Find the content

Search the site's repo, in this order:

| What to look for | Examples | In Pluma |
| --- | --- | --- |
| Site config | `site-config.js`, `config.json`, `_data/site.yml` | A **singleton** content type (`site_settings`) |
| Collections | `src/content/blog/*.md`, `content/events/*.json` | One content type per collection |
| Frontmatter | `title`, `date`, `slug`, `tags`, `image` | One field per key |
| Markdown body | Whatever comes after the frontmatter | A `rich_text` field |
| Images | `public/images/…`, `src/assets/…` | Files, uploaded once and referenced |

## 2. Propose the model, and show it before creating anything

Put together the list of types with their fields and **show it to the site owner before creating anything**. If the owner already told you to go ahead and publish ("I trust you"), that's the approval: say what you're about to create and continue. Mapping rules:

- One-line text → `symbol`. Long plain text → `text`. Markdown → `rich_text`.
- Prefer a precise type over free text: full links → `url`, addresses → `email`, hex colors → `color`, a fixed set of values (`"small" | "medium" | "large"`) → `select` with `options` (`multi_select` for several). A path on the site itself (`/menu`) isn't a `url`: keep it `symbol`, or use a `reference` to the entry.
- Dates → `date`. Numbers → `integer` or `number`. `true/false` → `boolean`.
- Lists of text (tags, categories) → `tags`.
- The URL identifier → `slug`, required.
- One image → `asset`; several → `assets`.
- Something that points to other content (author, category with its own page) → `reference`, with `link_content_types`.
- If a key appears only in some files, the field is not required.
- Field names: the same as in the frontmatter, in camelCase. That keeps the change in the site's code to a minimum.
- The type's API ID: **singular and in snake_case**: `blog_post`, `event`, `site_settings` (the `blog/` collection becomes the `blog_post` type).
- Rows of the same small shape (a menu, opening hours, social links) → a [`list`](content-model.md#lists) field with its item fields, not `json`: the owner edits them row by row. Keep `json` for data nobody edits by hand.
- **Dates:** send them as they are in the files. A date without a time (`2026-08-14`) stays without a time; one with a time zone (`-06:00`) keeps the zone. Don't convert them to UTC. Within the same field, always use the same format.
- **Author without their own page:** text (`symbol`). With their own page: their own type and a `reference`.
- **Links between pages inside markdown** (`[our rye](/menu/rye-seeded)`): rewrite them as [`entry:ID` links](../reference/rich-text.md#links-to-entries) once the target exists, so they don't break when a slug changes.
- **Menu and social links** from the config: a `list` field inside the singleton. If each item needs its own page or photo, a type of its own (`menu_item`) with references is better.
- **Site config:** a type with `"singleton": true`; Pluma rejects a second entry of that type.
- **Copy in config files** (`heroTitle`, `ctaLabel`, `footerNote`): one real field each, with a clear `name`, not a key/value `list` or `json`. Put related fields together with `group` (`Hero`, `Footer`, `Contact`): the owner gets a form with headings instead of a wall of keys.
- **Write `help` on each field for a person**, in one line: format, unit or example ("Shown under the logo"; "In USD, no symbol"). Up to 300 characters.
- **Pages built from blocks:** name the `references` field `sections` (or `blocks`), so the dashboard edits the sections inside their page. See [Composable pages](composable-pages.md).
- After creating the types, check `kind`, `subtitle_field`, `image_field` and `entry_order` in the answer. If one is wrong (a section listed as a collection, the wrong thumbnail), set it with `update_content_type`. If the files have an order (`order: 3` in frontmatter), an `integer` field named `order` sorts the type by it. See [How content is organized](content-model.md#how-content-is-organized).

## 3. Create the types

With `dry_run=true` first, then for real. See [Management API](../api/management.md). Read your site's doc (`GET /api/v1/spaces/:space_id/llms.txt`) to confirm it came out the way you wanted.

## 4. Import

- **Images first** (`POST /assets`), with a title and alt text. Keep the mapping `file path → asset id`.
- **Then the entries**, replacing image paths with asset ids: in `asset` fields goes the id; inside markdown, `![alt](asset:ID)`.
- References need the entry they point to to exist: import first what others reference (authors before posts).
- **Use the [batch](../api/management.md#batch)** for entries: up to 100 operations per request, all or nothing, and with `"$ref"` a post points to the author created in the same batch. The pilot (73 entries, 66 images) took 212 one-at-a-time calls; with batches it's just a few.
- Everything starts as a draft. **Publish only after the owner has reviewed it.**

## 5. Connect the site

- Create a `delivery` key (see [Agents → Keys for the site](../api/agents.md#keys-for-the-site)) and ask the owner to store it in the host's environment variables.
- Request the body with `rich_text=html` (or `markdown`): images already come with their URL. If your framework renders markdown, `markdown` changes less code.
- In the site's code, change **a single place** (the one that reads the files) so it reads from the Delivery API, with the files as a fallback while the trial lasts. Recipes in Frameworks.
- Do it on a branch and open a PR. Never straight to main. If the site isn't in git, edit the file and describe the change in the report.

## 6. Report

Leave the owner a summary: types created, entries imported per type, files uploaded, and **everything you couldn't map**, with the file and the reason.
