Build Your Own Blog with FluxPoster Integration
Learn how to create a self-hosted blog that automatically receives content from FluxPoster, including HTML text, inline images, videos, and thumbnails using open web standards.
Overview
FluxPoster uses open web standards to publish content to your blog, ensuring maximum compatibility and future-proofing. This guide will walk you through setting up your own blog to receive automatic content updates, including:
Rich HTML Content
Full HTML articles with proper formatting
Inline Images
Embedded images within your content
Video Embeds
YouTube, Vimeo, and custom video players
Featured Images
Thumbnails and cover images for posts
⚡ Quick Start
The fastest way to get started is using our Micropub endpoint. It's a W3C standard that works with dozens of existing blogging clients.
Jump to Micropub setupSupported Open Standards
FluxPoster supports multiple open standards for maximum compatibility with your existing infrastructure. Choose the one that best fits your setup:
| Standard | Best For | Features |
|---|---|---|
| Micropub (W3C)Recommended | Modern IndieWeb sites | Full CRUD, media uploads, syndication |
| REST API | Custom implementations | Full control, JSON payloads |
| XML-RPC (MetaWeblog) | WordPress, Blogger | Legacy support, wide compatibility |
| Webhooks | Event-driven systems | Push notifications, flexible |
| RSS/JSON Feed | Pull-based systems | Simple, universal support |
| AtomPub | Atom-based systems | Standards compliant, RESTful |
Micropub API (Recommended)
Micropubis a W3C recommendation for creating, updating, and deleting posts on websites. It's the modern standard for the IndieWeb and works with many existing tools.
1. Implement the Micropub Endpoint
Create an endpoint at /micropub on your server:
const express = require('express');
const app = express();
// Micropub endpoint
app.post('/micropub', express.json(), async (req, res) => {
// Verify the access token
const token = req.headers.authorization?.replace('Bearer ', '');
if (!verifyToken(token)) {
return res.status(401).json({ error: 'unauthorized' });
}
const { type, properties } = req.body;
// Handle h-entry (blog post)
if (type?.[0] === 'h-entry') {
const post = {
title: properties.name?.[0] || 'Untitled',
content: properties.content?.[0]?.html || properties.content?.[0],
excerpt: properties.summary?.[0],
tags: properties.category || [],
featuredImage: properties.photo?.[0],
published: properties['post-status']?.[0] !== 'draft',
};
// Save to your database
const savedPost = await savePost(post);
// Return 201 with Location header
res.status(201)
.header('Location', `https://yourblog.com/posts/${savedPost.slug}`)
.end();
}
});
// Configuration endpoint
app.get('/micropub', (req, res) => {
if (req.query.q === 'config') {
return res.json({
'media-endpoint': 'https://yourblog.com/micropub/media',
'post-types': [
{ type: 'article', name: 'Article' },
{ type: 'note', name: 'Note' },
],
});
}
res.status(400).json({ error: 'Unknown query' });
});
app.listen(3000);2. Add Media Endpoint for Images
To receive inline images and thumbnails, implement the media endpoint:
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/micropub/media', upload.single('file'), async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!verifyToken(token)) {
return res.status(401).json({ error: 'unauthorized' });
}
// Process the uploaded file
const file = req.file;
// Move to permanent storage (S3, local, etc.)
const publicUrl = await uploadToStorage(file);
// Return the URL in the Location header
res.status(201)
.header('Location', publicUrl)
.end();
});3. Configure FluxPoster
Add your Micropub endpoint as a destination in FluxPoster:
- 1Go to Brands → [Your Brand] → Destinations
- 2Click Add Destination and select Custom Blog
- 3Enter your Micropub endpoint URL
- 4Add your access token for authentication
REST API Integration
For custom implementations, you can use FluxPoster's REST API or implement your own REST endpoint to receive content.
Expected Payload Structure
FluxPoster sends a CanonicalPost object:
{
"id": "uuid-string",
"title": "Your Blog Post Title",
"slug": "your-blog-post-title",
"bodyHtml": "<h2>Introduction</h2><p>Your content...</p><img src=\"https://cdn.example.com/image.jpg\" alt=\"Description\">",
"excerpt": "A brief summary of the post",
"images": [
{
"id": "img-1",
"cdnUrl": "https://cdn.fluxposter.com/uploads/featured.jpg",
"alt": "Featured image description",
"role": "featured",
"width": 1200,
"height": 630,
"mimeType": "image/jpeg"
},
{
"id": "img-2",
"cdnUrl": "https://cdn.fluxposter.com/uploads/inline-1.jpg",
"alt": "Inline image",
"role": "inline"
}
],
"featuredImageId": "img-1",
"tags": ["technology", "ai", "automation"],
"categories": ["Blog"],
"metaTitle": "SEO-optimized title",
"metaDescription": "SEO meta description for search engines",
"status": "published",
"publishedAt": "2024-01-15T10:00:00Z",
"authorName": "Your Brand",
"sourceId": "draft-uuid",
"sourceBrandId": "brand-uuid"
}Implement Your REST Endpoint
from flask import Flask, request, jsonify
import uuid
app = Flask(__name__)
@app.route('/api/posts', methods=['POST'])
def create_post():
# Verify authentication
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not verify_token(token):
return jsonify({'error': 'Unauthorized'}), 401
data = request.json
# Extract post data
post = {
'id': str(uuid.uuid4()),
'title': data['title'],
'slug': data.get('slug') or slugify(data['title']),
'content': data['bodyHtml'],
'excerpt': data.get('excerpt'),
'tags': data.get('tags', []),
'featured_image': None,
'published': data['status'] == 'published',
}
# Handle images
for image in data.get('images', []):
if image['role'] == 'featured':
post['featured_image'] = image['cdnUrl']
# Inline images are already in bodyHtml as <img> tags
# Save to database
saved_post = save_to_database(post)
return jsonify({
'success': True,
'postId': saved_post['id'],
'postUrl': f"https://yourblog.com/posts/{saved_post['slug']}"
}), 201
if __name__ == '__main__':
app.run(port=5000)Webhook Integration
Webhooks allow FluxPoster to push content to your system in real-time. This is ideal for event-driven architectures or when you need to trigger additional workflows.
Webhook Events
| Event | Trigger |
|---|---|
| post.created | New post is published |
| post.updated | Existing post is modified |
| post.deleted | Post is removed |
| media.uploaded | New image/video uploaded |
{
"event": "post.created",
"timestamp": "2024-01-15T10:00:00Z",
"signature": "sha256=abc123...",
"data": {
"post": {
"id": "post-uuid",
"title": "New Blog Post",
"bodyHtml": "<p>Content...</p>",
"images": [...],
"status": "published"
},
"brand": {
"id": "brand-uuid",
"name": "Your Brand"
}
}
}Verify Webhook Signatures
Always verify the webhook signature to ensure requests are from FluxPoster:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}
// In your webhook handler
app.post('/webhook', express.json(), (req, res) => {
const signature = req.headers['x-fluxposter-signature'];
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook...
const { event, data } = req.body;
switch (event) {
case 'post.created':
handleNewPost(data.post);
break;
case 'post.updated':
handlePostUpdate(data.post);
break;
// ...
}
res.status(200).json({ received: true });
});Media Handling
FluxPoster supports multiple ways to handle images and videos in your content.
Image Delivery Options
CDN URLs
Use our CDN URLs directly in your HTML. Images are already optimized and served globally.
EasiestSelf-Hosted
Download images from our CDN and host them yourself for full control.
Full ControlPlatform Upload
We upload images directly to your platform's media library (WordPress, Ghost, etc.).
NativeProcessing Inline Images
If you want to download and self-host images, here's how to process the HTML:
const cheerio = require('cheerio');
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');
async function processPostImages(post) {
const $ = cheerio.load(post.bodyHtml);
const imagePromises = [];
// Find all images in the HTML
$('img').each((i, img) => {
const src = $(img).attr('src');
if (src && src.includes('cdn.fluxposter.com')) {
imagePromises.push(
downloadAndReplace($, img, src)
);
}
});
await Promise.all(imagePromises);
// Also handle featured image
if (post.featuredImageId) {
const featuredImage = post.images.find(
img => img.id === post.featuredImageId
);
if (featuredImage) {
post.localFeaturedImage = await downloadImage(
featuredImage.cdnUrl,
'featured-' + path.basename(featuredImage.cdnUrl)
);
}
}
post.bodyHtml = $.html();
return post;
}
async function downloadAndReplace($, imgElement, originalUrl) {
const filename = path.basename(originalUrl);
const localPath = await downloadImage(originalUrl, filename);
$(imgElement).attr('src', '/uploads/' + filename);
}
async function downloadImage(url, filename) {
const response = await fetch(url);
const buffer = await response.buffer();
const localPath = path.join('public/uploads', filename);
fs.writeFileSync(localPath, buffer);
return localPath;
}Video Embeds
Videos are embedded as standard <iframe> tags:
<!-- YouTube Embed -->
<div class="video-embed" data-provider="youtube">
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
width="560"
height="315"
frameborder="0"
allowfullscreen>
</iframe>
</div>
<!-- Vimeo Embed -->
<div class="video-embed" data-provider="vimeo">
<iframe
src="https://player.vimeo.com/video/VIDEO_ID"
width="560"
height="315"
frameborder="0"
allowfullscreen>
</iframe>
</div>Video thumbnails are included in the images array with role: "thumbnail".
Authentication
Secure your endpoints to ensure only FluxPoster can publish to your blog.
Bearer Token Authentication
The simplest method—FluxPoster sends a token in the Authorization header:
POST /micropub HTTP/1.1
Host: yourblog.com
Authorization: Bearer your-secret-token
Content-Type: application/jsonIndieAuth (OAuth for IndieWeb)
For a more robust solution, implementIndieAuth, which provides OAuth-style authentication:
1. FluxPoster discovers your auth endpoint via <link rel="authorization_endpoint">
2. User authorizes FluxPoster at your authorization endpoint
3. FluxPoster exchanges code for access token at your token endpoint
4. FluxPoster uses token for all subsequent requestsGenerate a Secure Token
const crypto = require('crypto');
// Generate a secure random token
const token = crypto.randomBytes(32).toString('hex');
console.log('Your API token:', token);
// Store this in your environment variables
// FLUXPOSTER_API_TOKEN=your-generated-tokenComplete Examples
Here are complete, runnable examples for popular frameworks:
Next.js (App Router)
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_KEY!
);
export async function POST(request: NextRequest) {
// Verify token
const token = request.headers.get('authorization')?.replace('Bearer ', '');
if (token !== process.env.MICROPUB_TOKEN) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const properties = body.properties || {};
// Create the post
const { data: post, error } = await supabase
.from('posts')
.insert({
title: properties.name?.[0] || 'Untitled',
content: properties.content?.[0]?.html || properties.content?.[0],
excerpt: properties.summary?.[0],
featured_image: properties.photo?.[0],
tags: properties.category || [],
published: properties['post-status']?.[0] !== 'draft',
created_at: new Date().toISOString(),
})
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return new NextResponse(null, {
status: 201,
headers: {
Location: `${process.env.NEXT_PUBLIC_URL}/posts/${post.id}`,
},
});
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
if (searchParams.get('q') === 'config') {
return NextResponse.json({
'media-endpoint': `${process.env.NEXT_PUBLIC_URL}/api/micropub/media`,
'post-types': [{ type: 'article', name: 'Article' }],
});
}
return NextResponse.json({ error: 'Unknown query' }, { status: 400 });
}Express.js (Complete Server)
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');
const app = express();
app.use(cors());
app.use(express.json());
const upload = multer({ dest: 'uploads/' });
const posts = new Map(); // In production, use a real database
// Middleware to verify token
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token !== process.env.API_TOKEN) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};
// Micropub endpoint
app.post('/micropub', authenticate, (req, res) => {
const { type, properties } = req.body;
const post = {
id: uuidv4(),
title: properties?.name?.[0] || 'Untitled',
content: properties?.content?.[0]?.html || properties?.content?.[0] || '',
excerpt: properties?.summary?.[0] || '',
featuredImage: properties?.photo?.[0],
tags: properties?.category || [],
published: properties?.['post-status']?.[0] !== 'draft',
createdAt: new Date().toISOString(),
};
posts.set(post.id, post);
res.status(201)
.header('Location', `http://localhost:3000/posts/${post.id}`)
.end();
});
// Micropub config
app.get('/micropub', (req, res) => {
if (req.query.q === 'config') {
return res.json({
'media-endpoint': 'http://localhost:3000/micropub/media',
'post-types': [{ type: 'article', name: 'Article' }],
});
}
res.status(400).json({ error: 'Unknown query' });
});
// Media upload endpoint
app.post('/micropub/media', authenticate, upload.single('file'), (req, res) => {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const publicUrl = `http://localhost:3000/uploads/${file.filename}`;
res.status(201)
.header('Location', publicUrl)
.end();
});
// Serve uploaded files
app.use('/uploads', express.static('uploads'));
// View posts
app.get('/posts/:id', (req, res) => {
const post = posts.get(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(post);
});
app.listen(3000, () => {
console.log('Blog server running on http://localhost:3000');
});More Examples
Find complete examples in our GitHub repository, including implementations for Django, Rails, Laravel, Go, and more.
RSS & JSON Feeds
If you prefer a pull-based approach, FluxPoster provides standard RSS and JSON feeds that your blog can consume:
RSS 2.0 Feed
/blog/feed.xmlJSON Feed
/blog/feed.jsonSet up a cron job to periodically fetch new content from these feeds and import into your blog. Both feeds include full HTML content, featured images, and metadata.