- Add comprehensive Micropub endpoint design document - Define token management approach for IndieAuth - Specify minimal V1 feature set (create posts, queries) - Defer media endpoint and advanced features to post-V1 - Add ADR-028 documenting implementation strategy - 8-10 day implementation timeline to unblock V1 The Micropub endpoint is the final blocker for V1.0.0 release.
32 KiB
Micropub Endpoint Design for StarPunk V1
Executive Summary
This document defines the architecture and implementation plan for adding Micropub support to StarPunk, the critical blocker for V1 release. The Micropub endpoint will enable external clients (like Indigenous, Quill, or Micropublish.net) to create, update, and delete posts on StarPunk installations.
Current State:
- ✅ IndieAuth authentication working (authorization endpoint with PKCE)
- ✅ Note CRUD operations via
notes.py - ✅ Markdown file storage with YAML frontmatter
- ✅ SQLite metadata database
- ❌ No Micropub endpoint (V1 blocker)
- ❌ No token endpoint (required for Micropub auth)
Target State:
- Full Micropub server implementation
- IndieAuth token endpoint for issuing access tokens
- Support for creating, updating, and deleting posts
- Minimal V1 implementation (no media endpoint initially)
Architecture Overview
System Components
┌─────────────────────────────────────────────────────────────┐
│ Micropub Client │
│ (Indigenous, Quill, etc.) │
└──────────────────────┬──────────────────────────────────────┘
│
│ HTTP Requests with Bearer Token
▼
┌─────────────────────────────────────────────────────────────┐
│ StarPunk Server │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Micropub Endpoint │ │
│ │ /micropub (Blueprint) │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ • Token Validation (Bearer auth) │ │
│ │ • Request Parsing (form/JSON) │ │
│ │ • Action Routing (create/update/delete/query) │ │
│ │ • Response Formatting │ │
│ └──────────────┬─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Token Management │ │
│ │ /auth/token (endpoint) │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ • Authorization code exchange │ │
│ │ • Access token generation │ │
│ │ • Scope validation │ │
│ │ • Token storage in database │ │
│ └──────────────┬─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Existing Notes CRUD │ │
│ │ starpunk/notes.py │ │
│ ├────────────────────────────────────────────────────┤ │
│ │ • create_note() │ │
│ │ • update_note() │ │
│ │ • delete_note() │ │
│ │ • get_note() │ │
│ └──────────────┬─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Storage Layer │ │
│ ├──────────────────────┬──────────────────────────────┤ │
│ │ SQLite Database │ Markdown Files │ │
│ │ • notes metadata │ /data/notes/YYYY/MM/ │ │
│ │ • tokens │ • YAML frontmatter │ │
│ │ • sessions │ • Markdown content │ │
│ └──────────────────────┴──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Detailed Component Design
1. Token Endpoint (/auth/token)
The token endpoint is required for IndieAuth authorization code flow, converting authorization codes into access tokens for API access.
Endpoint Specification
POST /auth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code={authorization_code}&
client_id={client_url}&
redirect_uri={redirect_url}&
code_verifier={pkce_verifier}
Response Format
Success (200 OK):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"scope": "create update delete",
"me": "https://example.com/"
}
Error (400 Bad Request):
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid or expired"
}
Implementation Details
# New file: starpunk/tokens.py
def generate_access_token() -> str:
"""Generate cryptographically secure access token"""
return secrets.token_urlsafe(32)
def create_token(me: str, client_id: str, scope: str) -> str:
"""
Create and store access token in database
Args:
me: User's identity URL
client_id: Client application URL
scope: Space-separated list of scopes
Returns:
Access token string
"""
token = generate_access_token()
token_hash = hashlib.sha256(token.encode()).hexdigest()
db = get_db()
db.execute("""
INSERT INTO tokens (token_hash, me, client_id, scope, created_at, expires_at)
VALUES (?, ?, ?, ?, datetime('now'), datetime('now', '+90 days'))
""", (token_hash, me, client_id, scope))
db.commit()
return token
def verify_token(token: str) -> Optional[dict]:
"""
Verify access token and return token info
Returns dict with: me, client_id, scope, or None if invalid
"""
token_hash = hashlib.sha256(token.encode()).hexdigest()
db = get_db()
row = db.execute("""
SELECT me, client_id, scope
FROM tokens
WHERE token_hash = ?
AND expires_at > datetime('now')
""", (token_hash,)).fetchone()
if row:
return dict(row)
return None
Database Schema Update
-- Update existing tokens table to use token_hash
ALTER TABLE tokens RENAME TO tokens_old;
CREATE TABLE tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token_hash TEXT UNIQUE NOT NULL, -- SHA256 hash of token
me TEXT NOT NULL,
client_id TEXT,
scope TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
last_used_at TIMESTAMP,
revoked_at TIMESTAMP
);
CREATE INDEX idx_tokens_hash ON tokens(token_hash);
CREATE INDEX idx_tokens_me ON tokens(me);
CREATE INDEX idx_tokens_expires ON tokens(expires_at);
2. Micropub Endpoint (/micropub)
The main Micropub endpoint handles all post operations.
Route Structure
# New file: starpunk/routes/micropub.py
from flask import Blueprint, request, jsonify, current_app
from starpunk.tokens import verify_token
from starpunk.micropub import (
handle_create,
handle_update,
handle_delete,
handle_query,
MicropubError
)
bp = Blueprint("micropub", __name__)
@bp.route("/micropub", methods=["GET", "POST"])
def micropub_endpoint():
"""Main Micropub endpoint"""
# Extract token from Authorization header or form
token = extract_bearer_token(request)
if not token:
return error_response("unauthorized", "No access token provided"), 401
# Verify token
token_info = verify_token(token)
if not token_info:
return error_response("unauthorized", "Invalid access token"), 401
# Handle query endpoints (GET requests)
if request.method == "GET":
return handle_query(request.args, token_info)
# Handle action endpoints (POST requests)
content_type = request.headers.get("Content-Type", "")
if "application/json" in content_type:
data = request.get_json()
action = data.get("action", "create")
else:
# Form-encoded or multipart
data = request.form.to_dict(flat=False)
action = data.get("action", ["create"])[0]
try:
if action == "create":
return handle_create(data, token_info)
elif action == "update":
return handle_update(data, token_info)
elif action == "delete":
return handle_delete(data, token_info)
else:
return error_response("invalid_request", f"Unknown action: {action}"), 400
except MicropubError as e:
return error_response("invalid_request", str(e)), 400
Content Creation Handler
# New file: starpunk/micropub.py
def handle_create(data: dict, token_info: dict) -> tuple:
"""
Handle post creation
Args:
data: Micropub request data (form or JSON)
token_info: Authenticated user info
Returns:
Response tuple (body, status, headers)
"""
# Check scope
if "create" not in token_info.get("scope", ""):
return error_response("insufficient_scope", "Token lacks create scope"), 403
# Extract properties
properties = normalize_properties(data)
# Map Micropub properties to StarPunk note format
title = extract_title(properties)
content = extract_content(properties)
tags = properties.get("category", [])
# Create note using existing CRUD
from starpunk.notes import create_note
try:
note = create_note(
title=title,
content=content,
tags=tags,
author=token_info["me"],
published=True # Micropub posts are published by default
)
# Build permalink URL
permalink = f"{current_app.config['SITE_URL']}/notes/{note.slug}"
# Return 201 Created with Location header
return "", 201, {"Location": permalink}
except Exception as e:
current_app.logger.error(f"Failed to create note via Micropub: {e}")
return error_response("server_error", "Failed to create post"), 500
def normalize_properties(data: dict) -> dict:
"""
Normalize Micropub properties from both form and JSON formats
Handles:
- Form: property[]=value arrays
- JSON: {"properties": {"property": ["value"]}}
"""
if "properties" in data:
# JSON format
return data["properties"]
# Form format - convert to properties dict
properties = {}
for key, value in data.items():
if key.startswith("mp-") or key in ["action", "url", "access_token"]:
continue # Skip reserved properties
# Handle array notation: property[] -> property
clean_key = key.rstrip("[]")
# Ensure value is always a list
if not isinstance(value, list):
value = [value]
properties[clean_key] = value
return properties
Update and Delete Handlers
def handle_update(data: dict, token_info: dict) -> tuple:
"""Handle post updates"""
if "update" not in token_info.get("scope", ""):
return error_response("insufficient_scope", "Token lacks update scope"), 403
url = data.get("url")
if not url:
return error_response("invalid_request", "No URL provided"), 400
# Extract slug from URL
slug = extract_slug_from_url(url)
# Get existing note
from starpunk.notes import get_note, update_note
note = get_note(slug)
if not note:
return error_response("invalid_request", "Post not found"), 400
# Apply updates based on operation type
if "replace" in data:
# Replace properties
for prop, values in data["replace"].items():
apply_property_replace(note, prop, values)
if "add" in data:
# Add to properties
for prop, values in data["add"].items():
apply_property_add(note, prop, values)
if "delete" in data:
# Delete properties or values
for prop, values in data["delete"].items():
apply_property_delete(note, prop, values)
# Save changes
update_note(note)
return "", 204 # No Content
def handle_delete(data: dict, token_info: dict) -> tuple:
"""Handle post deletion"""
if "delete" not in token_info.get("scope", ""):
return error_response("insufficient_scope", "Token lacks delete scope"), 403
url = data.get("url")
if not url:
return error_response("invalid_request", "No URL provided"), 400
# Extract slug and delete
slug = extract_slug_from_url(url)
from starpunk.notes import delete_note
try:
delete_note(slug, soft=True) # Soft delete for safety
return "", 204
except NoteNotFoundError:
return error_response("invalid_request", "Post not found"), 400
3. Query Endpoints
Micropub defines several query endpoints for client discovery:
def handle_query(args: dict, token_info: dict) -> tuple:
"""Handle Micropub query endpoints"""
q = args.get("q")
if q == "config":
# Return server configuration
config = {
"media-endpoint": None, # No media endpoint in V1
"syndicate-to": [], # No syndication targets in V1
"post-types": [
{
"type": "note",
"name": "Note",
"properties": ["content"]
}
]
}
return jsonify(config), 200
elif q == "source":
# Return source of a specific post
url = args.get("url")
if not url:
return error_response("invalid_request", "No URL provided"), 400
slug = extract_slug_from_url(url)
from starpunk.notes import get_note
note = get_note(slug)
if not note:
return error_response("invalid_request", "Post not found"), 400
# Convert note to Micropub format
mf2 = {
"type": ["h-entry"],
"properties": {
"content": [note.content],
"name": [note.title] if note.title else [],
"category": note.tags if note.tags else [],
"published": [note.created_at.isoformat()],
"url": [f"{current_app.config['SITE_URL']}/notes/{note.slug}"]
}
}
return jsonify(mf2), 200
elif q == "syndicate-to":
# Return syndication targets (none for V1)
return jsonify({"syndicate-to": []}), 200
else:
return error_response("invalid_request", f"Unknown query: {q}"), 400
4. Authorization Flow Integration
The complete authorization flow for Micropub:
┌──────────┐
│ Client │
└─────┬────┘
│
│ 1. GET /auth/authorization?
│ response_type=code&
│ client_id=https://client.example&
│ redirect_uri=https://client.example/callback&
│ state=1234567890&
│ scope=create+update+delete&
│ code_challenge=XXXXX&
│ code_challenge_method=S256
▼
┌──────────────┐
│ StarPunk │
│ (Authorize) │
└─────┬────────┘
│
│ 2. Redirect to IndieLogin.com for authentication
│ (existing PKCE flow)
│
│ 3. After successful auth, redirect back with code
│
▼
┌──────────┐
│ Client │
└─────┬────┘
│
│ 4. POST /auth/token
│ grant_type=authorization_code&
│ code=XXXXX&
│ client_id=https://client.example&
│ redirect_uri=https://client.example/callback&
│ code_verifier=XXXXX
▼
┌──────────────┐
│ StarPunk │
│ (Token) │
└─────┬────────┘
│
│ 5. Return access token
│ {
│ "access_token": "XXXXX",
│ "token_type": "Bearer",
│ "scope": "create update delete",
│ "me": "https://user.example"
│ }
▼
┌──────────┐
│ Client │
└─────┬────┘
│
│ 6. POST /micropub
│ Authorization: Bearer XXXXX
│ Content-Type: application/x-www-form-urlencoded
│
│ h=entry&content=Hello+world
▼
┌──────────────┐
│ StarPunk │
│ (Micropub) │
└──────────────┘
Implementation Plan
Phase 1: Token Management (2-3 days)
-
Database Migration
- Update tokens table schema
- Add indexes for performance
- Create migration script
-
Token Module (
starpunk/tokens.py)- Token generation functions
- Token storage with hashing
- Token verification
- Scope validation helpers
-
Token Endpoint (
/auth/token)- Authorization code validation
- PKCE verification
- Token generation
- Response formatting
-
Testing
- Unit tests for token functions
- Integration tests for token endpoint
- Security tests (token expiry, revocation)
Phase 2: Micropub Core (3-4 days)
-
Micropub Module (
starpunk/micropub.py)- Property normalization
- Content extraction
- Note format conversion
- Error handling
-
Micropub Routes (
starpunk/routes/micropub.py)- Main endpoint handler
- Bearer token extraction
- Content-type handling
- Action routing
-
Create Handler
- Form-encoded parsing
- JSON parsing
- Note creation via CRUD
- Location header generation
-
Testing
- Create post via form-encoded
- Create post via JSON
- Invalid token handling
- Scope validation
Phase 3: Extended Operations (2-3 days)
-
Update Handler
- Replace operations
- Add operations
- Delete operations
- Property mapping
-
Delete Handler
- URL parsing
- Soft deletion
- Response handling
-
Query Endpoints
- Config endpoint
- Source endpoint
- Syndicate-to endpoint
-
Testing
- Update operations
- Delete operations
- Query responses
Phase 4: Integration & Polish (1-2 days)
-
Authorization Endpoint Updates
- Add scope parameter handling
- Store requested scopes with auth state
- Pass scopes to token creation
-
Discovery Headers
- Add
Link: <https://site/micropub>; rel="micropub"header - Add
Link: <https://site/auth/authorization>; rel="authorization_endpoint"header - Add
Link: <https://site/auth/token>; rel="token_endpoint"header
- Add
-
Error Handling
- Consistent error responses
- Proper HTTP status codes
- Detailed error descriptions
-
Documentation
- API documentation
- Client configuration guide
- Testing with real clients
V1 Minimal Feature Set
In Scope for V1
✅ Required for Micropub Compliance:
- Bearer token authentication
- Create posts (form-encoded)
- Create posts (JSON)
- Return 201 Created with Location header
- Configuration query endpoint
- Source query endpoint
✅ Required for Functionality:
- Token endpoint for IndieAuth
- Token storage and validation
- Scope enforcement
- Integration with existing notes CRUD
Out of Scope for V1 (Future)
❌ Nice to Have but Not Required:
- Media endpoint (file uploads)
- Photo/video properties (without media endpoint)
- Syndication targets
- Complex post types (articles, replies, etc.)
- Batch operations
- Websub notifications
- Token introspection endpoint
- Token revocation endpoint
Security Considerations
Token Security
-
Storage
- Tokens stored as SHA256 hashes
- Original token never logged
- Secure random generation (32 bytes)
-
Validation
- Check token exists
- Check not expired (90 days)
- Check not revoked
- Verify scope for operation
-
Transport
- Require HTTPS in production
- Support both header and form parameter
- Never include in URLs
Scope Enforcement
SCOPE_REQUIREMENTS = {
"create": ["create"],
"update": ["update"],
"delete": ["delete"],
"query": [] # Queries don't require specific scopes
}
def check_scope(required: str, granted: str) -> bool:
"""Check if granted scopes include required scope"""
granted_scopes = set(granted.split())
return required in granted_scopes
Input Validation
-
Content Validation
- Sanitize HTML content
- Validate URLs
- Limit content size
- Check required properties
-
URL Validation
- Verify URL belongs to this site
- Extract valid slugs
- Prevent directory traversal
-
Rate Limiting
- Limit requests per token
- Limit failed authentication attempts
- Implement exponential backoff
Testing Strategy
Unit Tests
# tests/test_micropub.py
def test_normalize_properties_form():
"""Test form-encoded property normalization"""
data = {
"content": ["Hello world"],
"category[]": ["tag1", "tag2"],
"mp-slug": ["my-post"]
}
properties = normalize_properties(data)
assert properties["content"] == ["Hello world"]
assert properties["category"] == ["tag1", "tag2"]
assert "mp-slug" not in properties
def test_normalize_properties_json():
"""Test JSON property normalization"""
data = {
"type": ["h-entry"],
"properties": {
"content": ["Hello world"],
"category": ["tag1", "tag2"]
}
}
properties = normalize_properties(data)
assert properties["content"] == ["Hello world"]
assert properties["category"] == ["tag1", "tag2"]
def test_bearer_token_extraction():
"""Test token extraction from header and form"""
# Test header
request = Mock(headers={"Authorization": "Bearer abc123"})
token = extract_bearer_token(request)
assert token == "abc123"
# Test form parameter
request = Mock(headers={}, form={"access_token": "xyz789"})
token = extract_bearer_token(request)
assert token == "xyz789"
Integration Tests
# tests/test_micropub_integration.py
def test_create_post_form_encoded(client, auth_token):
"""Test creating post with form-encoded request"""
response = client.post("/micropub",
headers={"Authorization": f"Bearer {auth_token}"},
data={
"h": "entry",
"content": "Test post",
"category[]": ["test", "micropub"]
}
)
assert response.status_code == 201
assert "Location" in response.headers
assert "/notes/" in response.headers["Location"]
def test_create_post_json(client, auth_token):
"""Test creating post with JSON request"""
response = client.post("/micropub",
headers={
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json"
},
json={
"type": ["h-entry"],
"properties": {
"content": ["Test post"],
"category": ["test", "micropub"]
}
}
)
assert response.status_code == 201
assert "Location" in response.headers
def test_unauthorized_request(client):
"""Test request without token"""
response = client.post("/micropub",
data={"h": "entry", "content": "Test"}
)
assert response.status_code == 401
assert response.json["error"] == "unauthorized"
def test_insufficient_scope(client, read_only_token):
"""Test request with insufficient scope"""
response = client.post("/micropub",
headers={"Authorization": f"Bearer {read_only_token}"},
data={"h": "entry", "content": "Test"}
)
assert response.status_code == 403
assert response.json["error"] == "insufficient_scope"
Client Testing
Test with real Micropub clients:
-
Indigenous (iOS/Android)
- Configure with StarPunk URLs
- Test post creation
- Test photo uploads (when media endpoint added)
-
Quill (Web)
- Test at https://quill.p3k.io
- Enter StarPunk domain
- Complete authentication
- Create test posts
-
Micropublish.net (Web)
- Test at https://micropublish.net
- Full-featured client
- Test all operations
Migration Path
Database Migrations
-- Migration: Add Micropub support
-- Version: 0.10.0
-- 1. Update tokens table for better security
ALTER TABLE tokens RENAME TO tokens_old;
CREATE TABLE tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token_hash TEXT UNIQUE NOT NULL,
me TEXT NOT NULL,
client_id TEXT,
scope TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
last_used_at TIMESTAMP,
revoked_at TIMESTAMP
);
-- Migrate existing tokens (if any)
-- Note: Existing tokens will be invalid as we don't have original values
DROP TABLE tokens_old;
-- 2. Add authorization codes table for token exchange
CREATE TABLE IF NOT EXISTS authorization_codes (
code TEXT PRIMARY KEY,
me TEXT NOT NULL,
client_id TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
scope TEXT,
code_challenge TEXT,
code_challenge_method TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP
);
CREATE INDEX idx_auth_codes_expires ON authorization_codes(expires_at);
Configuration Updates
# config.py additions
# Micropub settings
MICROPUB_ENDPOINT = "/micropub"
TOKEN_ENDPOINT = "/auth/token"
TOKEN_EXPIRY_DAYS = 90
AUTHORIZATION_CODE_EXPIRY_MINUTES = 10
# Supported scopes
SUPPORTED_SCOPES = ["create", "update", "delete", "read"]
DEFAULT_SCOPE = "create"
Effort Estimates
Total Effort: 8-10 days
| Phase | Task | Effort | Priority |
|---|---|---|---|
| 1 | Token Management | 2-3 days | Critical |
| 2 | Micropub Core | 3-4 days | Critical |
| 3 | Extended Operations | 2-3 days | High |
| 4 | Integration & Polish | 1-2 days | High |
Resource Requirements
- Developer: 1 person full-time for 2 weeks
- Testing: Access to Micropub clients
- Documentation: 1 day included in estimates
Risk Factors
-
Token Security (High Impact, Low Probability)
- Mitigation: Follow security best practices, hash tokens
-
Client Compatibility (Medium Impact, Medium Probability)
- Mitigation: Test with multiple clients early
-
Scope Creep (Medium Impact, High Probability)
- Mitigation: Strict V1 feature set, defer nice-to-haves
Success Criteria
Functional Requirements
✅ Micropub endpoint responds to POST requests ✅ Bearer token authentication works ✅ Posts can be created via form-encoded requests ✅ Posts can be created via JSON requests ✅ Location header returned on creation ✅ Query endpoints return valid responses ✅ Token endpoint exchanges codes for tokens ✅ Scopes are enforced correctly
Performance Requirements
✅ Post creation < 500ms ✅ Token validation < 50ms ✅ Query responses < 200ms
Security Requirements
✅ Tokens stored as hashes ✅ Expired tokens rejected ✅ Invalid tokens return 401 ✅ Insufficient scope returns 403 ✅ HTTPS required in production
Compatibility Requirements
✅ Works with Indigenous app ✅ Works with Quill web client ✅ Works with Micropublish.net ✅ Passes Micropub Rocks validator
Conclusion
The Micropub implementation is straightforward given StarPunk's existing architecture:
- Existing CRUD operations in
notes.pyhandle the storage - Existing authentication provides the foundation for tokens
- Clear specification from W3C defines exact requirements
- Minimal V1 scope focuses on core functionality
The main work is:
- Building the token endpoint for IndieAuth
- Creating the Micropub request handler
- Mapping between Micropub and StarPunk formats
- Proper error handling and responses
With 8-10 days of focused development, StarPunk will have a fully functional Micropub endpoint, unblocking the V1 release.
Appendix: Example Requests
Creating a Note (Form-Encoded)
POST /micropub HTTP/1.1
Host: starpunk.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/x-www-form-urlencoded
h=entry&
content=Just+had+coffee+at+the+new+place+downtown.+Really+good!&
category[]=coffee&
category[]=portland
Creating a Note (JSON)
POST /micropub HTTP/1.1
Host: starpunk.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/json
{
"type": ["h-entry"],
"properties": {
"content": ["Just had coffee at the new place downtown. Really good!"],
"category": ["coffee", "portland"]
}
}
Updating a Note
POST /micropub HTTP/1.1
Host: starpunk.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/json
{
"action": "update",
"url": "https://starpunk.example.com/notes/2024/11/coffee-downtown",
"add": {
"category": ["recommended"]
},
"replace": {
"content": ["Just had coffee at the new place downtown. Really good! Will definitely return."]
}
}
Deleting a Note
POST /micropub HTTP/1.1
Host: starpunk.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/x-www-form-urlencoded
action=delete&
url=https://starpunk.example.com/notes/2024/11/test-post
Querying Configuration
GET /micropub?q=config HTTP/1.1
Host: starpunk.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Response:
{
"media-endpoint": null,
"syndicate-to": [],
"post-types": [
{
"type": "note",
"name": "Note",
"properties": ["content"]
}
]
}