2
0

refactor(pages): move AI prompts to plugin definitions and add JSON extraction

Simplify AI content generation by delegating to plugin system:
- Remove inline prompt instructions (now in LandingPageContentPlugin)
- Add extractJSON helper to handle markdown-wrapped responses
- Hardcode CTA URLs to repo URL instead of relying on AI
- Apply JSON extraction to both content generation and translation
- Reduces code duplication and improves maintainability
This commit is contained in:
2026-03-07 15:52:46 -05:00
parent fb89a8b55c
commit 85ab93145e

View File

@@ -71,45 +71,6 @@ func GenerateLandingPageContent(ctx context.Context, repo *repo_model.Repository
"stars": strconv.Itoa(repo.NumStars),
"forks": strconv.Itoa(repo.NumForks),
"readme": truncateReadme(readme),
"instruction": `You are a landing page copywriter. Analyze this open-source repository and generate compelling landing page content.
Return valid JSON with this exact structure:
{
"brand": {"name": "Project Name", "tagline": "Short tagline"},
"hero": {
"headline": "Compelling headline (max 10 words)",
"subheadline": "Supporting text explaining the value (1-2 sentences)",
"primary_cta": {"label": "Get Started", "url": "` + repoURL + `"},
"secondary_cta": {"label": "View on GitHub", "url": "` + repoURL + `"}
},
"stats": [
{"value": "...", "label": "..."}
],
"value_props": [
{"title": "...", "description": "...", "icon": "one of: zap,shield,rocket,check,star,heart,lock,globe,clock,gear,code,terminal,package,database,cloud,cpu,graph,people,tools,light-bulb"}
],
"features": [
{"title": "...", "description": "...", "icon": "same icon options as value_props"}
],
"cta_section": {
"headline": "Ready to get started?",
"subheadline": "...",
"button_label": "Get Started Free",
"button_url": "` + repoURL + `"
},
"seo": {
"title": "SEO title (50-60 chars)",
"description": "Meta description (150-160 chars)"
}
}
Guidelines:
- Generate 3-4 stats based on actual repo data (stars, forks, etc.) or compelling project metrics
- Generate exactly 3 value propositions that highlight the project's key strengths
- Generate 3-6 features based on what the README describes
- Use action-oriented, benefit-focused copy
- Keep it professional but engaging
- Icon choices should match the content semantically`,
},
})
if err != nil {
@@ -121,7 +82,7 @@ Guidelines:
}
var generated aiGeneratedConfig
if err := json.Unmarshal([]byte(resp.Result), &generated); err != nil {
if err := json.Unmarshal([]byte(extractJSON(resp.Result)), &generated); err != nil {
return nil, fmt.Errorf("failed to parse AI response: %w", err)
}
@@ -133,11 +94,11 @@ Guidelines:
config.Hero.Subheadline = generated.Hero.Subheadline
config.Hero.PrimaryCTA = pages_module.CTAButton{
Label: generated.Hero.PrimaryCTA.Label,
URL: generated.Hero.PrimaryCTA.URL,
URL: repoURL,
}
config.Hero.SecondaryCTA = pages_module.CTAButton{
Label: generated.Hero.SecondaryCTA.Label,
URL: generated.Hero.SecondaryCTA.URL,
URL: repoURL,
}
if len(generated.Stats) > 0 {
config.Stats = generated.Stats
@@ -153,7 +114,7 @@ Guidelines:
Subheadline: generated.CTASection.Subheadline,
Button: pages_module.CTAButton{
Label: generated.CTASection.ButtonLabel,
URL: generated.CTASection.ButtonURL,
URL: repoURL,
},
}
if generated.SEO.Title != "" {
@@ -174,6 +135,35 @@ func getPrimaryLanguageName(repo *repo_model.Repository) string {
return ""
}
// extractJSON extracts JSON from an AI response that may be wrapped in markdown code fences.
func extractJSON(s string) string {
s = strings.TrimSpace(s)
// Strip ```json ... ``` or ``` ... ``` wrapper
if strings.HasPrefix(s, "```") {
// Remove opening fence (```json or ```)
if idx := strings.Index(s, "\n"); idx != -1 {
s = s[idx+1:]
}
// Remove closing fence
if idx := strings.LastIndex(s, "```"); idx != -1 {
s = s[:idx]
}
s = strings.TrimSpace(s)
}
// If still not starting with {, try to find first { and last }
if !strings.HasPrefix(s, "{") {
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start != -1 && end != -1 && end > start {
s = s[start : end+1]
}
}
return s
}
// truncateReadme limits README content to avoid sending too much to the AI
func truncateReadme(readme string) string {
const maxLen = 4000
@@ -207,38 +197,6 @@ func TranslateLandingPageContent(ctx context.Context, repo *repo_model.Repositor
"target_language": langName,
"target_code": targetLang,
"source_content": sourceContent,
"instruction": `You are a professional translator. Translate the following landing page content to ` + langName + ` (` + targetLang + `).
Return valid JSON as a partial config overlay. Only include the fields that have translatable text content.
Do NOT translate URLs, brand names, or technical terms. Keep the same JSON structure.
Return this exact JSON structure (only include fields with actual translated text):
{
"hero": {
"headline": "translated headline",
"subheadline": "translated subheadline",
"primary_cta": {"label": "translated label"},
"secondary_cta": {"label": "translated label"}
},
"stats": [{"value": "keep original", "label": "translated label"}],
"value_props": [{"title": "translated", "description": "translated"}],
"features": [{"title": "translated", "description": "translated"}],
"cta_section": {
"headline": "translated",
"subheadline": "translated",
"button": {"label": "translated"}
},
"seo": {
"title": "translated",
"description": "translated"
}
}
Important:
- Maintain the exact same number of items in arrays (stats, value_props, features)
- Keep icon names, URLs, and image_urls unchanged
- Use natural, marketing-quality translations (not literal/robotic)
- Adapt idioms and expressions for the target culture`,
},
})
if err != nil {
@@ -250,12 +208,13 @@ Important:
}
// Validate it's valid JSON
result := extractJSON(resp.Result)
var check map[string]any
if err := json.Unmarshal([]byte(resp.Result), &check); err != nil {
if err := json.Unmarshal([]byte(result), &check); err != nil {
return "", fmt.Errorf("AI returned invalid JSON: %w", err)
}
return resp.Result, nil
return result, nil
}
// buildTranslatableContent extracts translatable text from a config for the AI