PostnHost is an open source CMS engine built with Rails, Hotwire, and TailwindCSS. Write, translate, and publish articles with a rich text editor, full version history, and proper SEO that works out of the box.
Mount this engine into an existing Rails app, or use the postnhost-app repository for a pre-configured, self-hosted Rails application.
- 📝 Rich Text Editor - TipTap-based WYSIWYG editor
- 🌍 Multilingual - Translatable articles, i18n support, and locale-aware SEO metadata
- 🖼️ Image Management - Optimized multisize WebP everywhere
- 📊 Version History - Published version history and rollback options
- 🤖 Suggested Articles - Manual picks plus automatic suggestions from related categories
- ⏰ Post Scheduling - Schedule publication in the chosen timezone
- 🌐 Localized SEO - Localized routes, language switcher, sitemap, and hreflang tags
- ⚙️ Settings - Manage the canonical site URL, pagination, key copy, and assets from the dashboard
- 🎨 Customizable Templates - Override any public view in your host application
- ⚡ Hotwire-powered - Fast, modern UI with Turbo Frames/Streams and Stimulus
- 👥 Authors - Multiple CMS users, author profiles, and per-article co-author bylines
- Ruby 3.4+
- libvips for image processing
Add PostnHost to your host application's Gemfile:
gem "postnhost"Run the installer:
bundle install
bin/rails g postnhost:installThis will:
- Create
config/initializers/postnhost.rbwith configuration options - Create
config/initializers/carrierwave.rb(if not exists) - Mount the engine in your routes
Run migrations:
bin/rails postnhost:install:migrations
bin/rails db:migrateIf there are no users yet, visit /blog/onboarding to create the first CMS administrator with credentials you choose. This is the default installer path; if you mount the engine somewhere else, open <mount-path>/onboarding. The setup flow can add optional sample content after the account is created.
Alternatively, create a CMS user interactively from the terminal:
bin/rails g postnhost:userThe generator prompts for the name, email, password, and password confirmation.
PostnHost ships precompiled CSS and a prebundled JavaScript ES module. The engine serves these files through Rails asset helpers, so the host application does not need to compile PostnHost's JavaScript or install its frontend dependencies.
| Host setup | Compatible | How PostnHost integrates |
|---|---|---|
| Propshaft | ✅ | The engine registers its packaged builds and images with the host asset pipeline. |
| Sprockets | ✅ | The engine registers its packaged builds and images for precompilation. |
importmap-rails |
✅ | The engine bundle is loaded separately from the host import map. No pins are required. |
jsbundling-rails with esbuild |
✅ | The engine uses its packaged bundle and does not need to be added to the host esbuild entry point. |
cssbundling-rails or host Tailwind CSS |
✅ | The packaged engine CSS works independently. Use PostnHost host-tailwind mode only when copied views introduce new Tailwind classes. |
A host application must use Propshaft or Sprockets to serve the packaged engine assets, and either pipeline can be paired with import maps or esbuild.
Edit config/initializers/postnhost.rb:
Postnhost.configure do |config|
# Site defaults used when the matching Dashboard → Settings field is blank
# config.site_url = 'https://example.com'
# config.public_page_size = 12
# config.default_timezone = 'UTC'
# Optional: required only for AI-assisted translations
# config.openai_api_key = 'sk-...'
# config.openai_gpt_model = 'gpt-5.6-luna'
# Required by the generated CarrierWave initializer in production
# config.aws_access_key_id = 'AKIA...'
# config.aws_secret_access_key = '...'
# config.aws_region = 'us-east-1'
# config.aws_bucket = 'my-bucket'
# config.aws_endpoint_url_s3 = 'https://fly.storage.tigris.dev'
endSite URL, pagination, and timezone values are optional defaults; values saved under Dashboard → Settings take priority. The OpenAI settings are optional and needed only for AI translations.
The generated CarrierWave initializer uses S3-compatible storage in production. Configure the relevant AWS/S3 values above before deploying; aws_endpoint_url_s3 is needed only for providers that use a custom endpoint. Alternatively, edit config/initializers/carrierwave.rb to use a different production storage backend.
Set dashboard overrides for the canonical site URL and public articles per page under Dashboard → Settings → Site. When both site URL sources are blank, public URLs use the incoming request origin.
Instead of defining values in the initializer, you can add them under postnhost: in Rails credentials (bin/rails credentials:edit).
postnhost:
# For AI translations
openai_access_token: ...
openai_gpt_model: gpt-5.6-luna
# For image uploads (S3-compatible)
aws_access_key_id: AKIA...
aws_secret_access_key: ...
aws_region: us-east-1
aws_bucket_name: my-bucket
aws_endpoint_url_s3: https://s3.us-east-1.amazonaws.com # or Tigris/other S3-compatiblePostnHost includes three public templates: Default, Swiss Editorial, and Workspace Journal. Open Template in the CMS, choose a template, preview it, and save the selection.
Copy views to your application for manual/vibecoding customization:
bin/rails g postnhost:views --views-scope=minimal
# or
bin/rails g postnhost:views --views-scope=fullHost-application copies override the packaged engine views. The CMS selection still controls which template is active.
By default, PostnHost serves packaged engine CSS (postnhost/application.css) for zero-setup installs.
If your copied views introduce new Tailwind utility classes, enable host-tailwind mode so those classes are compiled in the host app.
--views-scope=minimal:
- Copies the Default public content templates under
app/views/postnhost/public/templates/default/ - Copies shared public layout partials such as the footer, favicon, logo, and language switcher
- Excludes internal metadata partials under
content_for/ - Keeps SEO/meta internals in the engine by default
--views-scope=full:
- Copies all three public content template sets under
app/views/postnhost/public/templates/ - Copies their layouts under
app/views/layouts/postnhost/public/templates/ - Copies metadata, structured-data, and SEO partials
Enable host-tailwind mode in your host app:
bin/rails g postnhost:tailwindcss:install
bin/devThis generator creates:
app/assets/stylesheets/postnhost/host.tailwind.cssProcfile.devwatcher entry:postnhost_css: bundle exec rails postnhost:tailwindcss:watch
- optional
package.jsonscripts (ifpackage.jsonexists):postnhost:tailwindcsspostnhost:tailwindcss:watch
Host-tailwind mode builds one combined stylesheet from the engine and host view sources at app/assets/builds/postnhost/application.css. That host-owned file transparently overrides the packaged asset with the same logical path; the engine always requests postnhost/application. This avoids competing Tailwind builds and lets copied views use ordinary classes such as bg-red-500 or md:grid.
If the host build is absent, Rails uses the packaged engine asset automatically. Both packaged and combined stylesheets are limited to PostnHost layouts through the data-postnhost root attribute. You do not need to edit layout tags or prefix Tailwind classes. Host customization requires Tailwind CSS 4.
Update the host app public/robots.txt so it points to the correct PostnHost sitemap URL.
PostnHost generates sitemap.xml automatically from live public articles and pages.
If the engine is mounted at /:
Sitemap: https://your-domain.com/sitemap.xmlIf the engine is mounted at /blog:
Sitemap: https://your-domain.com/blog/sitemap.xmlSet the canonical production origin under Dashboard → Settings → Site or with config.site_url so sitemap URLs are generated with the correct host.
PostnHost renders favicon tags from app/views/layouts/postnhost/_favicon.html.erb and points to root-level public paths:
/favicon.ico/icon.svg/apple-touch-icon.png/site.webmanifest
Because these are absolute paths, they are served from the host app public/ directory.
Replace the files in your host app:
# From host app root
cp /path/to/your/favicon.ico public/favicon.ico
cp /path/to/your/icon.svg public/icon.svg
cp /path/to/your/apple-touch-icon.png public/apple-touch-icon.png
cp /path/to/your/site.webmanifest public/site.webmanifestIf you need different markup or paths, run either postnhost:views command from the template-customization section, then edit:
app/views/layouts/postnhost/_favicon.html.erb
PostnHost ships with locale files for: en, fr, de, ja, ko, pt, pl, es, ru.
You can use PostnHost without extra i18n setup for a single-locale site. If you use multiple locales (localized routes, language switcher, or custom config/locales/*.yml files), add this to config/application.rb:
config.i18n.default_locale = :en
config.i18n.available_locales = %i[en fr de ja ko pt pl es ru]
config.i18n.fallbacks = [:en]Required: config.i18n.fallbacks = [:en] (or a map that ultimately falls back to a default language).
If you want to add more languages, add the locale code to available_locales and add config/locales/<code>.yml. To scaffold English strings into your host app:
bin/rails g postnhost:locale itThen translate the values under postnhost.public as needed.
You can define static public pages directly in your host app under:
app/views/postnhost/static_pages/*.html.erb
You can generate starter templates:
bin/rails g postnhost:static_pages terms privacy aboutExamples:
app/views/postnhost/static_pages/terms.html.erb→/blog/termsapp/views/postnhost/static_pages/privacy.html.erb→/blog/privacyapp/views/postnhost/static_pages/about.html.erb→/blog/about
Link helpers:
<%= link_to "Terms", postnhost.public_static_page_path(slug: "terms") %>The engine mounts at /blog by default. You can change this in config/routes.rb:
mount Postnhost::Engine, at: "/blog"GET /- Blog indexGET /:locale- Blog index (localized)GET /search- Article searchGET /:locale/search- Article search (localized)GET /authors/:slug- Author show (when author pages are enabled)GET /:locale/authors/:slug- Author show (localized, when author pages are enabled)GET /:category_slug- Category articlesGET /:locale/:category_slug- Category articles (localized)GET /:slug- Article show or static pageGET /:locale/:slug- Article show or static page (localized)GET /preview/:id- Article previewGET /sitemap.xml- XML sitemapGET /sitemap.xsl- Sitemap stylesheet
Review these host-application settings before deployment:
- Replace the host app’s favicon and related icon files if needed.
- Update
public/robots.txtso its sitemap URL matches the production domain and engine mount path. - Review the host app’s static
public/404.htmlandpublic/500.htmlpages. - Configure the generated CarrierWave initializer for S3-compatible production storage, or replace it with another storage configuration.
- If you customize the public templates, copy the required views and enable host Tailwind support when your changes introduce new utility classes.
For engine development, use Ruby 4.0.6 and Node.js 24+ as listed in mise.toml. Install them with mise, or another version manager, then run:
cd postnhost
bundle install
yarn installRun both JS and CSS watchers in one command:
yarn devFrom the engine root:
bundle install
yarn install
bundle exec rake prepare_test_db
bundle exec rspecSystem specs run with Selenium headless by default. Use visible Chrome when needed:
SYSTEM_TESTS_BROWSER=1 bundle exec rspec spec/systemThe gem is available as open source under the terms of the MIT License.
