MaquinaNewsletters
A mountable Rails 8 engine for drafting, approving, scheduling, and batch-sending HTML newsletters (Action Text, with your choice of the Trix or Lexxy editor) from a host app's backstage area.
Installation
Add to your Gemfile and bundle:
gem "maquina_newsletters"
Mount the engine (typically under your backstage area):
# config/routes.rb
mount MaquinaNewsletters::Engine => "/backstage/newsletters"
The bundled installer wires up the rest:
bin/rails generate maquina_newsletters:install
bin/rails db:migrate
Requirements
The newsletter body is an Action Text rich-text field with Active Storage
image attachments, so the host app must have both set up (the installer runs
active_storage:install / action_text:install if they aren't already):
bin/rails active_storage:install
bin/rails action_text:install
Rich-text editor (Trix or Lexxy). The body editor is the host's choice —
the engine is editor-agnostic and ships scoped CSS for both. Pick with Rails 8.1's
config.action_text.editor:
- Trix (Rails default) — used by engine v1.0. Nothing extra to install;
action_text:installwires it up. - Lexxy (Lexical-based) — the default demonstrated by engine v1.5. Add the gem and its JS/CSS to the host:
# Gemfile
gem "lexxy"
# config/importmap.rb
pin "lexxy", to: "lexxy.js"
pin "@rails/activestorage", to: "activestorage.esm.js"
// app/javascript/application.js
import * as ActiveStorage from "@rails/activestorage"
import "lexxy"
ActiveStorage.start()
<%# in your layout — load AFTER your CSS build so Lexxy's styles win %>
<%= stylesheet_link_tag "lexxy" %>
Installing the gem already sets config.action_text.editor = :lexxy on Rails 8.1;
set it to :trix to keep Trix while Lexxy is installed. Both editors store the
same canonical Action Text HTML, so existing newsletters render unchanged when
you switch.
Image processing. Images embedded in a newsletter are rendered through
blob.representation(...), which resizes a variant. This requires the
image_processing gem and a
system image library — libvips (recommended) or
ImageMagick. Without it the /rails/active_storage/representations/... endpoint
returns 500 and images won't display.
# Gemfile
gem "image_processing", "~> 1.2"
# macOS
brew install vips # or: brew install imagemagick
# Debian/Ubuntu
apt-get install libvips # or: apt-get install imagemagick
Email image URLs. For images to display in delivered emails the URLs must be absolute, so set a default host for the mailer:
# config/environments/production.rb
config.action_mailer. = { host: "newsletters.example.com" }
On the web (the in-app preview) the engine uses the current request's host
automatically; in emails it uses this default_url_options host.
Configuration
Configure the engine in an initializer:
# config/initializers/maquina_newsletters.rb
MaquinaNewsletters.configure do |config|
# Recipient resolution — which records receive a newsletter.
config.recipient_model = "User" # constantized at use-time
config.recipient_scope = :active # a scope returning a relation
config.recipient_email_attr = :email_address # the email column
# Base controller — see "Authentication" below.
config.base_controller_class = "BackstageController"
end
If the initializer is absent, the defaults shown above are used (except
base_controller_class, which defaults to "ActionController::Base").
Authentication
The engine does not authenticate. Authentication is the host's responsibility. This mirrors rails/mission_control-jobs.
The engine's controllers inherit from a configurable base controller:
config.base_controller_class = "BackstageController"
- The value is a String, constantized at load time.
- It defaults to
"ActionController::Base", so the engine runs standalone. - Point it at your app's authenticated base controller (e.g. a
BackstageControllerthat already gates the backstage area with HTTP Basic Auth, a session check, etc.). Whatever auth that controller enforces automatically protects the engine, because every engine controller inherits from it.
Optional fallback: built-in HTTP Basic Auth
For hosts whose base controller does not already authenticate, the engine ships an optional HTTP Basic Auth fallback (off by default). Enable it with credentials:
MaquinaNewsletters.configure do |config|
config.http_basic_auth_enabled = true
config.http_basic_auth_user = ENV["NEWSLETTERS_USER"]
config.http_basic_auth_password = ENV["NEWSLETTERS_PASSWORD"]
end
When http_basic_auth_enabled is true:
- with a user and password configured, the engine challenges with HTTP Basic Auth;
- enabled but without credentials, it fails closed (401 on every request).
Leave it disabled when your base controller already handles auth — don't stack both.
Usage
Visit the mount path (e.g. /backstage/newsletters) to manage newsletters. Each
issue moves through a small lifecycle, and scheduling is a single, deliberate
step rather than something you set while composing.
Lifecycle
- Draft — New / Edit is content only: a subject and the body (Action Text — Trix or Lexxy — with image attachments). Saving creates a draft; no send time is set yet.
- Approved — approve a draft when it's ready to go out.
- Scheduled — on an approved issue's page you choose when and how it sends, then click Schedule (details below).
- Sending → Sent — once scheduled, a background job delivers the issue.
You can step backwards at any point with Back to draft, and Unschedule an issue to return it to approved.
Scheduling (the approve step)
The send time and batching are chosen in one place: the schedule form shown on an approved issue's page. It has three inputs:
- Date — a date picker restricted to today onward (no past dates).
- Time — a dropdown from 8:00 AM to 8:00 PM in 30-minute steps.
- Batch size — recipients per batch; 0 = send to everyone at once. With a positive size the send is split into batches delivered a day apart.
The chosen date + time compose into the issue's scheduled_at. If that moment has
already passed (e.g. today, at an earlier time), it rolls forward to the next
30-minute slot and the confirmation message says so. Because scheduled_at is
only written here, an issue's read-only summary (Recipients / Scheduled at / Batch
size / Sent at) appears once it's scheduled — a draft or approved issue shows no
phantom schedule.
Send now and test sends
- Send now — an overflow (⋮) action that delivers immediately to all recipients, behind a confirmation. Use it sparingly; the normal path is to schedule.
- Send a test — available while drafting/approving/scheduling: send exactly one email to any address you type, ignoring batch size and schedule. It never changes the issue's state.
Recipients
Recipients are resolved at send time from the configured model + scope (see Configuration), minus the per-issue exclusion list, then downcased, de-duplicated, and sorted for a stable batching order.
Contributing
While iterating, run the specific test for what you're touching, e.g.
bin/rails test test/models/maquina_newsletters/newsletter_test.rb. Before
considering a change done, run the full pipeline (style + tests):
bin/ci
Style is enforced with Standard —
autocorrect with bin/standardrb --fix.
License
The gem is available as open source under the terms of the MIT License.