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.
- Endpoint:
/.well-known/webfinger
- Formats Supported:
acct:blog@your-domain.com
https://your-domain.com/actor
- Response: JSON Resource Descriptor (JRD) format
- CORS: Enabled for cross-origin requests
- Endpoint:
/actor
- Type: Person
- Username:
blog
- Content Negotiation:
application/activity+json
โ ActivityPub JSON documentapplication/ld+json
โ ActivityPub JSON documenttext/html
โ Human-readable profile page
- Discovery: Linked from home page with
rel="alternate"
- Endpoint:
/
- Content Negotiation:
text/html
โ Blog homepage with postsapplication/activity+json
โ ActivityPub actor documentapplication/ld+json
โ ActivityPub actor document
- Discovery: Includes Link header pointing to
/actor
- Outbox (
/outbox
): Published blog posts as ActivityPub Create activities - Followers (
/followers
): Collection of active followers (real data) - Following (
/following
): Collection of accounts being followed (placeholder) - Inbox (
/inbox
): Processes ActivityPub activities (Follow, Unfollow, Like, Announce, Create)
- Follow Activities: Adds followers to database, sends Accept response
- Unfollow Activities: Removes followers from database
- Like Activities: Tracks likes per post, displays counts
- Announce Activities: Tracks shares/boosts per post, displays counts
- Create Activities: Tracks replies to posts, displays counts
- Followers Table: Stores follower information (actor ID, username, domain, inbox, etc.)
- Activities Table: Stores all received activities (likes, shares, replies)
- Activity Counts: Real-time counting and display on post pages
# 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.
- Using WebFinger format: Search for
@blog@your-domain.com
in your Mastodon client - Using direct URL: Paste
https://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
- HTML requests: Return human-readable web pages with proper styling
- ActivityPub requests: Return JSON-LD documents with
Content-Type: application/activity+json
- Link headers: HTML responses include
Link
headers pointing to ActivityPub alternatives - Smart prioritization: When both content types are present, prioritizes based on order in Accept header
- Home page ActivityPub: Returns the same actor document as
/actor
endpoint for consistency
โ Fully Implemented Features:
- HTTP Signatures: Cryptographic signatures for authentication โ
- Follow Processing: The inbox processes Follow/Unfollow activities โ
- Push Notifications: New posts are automatically pushed to followers โ
- Real Collections: Followers/following collections contain real data โ
- Public Key Management: Persistent RSA keys for signing โ
โ ๏ธ Setup Required:
- Generate RSA key pair for HTTP signatures (see setup instructions below)
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 format
Add 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:
- Keep the private key secret and secure
- Do not share the private key in public repositories
- Once set, do not change these keys unless necessary
- Changing keys will break federation with existing followers
After setting the keys:
- Restart your val
- Check the logs for "โ RSA key pair loaded from environment variables"
- Test federation by following your blog from Mastodon
If no persistent keys are set, the system will:
- Generate temporary keys on startup
- Log a warning about using temporary keys
- Still function, but keys will change on restart
- May cause federation issues with some servers
Use the /test-activitypub.ts
endpoint to verify all ActivityPub endpoints are working correctly.
- ActivityPub Specification
- WebFinger Specification
- ActivityStreams Vocabulary
- Mastodon API Documentation
Your blog is now discoverable and followable from the fediverse! ๐