Something went wrong. Try again.
Migrating content using the Ghost API to Offprint / Standard Site on atproto
Something went wrong. Try again.
GhostOff plan #
A TypeScript CLI tool that exports public posts from a Ghost CMS using its Content API, transforms the HTML body and images into Offprint's block-based format, and uploads the resulting records to an AT Protocol PDS as site.standard.document + app.offprint.document.article pairs.
Credentials and configuration #
Values can be read from a local .env file or passed as CLI flags:
GHOST_URL/--ghost-url— Ghost publication URLGHOST_API_KEY/--ghost-api-key— Ghost Content API keyATP_IDENTIFIER/--atproto-identifier— atproto handle or DIDATP_APP_PASSWORD/--atproto-app-password— atproto app passwordATP_SERVICE/--atproto-service— PDS/service URL (defaulthttps://bsky.social)ATPUBLICATION_AT_URI/--publication-at-uri— existingsite.standard.publicationAT-URI to attach documents to
Additional CLI flags:
--dry-run— build records without uploading--export-dir— local download/cache directory (defaultghostoff-export/)--state-file— idempotency state (defaultghostoff-state.json)
Tech stack #
- Node.js + TypeScript
- AT Protocol SDK:
@atcute/client+@atcute/password-session - AT Protocol schema/validation:
@atcute/atproto,@atcute/standard-site,@atcute/lexicons - DOM parsing:
linkedom - Image resize/compress:
sharp - CLI/config:
commander,dotenv
Layout #
src/
├── index.ts # CLI orchestration
├── config.ts # env + argv loading
├── ghost.ts # Ghost Content API client
├── assets.ts # image download, cache, resize
├── html-to-offprint.ts # Ghost HTML -> app.offprint.content blocks
├── facets.ts # inline formatting -> richtext facets
└── atproto.ts # auth, blob upload, create/put records, validation
Workflow #
- Load configuration from
.envand/or CLI flags. - Authenticate with the PDS via
@atcute/password-sessionand persist the session toghostoff-session.json. - Fetch all public Ghost posts with
?include=authors,tags&formats=html,plaintext, paginated. - For each post:
- Download the
feature_imageand any inline<img>assets into--export-dir. - Resize/compress images that approach the 1 MB atproto blob limit using
sharp. - Convert the post HTML into an
app.offprint.contentwrapper inside the document'scontentarray. - Map top-level block elements:
<p>->app.offprint.block.text<h1>-<h3>->app.offprint.block.heading<blockquote>->app.offprint.block.blockquote<pre><code>->app.offprint.block.codeBlock<ul>->app.offprint.block.bulletList<ol>->app.offprint.block.orderedList<hr>->app.offprint.block.horizontalRule<img>/<figure>->app.offprint.block.image- iframes/embeds ->
app.offprint.block.webEmbedwhere a URL can be extracted
- Inline formatting (
<strong>,<em>,<a>,<code>, etc.) is converted to byte-offset facets on each text block'splaintext. - Upload all image blobs to the PDS.
- Build the
site.standard.documentrecord:site= provided publication AT-URIpath= post slugtitle,description,publishedAt,updatedAt,tags,textContentcoverImage= feature image blob if presentcontent=[{ $type: 'app.offprint.content', items: [...] }]
- Validate the document against
@atcute/standard-sitebefore uploading. - Create or update the
site.standard.documentrecord (createRecordfirst run,putRecordon reruns). - Create or update the
app.offprint.document.articlerecord whosedocumentstrongRef points to the standard document. - Persist mapping
ghostUuid -> {documentUri, documentCid, articleUri, articleCid}inghostoff-state.json.
- Download the
- Print a summary of processed posts, skipped posts, and any failures.
Error handling #
- Image download or upload failures are logged and do not abort the run.
- Any other per-post failures are logged; the script continues to the next post.
- Top-level configuration/auth failures stop the script.
- Retry/idempotency logic will be refined later; the first version focuses on logging and stateful avoidance of duplicates.
Git and secrets #
.envmust never be committed.- Runtime artifacts (
ghostoff-export/,ghostoff-session.json,ghostoff-state.json) must never be committed.