@bernierllc/social-media-content-type-bluesky
v1.0.4
Published
BlueSky-specific content type definition and validation supporting AT Protocol (Authenticated Transfer Protocol) federation
Readme
@bernierllc/social-media-content-type-bluesky
BlueSky-specific content type definition and validation supporting AT Protocol (Authenticated Transfer Protocol) federation.
Installation
npm install @bernierllc/social-media-content-type-blueskyKey Features
- AT Protocol Support: Full support for decentralized AT Protocol with DIDs, AT URIs, and CIDs
- Rich Text Facets: Automatic extraction and validation of mentions, links, and hashtags with correct UTF-8 byte positioning
- Accessibility-First: Required alt text for all images (BlueSky platform requirement)
- Unicode/GraphemeHandling: Proper grapheme cluster counting for text limits (300 graphemes max)
- Content Transformation: Convert from generic social media, Twitter, and blog post formats to BlueSky
- Builder Pattern: Fluent API for constructing valid BlueSky content
- Thread Generation: Auto-split long content into properly formatted threads
Installation
npm install @bernierllc/social-media-content-type-blueskyUsage
The package provides three main classes for working with BlueSky content:
- BlueSkyContentBuilder - Fluent API for building BlueSky posts
- BlueSkyContentValidator - Validate content against BlueSky's rules
- BlueSkyContentTransformer - Convert content from other formats
Quick Start
import { BlueSkyContentBuilder, BlueSkyContentValidator } from '@bernierllc/social-media-content-type-bluesky';
// Build a simple post
const builder = new BlueSkyContentBuilder();
const content = await builder
.setText('Hello @alice.bsky.social! Check out https://example.com #bluesky')
.setLanguages(['en'])
.autoExtractFacets() // Automatically extracts mentions, links, hashtags
.build();
// Validate content
const validator = new BlueSkyContentValidator();
const result = validator.validate(content);
if (result.valid) {
console.log('Content is valid!');
console.log(`Text: ${result.graphemeCount} graphemes, ${result.textBytes} bytes`);
console.log(`Facets: ${result.facetCount}`);
console.log(`Alt Text Status: ${result.altTextStatus}`);
} else {
console.error('Validation errors:', result.errors);
}Core API
BlueSkyContentValidator
Validates BlueSky content against platform constraints:
const validator = new BlueSkyContentValidator();
// Validate complete content
const result = validator.validate(content);
// Validate specific aspects
validator.validateText(text); // Check text length (max 300 graphemes)
validator.validateFacets(facets, text); // Check facet byte positions
validator.validateAltText(embed); // Ensure images have alt text
validator.validateImages(images); // Check image count and sizesBlueSkyContentBuilder
Fluent API for building BlueSky content:
const content = await new BlueSkyContentBuilder()
.setText('Your post text here')
.addImage({
alt: 'Description of the image', // Required!
image: blobRef
})
.addMention('did:plc:abc123', '@alice')
.addLink('https://example.com', 'https://example.com')
.addHashtag('bluesky')
.setLanguages(['en'])
.buildAndValidate(); // Validates before returningBlueSkyContentTransformer
Transform content from other platforms to BlueSky:
const transformer = new BlueSkyContentTransformer();
// From generic social content
const bluesky = await transformer.fromGeneric({
text: 'Hello world!',
images: [{ url: 'https://...', alt: 'Image description' }],
hashtags: ['#test']
});
// From Twitter
const bluesky = await transformer.fromTwitter(twitterContent);
// From blog post
const bluesky = await transformer.fromBlogPost(blogContent);
// To generic format
const generic = transformer.toGeneric(blueskyContent);ThreadGenerator
Split long content into threads:
import { ThreadGenerator } from '@bernierllc/social-media-content-type-bluesky';
const generator = new ThreadGenerator();
const thread = generator.splitIntoThread(longText, {
maxGraphemes: 290, // Leave room for thread numbers
addThreadNumbers: true,
splitAtSentences: true,
preserveHashtags: true
});
// Returns array of BlueSkyContent objects
console.log(`Split into ${thread.length} posts`);BlueSkyDIDUtils
Work with Decentralized Identifiers:
import { BlueSkyDIDUtils } from '@bernierllc/social-media-content-type-bluesky';
// Parse DID
const { method, identifier } = BlueSkyDIDUtils.parseDID('did:plc:abc123');
// Validate DID
const isValid = BlueSkyDIDUtils.validateDID('did:plc:abc123');
// Resolve handle to DID
const did = await BlueSkyDIDUtils.resolveHandle('alice.bsky.social');
// Format AT URI
const uri = BlueSkyDIDUtils.formatATUri(did, 'app.bsky.feed.post', 'rkey');
// Returns: at://did:plc:abc123/app.bsky.feed.post/rkey
// Parse AT URI
const { did, collection, rkey } = BlueSkyDIDUtils.parseATUri(uri);BlueSkyFacetExtractor
Extract rich text annotations:
import { BlueSkyFacetExtractor } from '@bernierllc/social-media-content-type-bluesky';
const extractor = new BlueSkyFacetExtractor();
// Extract all facets (mentions, links, hashtags)
const facets = await extractor.extractAll('Hello @alice check https://example.com #test');
// Extract specific types
const mentions = await extractor.extractMentions(text);
const links = extractor.extractLinks(text);
const hashtags = extractor.extractHashtags(text);Platform Constraints
import { BLUESKY_CONSTRAINTS } from '@bernierllc/social-media-content-type-bluesky';
console.log(BLUESKY_CONSTRAINTS);
// {
// maxTextLength: 300, // Grapheme count, not bytes!
// maxTextBytes: 3000, // Max UTF-8 bytes
// maxImages: 4,
// maxFacets: 100,
// maxImageSize: 1048576, // 1MB
// maxVideoSize: 52428800, // 50MB
// altTextRequired: true, // BlueSky requirement
// maxAltTextLength: 1000,
// ...
// }Important: UTF-8 Byte Positioning
CRITICAL: BlueSky facets use UTF-8 byte positions, NOT character positions. This package handles this automatically, but if you're working with facets directly, be aware:
const text = "Hello 👋 @alice";
// Character positions: H=0, e=1, l=2, l=3, o=4, space=5, emoji=6, space=7, @=8...
// BUT byte positions: H=0, e=1, l=2, l=3, o=4, space=5, emoji=6-9 (4 bytes!), space=10, @=11...
// The mention "@alice" starts at byte 11, not character 8!Use countBytes() and findBytePosition() utilities to work with byte positions correctly.
Examples
Post with Image and Alt Text
const content = await new BlueSkyContentBuilder()
.setText('Beautiful sunset over the ocean')
.addImage({
alt: 'A vibrant orange and pink sunset reflecting on calm ocean waters',
image: {
$type: 'blob',
ref: { $link: 'bafyreiabc123...' }, // CID from BlueSky blob storage
mimeType: 'image/jpeg',
size: 524288
},
aspectRatio: { width: 1920, height: 1080 }
})
.buildAndValidate();Quote Post
const content = await new BlueSkyContentBuilder()
.setText('Great point!')
.setQuotePost({
uri: 'at://did:plc:abc123/app.bsky.feed.post/xyz789',
cid: 'bafyreiabc...'
})
.build();Reply with Threadgate
const content = await new BlueSkyContentBuilder()
.setText('Thanks for sharing!')
.setReply({
root: { uri: 'at://...', cid: '...' },
parent: { uri: 'at://...', cid: '...' }
})
.setThreadgate({
$type: 'app.bsky.feed.threadgate',
allow: [
{ $type: 'app.bsky.feed.threadgate#mentionRule' },
{ $type: 'app.bsky.feed.threadgate#followingRule' }
]
})
.build();Integration Status
Logger Integration
Status: Not applicable
Justification: This is a pure content type definition package with no runtime operations, side effects, or error conditions that require logging. The BlueSkyContentValidator, BlueSkyContentBuilder, and BlueSkyContentTransformer classes are stateless utility classes that perform validation, building, and transformation operations. All errors are thrown as exceptions that calling code can handle, and there are no background operations, network calls, or state changes that would benefit from structured logging.
Pattern: Pure functional utility - no logger integration needed. Logging is handled by consuming packages that use this content type.
NeverHub Integration
Status: Optional
Justification: This package can optionally register itself with NeverHub for service discovery. Content type packages can register themselves with NeverHub so that services can discover available content types at runtime. This enables dynamic content type discovery and allows services to adapt to available content types without hard-coded dependencies. The registration schema is available via BLUESKY_CONTENT_TYPE_REGISTRATION.
Pattern: Optional service discovery integration - package can register content type with NeverHub for runtime discovery.
Example Integration:
import { BLUESKY_CONTENT_TYPE_REGISTRATION } from '@bernierllc/social-media-content-type-bluesky';
// Register with NeverHub (if available)
if (typeof detectNeverHub === 'function') {
neverhub.registerContentType(BLUESKY_CONTENT_TYPE_REGISTRATION);
}Docs-Suite Integration
Status: Ready
Format: TypeDoc-compatible JSDoc comments are included throughout the source code. All public APIs are documented with examples and type information.
NeverHub Content Type Registration
import { BLUESKY_CONTENT_TYPE_REGISTRATION } from '@bernierllc/social-media-content-type-bluesky';
// Register with NeverHub for content type discovery
neverhub.registerContentType(BLUESKY_CONTENT_TYPE_REGISTRATION);
// Enables:
// - Content type validation
// - Automatic transformation routing
// - Schema-based validation
// - Platform capability discoveryTypeScript Types
All interfaces and types are fully exported for TypeScript users:
import type {
BlueSkyContent,
BlueSkyFacet,
BlueSkyEmbed,
BlueSkyImage,
BlueSkyVideo,
BlueSkyExternal,
BlueSkyRecord,
BlueSkyReply,
BlueSkyMetadata,
BlueSkyLabel,
BlueSkyThreadgate,
BlueSkyValidationResult,
BlueSkyValidationError,
BlueSkyValidationWarning
} from '@bernierllc/social-media-content-type-bluesky';See Also
- @bernierllc/social-media-content-type-twitter - Similar post structure
- @bernierllc/social-media-content-type-mastodon - Similar federated model
- @bernierllc/social-media-bluesky - Service layer for publishing to BlueSky
- @bernierllc/crypto-utils - Used for DID and hash generation
License
Copyright (c) 2025 Bernier LLC. All rights reserved.
