Micropub Implementation Guide

Overview

django-indieweb now provides a fully functional Micropub endpoint that can create content in your Django application. The implementation uses a pluggable content handler system that allows you to integrate Micropub with any Django content model.

Quick Start

1. Basic Setup

The Micropub endpoint is available at /indieweb/micropub/ by default. It requires authentication via IndieAuth tokens with the “post” scope.

2. Using the Default In-Memory Handler

For testing and development, django-indieweb includes an in-memory content handler that stores posts in memory:

# This is the default if no handler is configured
# Posts are stored in memory and lost on restart

3. Creating a Custom Content Handler

To integrate Micropub with your Django models, create a custom content handler:

# myapp/micropub_handler.py
from indieweb.handlers import MicropubContentHandler, MicropubEntry
from myapp.models import BlogPost

class BlogPostMicropubHandler(MicropubContentHandler):
    def create_entry(self, properties, user):
        # Extract properties
        content = properties.get('content', [''])[0]
        name = properties.get('name', [''])[0]
        categories = properties.get('category', [])

        # Create your model instance
        post = BlogPost.objects.create(
            author=user,
            title=name or 'Untitled',
            content=content,
            status='published'
        )

        # Add categories/tags
        for category in categories:
            post.tags.add(category)

        # Return MicropubEntry with the URL
        return MicropubEntry(
            url=post.get_absolute_url(),
            properties=properties
        )

    def get_entry(self, url, user):
        # Parse URL to get post
        try:
            post = BlogPost.objects.get(
                slug=url.split('/')[-2],  # Adjust based on your URL structure
                author=user
            )
            return MicropubEntry(
                url=post.get_absolute_url(),
                properties={
                    'name': [post.title],
                    'content': [post.content],
                    'published': [post.created.isoformat()],
                }
            )
        except BlogPost.DoesNotExist:
            return None

    def update_entry(self, url, updates, user):
        # Implement update logic
        post = self._get_post_from_url(url, user)

        if 'replace' in updates:
            for key, values in updates['replace'].items():
                if key == 'content':
                    post.content = values[0]
                elif key == 'name':
                    post.title = values[0]

        post.save()
        return self.get_entry(url, user)

    def delete_entry(self, url, user):
        post = self._get_post_from_url(url, user)
        post.delete()

    def undelete_entry(self, url, user):
        # Implement if you support soft deletes
        raise NotImplementedError("Undelete not supported")

4. Configure Your Handler

In your Django settings:

# settings.py
INDIEWEB_MICROPUB_HANDLER = 'myapp.micropub_handler.BlogPostMicropubHandler'

Supported Features

Content Types

The Micropub endpoint supports both form-encoded and JSON requests:

Form-encoded:

curl -X POST https://example.com/indieweb/micropub/ \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d "h=entry" \
  -d "content=Hello World!" \
  -d "category=indieweb,micropub"

JSON:

curl -X POST https://example.com/indieweb/micropub/ \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": ["h-entry"],
    "properties": {
      "content": ["Hello JSON!"],
      "category": ["indieweb", "json"]
    }
  }'

Supported Properties

Common h-entry properties are supported: - content - The main content - name - Title/name of the entry - category - Tags/categories (comma-separated or array) - location - Geographic location (geo URI format) - in-reply-to - URL this post is replying to - photo - Photo URL(s) - published - Publication date

Query Endpoints

Configuration:

curl https://example.com/indieweb/micropub/?q=config \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns supported post types and features.

Syndication Targets:

curl https://example.com/indieweb/micropub/?q=syndicate-to \
  -H "Authorization: Bearer YOUR_TOKEN"

Testing Your Implementation

  1. Get an access token via IndieAuth with “post” scope

  2. Create a test post:

curl -X POST http://localhost:8000/indieweb/micropub/ \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d "h=entry" \
  -d "content=Test post from Micropub!"
  1. Check the response:

  • Status: 201 Created

  • Location header contains the URL of the created post

Advanced Integration

Handling Different Post Types

def create_entry(self, properties, user):
    # Determine post type
    post_type = 'note'  # default

    if properties.get('name'):
        post_type = 'article'
    elif properties.get('photo'):
        post_type = 'photo'
    elif properties.get('in-reply-to'):
        post_type = 'reply'

    # Create appropriate model based on type
    if post_type == 'article':
        return self._create_article(properties, user)
    elif post_type == 'photo':
        return self._create_photo_post(properties, user)
    else:
        return self._create_note(properties, user)

Adding Syndication Support

def get_config(self, user):
    config = super().get_config(user)

    # Add syndication targets
    config['syndicate-to'] = [
        {
            'uid': 'https://twitter.com/username',
            'name': 'Twitter'
        },
        {
            'uid': 'https://mastodon.social/@username',
            'name': 'Mastodon'
        }
    ]

    return config

Error Handling

The Micropub endpoint returns appropriate HTTP status codes: - 201 Created - Success, with Location header - 400 Bad Request - Invalid request data - 401 Unauthorized - Missing or invalid token - 403 Forbidden - Token lacks required scope - 501 Not Implemented - For unimplemented features

Security Considerations

  1. Always validate user permissions in your handler

  2. Sanitize content before storing

  3. Validate URLs for properties like photo and in-reply-to

  4. Rate limiting is recommended for production use

Example: Integration with django-cast

# cast_micropub.py
from indieweb.handlers import MicropubContentHandler, MicropubEntry
from cast.models import Post

class CastMicropubHandler(MicropubContentHandler):
    def create_entry(self, properties, user):
        from cast.models import Blog

        # Get user's blog
        blog = Blog.objects.get(user=user)

        # Create post
        post = Post.objects.create(
            blog=blog,
            author=user,
            title=properties.get('name', [''])[0],
            content=properties.get('content', [''])[0],
            visible=True,
            published=True
        )

        # Handle categories
        categories = properties.get('category', [])
        for cat_name in categories:
            category, _ = Category.objects.get_or_create(
                blog=blog,
                name=cat_name
            )
            post.categories.add(category)

        return MicropubEntry(
            url=post.get_absolute_url(),
            properties=properties
        )

Then in settings:

INDIEWEB_MICROPUB_HANDLER = 'myproject.cast_micropub.CastMicropubHandler'

Next Steps

  • Implement update and delete operations

  • Add media endpoint support for file uploads

  • Implement WebSub for real-time updates

  • Add support for more post types (events, RSVPs, etc.)