Skip to content

Commit d508821

Browse files
newstlerclaude
andauthored
Fix testimonial headings, category links on community domain (#139)
* Fix testimonial heading collisions crashing generation job Remove hard uniqueness constraint on heading (DB index + model validation) to prevent crashes when AI generates duplicate headings. Instead, deduplicate at display time — homepage selects one testimonial per unique heading via GROUP BY subquery. Improve generation prompt: allow 1-3 word headings with richer examples, raise temperature to 0.8, increase retries from 3 to 5, and gracefully save on exhausted retries instead of crashing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix homepage testimonials to show random selection per unique heading The previous MIN(id) approach always picked the same testimonial per heading. Use a window function with RANDOM() ordering so each page load gets a truly random testimonial per unique heading, all in a single query. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove NOT NULL constraint from users.email column GitHub users may not have a public email, causing failures when syncing GitHub data. Allow email to be nullable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b0f828e commit d508821

9 files changed

Lines changed: 43 additions & 21 deletions

app/controllers/home_controller.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ def index
1313
.published
1414
.includes(:user)
1515
.order(created_at: :desc)
16-
@testimonials = Testimonial.published.includes(:user).order(Arel.sql("RANDOM()")).limit(20)
16+
subquery = Testimonial.published
17+
.select("testimonials.*", "ROW_NUMBER() OVER (PARTITION BY LOWER(heading) ORDER BY RANDOM()) AS rn")
18+
@testimonials = Testimonial
19+
.from(subquery, :testimonials)
20+
.where("rn = 1")
21+
.order(Arel.sql("RANDOM()"))
22+
.limit(10)
23+
.includes(:user)
1724
end
1825
end

app/jobs/generate_testimonial_fields_job.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
class GenerateTestimonialFieldsJob < ApplicationJob
22
queue_as :default
33

4-
MAX_HEADING_RETRIES = 3
4+
MAX_HEADING_RETRIES = 5
55

66
def perform(testimonial)
77
existing_headings = Testimonial.where.not(id: testimonial.id).where.not(heading: nil).pluck(:heading)
@@ -41,6 +41,10 @@ def perform(testimonial)
4141
return
4242
end
4343

44+
if heading_taken?(parsed["heading"], testimonial.id)
45+
Rails.logger.warn "Testimonial #{testimonial.id}: heading '#{parsed["heading"]}' still collides after #{MAX_HEADING_RETRIES} retries, saving anyway"
46+
end
47+
4448
testimonial.update!(
4549
heading: parsed["heading"],
4650
subheading: parsed["subheading"],
@@ -66,10 +70,13 @@ def build_system_prompt(existing_headings)
6670
You generate structured testimonial content for a Ruby programming language advocacy site.
6771
Given a user's quote about why they love Ruby, generate:
6872
69-
1. heading: A single unique 1-2 word heading that captures the THEME of the quote (e.g., "Elegance", "Joy", "Craft").
73+
1. heading: A unique 1-3 word heading that captures the THEME or FEELING of the quote.
74+
Be creative and specific. Go beyond generic words. Think of evocative nouns, metaphors, compound phrases, or poetic concepts.
75+
The heading must make sense as an answer to "Why Ruby?" — e.g. "Why Ruby?" → "Flow State", "Clarity", "Pure Joy".
76+
Good examples: "Spark", "Flow State", "Quiet Power", "Warm Glow", "First Love", "Playground", "Second Nature", "Deep Roots", "Readable Code", "Clean Slate", "Smooth Sailing", "Expressiveness", "Old Friend", "Sharp Tools", "Creative Freedom", "Solid Ground", "Calm Waters", "Poetic Logic", "Builder's Joy", "Sweet Spot", "Hidden Gem", "Fresh Start", "True North", "Clarity", "Belonging", "Empowerment", "Momentum", "Simplicity", "Trust", "Confidence"
7077
#{taken}
7178
2. subheading: A short tagline under 10 words.
72-
3. body_text: 2-3 sentences that EXTEND and DEEPEN the user's idea — add new angles, examples, or implications.
79+
3. body_text: 2-3 sentences that EXTEND and DEEPEN the user's idea. Add new angles, examples, or implications.
7380
Do NOT repeat or paraphrase what the user already said. Build on top of it.
7481
7582
WRITING STYLE — sound like a real person, not an AI:
@@ -136,7 +143,7 @@ def generate_with_anthropic(system_prompt, user_prompt)
136143
parameters: {
137144
model: "claude-3-haiku-20240307",
138145
max_tokens: 300,
139-
temperature: 0.7,
146+
temperature: 0.8,
140147
system: system_prompt,
141148
messages: [ { role: "user", content: user_prompt } ]
142149
}
@@ -161,7 +168,7 @@ def generate_with_openai(system_prompt, user_prompt)
161168
{ role: "system", content: system_prompt },
162169
{ role: "user", content: user_prompt }
163170
],
164-
temperature: 0.7,
171+
temperature: 0.8,
165172
max_tokens: 300
166173
}
167174
)

app/jobs/validate_testimonial_job.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ def perform(testimonial)
2121
VALIDATION RULES:
2222
1. First check the user's QUOTE against the content policy. If it violates (including being negative about Ruby), reject immediately with reject_reason "quote".
2323
2. If the quote is fine, check the AI-generated fields (heading/subheading/body). ONLY reject generation if there is a CLEAR problem:
24-
- The heading duplicates an existing one listed below
2524
- The body contradicts or misrepresents the quote
2625
- The subheading is nonsensical or unrelated
2726
- The content is factually wrong about Ruby
28-
Do NOT reject just because the fields could be "better" or "more creative". Good enough is good enough — publish it.
27+
Do NOT reject for duplicate headings (handled elsewhere). Do NOT reject just because the fields could be "better" or "more creative". Good enough is good enough — publish it.
2928
3. If everything looks acceptable, publish it.
3029
3130
AI-SOUNDING LANGUAGE CHECK:
@@ -37,7 +36,7 @@ def perform(testimonial)
3736
- Superficial -ing tack-ons ("ensuring...", "highlighting...", "fostering...")
3837
If the quote itself is fine but the generated text sounds like AI wrote it, set reject_reason to "generation" and explain which phrases sound artificial.
3938
40-
Existing published testimonials (avoid duplicate headings/themes):
39+
Existing published testimonials (for context):
4140
#{existing.presence || "None yet."}
4241
4342
Respond with valid JSON only: {"publish": true/false, "reject_reason": "quote" or "generation" or null, "feedback": "..."}

app/models/testimonial.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ class Testimonial < ApplicationRecord
33

44
validates :quote, length: { minimum: 140, maximum: 320 }, allow_blank: true
55
validates :user_id, uniqueness: true
6-
validates :heading, uniqueness: true, allow_nil: true
76

87
scope :published, -> { where(published: true) }
98
scope :ordered, -> { order(Arel.sql("position ASC NULLS LAST, created_at DESC")) }

app/views/layouts/application.html.erb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,10 @@
124124
<% else %>
125125
<% Category.with_posts.ordered.each do |category| %>
126126
<% if category.is_success_story? && has_success_stories? %>
127-
<%= link_to category.name, category_path(category),
127+
<%= link_to category.name, main_site_url(category_path(category)),
128128
class: "#{category_menu_active?(category) ? 'bg-red-100 text-red-700' : 'text-gray-600 hover:bg-gray-100'} px-3 py-2 rounded-md text-sm font-medium whitespace-nowrap" %>
129129
<% elsif !category.is_success_story? %>
130-
<%= link_to category.name, category_path(category),
130+
<%= link_to category.name, main_site_url(category_path(category)),
131131
class: "#{category_menu_active?(category) ? 'bg-red-100 text-red-700' : 'text-gray-600 hover:bg-gray-100'} px-3 py-2 rounded-md text-sm font-medium whitespace-nowrap" %>
132132
<% end %>
133133
<% end %>
@@ -202,10 +202,10 @@
202202
<% else %>
203203
<% Category.with_posts.ordered.limit(7).each do |category| %>
204204
<% if category.is_success_story? && has_success_stories? %>
205-
<%= link_to category.name, category_path(category),
205+
<%= link_to category.name, main_site_url(category_path(category)),
206206
class: "#{category_menu_active?(category) ? 'bg-red-100 text-red-700' : 'text-gray-600 hover:bg-gray-100'} block px-3 py-2 rounded-md text-base font-medium" %>
207207
<% elsif !category.is_success_story? %>
208-
<%= link_to category.name, category_path(category),
208+
<%= link_to category.name, main_site_url(category_path(category)),
209209
class: "#{category_menu_active?(category) ? 'bg-red-100 text-red-700' : 'text-gray-600 hover:bg-gray-100'} block px-3 py-2 rounded-md text-base font-medium" %>
210210
<% end %>
211211
<% end %>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class RemoveUniqueIndexFromTestimonialsHeading < ActiveRecord::Migration[8.2]
2+
def change
3+
remove_index :testimonials, :heading, unique: true
4+
add_index :testimonials, :heading
5+
end
6+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class RemoveEmailNotNullConstraintFromUsers < ActiveRecord::Migration[8.2]
2+
def change
3+
change_column_null :users, :email, true
4+
end
5+
end

db/schema.rb

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/models/testimonial_test.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,15 @@ class TestimonialTest < ActiveSupport::TestCase
2222
assert_includes duplicate.errors[:user_id], "has already been taken"
2323
end
2424

25-
test "validates uniqueness of heading allowing nil" do
25+
test "allows duplicate headings" do
2626
existing = testimonials(:published)
2727
other_user = users(:user_no_testimonial)
2828
new_testimonial = Testimonial.new(
2929
user: other_user,
30-
quote: "My quote",
30+
quote: "I love Ruby because it makes programming feel like poetry. The syntax reads so naturally that you can focus on solving problems instead of fighting the language. It truly is a joy.",
3131
heading: existing.heading
3232
)
33-
assert_not new_testimonial.valid?
34-
assert_includes new_testimonial.errors[:heading], "has already been taken"
33+
assert new_testimonial.valid?
3534
end
3635

3736
test "allows nil heading" do

0 commit comments

Comments
 (0)