social_construct 0.1.0 → 0.3.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 +4 -4
- data/README.md +97 -133
- data/app/controllers/concerns/social_construct/controller.rb +7 -10
- data/app/controllers/social_construct/previews_controller.rb +27 -14
- data/app/models/social_construct/base_card.rb +210 -33
- data/config/routes.rb +2 -0
- data/lib/generators/social_construct/install/install_generator.rb +1 -5
- data/lib/generators/social_construct/install/templates/example_social_card.html.erb +13 -19
- data/lib/generators/social_construct/install/templates/example_social_card.rb +1 -3
- data/lib/generators/social_construct/install/templates/example_social_card_preview.rb +0 -29
- data/lib/generators/social_construct/install/templates/social_cards_layout.html.erb +9 -8
- data/lib/generators/social_construct/install/templates/social_construct.rb +8 -1
- data/lib/social_construct/engine.rb +7 -4
- data/lib/social_construct/version.rb +1 -1
- data/lib/social_construct.rb +0 -3
- metadata +5 -6
- data/lib/generators/social_construct/install/templates/POST_INSTALL +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b1b86abf2f0675a09e15f221cbd4cbda23fbd5f5b37d0406130c30808e3f127
|
4
|
+
data.tar.gz: 5069906dc7297bddd8600b3bef4b321881c09e475138f63980cea760e0e4ff71
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a921f74dd5276deef7b2fc446ccfda8adf15d092ff9e2f43fb48c454f7487a4079286b9bf4a3a3346625aaa95716d956229a73f6dce8a4261fa1a0ffbbc7785b
|
7
|
+
data.tar.gz: 84b6f50e3d7a6fa73432304be9ded5dfb08f47c72f0c30d72b56e779c9ec0f1c73de02db0799f9582f6e94ecdfc6f28409e48247f3f4e6afd32cf171a7e4a667
|
data/README.md
CHANGED
@@ -1,190 +1,154 @@
|
|
1
|
-
|
1
|
+
<img src="https://s3.brnbw.com/Stack-C9apVxz8Pg.webp" width="600">
|
2
2
|
|
3
|
-
|
3
|
+
- Design using HTML/CSS
|
4
|
+
- Supports images, fonts, caching, ...
|
5
|
+
- Built-in previews in development (like mailers)
|
6
|
+
- Only requirement is Chrome(ium)
|
4
7
|
|
5
|
-
##
|
8
|
+
## Example
|
6
9
|
|
7
|
-
|
10
|
+
Create a card class:
|
8
11
|
|
9
12
|
```ruby
|
10
|
-
|
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
|
13
|
+
class PostSocialCard < ApplicationSocialCard
|
14
|
+
def initialize(post)
|
15
|
+
@post = post
|
51
16
|
end
|
52
17
|
|
53
|
-
private
|
54
|
-
|
55
18
|
def template_assigns
|
56
19
|
{
|
57
|
-
|
58
|
-
|
59
|
-
|
20
|
+
title: @post.title,
|
21
|
+
author: @post.author_name,
|
22
|
+
avatar: attachment_data_url(@post.author.avatar)
|
60
23
|
}
|
61
24
|
end
|
62
25
|
end
|
63
26
|
```
|
64
27
|
|
65
|
-
|
66
|
-
|
67
|
-
Templates go in `app/views/social_cards/` and should match your class names:
|
28
|
+
Create a template:
|
68
29
|
|
69
30
|
```erb
|
70
|
-
<!-- app/views/social_cards/
|
31
|
+
<!-- app/views/social_cards/post_social_card.html.erb -->
|
32
|
+
|
71
33
|
<div class="card">
|
72
|
-
<
|
73
|
-
|
34
|
+
<img src="<%= avatar %>" class="logo">
|
35
|
+
<h1><%= title %></h1>
|
36
|
+
<p>by <%= author %></p>
|
74
37
|
</div>
|
75
38
|
```
|
76
39
|
|
77
|
-
|
78
|
-
|
79
|
-
Create `app/views/layouts/social_cards.html.erb` for shared HTML structure:
|
40
|
+
Add a controller action:
|
80
41
|
|
81
|
-
```
|
82
|
-
|
83
|
-
|
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
|
-
```
|
42
|
+
```ruby
|
43
|
+
class PostsController < ApplicationController
|
44
|
+
include SocialConstruct::Controller
|
96
45
|
|
97
|
-
|
46
|
+
# ...
|
98
47
|
|
99
|
-
|
100
|
-
|
101
|
-
class ItemSocialCardPreview
|
102
|
-
def default
|
103
|
-
item = Item.first || Item.new(title: "Example Item")
|
104
|
-
ItemSocialCard.new(item)
|
105
|
-
end
|
48
|
+
def social_image
|
49
|
+
@post = Post.find(params[:id])
|
106
50
|
|
107
|
-
|
108
|
-
|
109
|
-
|
51
|
+
send_social_card(
|
52
|
+
PostSocialCard.new(@post),
|
53
|
+
cache_key: [@post.id, @post.updated_at]
|
54
|
+
)
|
110
55
|
end
|
111
56
|
end
|
112
57
|
```
|
113
58
|
|
114
|
-
|
59
|
+
## Setup
|
60
|
+
|
61
|
+
```sh
|
62
|
+
$ bundle add social_construct && bundle install
|
63
|
+
$ bin/rails generate social_construct:install
|
64
|
+
```
|
115
65
|
|
116
|
-
|
66
|
+
## Images
|
117
67
|
|
118
|
-
|
68
|
+
Convert ActiveStorage attachments to Base64 `data://` URLs:
|
119
69
|
|
120
70
|
```ruby
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
render ItemSocialCard.new(@item)
|
127
|
-
end
|
71
|
+
def template_assigns
|
72
|
+
{
|
73
|
+
cover_image: attachment_data_url(@post.cover_image),
|
74
|
+
avatar: attachment_data_url(@post.author.avatar, resize_to_limit: [200, 200])
|
75
|
+
}
|
128
76
|
end
|
129
77
|
```
|
130
78
|
|
131
|
-
|
79
|
+
## Fonts
|
132
80
|
|
133
|
-
|
134
|
-
- `.html` - Shows the HTML preview (useful for debugging)
|
81
|
+
### Remote fonts
|
135
82
|
|
136
|
-
|
83
|
+
Just import them normally and they should work.
|
137
84
|
|
138
|
-
```
|
139
|
-
|
140
|
-
@
|
85
|
+
```erb
|
86
|
+
<style>
|
87
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
|
88
|
+
body { font-family: 'Inter', sans-serif; }
|
89
|
+
</style>
|
90
|
+
```
|
141
91
|
|
142
|
-
|
143
|
-
"social-cards",
|
144
|
-
"item",
|
145
|
-
@item.id,
|
146
|
-
@item.updated_at.to_i
|
147
|
-
]
|
92
|
+
### Local fonts
|
148
93
|
|
149
|
-
|
94
|
+
Store fonts in `app/assets/fonts/` and embed as `data://` URLs:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
class LocalFontsCard < ApplicationSocialCard
|
98
|
+
def template_assigns
|
99
|
+
{
|
100
|
+
custom_font_css: generate_font_face(
|
101
|
+
"custom-font-name",
|
102
|
+
"Recursive_VF_1.085--subset-GF_latin_basic.woff2",
|
103
|
+
weight: "300 1000"
|
104
|
+
)
|
105
|
+
}
|
106
|
+
end
|
150
107
|
end
|
151
108
|
```
|
152
109
|
|
153
|
-
|
110
|
+
And include in template:
|
154
111
|
|
155
|
-
```
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
)
|
164
|
-
end
|
112
|
+
```erb
|
113
|
+
<style>
|
114
|
+
<%= custom_font_css %>
|
115
|
+
|
116
|
+
body {
|
117
|
+
font-family: 'custom-font-name', sans-serif;
|
118
|
+
}
|
119
|
+
</style>
|
165
120
|
```
|
166
121
|
|
167
|
-
##
|
122
|
+
## Previews
|
168
123
|
|
169
|
-
|
124
|
+
Mount the preview engine:
|
170
125
|
|
171
126
|
```ruby
|
172
|
-
|
127
|
+
Rails.application.routes.draw do
|
128
|
+
if Rails.env.development?
|
129
|
+
mount(SocialConstruct::Engine => "/rails/social_cards")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
173
133
|
|
174
|
-
|
175
|
-
Rails.application.config.social_construct.template_path = "custom_path"
|
134
|
+
Create preview classes:
|
176
135
|
|
177
|
-
|
178
|
-
|
179
|
-
|
136
|
+
```ruby
|
137
|
+
# test/social_cards/previews/post_social_card_preview.rb
|
138
|
+
class PostSocialCardPreview
|
139
|
+
def default
|
140
|
+
PostSocialCard.new(Post.first)
|
141
|
+
end
|
180
142
|
|
181
|
-
|
143
|
+
def long_title
|
144
|
+
post = Post.new(title: "A very long title that demonstrates text wrapping behavior")
|
145
|
+
PostSocialCard.new(post)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
182
149
|
|
183
|
-
|
184
|
-
- Ferrum (headless Chrome driver)
|
185
|
-
- Marcel (MIME type detection)
|
150
|
+
Visit `http://localhost:3000/rails/social_cards` to preview all cards.
|
186
151
|
|
187
152
|
## License
|
188
153
|
|
189
154
|
MIT
|
190
|
-
|
@@ -3,7 +3,6 @@ module SocialConstruct
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
# Register social card mime type if not already registered
|
7
6
|
Mime::Type.register "image/png", :png unless Mime[:png]
|
8
7
|
end
|
9
8
|
|
@@ -35,16 +34,14 @@ module SocialConstruct
|
|
35
34
|
|
36
35
|
# Allow using render with social cards
|
37
36
|
def render(*args)
|
38
|
-
|
39
|
-
card = args.first
|
40
|
-
options = args.second || {}
|
37
|
+
return super unless args.first.is_a?(SocialConstruct::BaseCard)
|
41
38
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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) }
|
48
45
|
end
|
49
46
|
end
|
50
47
|
|
@@ -1,5 +1,9 @@
|
|
1
1
|
module SocialConstruct
|
2
2
|
class PreviewsController < ActionController::Base
|
3
|
+
include SocialConstruct::Controller
|
4
|
+
|
5
|
+
before_action :ensure_previews_enabled
|
6
|
+
|
3
7
|
def index
|
4
8
|
@preview_classes = find_preview_classes
|
5
9
|
end
|
@@ -27,26 +31,29 @@ module SocialConstruct
|
|
27
31
|
|
28
32
|
@card = preview_class.new.send(example_name)
|
29
33
|
|
30
|
-
|
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
|
+
render(@card)
|
34
35
|
end
|
35
36
|
|
36
37
|
private
|
37
38
|
|
38
39
|
def find_preview_classes
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
40
|
+
return [] unless (paths = Rails.application.config.social_construct.preview_paths)
|
41
|
+
|
42
|
+
paths
|
43
|
+
.map do |path|
|
44
|
+
next [] unless path.exist?
|
45
|
+
|
46
|
+
glob = path.join("*_preview.rb")
|
47
|
+
|
48
|
+
Dir[glob]
|
49
|
+
.map do |file|
|
50
|
+
require_dependency(file)
|
51
|
+
class_name = File.basename(file, ".rb").camelize
|
52
|
+
class_name.constantize if Object.const_defined?(class_name)
|
53
|
+
end
|
54
|
+
.compact
|
48
55
|
end
|
49
|
-
.
|
56
|
+
.flatten
|
50
57
|
end
|
51
58
|
|
52
59
|
def find_preview_class(name)
|
@@ -57,5 +64,11 @@ module SocialConstruct
|
|
57
64
|
rescue NameError
|
58
65
|
nil
|
59
66
|
end
|
67
|
+
|
68
|
+
def ensure_previews_enabled
|
69
|
+
unless Rails.application.config.social_construct.show_previews
|
70
|
+
raise ActionController::RoutingError, "Social card previews are disabled"
|
71
|
+
end
|
72
|
+
end
|
60
73
|
end
|
61
74
|
end
|
@@ -6,14 +6,14 @@ module SocialConstruct
|
|
6
6
|
include ActionView::Helpers
|
7
7
|
include Rails.application.routes.url_helpers
|
8
8
|
|
9
|
-
attr_reader :width, :height
|
10
|
-
|
11
|
-
# Class-level debug setting
|
12
9
|
cattr_accessor :debug, default: false
|
13
10
|
|
14
|
-
def
|
15
|
-
@width
|
16
|
-
|
11
|
+
def width
|
12
|
+
@width || 1200
|
13
|
+
end
|
14
|
+
|
15
|
+
def height
|
16
|
+
@height || 630
|
17
17
|
end
|
18
18
|
|
19
19
|
def render
|
@@ -33,7 +33,7 @@ module SocialConstruct
|
|
33
33
|
browser_options = {
|
34
34
|
headless: true,
|
35
35
|
timeout: 30,
|
36
|
-
window_size: [
|
36
|
+
window_size: [width, height]
|
37
37
|
}
|
38
38
|
|
39
39
|
# Add Docker-specific options in production
|
@@ -81,13 +81,38 @@ module SocialConstruct
|
|
81
81
|
browser.goto("data:text/html;charset=utf-8,#{encoded_html}")
|
82
82
|
end
|
83
83
|
|
84
|
-
browser.set_viewport(width:
|
84
|
+
browser.set_viewport(width: width, height: height)
|
85
85
|
|
86
86
|
# Wait for the page to fully load
|
87
87
|
browser.network.wait_for_idle
|
88
88
|
|
89
|
-
#
|
90
|
-
|
89
|
+
# Wait for all images and fonts to load
|
90
|
+
browser.execute(
|
91
|
+
<<~JS
|
92
|
+
return new Promise((resolve) => {
|
93
|
+
// Check if all images are loaded
|
94
|
+
const images = Array.from(document.querySelectorAll('img'));
|
95
|
+
const imagePromises = images.map(img => {
|
96
|
+
if (img.complete) return Promise.resolve();
|
97
|
+
return new Promise(res => {
|
98
|
+
img.addEventListener('load', res);
|
99
|
+
img.addEventListener('error', res);
|
100
|
+
});
|
101
|
+
});
|
102
|
+
|
103
|
+
// Check document fonts
|
104
|
+
const fontPromise = document.fonts?.ready || Promise.resolve();
|
105
|
+
|
106
|
+
// Wait for everything
|
107
|
+
Promise.all([...imagePromises, fontPromise]).then(() => {
|
108
|
+
// Small delay to ensure rendering is complete
|
109
|
+
requestAnimationFrame(() => {
|
110
|
+
setTimeout(resolve, 50);
|
111
|
+
});
|
112
|
+
});
|
113
|
+
});
|
114
|
+
JS
|
115
|
+
)
|
91
116
|
|
92
117
|
# Log page readiness
|
93
118
|
if debug
|
@@ -108,6 +133,21 @@ module SocialConstruct
|
|
108
133
|
log_debug("Body HTML length: #{body_html_length}")
|
109
134
|
end
|
110
135
|
|
136
|
+
# Ensure content is painted before screenshot
|
137
|
+
browser.execute(
|
138
|
+
<<~JS
|
139
|
+
// Force layout and paint
|
140
|
+
document.body.offsetHeight;
|
141
|
+
// Check if we have visible content
|
142
|
+
const hasContent = document.body.textContent.trim().length > 0 ||
|
143
|
+
document.querySelectorAll('img').length > 0;
|
144
|
+
if (!hasContent) {
|
145
|
+
console.warn('Page appears to have no visible content');
|
146
|
+
}
|
147
|
+
return hasContent;
|
148
|
+
JS
|
149
|
+
)
|
150
|
+
|
111
151
|
screenshot = browser.screenshot(
|
112
152
|
encoding: :binary,
|
113
153
|
quality: 100,
|
@@ -116,27 +156,6 @@ module SocialConstruct
|
|
116
156
|
|
117
157
|
log_debug("Screenshot generated, size: #{screenshot.bytesize} bytes")
|
118
158
|
|
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
159
|
screenshot
|
141
160
|
rescue => e
|
142
161
|
log_debug("Ferrum screenshot failed: #{e.message}", :error)
|
@@ -177,7 +196,7 @@ module SocialConstruct
|
|
177
196
|
{}
|
178
197
|
end
|
179
198
|
|
180
|
-
def
|
199
|
+
def attachment_data_url(attachment, variant_options = {})
|
181
200
|
return nil unless attachment.attached?
|
182
201
|
|
183
202
|
begin
|
@@ -191,14 +210,93 @@ module SocialConstruct
|
|
191
210
|
|
192
211
|
content_type = blob.content_type || "image/jpeg"
|
193
212
|
image_data = blob.download
|
213
|
+
file_size = image_data.bytesize
|
214
|
+
|
215
|
+
# Check image size limitations
|
216
|
+
# 2MB hard limit
|
217
|
+
if file_size > 2_000_000
|
218
|
+
log_debug("Image is #{file_size} bytes (#{file_size / 1024 / 1024}MB), exceeds 2MB data URL limit", :error)
|
219
|
+
return nil
|
220
|
+
# 500KB warning threshold
|
221
|
+
elsif file_size > 500_000
|
222
|
+
log_debug(
|
223
|
+
"Image is #{file_size} bytes (#{file_size / 1024}KB), consider optimizing for better performance",
|
224
|
+
:warn
|
225
|
+
)
|
226
|
+
end
|
194
227
|
|
195
|
-
|
228
|
+
encoded_data = Base64.strict_encode64(image_data)
|
229
|
+
log_debug("Image loaded: #{file_size} bytes (#{encoded_data.bytesize} bytes encoded)")
|
230
|
+
|
231
|
+
"data:#{content_type};base64,#{encoded_data}"
|
196
232
|
rescue => e
|
197
233
|
log_debug("Failed to convert image to data URL: #{e.message}", :error)
|
198
234
|
nil
|
199
235
|
end
|
200
236
|
end
|
201
237
|
|
238
|
+
# Local font helper - converts font file to data URL
|
239
|
+
def font_to_data_url(font_path)
|
240
|
+
full_path = if font_path.start_with?("/")
|
241
|
+
font_path
|
242
|
+
else
|
243
|
+
Rails.root.join("app", "assets", "fonts", font_path)
|
244
|
+
end
|
245
|
+
|
246
|
+
return nil unless File.exist?(full_path)
|
247
|
+
|
248
|
+
begin
|
249
|
+
font_data = File.read(full_path)
|
250
|
+
file_size = font_data.bytesize
|
251
|
+
|
252
|
+
# Check file size limitations
|
253
|
+
# Most browsers have data URL limits around 2MB, but performance degrades after ~500KB
|
254
|
+
# 2MB hard limit
|
255
|
+
if file_size > 2_000_000
|
256
|
+
log_debug(
|
257
|
+
"Font file #{font_path} is #{file_size} bytes (#{file_size / 1024 / 1024}MB), exceeds 2MB data URL limit",
|
258
|
+
:error
|
259
|
+
)
|
260
|
+
return nil
|
261
|
+
# 500KB warning threshold
|
262
|
+
elsif file_size > 500_000
|
263
|
+
log_debug(
|
264
|
+
"Font file #{font_path} is #{file_size} bytes (#{file_size / 1024}KB), consider optimizing for better performance",
|
265
|
+
:warn
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
content_type = font_content_type(full_path)
|
270
|
+
encoded_data = Base64.strict_encode64(font_data)
|
271
|
+
|
272
|
+
# Base64 encoding increases size by ~33%
|
273
|
+
encoded_size = encoded_data.bytesize
|
274
|
+
log_debug("Font #{font_path} loaded: #{file_size} bytes (#{encoded_size} bytes encoded)")
|
275
|
+
|
276
|
+
"data:#{content_type};base64,#{encoded_data}"
|
277
|
+
rescue => e
|
278
|
+
log_debug("Failed to convert font to data URL: #{e.message}", :error)
|
279
|
+
nil
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Generate @font-face declaration for local fonts
|
284
|
+
def generate_font_face(family_name, font_path, weight: "normal", style: "normal", display: "swap")
|
285
|
+
data_url = font_to_data_url(font_path)
|
286
|
+
return "" unless data_url
|
287
|
+
|
288
|
+
<<~CSS
|
289
|
+
@font-face {
|
290
|
+
font-family: '#{family_name}';
|
291
|
+
src: url('#{data_url}');
|
292
|
+
font-weight: #{weight};
|
293
|
+
font-style: #{style};
|
294
|
+
font-display: #{display};
|
295
|
+
}
|
296
|
+
CSS
|
297
|
+
.html_safe
|
298
|
+
end
|
299
|
+
|
202
300
|
def template_path
|
203
301
|
Rails.application.config.social_construct.template_path
|
204
302
|
end
|
@@ -207,5 +305,84 @@ module SocialConstruct
|
|
207
305
|
return unless debug
|
208
306
|
Rails.logger.send(level, "[SocialCard] #{message}")
|
209
307
|
end
|
308
|
+
|
309
|
+
# Local image helper - converts image file to data URL
|
310
|
+
def image_data_url(image_path)
|
311
|
+
full_path = if image_path.start_with?("/")
|
312
|
+
image_path
|
313
|
+
else
|
314
|
+
Rails.root.join("app", "assets", "images", image_path)
|
315
|
+
end
|
316
|
+
|
317
|
+
return nil unless File.exist?(full_path)
|
318
|
+
|
319
|
+
begin
|
320
|
+
image_data = File.read(full_path)
|
321
|
+
file_size = image_data.bytesize
|
322
|
+
|
323
|
+
# Check file size limitations
|
324
|
+
if file_size > 2_000_000
|
325
|
+
log_debug(
|
326
|
+
"Image file #{image_path} is #{file_size} bytes (#{file_size / 1024 / 1024}MB), exceeds 2MB data URL limit",
|
327
|
+
:error
|
328
|
+
)
|
329
|
+
return nil
|
330
|
+
elsif file_size > 500_000
|
331
|
+
log_debug(
|
332
|
+
"Image file #{image_path} is #{file_size} bytes (#{file_size / 1024}KB), consider optimizing for better performance",
|
333
|
+
:warn
|
334
|
+
)
|
335
|
+
end
|
336
|
+
|
337
|
+
content_type = image_content_type(full_path)
|
338
|
+
encoded_data = Base64.strict_encode64(image_data)
|
339
|
+
log_debug("Image #{image_path} loaded: #{file_size} bytes (#{encoded_data.bytesize} bytes encoded)")
|
340
|
+
|
341
|
+
"data:#{content_type};base64,#{encoded_data}"
|
342
|
+
rescue => e
|
343
|
+
log_debug("Failed to convert image to data URL: #{e.message}", :error)
|
344
|
+
nil
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Determine MIME type for image files
|
349
|
+
def image_content_type(image_path)
|
350
|
+
extension = File.extname(image_path).downcase
|
351
|
+
case extension
|
352
|
+
when ".png"
|
353
|
+
"image/png"
|
354
|
+
when ".jpg", ".jpeg"
|
355
|
+
"image/jpeg"
|
356
|
+
when ".gif"
|
357
|
+
"image/gif"
|
358
|
+
when ".svg"
|
359
|
+
"image/svg+xml"
|
360
|
+
when ".webp"
|
361
|
+
"image/webp"
|
362
|
+
else
|
363
|
+
# fallback
|
364
|
+
"image/png"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Determine MIME type for font files
|
369
|
+
def font_content_type(font_path)
|
370
|
+
extension = File.extname(font_path).downcase
|
371
|
+
case extension
|
372
|
+
when ".woff2"
|
373
|
+
"font/woff2"
|
374
|
+
when ".woff"
|
375
|
+
"font/woff"
|
376
|
+
when ".ttf"
|
377
|
+
"font/truetype"
|
378
|
+
when ".otf"
|
379
|
+
"font/opentype"
|
380
|
+
when ".eot"
|
381
|
+
"application/vnd.ms-fontobject"
|
382
|
+
else
|
383
|
+
# fallback
|
384
|
+
"font/truetype"
|
385
|
+
end
|
386
|
+
end
|
210
387
|
end
|
211
388
|
end
|
data/config/routes.rb
CHANGED
@@ -28,7 +28,7 @@ module SocialConstruct
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def create_example_preview
|
31
|
-
template("example_social_card_preview.rb", "
|
31
|
+
template("example_social_card_preview.rb", "test/social_cards/previews/example_social_card_preview.rb")
|
32
32
|
end
|
33
33
|
|
34
34
|
def add_route
|
@@ -42,10 +42,6 @@ module SocialConstruct
|
|
42
42
|
|
43
43
|
route(route_string)
|
44
44
|
end
|
45
|
-
|
46
|
-
def display_post_install
|
47
|
-
readme("POST_INSTALL") if behavior == :invoke
|
48
|
-
end
|
49
45
|
end
|
50
46
|
end
|
51
47
|
end
|
@@ -1,7 +1,7 @@
|
|
1
|
-
|
1
|
+
<%% content_for :head do %>
|
2
2
|
<style>
|
3
3
|
body {
|
4
|
-
background-color:
|
4
|
+
background-color: <%%= background_color %>;
|
5
5
|
display: flex;
|
6
6
|
align-items: center;
|
7
7
|
justify-content: center;
|
@@ -9,11 +9,11 @@
|
|
9
9
|
text-align: center;
|
10
10
|
padding: 60px;
|
11
11
|
}
|
12
|
-
|
12
|
+
|
13
13
|
.content {
|
14
14
|
max-width: 900px;
|
15
15
|
}
|
16
|
-
|
16
|
+
|
17
17
|
.title {
|
18
18
|
font-size: 72px;
|
19
19
|
font-weight: 800;
|
@@ -21,26 +21,26 @@
|
|
21
21
|
margin-bottom: 24px;
|
22
22
|
letter-spacing: -2px;
|
23
23
|
}
|
24
|
-
|
24
|
+
|
25
25
|
.subtitle {
|
26
26
|
font-size: 32px;
|
27
27
|
font-weight: 400;
|
28
28
|
opacity: 0.8;
|
29
29
|
line-height: 1.3;
|
30
30
|
}
|
31
|
-
|
31
|
+
|
32
32
|
.logo {
|
33
33
|
position: absolute;
|
34
34
|
bottom: 60px;
|
35
35
|
right: 60px;
|
36
36
|
}
|
37
|
-
|
37
|
+
|
38
38
|
.logo img {
|
39
39
|
height: 40px;
|
40
40
|
width: auto;
|
41
41
|
opacity: 0.9;
|
42
42
|
}
|
43
|
-
|
43
|
+
|
44
44
|
.decoration {
|
45
45
|
position: absolute;
|
46
46
|
top: 0;
|
@@ -51,19 +51,13 @@
|
|
51
51
|
pointer-events: none;
|
52
52
|
}
|
53
53
|
</style>
|
54
|
-
|
54
|
+
<%% end %>
|
55
55
|
|
56
56
|
<div class="decoration"></div>
|
57
57
|
|
58
58
|
<div class="content">
|
59
|
-
<h1 class="title"
|
60
|
-
|
61
|
-
<p class="subtitle"
|
62
|
-
|
59
|
+
<h1 class="title"><%%= title %></h1>
|
60
|
+
<%% if subtitle.present? %>
|
61
|
+
<p class="subtitle"><%%= subtitle %></p>
|
62
|
+
<%% end %>
|
63
63
|
</div>
|
64
|
-
|
65
|
-
<% if logo_data_url %>
|
66
|
-
<div class="logo">
|
67
|
-
<img src="<%= logo_data_url %>" alt="Logo">
|
68
|
-
</div>
|
69
|
-
<% end %>
|
@@ -1,6 +1,5 @@
|
|
1
1
|
class ExampleSocialCard < ApplicationSocialCard
|
2
2
|
def initialize(title: "Hello World", subtitle: nil, background_color: "#1a1a1a")
|
3
|
-
super()
|
4
3
|
@title = title
|
5
4
|
@subtitle = subtitle
|
6
5
|
@background_color = background_color
|
@@ -12,8 +11,7 @@ class ExampleSocialCard < ApplicationSocialCard
|
|
12
11
|
{
|
13
12
|
title: @title,
|
14
13
|
subtitle: @subtitle,
|
15
|
-
background_color: @background_color
|
16
|
-
logo_data_url: logo_data_url
|
14
|
+
background_color: @background_color
|
17
15
|
}
|
18
16
|
end
|
19
17
|
end
|
@@ -5,33 +5,4 @@ class ExampleSocialCardPreview
|
|
5
5
|
subtitle: "Beautiful social cards for your Rails app"
|
6
6
|
)
|
7
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
8
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
|
-
<html lang="
|
2
|
+
<html lang="<%%= I18n.locale %>">
|
3
3
|
<head>
|
4
4
|
<meta charset="utf-8">
|
5
5
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
@@ -10,13 +10,13 @@
|
|
10
10
|
padding: 0;
|
11
11
|
box-sizing: border-box;
|
12
12
|
}
|
13
|
-
|
13
|
+
|
14
14
|
html {
|
15
15
|
width: 1200px;
|
16
16
|
height: 630px;
|
17
17
|
background: white;
|
18
18
|
}
|
19
|
-
|
19
|
+
|
20
20
|
body {
|
21
21
|
width: 1200px;
|
22
22
|
height: 630px;
|
@@ -28,22 +28,23 @@
|
|
28
28
|
-webkit-font-smoothing: antialiased;
|
29
29
|
-moz-osx-font-smoothing: grayscale;
|
30
30
|
}
|
31
|
-
|
31
|
+
|
32
32
|
/* Ensure images don't have borders */
|
33
33
|
img {
|
34
34
|
border: 0;
|
35
35
|
max-width: 100%;
|
36
36
|
}
|
37
|
-
|
37
|
+
|
38
38
|
/* Default text rendering */
|
39
39
|
h1, h2, h3, h4, h5, h6, p {
|
40
40
|
font-weight: normal;
|
41
41
|
margin: 0;
|
42
42
|
}
|
43
43
|
</style>
|
44
|
-
|
44
|
+
<%%= yield :head %>
|
45
45
|
</head>
|
46
46
|
<body>
|
47
|
-
|
47
|
+
<%%= yield %>
|
48
48
|
</body>
|
49
|
-
</html>
|
49
|
+
</html>
|
50
|
+
|
@@ -1,9 +1,16 @@
|
|
1
|
-
# SocialConstruct configuration
|
2
1
|
Rails.application.configure do
|
3
2
|
# Configure the template path for social card views
|
4
3
|
# Default: "social_cards"
|
5
4
|
# config.social_construct.template_path = "social_cards"
|
6
5
|
|
6
|
+
# Configure paths for social card previews
|
7
|
+
# Default: ["test/social_cards/previews"]
|
8
|
+
# config.social_construct.preview_paths = ["test/social_cards/previews"]
|
9
|
+
|
10
|
+
# Enable social card previews
|
11
|
+
# Default: true in development, false otherwise
|
12
|
+
# config.social_construct.show_previews = Rails.env.development?
|
13
|
+
|
7
14
|
# Enable debug logging for social card generation
|
8
15
|
# Default: false
|
9
16
|
# SocialConstruct::BaseCard.debug = true
|
@@ -4,11 +4,14 @@ module SocialConstruct
|
|
4
4
|
|
5
5
|
config.social_construct = ActiveSupport::OrderedOptions.new
|
6
6
|
config.social_construct.template_path = "social_cards"
|
7
|
+
config.social_construct.preview_paths = []
|
8
|
+
config.social_construct.show_previews = Rails.env.development?
|
7
9
|
|
8
|
-
#
|
9
|
-
config.
|
10
|
-
|
11
|
-
|
10
|
+
# Set default preview path after application initializes
|
11
|
+
config.after_initialize do |app|
|
12
|
+
if app.config.social_construct.preview_paths.empty?
|
13
|
+
app.config.social_construct.preview_paths = [Rails.root.join("test/social_cards/previews")]
|
14
|
+
end
|
12
15
|
end
|
13
16
|
end
|
14
17
|
end
|
data/lib/social_construct.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: social_construct
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mikkel Malmberg
|
@@ -68,7 +68,6 @@ files:
|
|
68
68
|
- app/views/social_construct/previews/show.html.erb
|
69
69
|
- config/routes.rb
|
70
70
|
- lib/generators/social_construct/install/install_generator.rb
|
71
|
-
- lib/generators/social_construct/install/templates/POST_INSTALL
|
72
71
|
- lib/generators/social_construct/install/templates/application_social_card.rb
|
73
72
|
- lib/generators/social_construct/install/templates/example_social_card.html.erb
|
74
73
|
- lib/generators/social_construct/install/templates/example_social_card.rb
|
@@ -78,12 +77,12 @@ files:
|
|
78
77
|
- lib/social_construct.rb
|
79
78
|
- lib/social_construct/engine.rb
|
80
79
|
- lib/social_construct/version.rb
|
81
|
-
homepage: https://github.com/
|
80
|
+
homepage: https://github.com/mikker/social_construct
|
82
81
|
licenses: []
|
83
82
|
metadata:
|
84
|
-
homepage_uri: https://github.com/
|
85
|
-
source_code_uri: https://github.com/
|
86
|
-
changelog_uri: https://github.com/
|
83
|
+
homepage_uri: https://github.com/mikker/social_construct
|
84
|
+
source_code_uri: https://github.com/mikker/social_construct
|
85
|
+
changelog_uri: https://github.com/mikker/social_construct/blob/main/CHANGELOG.md
|
87
86
|
rdoc_options: []
|
88
87
|
require_paths:
|
89
88
|
- lib
|
@@ -1,34 +0,0 @@
|
|
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
|
-
===============================================================================
|