npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-bluesky

Key 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-bluesky

Usage

The package provides three main classes for working with BlueSky content:

  1. BlueSkyContentBuilder - Fluent API for building BlueSky posts
  2. BlueSkyContentValidator - Validate content against BlueSky's rules
  3. 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 sizes

BlueSkyContentBuilder

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 returning

BlueSkyContentTransformer

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 discovery

TypeScript 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

License

Copyright (c) 2025 Bernier LLC. All rights reserved.