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:

  1. Configure helios-seo in config/initializers/helios_seo.rb — set site_name, site_url, author, twitter_site / twitter_creator, and default_og_image. These now live in helios-seo; the old Helios::Press SEO settings (twitter_site, default_image_url) are superseded. (config.site_name on Press is still used as the layout <title> fallback.)
  2. Layout — make sure your layout yields :head inside <head>. The bundled Press layout already does.
  3. 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).
  4. Canonical URLs default to "{site_url}/{slug}", which matches the public get ":slug" route when the Public engine is mounted at root. On a subpath mount, set config.canonical_url in the helios-seo initializer.
  5. /llms.txt (optional) — mount Helios::Seo::Engine at root to serve it; set config.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 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. 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/activestorage
  • trix and @rails/actiontextrequired 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:

  1. Explicit references: Use src="helios://image/<reference_key>" in your body_html and provide the actual download URL in the images array. The ingestor matches them by reference_key.

  2. Auto-import: Any src="https://..." URL in body_html is 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 show view now renders helios_seo_tags(@post, title: false) instead of the bundled _meta_tags partial, 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.erb and app/helpers/helios/press/seo_helper.rb (superseded by helios-seo).
  • Post gains an optional og_image attachment (a "Social share image" field in the admin form) plus Post#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.