GitHub - matteomarjanovic/juttu: A Bluesky-based commenting tool for websites.

3 min read Original article ↗

Juttu logo

Juttu is an open-source Bluesky-powered comment widget. Add a comment section to any article with three lines of HTML — comments are backed by Bluesky threads.

How it works

  • Each embedded widget links an article to a root Bluesky post via the site.standard.document AT Protocol lexicon from standard.site.
  • Comments are the replies to that Bluesky post, fetched via the Bluesky API.
  • Visitors can log in with their Bluesky account directly inside the widget to like, repost, or reply.

Installation

Add the following to your page. The <link> and <script> tags go in <head>; the <div> goes in <body> where you want comments to appear:

<!-- in <head> -->
<link rel="site.standard.document" href="at://did:plc:abc123/site.standard.document/my-article-slug" />
<script defer src="https://cdn.jsdelivr.net/npm/juttu@latest/juttu-embed.js" data-theme="auto"></script>

<!-- in <body>, where you want comments -->
<div id="juttu-comments"></div>

The AT URI (at://…) identifies your article using the site.standard.document standard. Replace did:plc:abc123 with your Bluesky DID and my-article-slug with a stable, unique slug for the article (letters, digits, ., _, ~, -, up to 512 chars).

To find your DID: go to this docs page.

The comment section resizes itself dynamically — no fixed height needed. On first load, you'll be prompted to log in with your Bluesky account to link the article to a Bluesky post — that post's reply thread becomes the comment section.

Attributes

Attribute Default Description
data-theme auto light, dark, or auto (follows the site's color scheme)
data-api-url https://api.juttu.app API backend URL (override when self-hosting)

Self-hosting

Run your own instance with Docker Compose. The backend serves both the API and the embed script.

Requirements

Create a .env file with:

SESSION_SECRET=your-random-secret
CLIENT_HOSTNAME=comments.example.com
CLIENT_SECRET_KEY=your-p256-private-key-in-multibase

SESSION_SECRET is a random string for cookie signing. CLIENT_HOSTNAME is your public domain (without https://). CLIENT_SECRET_KEY is a P-256 private key in multibase encoding for confidential OAuth.

Docker Compose

services:
  app:
    image: ghcr.io/matteomarjanovic/juttu:latest
    ports:
      - "8080:8080"
    env_file:
      - .env
    environment:
      REDIS_URL: redis://redis:6379/0
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  redis_data:

Then update your embed snippet to point at your own domain:

<script defer src="https://comments.example.com/embed/juttu-embed.js"
  data-api-url="https://comments.example.com"
  data-theme="auto"></script>

Building from source

Development

Backend

Requires Go 1.25+ and a running Redis instance.

Build the embed script first, then run the server:

cd embed && npm install && node minify.js && cp juttu-embed.js ../backend/
cd ../backend && go run . --development

The --development flag serves a test UI at /. You'll need a .env file in backend/ with at least:

SESSION_SECRET=dev-secret
REDIS_URL=redis://localhost:6379/0

Embed script

cd embed && npm install && node minify.js

Bundles juttu-embed.ts into a single minified IIFE at juttu-embed.js.

License

AGPL-3.0 License. See LICENSE.