A modern blog platform inspired by Posterous that allows publishing posts via email.
Your email blog platform is fully functional! Send an email to start publishing.
- ๐ง Email-to-Publish: Send an email to publish posts instantly
- ๐ Email Security: Allowlist and verification system to prevent unauthorized posts
- โ๏ธ Post Editing: Secure web interface for editing and deleting posts with admin authentication
- ๐จ Multi-format Support: HTML and plain text posts
- ๐ผ๏ธ Advanced Image Handling:
- Email attachments automatically processed and added to posts
- Inline images (cid: references) properly rendered
- Unreferenced attachments displayed in dedicated section
- Support for JPEG, PNG, GIF, WebP, and SVG formats
- ๐ก RSS Feed: Full RSS 2.0 support for syndication
- ๐ WebSub: Real-time feed updates via WebSub protocol (configured)
- ๐ ActivityPub: Full federated social networking with followers, likes, and shares โ
- Content negotiation for HTML/JSON responses
- Actor profile accessible from browsers and ActivityPub clients
- ๐ฆ AT Protocol: Bluesky integration for cross-platform syndication (configured)
- ๐ SEO Friendly: Clean URLs with slugified titles
- ๐ฑ Responsive Design: Mobile-first responsive interface with TailwindCSS
- โก Fast: Built on Val Town with SQLite storage and static HTML generation
- ๐ No JavaScript: Pure HTML/CSS frontend for maximum performance
index.ts
- Main Hono server with HTML generation and API routes โdatabase/
- SQLite schema and query functions โservices/
- External service integrations โ
- Email trigger handler with security verification โ
- Allowlist checking and draft post creation โ
- Automated verification email sending โ
- Server-side HTML rendering with TailwindCSS โ
- No client-side JavaScript dependencies โ
- Fast loading and SEO optimized โ
- Configure Security: Set
ALLOWED_EMAIL_ADDRESSES
environment variable (see Security section) - Send Email: Send email from an allowed address to your Val Town email address
- Verify Email: Click the verification link sent to your email
- View Blog: Visit your backend HTTP val URL after verification
- RSS Feed: Access
/rss
for syndication - Individual Posts: Visit
/post/[slug]
See SETUP.md for detailed setup instructions and ACTIVITYPUB.md for ActivityPub federation details.
ALLOWED_EMAIL_ADDRESSES
- Comma-separated list of allowed email addresses (required for email publishing)BASE_URL
- Your blog's base URL (e.g., https://myblog.com or just myblog.com) - Required for custom domains and ActivityPub federationUPLOAD_PASSWORD
- Password required for image uploads (required to enable image upload functionality)
WEBSUB_HUB_URL
- WebSub hub URLACTIVITYPUB_DOMAIN
- Domain for ActivityPub federation (deprecated - use BASE_URL instead)ATPROTO_HANDLE
- AT Protocol handleATPROTO_PASSWORD
- AT Protocol app passwordADMIN_PASSWORD
- Admin password for advanced features (falls back to UPLOAD_PASSWORD if not set)
For proper ActivityPub federation with HTTP signatures (recommended for production):
ACTIVITYPUB_PUBLIC_KEY
- RSA public key in PEM formatACTIVITYPUB_PRIVATE_KEY
- RSA private key in PEM format
To generate these keys:
- Visit
/generate-keys.ts
(your key generator val) - Copy the generated keys to your environment variables
- Restart your val
Without these keys, the system will generate temporary keys that change on restart, which may cause federation issues.
The platform includes several testing and debugging endpoints that are only accessible in development mode for security.
Set one of these environment variables to enable testing endpoints:
NODE_ENV=development
DEV_MODE=true
ENABLE_TEST_ENDPOINTS=true
When development mode is enabled, you can access:
/test-activitypub.ts
- Test ActivityPub and WebFinger endpoints/test-publish.ts
- Create a test blog post/test-follow.ts
- Test Follow activity processing/test-verification.ts
- Test email verification flow/test-http-signatures.ts
- Test HTTP signatures implementation/test-activitypub-inbox.ts
- Test ActivityPub inbox functionality/test-activitypub-delivery.ts
- Test ActivityPub delivery with HTTP signatures/debug-config.ts
- Debug email security configuration/debug-signatures.ts
- Debug HTTP signatures in detail/generate-keys.ts
- Generate RSA keys for ActivityPub
In production (when development mode is disabled), all test endpoints return a 404 response, ensuring your production environment remains secure while allowing full testing capabilities during development.
To use a custom domain with ActivityPub federation:
- Set up your custom domain in Val Town (see Val Town documentation)
- Set the BASE_URL environment variable to your custom domain:
BASE_URL=https://yourdomain.com
(with protocol)- OR
BASE_URL=yourdomain.com
(protocol will be assumed as https)
- Test ActivityPub discovery by visiting
https://yourdomain.com/.well-known/webfinger?resource=acct:blog@yourdomain.com
Without BASE_URL set, the system will use the default Val Town URL for ActivityPub federation.
GET /
- Main blog interface (with ActivityPub Link header)GET /post/:slug
- Individual post page (supports content negotiation for ActivityPub)GET /edit
- Post editing interface (admin only) โGET /upload
- Image upload interface โGET /images/:filename
- Serve uploaded images โGET /rss
- RSS 2.0 feedGET /api/posts
- JSON API for postsGET /api/posts/:slug
- JSON API for single postPUT /api/posts/:slug
- Update a post (admin only) โDELETE /api/posts/:slug
- Delete a post (admin only) โDELETE /api/posts/author/:email
- Bulk delete posts by author email (admin only) โPOST /api/images/upload
- Upload image endpoint โGET /api/posts/:slug/images
- Get images for a specific post โGET /api/images/user/:email
- Get images uploaded by a user โGET /api/images
- Get all images (admin endpoint) โDELETE /api/images/:filename
- Delete an image โGET /websub
- WebSub subscription endpointGET /.well-known/webfinger
- WebFinger discovery for ActivityPub โGET /actor
- ActivityPub actor document with rich metadata โGET /outbox
- ActivityPub outbox (paginated published activities) โGET /outbox?page=N
- Paginated outbox pages โGET /followers
- ActivityPub followers collection (with pagination support) โGET /followers-list
- Human-readable followers page โGET /api/followers
- JSON API for followers list โGET /following
- ActivityPub following collection โPOST /inbox
- ActivityPub inbox (processes Follow, Like, Announce, Undo) โGET /api/posts/:slug/activities
- Get activity counts (likes, shares, replies) โGET /verify-email
- Email verification endpoint โGET /health
- Health check
Only emails from pre-configured addresses can publish posts. Configure via environment variable:
ALLOWED_EMAIL_ADDRESSES=user1@example.com,user2@example.com,admin@myblog.com
To prevent spoofing, all emails go through a verification process:
- Email Received: Email is stored as a draft (not published)
- Verification Sent: Automated verification email sent to sender
- User Clicks Link: Sender clicks verification link in email
- Post Published: After verification, post is published and syndicated
- Anti-spoofing: Prevents unauthorized users from publishing posts
- Email confirmation: Ensures the sender actually sent the email
- Time-limited: Verification links expire after 24 hours
- Automatic cleanup: Expired drafts are automatically removed
The platform includes comprehensive image storage capabilities with advanced email attachment processing.
The platform provides a secure web interface for editing and deleting posts with proper authentication.
- Visit the Edit Page: Go to
/edit
on your blog - Admin Authentication: Enter your admin password (ADMIN_PASSWORD or UPLOAD_PASSWORD)
- Load Post: Enter the post slug and click "Load Post"
- Edit Content: Use the tabbed interface to edit content and preview changes
- Save Changes: Click "Save Changes" to update the post
Note: The edit and config buttons have been removed from the main header for a cleaner interface. You can access editing functionality directly via
/edit
or through the edit button on individual post pages.
- ๐ Secure Authentication: Requires admin password to access
- ๐ Rich Editor: Tabbed interface with edit and preview modes
- ๐จ Content Type Support: Switch between HTML and plain text
- ๐๏ธ Live Preview: Real-time preview of your changes
- ๐พ Auto-save: Save changes with a single click
- ๐๏ธ Post Deletion: Secure post deletion with confirmation
- ๐ Direct Links: Edit links on individual post pages with pre-filled slugs
- Password Protection: All editing operations require admin authentication
- Input Validation: Proper validation of all form inputs
- XSS Prevention: Content is properly escaped and sanitized
- CSRF Protection: Operations are protected against cross-site request forgery
- Audit Trail: All edit and delete operations are logged
// Update a post (requires admin password)
const response = await fetch(`/api/posts/${slug}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
password: 'your-admin-password',
title: 'Updated Title',
content: 'Updated content',
content_type: 'html'
})
});
// Delete a post (requires admin password)
const response = await fetch(`/api/posts/${slug}?password=your-admin-password`, {
method: 'DELETE'
});
// Bulk delete posts by author email (requires admin password)
const response = await fetch(`/api/posts/author/${email}?password=your-admin-password`, {
method: 'DELETE'
});
- From Post Pages: Click the "โ๏ธ Edit" button on any post page
- Pre-filled Forms: Post slug is automatically filled when accessing from post pages
- Direct URLs: Use
/edit?slug=your-post-slug
for direct access
The platform includes comprehensive image storage capabilities with advanced email attachment processing.
- Web Interface: Visit
/upload
(requires UPLOAD_PASSWORD) to upload images via drag-and-drop interface - Email Attachments: Images attached to blog post emails are automatically processed and stored
The system handles two types of email images:
- Content-ID References:
cid:filename.jpg
references are automatically converted to proper URLs - Broken Image Tags: Empty or invalid
src
attributes are fixed - Multiple Formats: Supports various email client reference patterns
- Automatic Replacement: Images are seamlessly integrated into post content
- Unreferenced Attachments: Images attached but not referenced in email content are automatically added
- Dedicated Section: Unreferenced images appear in a styled "Attachments" section
- Proper Styling: Images are displayed with responsive styling and captions
- JPEG/JPG
- PNG
- GIF
- WebP
- SVG
File Size Limit: 5MB per image
- Filename Escaping: Special characters in filenames are properly handled
- URL Generation: Automatic generation of proper image URLs with base domain
- Alt Text Preservation: Original alt text and filenames are preserved
- Responsive Styling: Images are automatically styled for responsive display
- Error Handling: Graceful handling of corrupted or invalid images
- Password Protection: Upload interface protected by UPLOAD_PASSWORD environment variable
- Automatic Processing: Email attachments are automatically extracted and stored
- Content Reference Replacement: Image references in email content are automatically updated to use stored URLs
- Metadata Storage: Original filename, MIME type, file size, alt text, and post associations
- Secure Access: Password-protected upload system prevents unauthorized access
- Blob Storage: Images stored using Val Town's blob storage with unique filenames
- URL Generation: Clean
/images/filename
URLs for serving images
// Upload an image (requires password)
const formData = new FormData();
formData.append('image', file);
formData.append('password', 'your-upload-password');
formData.append('email', 'your-email@example.com');
formData.append('altText', 'Description of image');
formData.append('postSlug', 'my-blog-post'); // optional
const response = await fetch('/api/images/upload', {
method: 'POST',
body: formData
});
// Get images for a post (no password required)
const images = await fetch('/api/posts/my-post-slug/images').then(r => r.json());
// Get images by user (requires password)
const userImages = await fetch('/api/images/user/user@example.com?password=your-upload-password').then(r => r.json());
// Get all images (requires password)
const allImages = await fetch('/api/images?password=your-upload-password&limit=50').then(r => r.json());
When you send an email with image attachments:
- Automatic Detection: System identifies image attachments by MIME type
- Storage: Images are stored with unique filenames in blob storage
- Database Records: Metadata is saved linking images to the post
- Content Updates: Email content is updated to reference stored image URLs
- Reference Replacement: Common patterns like
cid:image.jpg
are replaced with proper URLs
Example email with attachment:
To: your-blog@val.town
From: author@example.com
Subject: My Post with Images
Attachments: photo.jpg, diagram.png
<p>Check out this photo:</p>
<img src="cid:photo.jpg" alt="My photo" />
<p>And this diagram: [diagram.png]</p>
After processing, the content becomes:
<p>Check out this photo:</p> <img src="/images/1234567890-abc123-photo.jpg" alt="My photo" /> <p>And this diagram: </p>
Use /test-publish.ts
to create sample posts for testing.
Send an email like this:
To: your-email-val@val.town
From: allowed-user@example.com (must be in ALLOWED_EMAIL_ADDRESSES)
Subject: My Amazing Blog Post
Body: <h2>Hello World!</h2><p>This post was published via email!</p>
The process will be:
- Email received and stored as draft
- Verification email sent to
allowed-user@example.com
- User clicks verification link
- Post published with:
- Title: "My Amazing Blog Post"
- Slug: "my-amazing-blog-post"
- Content: Rendered HTML
- Author: Extracted from email address
- Edit HTML generation functions in
/backend/index.ts
- Modify CSS styles in the
getCustomCSS()
function - Update branding in HTML templates
- Configure federation services via environment variables
Posts are automatically syndicated to:
- RSS feed (always enabled)
- WebSub subscribers (if configured)
- ActivityPub followers (with full interaction support) โ
- AT Protocol/Bluesky (if configured)
Your blog is now fully federated with ActivityPub! Users can:
- Follow your blog from Mastodon, Pleroma, and other ActivityPub platforms
- Like your blog posts (shows โค๏ธ count on posts)
- Share/Boost your posts (shows ๐ count on posts)
- Reply to your posts (shows ๐ฌ count on posts)
- Preview posts directly in Mastodon timeline with proper formatting
Discovery formats:
- WebFinger:
@blog@your-domain.com
- Direct actor URL:
https://your-domain.com/actor
Configuration for custom domains:
- Set
BASE_URL=https://your-custom-domain.com
in your environment variables - This ensures all ActivityPub URLs use your custom domain instead of the default Val Town URL
Enhanced ActivityPub Features:
- โ Paginated Outbox: Posts are served with proper pagination for better performance
- โ Rich Post Previews: Posts display with titles, summaries, and full content in Mastodon
- โ Content Negotiation: Individual posts serve both HTML and ActivityPub JSON based on Accept headers
- โ Profile Metadata: Actor profile includes avatar, header image, and custom fields
- โ Link Discovery: Proper HTTP Link headers for ActivityPub discovery
- โ Post Permalinks: Each post has its own ActivityPub Note endpoint
Real-time interaction tracking:
- All likes, shares, and replies are stored and displayed on individual post pages
- Follower count is maintained and accurate
- Full ActivityPub inbox processing for Follow/Unfollow activities
Example: If your blog is at myblog.com
(with BASE_URL=https://myblog.com
), users can follow @blog@myblog.com
from their Mastodon client and see your posts with rich previews in their timeline!
Ready to blog via email? Send your first post now! ๐งโจ