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
# SEO / social metadata
config.site_name = "My Blog" # og:site_name and <title> fallback
config.twitter_site = "@myhandle" # twitter:site card handle
config.default_image_url = nil # fallback og:image when a post has no image
# 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
SEO (helios-seo)
helios-press delegates the entire page <head> — meta description, canonical,
robots, Open Graph, Twitter cards, and the JSON-LD graph — to
helios-seo, which it depends on. Press only supplies the
per-post share image; the post show view calls
helios_seo_tags(@post, title: false) (the layout keeps ownership of <title>).
Integration checklist:
- Configure helios-seo in
config/initializers/helios_seo.rb— setsite_name,site_url,author,twitter_site/twitter_creator, anddefault_og_image. These now live in helios-seo; the oldHelios::PressSEO settings (twitter_site,default_image_url) are superseded. (config.site_nameon Press is still used as the layout<title>fallback.) - Layout — make sure your layout yields
:headinside<head>. The bundled Press layout already does. - Per-post share image — authors can upload a "Social share image" in the
post form to override; otherwise the post's banner (its first image block) is
used. Falls back to helios-seo's site-wide
default_og_image. Requires ActiveStorage (already used by Press image blocks). - Canonical URLs default to
"{site_url}/{slug}", which matches the publicget ":slug"route when the Public engine is mounted at root. On a subpath mount, setconfig.canonical_urlin the helios-seo initializer. /llms.txt(optional) — mountHelios::Seo::Engineat root to serve it; setconfig.llms_posts = -> { Helios::Press::Post.published.reverse_sorted }.
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. You must also import trix and @rails/actiontext — without these imports, the rich text editor will not render in text blocks:
import 'trix'
import '@rails/actiontext'
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— required for text block editing
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 with <img src=\"https://example.com/photo.jpg\"> tags...</p>",
"published": true,
"images": [
{
"reference_key": "hero1",
"url": "https://example.com/hero.jpg",
"alt": "Hero image",
"caption": "Photo credit: Example"
}
]
}
Image Ingestion
When a post is ingested via the API, all images in body_html are automatically downloaded, stored via ActiveStorage (e.g., to S3), and the <img> src attributes are rewritten to local proxy URLs. This ensures no external image hotlinking in published posts.
Two modes are supported:
-
Explicit references: Use
src="helios://image/<reference_key>"in yourbody_htmland provide the actual download URL in theimagesarray. The ingestor matches them byreference_key. -
Auto-import: Any
src="https://..."URL inbody_htmlis automatically downloaded. Images are deduplicated by a SHA256 hash of the URL path (ignoring query strings/signatures), so re-ingesting the same image with different signed URLs won't create duplicates.
Images are stored as BlockImage records with ActiveStorage attachments and served via rails_storage_proxy_path. Failed downloads are logged and skipped (soft failure — the original src is preserved).
Releases
0.5
- SEO delegated to helios-seo (new dependency). The post
showview now rendershelios_seo_tags(@post, title: false)instead of the bundled_meta_tagspartial, adding a meta description,robots, and a JSON-LD graph on top of the previous Open Graph / Twitter tags. - Removed
app/views/helios/press/posts/_meta_tags.html.erbandapp/helpers/helios/press/seo_helper.rb(superseded by helios-seo). Postgains an optionalog_imageattachment (a "Social share image" field in the admin form) plusPost#og_image_url, which builds a stable, absolute ActiveStorage proxy URL for the post's share image, falling back to the post's banner (first image block). See the SEO section.
License
MIT License. See MIT-LICENSE for details.