Helios::Press
A Rails engine providing a WordPress-like block editor for blog posts. Supports text (ActionText), image stacks with configurable grid layouts, and video blocks (via helios-videos) with drag-to-reorder.
Installation
Add to your Gemfile:
gem "helios-press"
gem "helios-videos" # Optional: enables video blocks
Then:
bundle install
bin/rails helios_press:install:migrations
bin/rails db:migrate
Prerequisites:
- ActionText must be installed in your host app (
bin/rails action_text:install) - ActiveStorage must be installed (
bin/rails active_storage:install)
Configuration
Create config/initializers/helios_press.rb:
Helios::Press.configure do |config|
# Parent controller for admin views (must provide authentication)
config.admin_parent_controller = "Admin::BaseController"
# Parent controller for public views (blog index/show)
config.public_parent_controller = "ApplicationController"
# Optional slug prefix for posts
config.post_slug_prefix = nil # e.g., "blog/" for /blog/my-post
# API authentication (for external post ingestion)
# Default: checks X-API-Key header against BLOG_INGEST_API_KEY env var
config.api_authentication = ->(controller) {
expected = ENV["BLOG_INGEST_API_KEY"]
provided = controller.request.headers["X-API-Key"]
unless expected.present? && ActiveSupport::SecurityUtils.secure_compare(expected, provided.to_s)
controller.render json: { error: "unauthorized" }, status: :unauthorized
end
}
end
Routes
Helios::Press provides three independent engines that you can mount wherever you want:
# Admin block editor — mount behind your auth
mount Helios::Press::Admin::Engine, at: "/admin/press"
# Public blog index/show — mount at your preferred public path
mount Helios::Press::Public::Engine, at: "/blog"
# API for external post ingestion
mount Helios::Press::Api::Engine, at: "/api/press"
Mount only the engines you need. For example, if you only want the admin editor and will build your own public views:
mount Helios::Press::Admin::Engine, at: "/admin/press"
Routes provided
Admin Engine:
GET /— Posts listGET /posts/new— New post formGET /posts/:id/edit— Block editorPOST/PATCH/DELETE /posts/:id— CRUD- Block and image sub-resources
Public Engine:
GET /— Published posts indexGET /:slug— Single post view
API Engine:
POST /posts— Upsert a post byexternal_id
JavaScript Setup
Register the Stimulus controllers in your host app:
import {
HeliosPressBlocksController,
HeliosPressTextBlockController,
HeliosPressImageBlockController
} from "helios/press"
application.register("helios-press-blocks", HeliosPressBlocksController)
application.register("helios-press-text-block", HeliosPressTextBlockController)
application.register("helios-press-image-block", HeliosPressImageBlockController)
npm dependencies (add to your host app's package.json):
sortablejs@hotwired/stimulus@rails/activestoragetrixand@rails/actiontext
Vite
If your host app uses Vite, add an alias so Vite can resolve the gem's JavaScript:
// vite.config.mts
resolve: {
alias: {
'helios/press': resolve(__dirname, '/path/to/helios-press/app/javascript/helios/press'),
},
},
When using a local path gem, point to the local checkout. When using the published gem, point to the installed gem path (e.g., via bundle show helios-press).
CSS
Include the block editor styles in your stylesheet:
@import 'helios_press_blocks';
You can either symlink or copy the file from the gem:
# Symlink (local development)
ln -s /path/to/helios-press/app/assets/stylesheets/helios/press/blocks.css \
app/assets/stylesheets/_helios_press_blocks.scss
Block Types
- Text: Rich text editing via ActionText. Double-click to edit, Save/Cancel buttons.
- Image Container: Drag images to add. Configurable grid (1-6 images per row). Reorder by dragging. Click captions to edit.
- Video Container (requires helios-videos): Drag a video file to create. Direct upload to S3, automatic processing via Mux/Cloudflare.
API Ingestion
POST to your mounted API path with X-API-Key header:
{
"external_id": "my-post-123",
"title": "My Post Title",
"slug": "my-post-title",
"description": "Meta description",
"keywords": "keyword1, keyword2",
"body_html": "<p>Post content...</p>",
"published": true
}
License
Proprietary. All rights reserved.