# Building GhostOff: from Ghost CMS to Offprint in one session
GhostOff is a small TypeScript tool that exports public posts from a Ghost CMS and uploads them to Offprint using the AT Protocol and Standard.site lexicons. This post walks through the decisions and gotchas from the first build session.
## Starting point
The goal was straightforward: take content that lives in Ghost and make it available on Offprint, an atproto-based long-form publishing platform. Ghost exposes a read-only Content API, and Offprint's underlying data format is a set of atproto records (`site.standard.document` plus `app.offprint.document.article`). So the task reduces to:
1. Fetch posts from Ghost.
2. Download and re-host images as atproto blobs.
3. Convert Ghost HTML into Offprint blocks.
4. Write the records to a PDS.
5. Keep track of what was uploaded so reruns are idempotent.
## Stack choices
We picked TypeScript because the atproto ecosystem has first-class tooling there. For the atproto side we used the atcute packages: `@atcute/client` for XRPC, `@atcute/password-session` for password-based auth, and `@atcute/standard-site` plus `@atcute/lexicons` for schemas and runtime validation.
For DOM parsing we used `linkedom`, which is light and Node-friendly. For image resizing we used `sharp`. For the CLI surface we used `commander` and `dotenv`.
## Mapping the two formats
Ghost posts come back as HTML with a JSON representation of the Ghost editor's cards: headings, paragraphs, galleries, embeds, code blocks, and custom HTML. Offprint expects a block array: `app.offprint.block.text`, `heading`, `bulletList`, `orderedList`, `codeBlock`, `image`, `imageGrid`, `webEmbed`, `blockquote`, and so on.
The converter walks the body of the post and maps top-level elements:
- `