This document explains the ActivityPub and WebFinger implementation in the Email Blog platform.
ActivityPub is a decentralized social networking protocol that allows your blog to be discovered and followed from Mastodon, Pleroma, and other federated social networks. WebFinger is the discovery mechanism that allows users to find your blog using familiar @username@domain.com
syntax.
/.well-known/webfinger
acct:blog@your-domain.com
https://your-domain.com/actor
/actor
blog
application/activity+json
โ ActivityPub JSON documentapplication/ld+json
โ ActivityPub JSON documenttext/html
โ Human-readable profile pagerel="alternate"
/
text/html
โ Blog homepage with postsapplication/activity+json
โ ActivityPub actor documentapplication/ld+json
โ ActivityPub actor document/actor
/outbox
): Published blog posts as ActivityPub Create activities/followers
): Collection of active followers (real data)/following
): Collection of accounts being followed (placeholder)/inbox
): Processes ActivityPub activities (Follow, Unfollow, Like, Announce, Create)# Optional: Set a custom domain for ActivityPub ACTIVITYPUB_DOMAIN=your-custom-domain.com
If not set, the system will automatically detect the domain from the request headers.
@blog@your-domain.com
in your Mastodon clienthttps://your-domain.com/actor
in the search box# Test with acct format curl "https://your-domain.com/.well-known/webfinger?resource=acct:blog@your-domain.com" # Test with direct URL format curl "https://your-domain.com/.well-known/webfinger?resource=https://your-domain.com/actor"
# Get actor document curl -H "Accept: application/activity+json" https://your-domain.com/actor # Get outbox (published posts) curl -H "Accept: application/activity+json" https://your-domain.com/outbox # Get followers collection curl -H "Accept: application/activity+json" https://your-domain.com/followers
{ "subject": "acct:blog@your-domain.com", "aliases": [ "https://your-domain.com/actor", "https://your-domain.com/" ], "properties": { "http://schema.org/name": "Email Blog" }, "links": [ { "rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://your-domain.com/" }, { "rel": "self", "type": "application/activity+json", "href": "https://your-domain.com/actor" }, { "rel": "http://ostatus.org/schema/1.0/subscribe", "template": "https://your-domain.com/authorize_interaction?uri={uri}" } ] }
{ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" ], "type": "Person", "id": "https://your-domain.com/actor", "preferredUsername": "blog", "name": "Email Blog", "summary": "A blog powered by email publishing", "url": "https://your-domain.com", "inbox": "https://your-domain.com/inbox", "outbox": "https://your-domain.com/outbox", "followers": "https://your-domain.com/followers", "following": "https://your-domain.com/following", "publicKey": { "id": "https://your-domain.com/actor#main-key", "owner": "https://your-domain.com/actor", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\\n...\\n-----END PUBLIC KEY-----" } }
When you publish a blog post via email, it automatically becomes available in the ActivityPub outbox as a Create activity:
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Create", "id": "https://your-domain.com/post/my-post#create", "actor": "https://your-domain.com/actor", "published": "2024-01-01T12:00:00Z", "object": { "type": "Note", "id": "https://your-domain.com/post/my-post#note", "attributedTo": "https://your-domain.com/actor", "content": "<h2>My Post Title</h2><p>Post content...</p>", "published": "2024-01-01T12:00:00Z", "url": "https://your-domain.com/post/my-post", "to": ["https://www.w3.org/ns/activitystreams#Public"] } }
You can test the content negotiation functionality using curl or any HTTP client:
# Request HTML (browser behavior) curl -H "Accept: text/html" https://your-domain.com/ # Request ActivityPub JSON (returns actor document) curl -H "Accept: application/activity+json" https://your-domain.com/ # Request JSON-LD (alternative ActivityPub format) curl -H "Accept: application/ld+json" https://your-domain.com/ # Mixed Accept header (browser with ActivityPub support) - prioritizes HTML curl -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,application/activity+json;q=0.8,*/*;q=0.7" https://your-domain.com/ # ActivityPub client request - prioritizes ActivityPub curl -H "Accept: application/activity+json, text/html;q=0.8" https://your-domain.com/
# Request HTML profile page curl -H "Accept: text/html" https://your-domain.com/actor # Request ActivityPub actor document curl -H "Accept: application/activity+json" https://your-domain.com/actor # Request JSON-LD actor document curl -H "Accept: application/ld+json" https://your-domain.com/actor
Content-Type: application/activity+json
Link
headers pointing to ActivityPub alternatives/actor
endpoint for consistencyโ Fully Implemented Features:
โ ๏ธ Setup Required:
For proper ActivityPub federation, you need to set up persistent RSA keys for HTTP signatures:
Visit your /generate-keys.ts
val to generate a new RSA key pair. This will provide you with:
ACTIVITYPUB_PUBLIC_KEY
- Public key in PEM formatACTIVITYPUB_PRIVATE_KEY
- Private key in PEM formatAdd these to your Val Town environment variables:
ACTIVITYPUB_PUBLIC_KEY=-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY----- ACTIVITYPUB_PRIVATE_KEY=-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC... -----END PRIVATE KEY-----
๐ IMPORTANT:
After setting the keys:
If no persistent keys are set, the system will:
Use the /test-activitypub.ts
endpoint to verify all ActivityPub endpoints are working correctly.
Your blog is now discoverable and followable from the fediverse! ๐