Skip to content

Commit c277589

Browse files
authored
Merge pull request #91 from newstler/feature/multitenancy
Add multitenancy, billing, MCP tools, and admin panel
2 parents b87cc5d + 399f0bc commit c277589

File tree

475 files changed

+28413
-1007
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

475 files changed

+28413
-1007
lines changed

.claude/agents/code-reviewer.md

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
---
2+
name: og-image
3+
description: Generate social media preview images (Open Graph) for Rails apps. Creates an OG image using Ruby image libraries and configures meta tags in the layout head.
4+
---
5+
6+
This skill creates Open Graph images for social media sharing in Rails applications. It generates an image file and adds the necessary meta tags to the layout.
7+
8+
## Workflow
9+
10+
### Phase 1: Codebase Analysis
11+
12+
Explore the project to understand:
13+
14+
1. **Design System Discovery (Tailwind 4)**
15+
- Check CSS config in `app/assets/stylesheets/application.css`:
16+
```css
17+
@import "tailwindcss";
18+
19+
@theme {
20+
--color-primary: oklch(0.7 0.15 250);
21+
--color-background: oklch(0.15 0.02 260);
22+
--font-display: "Inter", sans-serif;
23+
}
24+
```
25+
- Find color tokens and fonts from `@theme` block
26+
27+
2. **Branding Assets**
28+
- Find logo in `app/assets/images/` or `public/`
29+
- Check for favicon in `public/`
30+
31+
3. **Product Information**
32+
- Extract product name from landing page
33+
- Find tagline/description
34+
35+
### Phase 2: Generate OG Image
36+
37+
Generate `public/og-image.png` (1200×630px) using one of these approaches:
38+
39+
**Option A: Using ruby-vips (already in Rails 7+)**
40+
41+
```ruby
42+
# lib/tasks/og_image.rake
43+
namespace :og do
44+
desc "Generate OG image"
45+
task generate: :environment do
46+
require "vips"
47+
48+
width = 1200
49+
height = 630
50+
51+
# Create background with gradient
52+
background = Vips::Image.black(width, height).add([30, 41, 59]) # slate-800
53+
54+
# Add text
55+
title = Vips::Image.text(
56+
"Product Name",
57+
font: "Inter Bold 72",
58+
width: width - 200
59+
).gravity("centre", width, 200)
60+
61+
tagline = Vips::Image.text(
62+
"Your tagline here",
63+
font: "Inter 36",
64+
width: width - 200
65+
).gravity("centre", width, 100)
66+
67+
# Composite layers
68+
result = background
69+
.composite(title.add([255, 255, 255]), :over, x: 0, y: 200)
70+
.composite(tagline.add([148, 163, 184]), :over, x: 0, y: 350)
71+
72+
result.write_to_file(Rails.root.join("public/og-image.png").to_s)
73+
puts "✓ Generated public/og-image.png"
74+
end
75+
end
76+
```
77+
78+
**Option B: Using MiniMagick**
79+
80+
```ruby
81+
# Gemfile
82+
gem "mini_magick"
83+
84+
# lib/tasks/og_image.rake
85+
namespace :og do
86+
desc "Generate OG image"
87+
task generate: :environment do
88+
require "mini_magick"
89+
90+
MiniMagick::Tool::Convert.new do |img|
91+
img.size "1200x630"
92+
img << "xc:#1e293b" # slate-800 background
93+
img.gravity "center"
94+
img.font "Inter-Bold"
95+
img.pointsize 72
96+
img.fill "white"
97+
img.annotate "+0-50", "Product Name"
98+
img.font "Inter-Regular"
99+
img.pointsize 36
100+
img.fill "#94a3b8" # slate-400
101+
img.annotate "+0+50", "Your tagline here"
102+
img << Rails.root.join("public/og-image.png").to_s
103+
end
104+
105+
puts "✓ Generated public/og-image.png"
106+
end
107+
end
108+
```
109+
110+
**Option C: Playwright MCP (for complex designs)**
111+
112+
For complex designs, create temp HTML and screenshot:
113+
114+
```ruby
115+
# lib/tasks/og_image.rake
116+
namespace :og do
117+
desc "Generate OG image HTML for Playwright screenshot"
118+
task html: :environment do
119+
html_path = Rails.root.join("tmp/og-image.html")
120+
121+
File.write(html_path, <<~HTML)
122+
<!DOCTYPE html>
123+
<html>
124+
<head>
125+
<style>
126+
* { margin: 0; padding: 0; box-sizing: border-box; }
127+
body {
128+
width: 1200px;
129+
height: 630px;
130+
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
131+
display: flex;
132+
flex-direction: column;
133+
align-items: center;
134+
justify-content: center;
135+
font-family: system-ui, sans-serif;
136+
}
137+
h1 { color: white; font-size: 72px; font-weight: 700; }
138+
p { color: #94a3b8; font-size: 36px; margin-top: 24px; }
139+
.domain { position: absolute; bottom: 40px; color: #64748b; }
140+
</style>
141+
</head>
142+
<body>
143+
<h1>Product Name</h1>
144+
<p>Your tagline here</p>
145+
<div class="domain">yourproduct.com</div>
146+
</body>
147+
</html>
148+
HTML
149+
150+
puts "Use Playwright MCP:"
151+
puts " browser_navigate file://#{html_path}"
152+
puts " browser_resize 1200x630"
153+
puts " browser_screenshot → public/og-image.png"
154+
end
155+
end
156+
```
157+
158+
### Phase 3: Add Meta Tags to Layout
159+
160+
**Create helper** (`app/helpers/meta_tags_helper.rb`):
161+
162+
```ruby
163+
module MetaTagsHelper
164+
def meta_tags(options = {})
165+
defaults = {
166+
title: "Product Name",
167+
description: "Your product description for social sharing",
168+
image: og_image_url,
169+
url: request.original_url,
170+
twitter_handle: nil
171+
}
172+
tags = defaults.merge(options)
173+
174+
safe_join([
175+
tag.meta(name: "description", content: tags[:description]),
176+
tag.meta(name: "theme-color", content: "#1e293b"),
177+
178+
# Open Graph
179+
tag.meta(property: "og:type", content: "website"),
180+
tag.meta(property: "og:title", content: tags[:title]),
181+
tag.meta(property: "og:description", content: tags[:description]),
182+
tag.meta(property: "og:url", content: tags[:url]),
183+
tag.meta(property: "og:image", content: tags[:image]),
184+
tag.meta(property: "og:image:width", content: "1200"),
185+
tag.meta(property: "og:image:height", content: "630"),
186+
187+
# Twitter/X
188+
tag.meta(name: "twitter:card", content: "summary_large_image"),
189+
tag.meta(name: "twitter:title", content: tags[:title]),
190+
tag.meta(name: "twitter:description", content: tags[:description]),
191+
tag.meta(name: "twitter:image", content: tags[:image]),
192+
tags[:twitter_handle] ? tag.meta(name: "twitter:site", content: tags[:twitter_handle]) : nil
193+
].compact, "\n")
194+
end
195+
196+
private
197+
198+
def og_image_url
199+
host = Rails.application.config.action_mailer.default_url_options&.dig(:host) || "localhost:3000"
200+
protocol = Rails.env.production? ? "https" : "http"
201+
"#{protocol}://#{host}/og-image.png"
202+
end
203+
end
204+
```
205+
206+
**Update layout** (`app/views/layouts/application.html.erb`):
207+
208+
```erb
209+
<!DOCTYPE html>
210+
<html>
211+
<head>
212+
<title><%= content_for(:title) || "Product Name" %></title>
213+
<meta name="viewport" content="width=device-width,initial-scale=1">
214+
<%= csrf_meta_tags %>
215+
<%= csp_meta_tag %>
216+
217+
<%= meta_tags(content_for(:meta_tags) || {}) %>
218+
219+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
220+
<%= javascript_importmap_tags %>
221+
</head>
222+
<body>
223+
<%= yield %>
224+
</body>
225+
</html>
226+
```
227+
228+
**Per-page custom meta** (optional):
229+
230+
```erb
231+
<% content_for :meta_tags do %>
232+
<%= meta_tags(
233+
title: "Custom Page Title",
234+
description: "Custom description"
235+
) %>
236+
<% end %>
237+
```
238+
239+
### Phase 4: Verification
240+
241+
```bash
242+
# Generate image
243+
bin/rails og:generate
244+
245+
# Verify
246+
ls -la public/og-image.png
247+
248+
# Test validators
249+
# - Facebook: https://developers.facebook.com/tools/debug/
250+
# - Twitter: https://cards-dev.twitter.com/validator
251+
# - LinkedIn: https://www.linkedin.com/post-inspector/
252+
```
253+
254+
## Files Created
255+
256+
```
257+
lib/tasks/og_image.rake # Rake task to generate image
258+
app/helpers/meta_tags_helper.rb # Meta tag helper
259+
public/og-image.png # Generated image (1200×630)
260+
```
261+
262+
## Quality Checklist
263+
264+
- [ ] Image is 1200×630 pixels
265+
- [ ] Image saved to `public/og-image.png`
266+
- [ ] `meta_tags` helper in layout `<head>`
267+
- [ ] Production URL configured for absolute image path
268+
- [ ] Tested with social media validators

0 commit comments

Comments
 (0)