Getting Your Ghost Blog Verified on Bluesky (and Dropping the "[Unofficial]" Label)
Ghost's social web integration is great: turn it on and your blog gets a presence on the fediverse and, via Bridgy Fed, on Bluesky. But out of the box, the Bluesky side of that looks a little sad. Your account gets a handle like yoursite.com.web.brid.gy and a display name with an [Unofficial] suffix bolted onto it — which reads like a warning label not to trust your own website.
Fixing it took me an embarrassing amount of trial and error, some DNS archaeology, and eventually reading Bridgy Fed's source code. Here's the recipe so you don't have to do any of that.
TL;DR
- Add two redirects to your Ghost site via
redirects.yaml. - Point a DNS TXT record at your bridged account's DID.
- Log into Bridgy Fed and set your Bluesky handle to your bare domain.
- Hit the profile refresh button.
The details — and the traps — are below.
First: what doesn't work
My first attempt was adding rel="me" links to my site header, because that's what every verification guide on the internet tells you to do. Save yourself the trouble: rel="me" is a Mastodon/fediverse thing. Bluesky ignores it completely. It's still worth having exactly one of these for fediverse green checks:
<link rel="me" href="https://web.brid.gy/r/https://yoursite.com/">
…but it will never touch the Bluesky label.
So what does? Bridgy Fed's source is public, and the logic is right here in protocol.py: the [Unofficial] suffix is skipped only if your site has done one of three things —
- Sent Bridgy Fed a webmention. Don't do this one! Ghost bridges your posts by reading your RSS feed, and the moment Bridgy Fed receives any webmention from your site, it permanently stops reading your feed.
- Set up webfinger redirects to fed.brid.gy. Also don't do this one — Ghost's native ActivityPub serves
/.well-known/webfingeritself, and redirecting it away breaks your native fediverse account. - Made your Bluesky handle exactly your domain. This is the one. And it has to be exactly your domain — the code compares the handle to your site's domain literally, so a subdomain like
blog.yoursite.comwon't drop the label. I learned this the hard way.
Step 1: the redirects
Ghost lets you upload custom redirects: Settings → Labs → Redirects. Add these to your redirects.yaml (if you already have one, download it from that same screen first and merge — uploads replace the whole file):
302:
/.well-known/site.standard.publication: https://fed.brid.gy/.well-known/site.standard.publication?protocol=web&domain=yoursite.com
/.well-known/atproto-did: https://fed.brid.gy/.well-known/atproto-did?protocol=web&id=yoursite.com
The first one verifies your site ownership with Bridgy Fed and marks your site as a verified publication for Bluesky's long-form content standard. The second serves your bridged account's DID over HTTPS — it's the atproto "well-known" handle resolution method, and it's a nice safety net that keeps your handle resolving even if your DNS record ever goes missing.
Step 2: find your bridged account's DID
Your bridged account's DID is on your Bridgy Fed status page at https://fed.brid.gy/web/yoursite.com, or in the URL of your bridged account's avatar, or via the API:
curl 'https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=yoursite.com.web.brid.gy'
It looks like did:plc:yijb6jmjnjjs7lzay5nlikn4 (that one's mine).
Step 3: the DNS record — and the trap
Add a TXT record:
- Name:
_atproto(i.e._atproto.yoursite.com) - Value:
did=did:plc:YOUR_BRIDGED_DID
The trap: if you already use @yoursite.com as the handle of your personal Bluesky account — like I did — you have to move it first. A handle maps to exactly one account, and atproto treats multiple did= TXT records on the same name as a failed resolution. Don't stack a second record next to the old one; migrate:
- Add a TXT record for a new personal handle, e.g.
_atproto.me→did=did:plc:YOUR_PERSONAL_DID. (Handle changes are safe — followers, posts, and old mentions all follow your DID, not your handle.) - In the Bluesky app: Settings → Account → Handle → "I have my own domain" →
me.yoursite.com. - Then delete your personal DID's record from
_atproto.yoursite.com, leaving only the bridged account's record.
The other trap: if you hit "Verify DNS Record" before your record has propagated, Bluesky's resolver caches the failure — for my zone that was 30 minutes. Repeatedly mashing the verify button does nothing; check your record at bsky-debug.app/handle, then wait out the cache and try once more.
Step 4: log into Bridgy Fed
Go to fed.brid.gy/login and use the "Your website" box, which authenticates with IndieAuth. IndieAuth works by scanning your homepage for rel="me" links to an account that can vouch for you — and your existing social links probably won't qualify (X, Bluesky, Threads, and mastodon.social all aren't supported providers). The easy fix is GitHub: add
<link rel="me" href="https://github.com/yourusername">
to your Ghost header code injection, and put https://yoursite.com in the website field of your GitHub profile. Refresh the login page and GitHub appears as a sign-in option. (A mailto: link works too, but then your email address is sitting in your page source for every scraper to harvest. You can delete the link after logging in either way — it's only needed at authentication time.)
Step 5: set the handle and refresh
On your Bridgy Fed accounts page, enter your bare domain in Set Bluesky handle and hit Go, then click the ⟳ profile refresh button next to your site's name. The next profile push goes out without the suffix.
Give it time — Bridgy Fed caches aggressively, and in my case the old handle lingered in its internal state for a while even after the network-side identity had updated everywhere. Patience (and one more ⟳) beats more clicking.
Verifying it all worked
# your site serves the ownership redirect
curl -sI https://yoursite.com/.well-known/site.standard.publication
# your site serves the bridged DID
curl -sL https://yoursite.com/.well-known/atproto-did
# the handle resolves to the bridged account
curl 'https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=yoursite.com'
# the display name, sans suffix
curl -s 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=yoursite.com'
The end state: your blog posts to Bluesky as @yoursite.com, no [Unofficial] label, with your personal account alive and well at @me.yoursite.com — and now people can trust that these accounts are really yours.