social_construct 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 10cb6057e914282f135714277ab5459c5794e1892a4a2a0825c24cc64ec6259c
4
+ data.tar.gz: 239310809089b66d572365265074e18a32d1ddc7c693374d60090276a38d793f
5
+ SHA512:
6
+ metadata.gz: f269fe451a28fae49d20e8c78953b810fe39c7db26559c80d8f555a8cc03ec9f0d80ce811ff6aa94466ba815fd6b4dfe4c19542477878729201b52f70dfbf4aa
7
+ data.tar.gz: 868aa2dd22efbae1a7e105c8d10f0cfb2bbeee80f71386acdc44c1205b8672a279eefbca334255255230808863d5bde084de1b3ab5c936da39359d5da5dd4f32
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # SocialConstruct
2
+
3
+ A Rails engine for generating social media preview cards (Open Graph images) with built-in preview functionality.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "social_construct"
11
+ ```
12
+
13
+ Run the installation generator:
14
+
15
+ ```bash
16
+ bundle install
17
+ bin/rails generate social_construct:install
18
+ ```
19
+
20
+ This will:
21
+
22
+ - Create a configuration initializer
23
+ - Set up ApplicationSocialCard base class
24
+ - Create an example social card with template
25
+ - Add a shared layout for social cards
26
+ - Mount the preview interface in development
27
+ - Create example preview classes
28
+
29
+ ## Usage
30
+
31
+ ### 1. Create your base social card class
32
+
33
+ ```ruby
34
+ # app/social_cards/application_social_card.rb
35
+ class ApplicationSocialCard < SocialConstruct::BaseCard
36
+ include SocialConstruct::CardConcerns
37
+
38
+ # Set the logo path for your application
39
+ self.logo_path = Rails.root.join("app/assets/images/logo.png")
40
+ end
41
+ ```
42
+
43
+ ### 2. Create specific social card classes
44
+
45
+ ```ruby
46
+ # app/social_cards/item_social_card.rb
47
+ class ItemSocialCard < ApplicationSocialCard
48
+ def initialize(item)
49
+ super()
50
+ @item = item
51
+ end
52
+
53
+ private
54
+
55
+ def template_assigns
56
+ {
57
+ item: @item,
58
+ cover_image_data_url: image_to_data_url(@item.cover_image, resize_to_limit: [480, 630], saver: {quality: 75}),
59
+ logo_data_url: logo_data_url
60
+ }
61
+ end
62
+ end
63
+ ```
64
+
65
+ ### 3. Create templates
66
+
67
+ Templates go in `app/views/social_cards/` and should match your class names:
68
+
69
+ ```erb
70
+ <!-- app/views/social_cards/item_social_card.html.erb -->
71
+ <div class="card">
72
+ <h1><%= item.title %></h1>
73
+ <!-- Your card HTML -->
74
+ </div>
75
+ ```
76
+
77
+ ### 4. Optional: Use a shared layout
78
+
79
+ Create `app/views/layouts/social_cards.html.erb` for shared HTML structure:
80
+
81
+ ```erb
82
+ <!DOCTYPE html>
83
+ <html>
84
+ <head>
85
+ <meta charset="utf-8">
86
+ <style>
87
+ /* Shared styles */
88
+ </style>
89
+ <%= yield :head %>
90
+ </head>
91
+ <body>
92
+ <%= yield %>
93
+ </body>
94
+ </html>
95
+ ```
96
+
97
+ ### 5. Create preview classes for development
98
+
99
+ ```ruby
100
+ # app/social_cards/previews/item_social_card_preview.rb
101
+ class ItemSocialCardPreview
102
+ def default
103
+ item = Item.first || Item.new(title: "Example Item")
104
+ ItemSocialCard.new(item)
105
+ end
106
+
107
+ def with_long_title
108
+ item = Item.new(title: "This is a very long title that will test text wrapping")
109
+ ItemSocialCard.new(item)
110
+ end
111
+ end
112
+ ```
113
+
114
+ Visit `/rails/social_cards` in development to see all your previews.
115
+
116
+ ### 6. Use in your controllers
117
+
118
+ Include the controller concern:
119
+
120
+ ```ruby
121
+ class ItemsController < ApplicationController
122
+ include SocialConstruct::Controller
123
+
124
+ def og
125
+ @item = Item.find(params[:id])
126
+ render ItemSocialCard.new(@item)
127
+ end
128
+ end
129
+ ```
130
+
131
+ The `render` method automatically handles both formats:
132
+
133
+ - `.png` - Generates the actual PNG image
134
+ - `.html` - Shows the HTML preview (useful for debugging)
135
+
136
+ Or with caching:
137
+
138
+ ```ruby
139
+ def og
140
+ @item = Item.find(params[:id])
141
+
142
+ cache_key = [
143
+ "social-cards",
144
+ "item",
145
+ @item.id,
146
+ @item.updated_at.to_i
147
+ ]
148
+
149
+ render ItemSocialCard.new(@item), cache_key: cache_key
150
+ end
151
+ ```
152
+
153
+ Alternative API:
154
+
155
+ ```ruby
156
+ def og
157
+ @item = Item.find(params[:id])
158
+ card = ItemSocialCard.new(@item)
159
+
160
+ send_social_card(card,
161
+ cache_key: ["social-cards", @item.id, @item.updated_at.to_i],
162
+ expires_in: 7.days
163
+ )
164
+ end
165
+ ```
166
+
167
+ ## Configuration
168
+
169
+ Configure in your initializer:
170
+
171
+ ```ruby
172
+ # config/initializers/social_construct.rb
173
+
174
+ # Template path (default: "social_cards")
175
+ Rails.application.config.social_construct.template_path = "custom_path"
176
+
177
+ # Enable debug logging (default: false)
178
+ SocialConstruct::BaseCard.debug = true
179
+ ```
180
+
181
+ ## Requirements
182
+
183
+ - Rails 7.0+
184
+ - Ferrum (headless Chrome driver)
185
+ - Marcel (MIME type detection)
186
+
187
+ ## License
188
+
189
+ MIT
190
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load("rails/tasks/engine.rake")
5
+
6
+ load("rails/tasks/statistics.rake")
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,66 @@
1
+ module SocialConstruct
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Register social card mime type if not already registered
7
+ Mime::Type.register "image/png", :png unless Mime[:png]
8
+ end
9
+
10
+ # Render a social card as PNG with caching support
11
+ def send_social_card(card, cache_key: nil, expires_in: 7.days, cache_in_development: false)
12
+ # Build cache key if provided
13
+ if cache_key && (!Rails.env.development? || cache_in_development)
14
+ cache_key = Array(cache_key).join("-") if cache_key.is_a?(Array)
15
+
16
+ png_data = Rails.cache.fetch(cache_key, expires_in: expires_in) do
17
+ card.to_png
18
+ end
19
+ else
20
+ png_data = card.to_png
21
+ end
22
+
23
+ # Set caching headers
24
+ expires_in(1.day, public: true) unless Rails.env.development?
25
+
26
+ send_data(
27
+ png_data,
28
+ type: "image/png",
29
+ disposition: "inline",
30
+ filename: "#{controller_name.singularize}-social-card.png"
31
+ )
32
+ rescue => e
33
+ handle_social_card_error(e)
34
+ end
35
+
36
+ # Allow using render with social cards
37
+ def render(*args)
38
+ if args.first.is_a?(SocialConstruct::BaseCard)
39
+ card = args.first
40
+ options = args.second || {}
41
+
42
+ respond_to do |format|
43
+ format.png { send_social_card(card, **options) }
44
+ format.html { render(html: card.render.html_safe, layout: false) }
45
+ end
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def handle_social_card_error(error)
54
+ Rails.logger.error("Social card generation failed: #{error.message}")
55
+
56
+ # Send a fallback 1x1 transparent PNG
57
+ send_data(
58
+ Base64.decode64(
59
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
60
+ ),
61
+ type: "image/png",
62
+ disposition: "inline"
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ module SocialConstruct
2
+ class PreviewsController < ActionController::Base
3
+ def index
4
+ @preview_classes = find_preview_classes
5
+ end
6
+
7
+ def show
8
+ @preview_class = find_preview_class(params[:preview_name])
9
+ @preview_name = params[:preview_name]
10
+
11
+ unless @preview_class
12
+ redirect_to(previews_path, alert: "Preview not found")
13
+ return
14
+ end
15
+
16
+ @examples = @preview_class.instance_methods(false).sort
17
+ end
18
+
19
+ def preview
20
+ preview_class = find_preview_class(params[:preview_name])
21
+ example_name = params[:example_name]
22
+
23
+ unless preview_class && example_name
24
+ redirect_to(previews_path, alert: "Preview not found")
25
+ return
26
+ end
27
+
28
+ @card = preview_class.new.send(example_name)
29
+
30
+ respond_to do |format|
31
+ format.html { render(html: @card.render.html_safe, layout: false) }
32
+ format.png { send_data(@card.to_png, type: "image/png", disposition: "inline") }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def find_preview_classes
39
+ # Look for preview classes in the host app
40
+ preview_path = Rails.root.join("app/social_cards/previews")
41
+ return [] unless preview_path.exist?
42
+
43
+ Dir[preview_path.join("*_preview.rb")]
44
+ .map do |file|
45
+ require_dependency(file)
46
+ class_name = File.basename(file, ".rb").camelize
47
+ class_name.constantize if Object.const_defined?(class_name)
48
+ end
49
+ .compact
50
+ end
51
+
52
+ def find_preview_class(name)
53
+ return nil unless name
54
+
55
+ class_name = "#{name.camelize}Preview"
56
+ class_name.constantize if Object.const_defined?(class_name)
57
+ rescue NameError
58
+ nil
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,211 @@
1
+ require "base64"
2
+ require "tempfile"
3
+
4
+ module SocialConstruct
5
+ class BaseCard
6
+ include ActionView::Helpers
7
+ include Rails.application.routes.url_helpers
8
+
9
+ attr_reader :width, :height
10
+
11
+ # Class-level debug setting
12
+ cattr_accessor :debug, default: false
13
+
14
+ def initialize
15
+ @width = 1200
16
+ @height = 630
17
+ end
18
+
19
+ def render
20
+ ApplicationController.render(
21
+ template: template_name,
22
+ layout: layout_name,
23
+ locals: template_assigns.merge(
24
+ default_url_options: Rails.application.config.action_controller.default_url_options
25
+ )
26
+ )
27
+ end
28
+
29
+ def to_png
30
+ log_debug("Starting PNG generation for #{self.class.name}")
31
+
32
+ # Use 1x resolution for better performance and reliability
33
+ browser_options = {
34
+ headless: true,
35
+ timeout: 30,
36
+ window_size: [@width, @height]
37
+ }
38
+
39
+ # Add Docker-specific options in production
40
+ if Rails.env.production? || ENV["DOCKER_CONTAINER"].present?
41
+ browser_options[:browser_options] = {
42
+ :"no-sandbox" => nil,
43
+ :"disable-dev-shm-usage" => nil,
44
+ :"disable-gpu" => nil,
45
+ :"disable-software-rasterizer" => nil,
46
+ :"disable-web-security" => nil,
47
+ :"force-color-profile" => "srgb"
48
+ }
49
+ log_debug("Using production browser options")
50
+ end
51
+
52
+ browser = Ferrum::Browser.new(browser_options)
53
+
54
+ html_content = render
55
+ log_debug("HTML content length: #{html_content.length} bytes")
56
+ log_debug("HTML encoding: #{html_content.encoding}")
57
+
58
+ # For large HTML content (with embedded images), use a temp file instead of data URL
59
+ # 1MB threshold
60
+ if html_content.length > 1_000_000
61
+ log_debug("HTML too large for data URL, using temp file")
62
+
63
+ require "tempfile"
64
+
65
+ temp_file = Tempfile.new(["social_card", ".html"])
66
+ temp_file.write(html_content)
67
+ temp_file.rewind
68
+ temp_file.close
69
+
70
+ begin
71
+ browser.goto("file://#{temp_file.path}")
72
+ ensure
73
+ # Will be deleted after browser loads it
74
+ temp_file.unlink
75
+ end
76
+ else
77
+ # Ensure UTF-8 encoding
78
+ html_content = html_content.force_encoding("UTF-8")
79
+ encoded_html = ERB::Util.url_encode(html_content)
80
+
81
+ browser.goto("data:text/html;charset=utf-8,#{encoded_html}")
82
+ end
83
+
84
+ browser.set_viewport(width: @width, height: @height)
85
+
86
+ # Wait for the page to fully load
87
+ browser.network.wait_for_idle
88
+
89
+ # Add extra wait for complex pages with large images
90
+ sleep(0.5)
91
+
92
+ # Log page readiness
93
+ if debug
94
+ page_ready = browser.evaluate("document.readyState")
95
+ log_debug("Page ready state: #{page_ready}")
96
+
97
+ # Log computed styles to check if CSS is applied
98
+ body_bg = browser.evaluate("window.getComputedStyle(document.body).backgroundColor")
99
+ log_debug("Body background color: #{body_bg}")
100
+
101
+ # Check if content is visible
102
+ has_title = browser.evaluate("!!document.querySelector('.title')")
103
+ title_text = browser.evaluate("document.querySelector('.title')?.textContent") if has_title
104
+ log_debug("Has title element: #{has_title}, Title text: #{title_text}")
105
+
106
+ # Check HTML body content
107
+ body_html_length = browser.evaluate("document.body.innerHTML.length")
108
+ log_debug("Body HTML length: #{body_html_length}")
109
+ end
110
+
111
+ screenshot = browser.screenshot(
112
+ encoding: :binary,
113
+ quality: 100,
114
+ full: false
115
+ )
116
+
117
+ log_debug("Screenshot generated, size: #{screenshot.bytesize} bytes")
118
+
119
+ # Check if screenshot might be blank (very small file size indicates mostly white/single color)
120
+ # Less than 10KB usually means it's mostly one color
121
+ if screenshot.bytesize < 10_000
122
+ log_debug("Screenshot seems too small, might be blank. Retrying with delay...", :warn)
123
+
124
+ # Wait a bit more and try again
125
+ sleep(1)
126
+
127
+ # Force a paint
128
+ browser.execute(
129
+ "document.body.style.display = 'none'; document.body.offsetHeight; document.body.style.display = 'flex';"
130
+ )
131
+
132
+ screenshot = browser.screenshot(
133
+ encoding: :binary,
134
+ quality: 100,
135
+ full: false
136
+ )
137
+ log_debug("Retry screenshot size: #{screenshot.bytesize} bytes")
138
+ end
139
+
140
+ screenshot
141
+ rescue => e
142
+ log_debug("Ferrum screenshot failed: #{e.message}", :error)
143
+ log_debug("Backtrace: #{e.backtrace.first(5).join("\n")}", :error) if debug
144
+ raise
145
+ ensure
146
+ browser&.quit
147
+ end
148
+
149
+ private
150
+
151
+ def template_name
152
+ # Use configured template path from engine
153
+ template_path = Rails.application.config.social_construct.template_path
154
+ "#{template_path}/#{self.class.name.demodulize.underscore}"
155
+ end
156
+
157
+ def layout_name
158
+ # Check if a social cards layout exists
159
+ layout_path = "layouts/#{Rails.application.config.social_construct.template_path}"
160
+ if template_exists?(layout_path)
161
+ layout_path
162
+ else
163
+ false
164
+ end
165
+ end
166
+
167
+ def template_exists?(path)
168
+ ApplicationController.view_paths.any? do |resolver|
169
+ resolver.find_all(path, [], false, locale: [], formats: [:html], variants: [], handlers: [:erb]).any?
170
+ end
171
+
172
+ rescue
173
+ false
174
+ end
175
+
176
+ def template_assigns
177
+ {}
178
+ end
179
+
180
+ def image_to_data_url(attachment, variant_options = {})
181
+ return nil unless attachment.attached?
182
+
183
+ begin
184
+ # Ensure high quality defaults
185
+ options = {
186
+ saver: {quality: 90, strip: true}
187
+ }.deep_merge(variant_options)
188
+
189
+ variant = attachment.variant(options)
190
+ blob = variant.processed
191
+
192
+ content_type = blob.content_type || "image/jpeg"
193
+ image_data = blob.download
194
+
195
+ "data:#{content_type};base64,#{Base64.strict_encode64(image_data)}"
196
+ rescue => e
197
+ log_debug("Failed to convert image to data URL: #{e.message}", :error)
198
+ nil
199
+ end
200
+ end
201
+
202
+ def template_path
203
+ Rails.application.config.social_construct.template_path
204
+ end
205
+
206
+ def log_debug(message, level = :info)
207
+ return unless debug
208
+ Rails.logger.send(level, "[SocialCard] #{message}")
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,53 @@
1
+ <!DOCTYPE html>
2
+ <html class="h-full">
3
+ <head>
4
+ <title>Social Card Previews</title>
5
+ <script src="https://cdn.tailwindcss.com"></script>
6
+ </head>
7
+ <body class="h-full bg-gray-50">
8
+ <div class="min-h-full">
9
+ <header class="bg-white shadow-sm">
10
+ <div class="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
11
+ <h1 class="text-2xl font-semibold leading-6 text-gray-900">Social Card Previews</h1>
12
+ </div>
13
+ </header>
14
+ <main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
15
+
16
+ <div class="overflow-hidden bg-white shadow rounded-lg">
17
+ <% if @preview_classes.any? %>
18
+ <ul role="list" class="divide-y divide-gray-200">
19
+ <% @preview_classes.each do |preview_class| %>
20
+ <li class="px-6 py-4 hover:bg-gray-50">
21
+ <% preview_name = preview_class.name.underscore.sub(/_preview$/, '') %>
22
+ <%= link_to social_construct.preview_path(preview_name), class: "block" do %>
23
+ <div class="flex items-center justify-between">
24
+ <div>
25
+ <p class="text-lg font-medium text-indigo-600 hover:text-indigo-500">
26
+ <%= preview_class.name.sub(/Preview$/, '').underscore.humanize %>
27
+ </p>
28
+ <p class="mt-1 text-sm text-gray-500">
29
+ <%= preview_class.name %>
30
+ </p>
31
+ </div>
32
+ <svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
33
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
34
+ </svg>
35
+ </div>
36
+ <% end %>
37
+ </li>
38
+ <% end %>
39
+ </ul>
40
+ <% else %>
41
+ <div class="px-6 py-12 text-center">
42
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
44
+ </svg>
45
+ <h3 class="mt-2 text-sm font-semibold text-gray-900">No preview classes</h3>
46
+ <p class="mt-1 text-sm text-gray-500">Create preview classes in <code class="font-mono text-xs">app/social_cards/previews/</code></p>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ </main>
51
+ </div>
52
+ </body>
53
+ </html>
@@ -0,0 +1,56 @@
1
+ <!DOCTYPE html>
2
+ <html class="h-full">
3
+ <head>
4
+ <title><%= @preview_class.name.sub(/Preview$/, '').underscore.humanize %> - Social Card Previews</title>
5
+ <script src="https://cdn.tailwindcss.com"></script>
6
+ </head>
7
+ <body class="h-full bg-gray-50">
8
+ <div class="min-h-full">
9
+ <header class="bg-white shadow-sm">
10
+ <div class="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
11
+ <nav class="mb-4">
12
+ <%= link_to social_construct.previews_path, class: "inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700" do %>
13
+ <svg class="mr-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
14
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
15
+ </svg>
16
+ Back to all previews
17
+ <% end %>
18
+ </nav>
19
+ <h1 class="text-2xl font-semibold leading-6 text-gray-900"><%= @preview_class.name.sub(/Preview$/, '').underscore.humanize %></h1>
20
+ </div>
21
+ </header>
22
+ <main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
23
+
24
+ <div class="overflow-hidden bg-white shadow rounded-lg">
25
+ <ul role="list" class="divide-y divide-gray-200">
26
+ <% @examples.each do |example| %>
27
+ <li class="px-6 py-4">
28
+ <div class="flex items-center justify-between">
29
+ <div>
30
+ <%= link_to social_construct.example_preview_path(@preview_name, example), class: "text-lg font-medium text-indigo-600 hover:text-indigo-500" do %>
31
+ <%= example.to_s.humanize %>
32
+ <% end %>
33
+ </div>
34
+ <div class="ml-4 flex flex-shrink-0 space-x-2">
35
+ <%= link_to social_construct.example_preview_path(@preview_name, example, format: :html), class: "inline-flex items-center rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" do %>
36
+ <svg class="-ml-0.5 mr-1.5 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
38
+ </svg>
39
+ HTML
40
+ <% end %>
41
+ <%= link_to social_construct.example_preview_path(@preview_name, example, format: :png), class: "inline-flex items-center rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500" do %>
42
+ <svg class="-ml-0.5 mr-1.5 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
44
+ </svg>
45
+ PNG
46
+ <% end %>
47
+ </div>
48
+ </div>
49
+ </li>
50
+ <% end %>
51
+ </ul>
52
+ </div>
53
+ </main>
54
+ </div>
55
+ </body>
56
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ SocialConstruct::Engine.routes.draw do
2
+ resources(:previews, only: [:index, :show], param: :preview_name) do
3
+ member do
4
+ get(":example_name", action: :preview, as: :example)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,51 @@
1
+ require "rails/generators/base"
2
+
3
+ module SocialConstruct
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates SocialConstruct initializer, base classes, and example files"
9
+
10
+ def create_initializer_file
11
+ template("social_construct.rb", "config/initializers/social_construct.rb")
12
+ end
13
+
14
+ def create_application_social_card
15
+ template("application_social_card.rb", "app/social_cards/application_social_card.rb")
16
+ end
17
+
18
+ def create_example_social_card
19
+ template("example_social_card.rb", "app/social_cards/example_social_card.rb")
20
+ end
21
+
22
+ def create_example_template
23
+ template("example_social_card.html.erb", "app/views/social_cards/example_social_card.html.erb")
24
+ end
25
+
26
+ def create_social_cards_layout
27
+ template("social_cards_layout.html.erb", "app/views/layouts/social_cards.html.erb")
28
+ end
29
+
30
+ def create_example_preview
31
+ template("example_social_card_preview.rb", "app/social_cards/previews/example_social_card_preview.rb")
32
+ end
33
+
34
+ def add_route
35
+ route_string = <<-RUBY
36
+
37
+ # Social card previews (development only)
38
+ if Rails.env.development?
39
+ mount SocialConstruct::Engine, at: "/rails/social_cards"
40
+ end
41
+ RUBY
42
+
43
+ route(route_string)
44
+ end
45
+
46
+ def display_post_install
47
+ readme("POST_INSTALL") if behavior == :invoke
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ ===============================================================================
2
+
3
+ SocialConstruct has been successfully installed! 🎉
4
+
5
+ Next steps:
6
+
7
+ 1. Update the logo path in app/social_cards/application_social_card.rb
8
+ to point to your actual logo file.
9
+
10
+ 2. Run your Rails server and visit:
11
+ http://localhost:3000/rails/social_cards
12
+
13
+ You should see the example social card previews.
14
+
15
+ 3. Create your own social card classes:
16
+ - Inherit from ApplicationSocialCard
17
+ - Create matching templates in app/views/social_cards/
18
+ - Add preview classes in app/social_cards/previews/
19
+
20
+ 4. Use in your controllers:
21
+
22
+ class YourController < ApplicationController
23
+ include SocialConstruct::Controller
24
+
25
+ def og
26
+ @model = Model.find(params[:id])
27
+ render YourSocialCard.new(@model)
28
+ end
29
+ end
30
+
31
+ For more information, see:
32
+ https://github.com/brnbw/social_construct
33
+
34
+ ===============================================================================
@@ -0,0 +1,4 @@
1
+ class ApplicationSocialCard < SocialConstruct::BaseCard
2
+ # You can add any shared methods or configuration here
3
+ # that will be available to all your social card classes
4
+ end
@@ -0,0 +1,69 @@
1
+ <% content_for :head do %>
2
+ <style>
3
+ body {
4
+ background-color: <%= background_color %>;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ color: white;
9
+ text-align: center;
10
+ padding: 60px;
11
+ }
12
+
13
+ .content {
14
+ max-width: 900px;
15
+ }
16
+
17
+ .title {
18
+ font-size: 72px;
19
+ font-weight: 800;
20
+ line-height: 1.1;
21
+ margin-bottom: 24px;
22
+ letter-spacing: -2px;
23
+ }
24
+
25
+ .subtitle {
26
+ font-size: 32px;
27
+ font-weight: 400;
28
+ opacity: 0.8;
29
+ line-height: 1.3;
30
+ }
31
+
32
+ .logo {
33
+ position: absolute;
34
+ bottom: 60px;
35
+ right: 60px;
36
+ }
37
+
38
+ .logo img {
39
+ height: 40px;
40
+ width: auto;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .decoration {
45
+ position: absolute;
46
+ top: 0;
47
+ left: 0;
48
+ right: 0;
49
+ bottom: 0;
50
+ background: linear-gradient(135deg, transparent 0%, rgba(255,255,255,0.1) 100%);
51
+ pointer-events: none;
52
+ }
53
+ </style>
54
+ <% end %>
55
+
56
+ <div class="decoration"></div>
57
+
58
+ <div class="content">
59
+ <h1 class="title"><%= title %></h1>
60
+ <% if subtitle.present? %>
61
+ <p class="subtitle"><%= subtitle %></p>
62
+ <% end %>
63
+ </div>
64
+
65
+ <% if logo_data_url %>
66
+ <div class="logo">
67
+ <img src="<%= logo_data_url %>" alt="Logo">
68
+ </div>
69
+ <% end %>
@@ -0,0 +1,19 @@
1
+ class ExampleSocialCard < ApplicationSocialCard
2
+ def initialize(title: "Hello World", subtitle: nil, background_color: "#1a1a1a")
3
+ super()
4
+ @title = title
5
+ @subtitle = subtitle
6
+ @background_color = background_color
7
+ end
8
+
9
+ private
10
+
11
+ def template_assigns
12
+ {
13
+ title: @title,
14
+ subtitle: @subtitle,
15
+ background_color: @background_color,
16
+ logo_data_url: logo_data_url
17
+ }
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ class ExampleSocialCardPreview
2
+ def default
3
+ ExampleSocialCard.new(
4
+ title: "Welcome to SocialConstruct",
5
+ subtitle: "Beautiful social cards for your Rails app"
6
+ )
7
+ end
8
+
9
+ def dark_theme
10
+ ExampleSocialCard.new(
11
+ title: "Dark Theme Example",
12
+ subtitle: "Perfect for modern applications",
13
+ background_color: "#0a0a0a"
14
+ )
15
+ end
16
+
17
+ def colorful
18
+ ExampleSocialCard.new(
19
+ title: "Colorful Background",
20
+ subtitle: "Make your cards stand out",
21
+ background_color: "#6366f1"
22
+ )
23
+ end
24
+
25
+ def long_title
26
+ ExampleSocialCard.new(
27
+ title: "This is a very long title that demonstrates how text wrapping works in social cards",
28
+ subtitle: "Subtitle remains readable"
29
+ )
30
+ end
31
+
32
+ def no_subtitle
33
+ ExampleSocialCard.new(
34
+ title: "Simple and Clean"
35
+ )
36
+ end
37
+ end
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
+ <style>
7
+ /* Reset and base styles for all social cards */
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ html {
15
+ width: 1200px;
16
+ height: 630px;
17
+ background: white;
18
+ }
19
+
20
+ body {
21
+ width: 1200px;
22
+ height: 630px;
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Arial', sans-serif;
24
+ margin: 0;
25
+ padding: 0;
26
+ position: relative;
27
+ overflow: hidden;
28
+ -webkit-font-smoothing: antialiased;
29
+ -moz-osx-font-smoothing: grayscale;
30
+ }
31
+
32
+ /* Ensure images don't have borders */
33
+ img {
34
+ border: 0;
35
+ max-width: 100%;
36
+ }
37
+
38
+ /* Default text rendering */
39
+ h1, h2, h3, h4, h5, h6, p {
40
+ font-weight: normal;
41
+ margin: 0;
42
+ }
43
+ </style>
44
+ <%= yield :head %>
45
+ </head>
46
+ <body>
47
+ <%= yield %>
48
+ </body>
49
+ </html>
@@ -0,0 +1,10 @@
1
+ # SocialConstruct configuration
2
+ Rails.application.configure do
3
+ # Configure the template path for social card views
4
+ # Default: "social_cards"
5
+ # config.social_construct.template_path = "social_cards"
6
+
7
+ # Enable debug logging for social card generation
8
+ # Default: false
9
+ # SocialConstruct::BaseCard.debug = true
10
+ end
@@ -0,0 +1,14 @@
1
+ module SocialConstruct
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace SocialConstruct
4
+
5
+ config.social_construct = ActiveSupport::OrderedOptions.new
6
+ config.social_construct.template_path = "social_cards"
7
+
8
+ # Ensure dependencies are loaded
9
+ config.before_initialize do
10
+ require "ferrum"
11
+ require "marcel"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module SocialConstruct
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,12 @@
1
+ require "social_construct/version"
2
+ require "social_construct/engine"
3
+
4
+ # Dependencies
5
+ require "ferrum"
6
+ require "marcel"
7
+
8
+ module SocialConstruct
9
+ # Configuration for template paths and other settings
10
+ mattr_accessor :template_path
11
+ @@template_path = "social_cards"
12
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: social_construct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mikkel Malmberg
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ferrum
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.13'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.13'
40
+ - !ruby/object:Gem::Dependency
41
+ name: marcel
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ description: A flexible Rails engine for generating social media preview cards (Open
55
+ Graph images) with built-in preview functionality
56
+ email:
57
+ - mikkel@brnbw.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - Rakefile
64
+ - app/controllers/concerns/social_construct/controller.rb
65
+ - app/controllers/social_construct/previews_controller.rb
66
+ - app/models/social_construct/base_card.rb
67
+ - app/views/social_construct/previews/index.html.erb
68
+ - app/views/social_construct/previews/show.html.erb
69
+ - config/routes.rb
70
+ - lib/generators/social_construct/install/install_generator.rb
71
+ - lib/generators/social_construct/install/templates/POST_INSTALL
72
+ - lib/generators/social_construct/install/templates/application_social_card.rb
73
+ - lib/generators/social_construct/install/templates/example_social_card.html.erb
74
+ - lib/generators/social_construct/install/templates/example_social_card.rb
75
+ - lib/generators/social_construct/install/templates/example_social_card_preview.rb
76
+ - lib/generators/social_construct/install/templates/social_cards_layout.html.erb
77
+ - lib/generators/social_construct/install/templates/social_construct.rb
78
+ - lib/social_construct.rb
79
+ - lib/social_construct/engine.rb
80
+ - lib/social_construct/version.rb
81
+ homepage: https://github.com/brnbw/social_construct
82
+ licenses: []
83
+ metadata:
84
+ homepage_uri: https://github.com/brnbw/social_construct
85
+ source_code_uri: https://github.com/brnbw/social_construct
86
+ changelog_uri: https://github.com/brnbw/social_construct/blob/main/CHANGELOG.md
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.6.9
102
+ specification_version: 4
103
+ summary: Rails engine for generating social media preview cards
104
+ test_files: []