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¶
Get an access token via IndieAuth with “post” scope
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!"
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)
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¶
Always validate user permissions in your handler
Sanitize content before storing
Validate URLs for properties like photo and in-reply-to
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.)