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 list
  • GET /posts/new — New post form
  • GET /posts/:id/edit — Block editor
  • POST/PATCH/DELETE /posts/:id — CRUD
  • Block and image sub-resources

Public Engine:

  • GET / — Published posts index
  • GET /:slug — Single post view

API Engine:

  • POST /posts — Upsert a post by external_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/activestorage
  • trix and @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.