I wanted to draft travel blog posts offline. Ghost’s admin editor is good, but it assumes a connection, and travel posts have a lot of photos. The first version of this app, in July 2026, was a SwiftData model with an entry, ordered blocks (text, image, gallery, embed), and a photo importer that copied selected images into the app’s own storage so a draft no longer depended on the Photos library or the network. Galleries were capped at nine photos to match Ghost’s gallery card.
That scaffold grew into Spectrapress, a native Ghost editor for Mac, iPhone, and iPad. It’s about 8,000 lines of Swift and it is not released yet. It’s a 0.1 pre-beta.
What It Does Now
- Offline-first drafting. Everything you write is local. The network only matters when you pull or push.
- Multi-site library. A site-scoped library with direct switching and filters for All, Posts, Pages, and Review.
- Real Ghost sign-in. Account session login with verification codes and Keychain storage that stays on the device, with an Admin API key as an “Advanced” fallback.
- Pull and push. Full pulls of posts and pages with content, feature images, tags, and dates.
- Compare, undo, revert. A local-versus-server comparison view, undo and redo, conflict states, and a reviewed server revert.
- Cards it can’t edit. Ghost card types the app doesn’t support natively are preserved raw, so they survive a round trip untouched.
- Ready for Review. A shared handoff through a private Ghost workflow tag, so another staff member can pull the draft and do the final publish without Spectrapress needing an account service of its own.
The Hard Part Is Not Losing Anything
An editor that talks to a CMS has one job above all others, which is to never destroy content. Most of the roadmap is organized around that.
Site removal used to be a delete. I replaced it with a flow that disconnects while keeping a reconnectable identity, keeps the entries as local copies, lets you transfer them to another site, or deletes only behind an explicit destructive confirmation. Reconnecting the same Ghost URL recovers the existing identity instead of creating a duplicate and hiding what was imported. Deleting an entry spells out the local versus Ghost consequences and never removes remote content implicitly.
Then there is the round trip. The test plan I’m working toward is to build a fixture library covering every supported card, multiple authors, visibility tiers, newsletters, scheduled dates, and galleries from zero to nine images. For each fixture: pull, edit one field, push, reopen in Ghost Admin, pull again, and verify the untouched content is unchanged. Preserved cards have to survive without mutation. Most of those boxes are still unchecked, which is the honest state of the project.
Permissions Are a Product Feature
Ghost has roles, and an app that ignores them will fail in confusing ways. Spectrapress detects the signed-in staff member’s role and uses it before a push. Some of what that covers:
- A Contributor can’t start a Ready for Review push that needs the private tag.
- Authors and Contributors inherit the site’s default visibility on create and preserve the remote value on update, instead of sending a forbidden change.
- Create calls identify the signed-in user as author, and update calls omit authors so the byline isn’t overwritten.
- A 403 from Ghost shows its structured message in the alert instead of a generic failure.
That last one came with a SwiftUI lesson: driving the alert from a writable presentation binding avoids a presentation cycle when a push fails.
The open item is a full per-action capability matrix, with a rule I wrote at the top of it: never imply Spectrapress grants rights Ghost hasn’t granted.
Bugs That Taught Me Something
Two early ones are worth keeping:
- I named a SwiftData model
Entry, and the compiler failed withExpansion of macro 'Entry()' requires leading '#'. SwiftUI’s@Entrymacro for environment values collides with a type of that name in any file importing SwiftUI. It becameJournalEntry. - Swift 6 strict concurrency rejected a photo import service because a nonisolated property referenced an actor-isolated directory. The service does no UI work, so the fix was to remove
@MainActor, not to bend the code around it.
Later, performance work moved image decoding off the interface thread, cached display-sized thumbnails, and removed repeated Keychain reads from SwiftUI redraws. All three are the sort of thing that looks fine on a short post and falls over on a gallery.
What Blocks a Beta
The roadmap has a short list of things that must be true before external testers get a build:
- Round-trip safety proven, not assumed.
- Sign-in tested against both Ghost(Pro) and a current self-hosted site, including bad passwords, email 2FA, expired sessions, and offline launch.
- CloudKit sync tested between a real iPhone, iPad, and Mac, including edits made while one device is offline. Right now a model-container failure ends in a
fatalErrorat launch, which should become a recoverable screen. - Accessibility checks: VoiceOver, 44-point targets, and Move Up and Move Down alternatives wherever drag and drop exists.
- A privacy policy, support URL, and an app identity I won’t regret. The bundle ID and CloudKit container still carry the old
TravelJournalname, and changing them later changes data continuity.
The test suite is 25 tests so far, using checked-in fixtures and mocked Ghost networking. Roadmap items add much more.
Should It Be a Business?
I spent some time on this, mostly to avoid building something I couldn’t justify supporting. The conclusion of my own analysis was modest. Ghost is healthy but narrow, the free Ghost editor keeps improving, and a direct iPhone-only competitor sells for a few dollars. So this is not a salary replacement. It could be a durable side business if the beta proves reliability, since reliability is part of what people would be paying for.
The working plan, which is a plan and not a commitment, is to keep TestFlight free, launch with a free trial and a one-time unlock, and not add a subscription until there’s a real recurring service behind it. Whatever happens with pricing, the trial will leave read, preview, and export available afterward, so nobody is ever locked away from their own writing.
Where It Goes
The immediate goal is a small, trustworthy TestFlight release for real Ghost publishers, not feature completeness. When it can connect, pull, edit, and push without losing content, recover clearly when something goes wrong, and give testers enough context to report useful feedback, it’s ready.