Migrating content using the Ghost API to Offprint / Standard Site on atproto
Something went wrong. Try again.
TypeScript 100%
README.md
GhostOff #
Export public posts from a Ghost CMS and upload them to Offprint via the AT Protocol / Standard.site lexicons.
The live test publication is at https://ghostoff.offprint.app.
What it does #
- Paginates through Ghost's public Content API for posts.
- Downloads each post's images, resizes them to fit atproto's blob limits, and uploads them as blobs to a PDS.
- Converts Ghost HTML bodies into Offprint's block-based format (
app.offprint.content). - Writes a
site.standard.documentrecord per post and anapp.offprint.document.articlerecord that points back to it. - Tracks progress in a local state file so reruns update existing records instead of duplicating them.
Quick start #
Copy .env.example to .env and fill in the secrets:
cp .env.example .env
GHOST_URL=https://your-ghost-site.com
GHOST_API_KEY=your-ghost-content-api-key
ATP_IDENTIFIER=your-handle-or-did
ATP_APP_PASSWORD=your-app-password
ATP_SERVICE=https://selfhosted.social
ATPUBLICATION_AT_URI=at://did:plc:.../site.standard.publication/...
Run a dry first pass to inspect what would be uploaded:
npm install
npm run dev -- --dry-run
When you're ready, run the real migration:
npm run dev
Rerunning the same command will putRecord existing records instead of creating duplicates.
CLI options #
| Flag | Environment variable | Description |
|---|---|---|
--ghost-url |
GHOST_URL |
Ghost publication URL |
--ghost-api-key |
GHOST_API_KEY |
Ghost Content API key |
--atproto-identifier |
ATP_IDENTIFIER |
atproto handle or DID |
--atproto-app-password |
ATP_APP_PASSWORD |
atproto app password |
--atproto-service |
ATP_SERVICE |
PDS/service URL |
--publication-at-uri |
ATPUBLICATION_AT_URI |
Existing site.standard.publication AT-URI |
--dry-run |
— | Build records without uploading |
--export-dir |
— | Local asset cache directory (default: ghostoff-export) |
--state-file |
— | Idempotency state file (default: ghostoff-state.json) |
--verbose |
— | Verbose logging |
Scripts #
npm run dev # run the migration
npm run build # compile TypeScript to dist/
npm run typecheck # type-check without emitting
Project layout #
src/
├── index.ts # CLI orchestration
├── config.ts # env + argv loading
├── ghost.ts # Ghost Content API client
├── assets.ts # image download, cache, resize, blob upload
├── html-to-offprint.ts # Ghost HTML -> Offprint blocks
├── facets.ts # inline formatting -> richtext facets
├── atproto.ts # auth, record create/put, validation
├── state.ts # idempotency state file
├── rate-limit.ts # retry + rate-limit helpers
└── types.ts # shared type definitions
Learning more #
PLAN.md— the original migration plan.docs/session-summary.md— a narrative of how the first version was built.docs/development.md— how the codebase is organized.docs/next-steps.md— ideas for what to add or improve next.