Quote Posts for Static Sites: A Practical Guide to FEP-044f Implementation
Transform your static blog into a consent-respecting quote-enabled node in the fediverse. This guide shows you how to implement quote post support that works with Mastodon, GoToSocial, and other ActivityPub servers while respecting author preferences.
In this guide: You’ll learn to build quote-enabled blog posts that can be responsibly shared across the fediverse
Why Quote Posts Matter (And Why They’re Controversial)
The User Experience Problem
Picture this: Someone finds your blog post fascinating and wants to share it with their followers, but they also want to add their own perspective or why is important. Without quote posts, they have two unsatisfying options:
- Simple share: Just boost with no commentary (or reply)
- Link sharing: Add a link to the blog post in their note
Neither option creates the rich, attributed conversations that make social media engaging.
The Solution: Consent-First Quote Implementation
We’re implementing FEP-044f: Consent-respecting quote posts in our federated blog.
What this means for your readers:
- They can quote your posts with confidence that you’ve opted in
- Their quotes include proper attribution and linking
What this means for you:
- Automatic handling of quote requests
- Future-ready for advanced moderation features (like in the fuuutuuure)
Implementation Overview
We are going to:
- Modify the Notes JSON to assert that the notes are quotable.
- Modify our Index function (the only dynamic POST endpoint) to handle quote requests and send the appropriate approval back (blanket approval).
1. Modifying the Notes: Enhanced ActivityPub Context
What We Changed:
Extended the @context from a simple string to a rich object array supporting the GoToSocial namespace.
Before:
"@context": "https://www.w3.org/ns/activitystreams"
After:
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"gts": "https://gotosocial.org/ns#",
"interactionPolicy": {"@id": "gts:interactionPolicy", "@type": "@id"},
"canQuote": {"@id": "gts:canQuote", "@type": "@id"},
"automaticApproval": {"@id": "gts:automaticApproval", "@type": "@id"}
}
]
We are also adding this section at the end of the Note:
"interactionPolicy": {
"canQuote": {
"automaticApproval": "https://www.w3.org/ns/activitystreams#Public"
}
}
If you want to be specific about who can quote your post, this is where you do it, read more in here.
You can see an example of the implementation in RssUtils.cs - in the GetNote method.
2: Quote Request Processing
Now we need to add the quote request handling system that processes incoming quote requests and automatically approves them based on our interaction policy.
New Components:
- QuoteRequestService: Processes incoming quote requests from the fediverse
- Auto-Approval Logic: Automatically approves public quote requests as defined in our interaction policy
- Quote Authorization: Issues authorization tokens (stamps) for approved quotes
The Quote Request Flow:
sequenceDiagram
participant Requester as Fediverse User
participant Inbox as Our Inbox
participant QRS as QuoteRequestService
participant Target as Target Instance
Requester->>Inbox: QuoteRequest for our post
Inbox->>QRS: Process quote request
QRS->>QRS: Check interaction policy
QRS->>QRS: Generate authorization stamp
QRS->>Target: Send Accept + Authorization
Target->>Requester: Quote approved
Checkout the implementation in the QuoteRequestService.cs.
3: The Missing Piece - Quote Authorization Stamps
After implementing steps 1 and 2, I tried to test quotes and it seems to be working. However, although the post looked like quotes in my home instance (e.g. hachyderm.io) the same post in other instances (e.g. theforkiverse.com) was still showing as pending. This is because we’re missing the QuoteAuthorization verification mechanism described in FEP-044f.
The Solution:
When we approve a quote request, we need to generate a static QuoteAuthorization file at the stamp URL that third-party instances can fetch to verify the quote is legitimate. The home instance does not need this because it relies on the original Accept handshake, but it will send an Update activity to other instances with the stamp URL. It is in that URL where we need to store our quote authorization.
Implementation:
This is very similar to the replies generation explained in part 8, where each time we receive a reply we update a collection of the note.
You can take a look at my dontnet implementation in StampsGenerator.cs.
The trick here, and something to pay attention to, is the interactingObject, interactionTarget, and attributedTo fields. This is one example:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"QuoteAuthorization": "https://w3id.org/fep/044f#QuoteAuthorization",
"gts": "https://gotosocial.org/ns#",
"interactingObject": {
"@id": "gts:interactingObject",
"@type": "@id"
},
"interactionTarget": {
"@id": "gts:interactionTarget",
"@type": "@id"
}
}
],
"type": "QuoteAuthorization",
"id": "https://maho.dev/socialweb/quotes/e7f5a28d-26d8-4c19-8bae-6d9bbb040b5e",
"attributedTo": "https://maho.dev/@blog",
"interactingObject": "https://theforkiverse.com/ap/users/115868408494460202/statuses/116014861405475931",
"interactionTarget": "https://maho.dev/socialweb/notes/6078d5c5d08f3a17aca4642bba3d61f9"
}
It took me a few times to actually get it right. attributedTo and interactionTarget refer to the original quoted post, while interactingObject is the note doing the quoting (from the remote server).
I decided to go with this file structure, but you can do whatever you want—also not necessarily unique GUIDs, but maybe hashes instead:
File Structure:
your-site.com/socialweb/
├── quotes/
│ └── {guid}/ # QuoteAuthorization stamps files
└── notes/
└── {post-id}/ # Your original posts
A clever alternative
Claire from a Mastodon dev chat suggested a clever approach in case you use dynamic endpoints: instead of storing the actual authorization, you craft it at runtime. The cleverness is that you can encode interactingObject and interactionTarget as base64 and include them as part of the URL, e.g.:
https://maho.dev/socialweb/quotes/{base64-encoded-interacting-object}/{base64-encoded-interaction-target}
Example:
interactingObject:https://theforkiverse.com/ap/users/115868408494460202/statuses/116014861405475931interactionTarget:https://maho.dev/socialweb/notes/6078d5c5d08f3a17aca4642bba3d61f9
When base64-encoded and used in the URL:
https://maho.dev/socialweb/quotes/aHR0cHM6Ly90aGVmb3JraXZlcnNlLmNvbS9hcC91c2Vycy8xMTU4Njg0MDg0OTQ0NjAyMDIvc3RhdHVzZXMvMTE2MDE0ODYxNDA1NDc1OTMx/aHR0cHM6Ly9tYWhvLmRldi9zb2NpYWx3ZWIvbm90ZXMvNjA3OGQ1YzVkMDhmM2ExN2FjYTQ2NDJiYmEzZDYxZjk%3D
This approach eliminates the need for static file storage while maintaining the same verification capabilities. Your dynamic endpoint can decode the URLs and generate the QuoteAuthorization JSON on-demand. And just in case it’s not obvious, this is the URL that you return as stamp ID in the QuoteAuthorization on step 2.
Key Takeaways
By implementing FEP-044f, we’re not just adding quote functionality - we’re building consent-respecting social interactions into the protocol level.
Why This Matters:
This implementation shows how static sites can participate in modern social web standards while keeping their simplicity and performance benefits. Right now, we’re automatically allowing all public quotes, but this foundation sets us up for more granular consent controls in the future - like requiring approval for specific users or implementing follower-only quoting.
The consent-respecting approach means our content can be shared thoughtfully across the fediverse, with the infrastructure already in place to handle more sophisticated permission systems as they evolve.
Next Steps: The Quote Visualization Challenge
Now that we’ve successfully implemented the backend infrastructure for consent-respecting quote posts, we face an equally important question: How should we display these quotes on our website?
Treat quoted posts as special reply types? Quotes have different semantic meaning than replies - they’re more like “shared with commentary” So maybe create a separate “Quoted By” section similar to how we handle likes and shares?
Any ideas?