Migrating content using the Ghost API to Offprint / Standard Site on atproto
ghostoff PLAN.md
4.5 kB · 87 lines
Markdown
at main

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 URL
  • GHOST_API_KEY / --ghost-api-key — Ghost Content API key
  • ATP_IDENTIFIER / --atproto-identifier — atproto handle or DID
  • ATP_APP_PASSWORD / --atproto-app-password — atproto app password
  • ATP_SERVICE / --atproto-service — PDS/service URL (default https://bsky.social)
  • ATPUBLICATION_AT_URI / --publication-at-uri — existing site.standard.publication AT-URI to attach documents to

Additional CLI flags:

  • --dry-run — build records without uploading
  • --export-dir — local download/cache directory (default ghostoff-export/)
  • --state-file — idempotency state (default ghostoff-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 #

  1. Load configuration from .env and/or CLI flags.
  2. Authenticate with the PDS via @atcute/password-session and persist the session to ghostoff-session.json.
  3. Fetch all public Ghost posts with ?include=authors,tags&formats=html,plaintext, paginated.
  4. For each post:
    • Download the feature_image and 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.content wrapper inside the document's content array.
    • 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.webEmbed where a URL can be extracted
    • Inline formatting (<strong>, <em>, <a>, <code>, etc.) is converted to byte-offset facets on each text block's plaintext.
    • Upload all image blobs to the PDS.
    • Build the site.standard.document record:
      • site = provided publication AT-URI
      • path = post slug
      • title, description, publishedAt, updatedAt, tags, textContent
      • coverImage = feature image blob if present
      • content = [{ $type: 'app.offprint.content', items: [...] }]
    • Validate the document against @atcute/standard-site before uploading.
    • Create or update the site.standard.document record (createRecord first run, putRecord on reruns).
    • Create or update the app.offprint.document.article record whose document strongRef points to the standard document.
    • Persist mapping ghostUuid -> {documentUri, documentCid, articleUri, articleCid} in ghostoff-state.json.
  5. 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 #

  • .env must never be committed.
  • Runtime artifacts (ghostoff-export/, ghostoff-session.json, ghostoff-state.json) must never be committed.