Skip to main content

Pixelfed Compatibility

Pixelfed is a federated photo-sharing platform similar to Instagram. It focuses on image-centric content with albums, collections, and stories. This guide covers federating with Pixelfed instances.

Pixelfed's Content Model

┌────────────────────────────────────────────────────────────┐
│ PIXELFED STRUCTURE │
├────────────────────────────────────────────────────────────┤
│ │
│ Post Types: │
│ - Photo posts (single image or album) │
│ - Stories (ephemeral content) │
│ - Collections (curated albums) │
│ │
│ Key Features: │
│ - Image-first design │
│ - Alt text support │
│ - NSFW/sensitive content handling │
│ - Square or original aspect ratio │
│ │
└────────────────────────────────────────────────────────────┘

Actor Structure

{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type": "Person",
"id": "https://pixelfed.example/users/photographer",
"preferredUsername": "photographer",
"name": "Photo Enthusiast",
"summary": "<p>Sharing beautiful moments</p>",
"inbox": "https://pixelfed.example/users/photographer/inbox",
"outbox": "https://pixelfed.example/users/photographer/outbox",
"followers": "https://pixelfed.example/users/photographer/followers",
"following": "https://pixelfed.example/users/photographer/following",
"url": "https://pixelfed.example/photographer",
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://pixelfed.example/storage/avatars/photographer.jpg"
},
"publicKey": {
"id": "https://pixelfed.example/users/photographer#main-key",
"owner": "https://pixelfed.example/users/photographer",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n..."
},
"endpoints": {
"sharedInbox": "https://pixelfed.example/f/inbox"
}
}

Photo Posts

Single Image Post

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"id": "https://pixelfed.example/p/photographer/123456789",
"attributedTo": "https://pixelfed.example/users/photographer",
"content": "<p>Beautiful sunset at the beach! 🌅</p>",
"published": "2024-01-15T18:30:00Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://pixelfed.example/users/photographer/followers"],
"sensitive": false,
"attachment": [
{
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://pixelfed.example/storage/m/photo123.jpg",
"name": "A colorful sunset over the ocean with silhouettes of palm trees",
"blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
"width": 1080,
"height": 1080,
"focalPoint": [0.0, 0.0]
}
],
"url": "https://pixelfed.example/p/photographer/123456789",
"tag": [
{
"type": "Hashtag",
"href": "https://pixelfed.example/discover/tags/sunset",
"name": "#sunset"
},
{
"type": "Hashtag",
"href": "https://pixelfed.example/discover/tags/photography",
"name": "#photography"
}
]
}

Album Post (Multiple Images)

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"id": "https://pixelfed.example/p/photographer/987654321",
"attributedTo": "https://pixelfed.example/users/photographer",
"content": "<p>Travel diary: Japan 🇯🇵</p>",
"published": "2024-01-15T12:00:00Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://pixelfed.example/users/photographer/followers"],
"attachment": [
{
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://pixelfed.example/storage/m/tokyo1.jpg",
"name": "Tokyo Tower at night",
"blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
"width": 1080,
"height": 1350
},
{
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://pixelfed.example/storage/m/kyoto1.jpg",
"name": "Traditional temple in Kyoto",
"blurhash": "L6Pj0^jE.AyE_3t7t7R**0LJxvt6",
"width": 1080,
"height": 1350
},
{
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://pixelfed.example/storage/m/osaka1.jpg",
"name": "Osaka street food market",
"blurhash": "LGF5]+Yk^6#M@-5c,1J5@[or[Q6.",
"width": 1080,
"height": 1080
}
],
"url": "https://pixelfed.example/p/photographer/987654321"
}

Image Requirements

FormatDimensionsUse Case
Square1080×1080Standard post
Portrait1080×13504:5 aspect ratio
Landscape1080×60816:9 aspect ratio
Story1080×19209:16 vertical

Media Properties

{
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://example.com/image.jpg",
"name": "Alt text description for accessibility",
"blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
"width": 1080,
"height": 1080,
"focalPoint": [0.5, 0.33]
}
PropertyDescription
nameAlt text for accessibility
blurhashBlurHash placeholder string
width / heightImage dimensions in pixels
focalPointFocus point for cropping [x, y] from -1 to 1

Sensitive Content

Marking Content as Sensitive

{
"type": "Note",
"sensitive": true,
"summary": "Artistic nudity",
"content": "<p>Figure study</p>",
"attachment": [
{
"type": "Document",
"url": "https://pixelfed.example/storage/m/sensitive.jpg",
"name": "Figure study in charcoal"
}
]
}

Processing Sensitive Content

function renderPixelfedPost(note) {
const isSensitive = note.sensitive === true;

if (isSensitive) {
return `
<article class="post sensitive">
<div class="content-warning">
<p>${note.summary || 'Sensitive content'}</p>
<button onclick="revealContent(this)">Show content</button>
</div>
<div class="hidden-content" style="display:none">
${renderAttachments(note.attachment)}
</div>
</article>
`;
}

return renderNormalPost(note);
}

Implementing Pixelfed Support

Handling Image-Heavy Posts

async function processPixelfedPost(activity) {
const note = activity.object;
const attachments = note.attachment || [];

// Validate attachments are images
const images = attachments.filter(a =>
a.type === 'Document' &&
a.mediaType?.startsWith('image/')
);

if (images.length === 0) {
// Not a photo post
return processRegularNote(activity);
}

// Cache images locally (optional)
for (const image of images) {
await cacheRemoteMedia(image.url, {
blurhash: image.blurhash,
width: image.width,
height: image.height,
altText: image.name
});
}

await db.posts.insert({
id: note.id,
type: 'photo',
content: note.content,
author: note.attributedTo,
images: images,
sensitive: note.sensitive || false,
contentWarning: note.summary,
published: note.published
});
}

Rendering Albums

function renderPhotoAlbum(attachments) {
if (attachments.length === 1) {
return renderSingleImage(attachments[0]);
}

// Grid layout for multiple images
const gridClass = attachments.length <= 4 ? 'grid-2x2' : 'grid-scroll';

return `
<div class="photo-album ${gridClass}">
${attachments.map((img, index) => `
<div class="album-image">
<img
src="${img.url}"
alt="${img.name || `Image ${index + 1}`}"
loading="lazy"
style="background: url('data:image/svg+xml,${generateBlurhashSvg(img.blurhash)}')"
/>
</div>
`).join('')}
</div>
`;
}

Creating Photo Posts for Pixelfed

async function createPhotoPost(author, images, caption, options = {}) {
// Process images
const attachments = await Promise.all(images.map(async (image) => {
const metadata = await processImage(image);

return {
type: 'Document',
mediaType: metadata.mimeType,
url: metadata.url,
name: image.altText || '',
blurhash: await generateBlurhash(image),
width: metadata.width,
height: metadata.height
};
}));

// Extract hashtags
const hashtags = extractHashtags(caption);

const note = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Note',
id: `https://${config.domain}/posts/${uuid()}`,
attributedTo: author.id,
content: formatCaption(caption),
published: new Date().toISOString(),
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [`${author.id}/followers`],
attachment: attachments,
sensitive: options.sensitive || false,
summary: options.contentWarning || null,
tag: hashtags.map(tag => ({
type: 'Hashtag',
href: `https://${config.domain}/tags/${tag}`,
name: `#${tag}`
}))
};

const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Create',
id: `${note.id}/activity`,
actor: author.id,
object: note
};

await deliverToFollowers(activity, author);

return note;
}

Collections

Pixelfed supports curated collections:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://pixelfed.example/users/photographer/collections/nature",
"name": "Nature Photography",
"summary": "My favorite nature shots",
"attributedTo": "https://pixelfed.example/users/photographer",
"totalItems": 25,
"orderedItems": [
"https://pixelfed.example/p/photographer/123",
"https://pixelfed.example/p/photographer/456"
]
}

Stories (Ephemeral Content)

Pixelfed stories are temporary posts that expire:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"id": "https://pixelfed.example/stories/photographer/abc123",
"attributedTo": "https://pixelfed.example/users/photographer",
"content": "",
"published": "2024-01-15T10:00:00Z",
"expires": "2024-01-16T10:00:00Z",
"to": ["https://pixelfed.example/users/photographer/followers"],
"attachment": [
{
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://pixelfed.example/storage/stories/story123.jpg",
"width": 1080,
"height": 1920
}
]
}

Handling Expiring Content

async function processStory(activity) {
const note = activity.object;

if (note.expires) {
const expiresAt = new Date(note.expires);

// Store with expiration
await db.stories.insert({
id: note.id,
content: note,
expiresAt
});

// Schedule cleanup
scheduleJob(expiresAt, async () => {
await db.stories.delete({ id: note.id });
});
}
}

Interactions

Likes

{
"type": "Like",
"actor": "https://mastodon.example/users/alice",
"object": "https://pixelfed.example/p/photographer/123456789"
}

Comments

{
"type": "Create",
"actor": "https://mastodon.example/users/alice",
"object": {
"type": "Note",
"content": "<p>Stunning shot! 📸</p>",
"inReplyTo": "https://pixelfed.example/p/photographer/123456789"
}
}

Sharing/Reblogging

{
"type": "Announce",
"actor": "https://mastodon.example/users/alice",
"object": "https://pixelfed.example/p/photographer/123456789"
}

Best Practices

Image Optimization

const sharp = require('sharp');

async function optimizeForFederation(imagePath) {
const image = sharp(imagePath);
const metadata = await image.metadata();

// Resize if too large
let resized = image;
if (metadata.width > 2048 || metadata.height > 2048) {
resized = image.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
});
}

// Convert to JPEG with good quality
const buffer = await resized
.jpeg({ quality: 85, progressive: true })
.toBuffer();

return {
buffer,
width: metadata.width,
height: metadata.height,
mimeType: 'image/jpeg'
};
}

Alt Text Handling

function renderImage(attachment) {
const altText = attachment.name || 'Image';
const hasAltText = attachment.name && attachment.name.length > 0;

return `
<figure>
<img
src="${attachment.url}"
alt="${escapeHtml(altText)}"
loading="lazy"
/>
${hasAltText ? `
<figcaption class="alt-text">
<button onclick="showAltText(this)">ALT</button>
<span class="hidden">${escapeHtml(attachment.name)}</span>
</figcaption>
` : ''}
</figure>
`;
}

CSS for Photo Layouts

/* Photo post grid */
.photo-album {
display: grid;
gap: 2px;
border-radius: 8px;
overflow: hidden;
}

.grid-2x2 {
grid-template-columns: repeat(2, 1fr);
}

.grid-scroll {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

/* Square crop for grid */
.album-image img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
}

/* Preserve aspect ratio for single image */
.single-image img {
max-width: 100%;
height: auto;
}

/* Blur placeholder */
.loading-blur {
filter: blur(20px);
transform: scale(1.1);
}

Testing Pixelfed Federation

describe('Pixelfed Compatibility', () => {
it('should handle photo posts', async () => {
const note = {
type: 'Note',
id: 'https://pixelfed.example/p/user/1',
content: '<p>Test</p>',
attachment: [{
type: 'Document',
mediaType: 'image/jpeg',
url: 'https://pixelfed.example/image.jpg',
name: 'Test image'
}]
};

await processPixelfedPost({ type: 'Create', object: note });

const stored = await db.posts.findOne({ id: note.id });
expect(stored.type).toBe('photo');
expect(stored.images).toHaveLength(1);
});

it('should handle albums', async () => {
const note = {
type: 'Note',
attachment: [
{ type: 'Document', url: 'https://pixelfed.example/1.jpg' },
{ type: 'Document', url: 'https://pixelfed.example/2.jpg' },
{ type: 'Document', url: 'https://pixelfed.example/3.jpg' }
]
};

const result = await processPixelfedPost({ type: 'Create', object: note });
expect(result.images).toHaveLength(3);
});
});

Next Steps