helios-press 0.4 → 0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ea59768263242ec8f88e1bd68e8d33f67f4b5a313d32626ac735632be159672
4
- data.tar.gz: f2aa7c604191d1e340f18c1c9cf980d6f988407deff6127953a023572bcd1ef6
3
+ metadata.gz: 81300e27ac6528a2ed98720628689300cb0be7257061154bbecc6ecfb8734ca6
4
+ data.tar.gz: 145396036b9ce29b1ea7eb700e3a8af95f0d33329852c8f79ac375b913a0df50
5
5
  SHA512:
6
- metadata.gz: fa4299b094dca0fc56ef35d1c930146cabbf6020bc4561f7482c140a1d9fac88a995f56a18a7aa53da51331f03e1b75e9d4d88f3c26c66b52968594673567752
7
- data.tar.gz: 6e804a7772de4fe2f6b29ed41b1f111bede76ef00b86e39e3215bf07f311ac30344e28cafde50df2e11e5878dc843cd585272536bfc4bd9bb43634a1168c6352
6
+ metadata.gz: 469105b200430a999c911d10e90744e2936824e5224079cb91fe97fb4753599f398ba197a6bbfe232fcf7e52784b519904b62e9b71799d0d48770950100a77e8
7
+ data.tar.gz: ed7b189d44a41125b0fa0fe3d44c59c4912ffbf48b20cee7a0f6b1e49c6fd924e191d0ad38020737435982d9721e7a8fed239e16b80ad468dab983dffd13f707
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright TODO: Write your name
1
+ Copyright (c) 2026 Jason Fleetwood-Boldt
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -38,6 +38,11 @@ Helios::Press.configure do |config|
38
38
  # Optional slug prefix for posts
39
39
  config.post_slug_prefix = nil # e.g., "blog/" for /blog/my-post
40
40
 
41
+ # SEO / social metadata
42
+ config.site_name = "My Blog" # og:site_name and <title> fallback
43
+ config.twitter_site = "@myhandle" # twitter:site card handle
44
+ config.default_image_url = nil # fallback og:image when a post has no image
45
+
41
46
  # API authentication (for external post ingestion)
42
47
  # Default: checks X-API-Key header against BLOG_INGEST_API_KEY env var
43
48
  config.api_authentication = ->(controller) {
@@ -50,6 +55,34 @@ Helios::Press.configure do |config|
50
55
  end
51
56
  ```
52
57
 
58
+ ## SEO (helios-seo)
59
+
60
+ helios-press delegates the entire page `<head>` — meta description, canonical,
61
+ robots, Open Graph, Twitter cards, and the JSON-LD graph — to
62
+ [helios-seo](../helios-seo), which it depends on. Press only supplies the
63
+ per-post share image; the post `show` view calls
64
+ `helios_seo_tags(@post, title: false)` (the layout keeps ownership of `<title>`).
65
+
66
+ Integration checklist:
67
+
68
+ 1. **Configure helios-seo** in `config/initializers/helios_seo.rb` — set
69
+ `site_name`, `site_url`, `author`, `twitter_site` / `twitter_creator`, and
70
+ `default_og_image`. These now live in helios-seo; the old
71
+ `Helios::Press` SEO settings (`twitter_site`, `default_image_url`) are
72
+ superseded. (`config.site_name` on Press is still used as the layout
73
+ `<title>` fallback.)
74
+ 2. **Layout** — make sure your layout yields `:head` inside `<head>`. The
75
+ bundled Press layout already does.
76
+ 3. **Per-post share image** — authors can upload a "Social share image" in the
77
+ post form to override; otherwise the post's banner (its first image block) is
78
+ used. Falls back to helios-seo's site-wide `default_og_image`. Requires
79
+ ActiveStorage (already used by Press image blocks).
80
+ 4. **Canonical URLs** default to `"{site_url}/{slug}"`, which matches the public
81
+ `get ":slug"` route when the Public engine is mounted at root. On a subpath
82
+ mount, set `config.canonical_url` in the helios-seo initializer.
83
+ 5. **`/llms.txt`** (optional) — mount `Helios::Seo::Engine` at root to serve it;
84
+ set `config.llms_posts = -> { Helios::Press::Post.published.reverse_sorted }`.
85
+
53
86
  ## Routes
54
87
 
55
88
  Helios::Press provides three independent engines that you can mount wherever you want:
@@ -185,6 +218,21 @@ Two modes are supported:
185
218
 
186
219
  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).
187
220
 
221
+ ## Releases
222
+
223
+ ### 0.5
224
+
225
+ - SEO delegated to [helios-seo](../helios-seo) (new dependency). The post `show`
226
+ view now renders `helios_seo_tags(@post, title: false)` instead of the bundled
227
+ `_meta_tags` partial, adding a meta description, `robots`, and a JSON-LD graph
228
+ on top of the previous Open Graph / Twitter tags.
229
+ - Removed `app/views/helios/press/posts/_meta_tags.html.erb` and
230
+ `app/helpers/helios/press/seo_helper.rb` (superseded by helios-seo).
231
+ - `Post` gains an optional `og_image` attachment (a "Social share image" field in
232
+ the admin form) plus `Post#og_image_url`, which builds a stable, absolute
233
+ ActiveStorage proxy URL for the post's share image, falling back to the post's
234
+ banner (first image block). See the [SEO](#seo-helios-seo) section.
235
+
188
236
  ## License
189
237
 
190
- Proprietary. All rights reserved.
238
+ MIT License. See [MIT-LICENSE](MIT-LICENSE) for details.
@@ -60,7 +60,7 @@ module Helios
60
60
  end
61
61
 
62
62
  def post_params
63
- params.require(:post).permit(:name, :published, :slug, :keywords, :description)
63
+ params.require(:post).permit(:name, :published, :slug, :keywords, :description, :og_image)
64
64
  end
65
65
 
66
66
  def helios_press_admin
@@ -3,6 +3,10 @@ module Helios
3
3
  class Post < ActiveRecord::Base
4
4
  self.table_name = "helios_press_posts"
5
5
 
6
+ # Optional explicit social-share image. When set it overrides the banner;
7
+ # otherwise the post's banner (first image block) is used. See #og_image_url.
8
+ has_one_attached :og_image
9
+
6
10
  has_many :blocks, -> { order(position: :asc) },
7
11
  class_name: "Helios::Press::Block",
8
12
  foreign_key: :post_id,
@@ -18,8 +22,37 @@ module Helios
18
22
  scope :published, -> { where(published: true) }
19
23
  scope :reverse_sorted, -> { order(updated_at: :desc) }
20
24
 
25
+ # Absolute URL for the post's social-share image, sized to the OG-
26
+ # recommended 1200x630. Prefers the explicit `og_image` override, then
27
+ # falls back to the post's banner (the first attached image block). Returns
28
+ # nil when neither exists, so helios-seo can fall back to its site-wide
29
+ # default.
30
+ #
31
+ # `view` is the ActionView context helios-seo passes into the resolver. We
32
+ # use `view.polymorphic_url` so the result is the stable ActiveStorage
33
+ # proxy/redirect URL (per the app's resolve_model_to_route) — absolute,
34
+ # using the request host — NOT the short-lived signed service URL that
35
+ # `.url` returns. Degrades to nil on any failure (e.g. no view context).
36
+ def og_image_url(view: nil)
37
+ attachment = og_image.attached? ? og_image : banner_image
38
+ return nil unless attachment && view
39
+
40
+ variant = attachment.variant(resize_to_limit: [ 1200, 630 ])
41
+ view.polymorphic_url(variant)
42
+ rescue StandardError
43
+ nil
44
+ end
45
+
21
46
  private
22
47
 
48
+ # The post's banner: the first attached image in the first image block.
49
+ def banner_image
50
+ image_block = blocks.detect do |block|
51
+ block.block_type == "image_container" && block.block_images.any?
52
+ end
53
+ image_block&.block_images&.detect { |bi| bi.file.attached? }&.file
54
+ end
55
+
23
56
  def generate_slug
24
57
  prefix = Helios::Press.configuration.post_slug_prefix
25
58
  base_slug = name.parameterize
@@ -1,4 +1,4 @@
1
- <%= form_with(model: post, url: post.persisted? ? helios_press_admin.post_path(post) : helios_press_admin.posts_path, local: true) do |form| %>
1
+ <%= form_with(model: post, url: post.persisted? ? helios_press_admin.post_path(post) : helios_press_admin.posts_path, local: true, multipart: true) do |form| %>
2
2
  <% if post.errors.any? %>
3
3
  <div class="alert alert-danger">
4
4
  <h4><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h4>
@@ -31,6 +31,12 @@
31
31
  <%= form.text_field :keywords, class: "form-control", placeholder: "Separate keywords with commas" %>
32
32
  </div>
33
33
 
34
+ <div class="mb-3">
35
+ <%= form.label :og_image, "Social share image", class: "form-label" %>
36
+ <%= form.file_field :og_image, class: "form-control", accept: "image/*" %>
37
+ <div class="form-text">Optional. Overrides the post's banner image for social/link previews.</div>
38
+ </div>
39
+
34
40
  <div class="mb-3">
35
41
  <div class="form-check">
36
42
  <%= form.check_box :published, class: "form-check-input" %>
@@ -1,26 +1,9 @@
1
1
  <% content_for :title, @post.name %>
2
2
 
3
+ <%# helios-seo renders the full head layer: description, canonical, robots, OG,
4
+ Twitter, and JSON-LD. title: false because the layout owns the <title>. %>
3
5
  <% content_for :head do %>
4
- <%
5
- first_image_block = @post.blocks.find { |b| b.block_type == "image_container" && b.block_images.any? }
6
- %>
7
- <% if @post.keywords.present? %>
8
- <meta name="keywords" content="<%= @post.keywords %>">
9
- <% end %>
10
-
11
- <meta property="og:title" content="<%= @post.name %>">
12
- <meta property="og:type" content="article">
13
- <meta property="og:url" content="<%= request.base_url %>/<%= @post.slug %>">
14
- <% if @post.description.present? %>
15
- <meta property="og:description" content="<%= @post.description %>">
16
- <% end %>
17
- <% if first_image_block&.block_images&.first&.file&.attached? %>
18
- <meta property="og:image" content="<%= url_for(first_image_block.block_images.first.file.variant(resize_to_limit: [1200, 630])) %>">
19
- <% end %>
20
- <% if @post.published? %>
21
- <meta property="article:published_time" content="<%= @post.created_at.iso8601 %>">
22
- <meta property="article:modified_time" content="<%= @post.updated_at.iso8601 %>">
23
- <% end %>
6
+ <%= helios_seo_tags(@post, title: false) %>
24
7
  <% end %>
25
8
 
26
9
  <div class="container my-5">
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>Helios press</title>
4
+ <title><%= content_for?(:title) ? yield(:title) : (Helios::Press.configuration.site_name.presence || "Helios press") %></title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
7
 
@@ -5,7 +5,10 @@ module Helios
5
5
  :public_parent_controller,
6
6
  :api_parent_controller,
7
7
  :api_authentication,
8
- :post_slug_prefix
8
+ :post_slug_prefix,
9
+ :site_name,
10
+ :twitter_site,
11
+ :default_image_url
9
12
 
10
13
  def initialize
11
14
  @admin_parent_controller = "ApplicationController"
@@ -13,6 +16,9 @@ module Helios
13
16
  @api_parent_controller = "ActionController::API"
14
17
  @api_authentication = nil
15
18
  @post_slug_prefix = nil
19
+ @site_name = nil
20
+ @twitter_site = nil
21
+ @default_image_url = nil
16
22
  end
17
23
  end
18
24
  end
@@ -12,6 +12,24 @@ module Helios
12
12
  helper Rails.application.routes.url_helpers
13
13
  end
14
14
  end
15
+
16
+ # Adapt Post to helios-seo: supply the per-post social-share image. The
17
+ # host app still provides site identity (site_name, site_url, author, ...)
18
+ # and may override this resolver. Canonical URLs are left to helios-seo's
19
+ # default ("{site_url}/{slug}"), which matches the public "get :slug" route
20
+ # when the Public engine is mounted at root; hosts on a subpath set their
21
+ # own `canonical_url`.
22
+ initializer "helios_press.seo" do
23
+ if defined?(Helios::Seo)
24
+ Helios::Seo.configure do |seo|
25
+ # Two-arg form: helios-seo passes the view context so we can build an
26
+ # absolute ActiveStorage proxy URL for the share image.
27
+ seo.image_resolver = ->(post, view) do
28
+ post.respond_to?(:og_image_url) ? post.og_image_url(view: view) : nil
29
+ end
30
+ end
31
+ end
32
+ end
15
33
  end
16
34
 
17
35
  module Admin
@@ -1,5 +1,5 @@
1
1
  module Helios
2
2
  module Press
3
- VERSION = "0.4"
3
+ VERSION = "0.5"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: helios-press
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.4'
4
+ version: '0.5'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Fleetwood-Boldt
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-05-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -80,6 +79,20 @@ dependencies:
80
79
  - - ">="
81
80
  - !ruby/object:Gem::Version
82
81
  version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: helios-seo
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
83
96
  description: A Rails engine providing a WordPress-like block editor for blog posts.
84
97
  Supports text (ActionText), image stacks, and video blocks with drag-to-reorder.
85
98
  email:
@@ -146,11 +159,10 @@ files:
146
159
  - lib/tasks/helios/press_tasks.rake
147
160
  homepage: https://github.com/heliosdev-shop/helios-press
148
161
  licenses:
149
- - Nonstandard
162
+ - MIT
150
163
  metadata:
151
164
  homepage_uri: https://github.com/heliosdev-shop/helios-press
152
165
  source_code_uri: https://github.com/heliosdev-shop/helios-press
153
- post_install_message:
154
166
  rdoc_options: []
155
167
  require_paths:
156
168
  - lib
@@ -165,8 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
165
177
  - !ruby/object:Gem::Version
166
178
  version: '0'
167
179
  requirements: []
168
- rubygems_version: 3.4.19
169
- signing_key:
180
+ rubygems_version: 3.6.9
170
181
  specification_version: 4
171
182
  summary: Multi-block blog post editor for Rails with drag-to-reorder
172
183
  test_files: []