popeye-cli 1.4.7 → 1.6.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.
- package/CHANGELOG.md +54 -0
- package/README.md +264 -63
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +9 -4
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +1 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +9 -4
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +35 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +132 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +8 -2
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +37 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +64 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +407 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +8 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +8 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts +33 -0
- package/dist/generators/templates/website-components.d.ts.map +1 -0
- package/dist/generators/templates/website-components.js +303 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +55 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +425 -0
- package/dist/generators/templates/website-config.js.map +1 -0
- package/dist/generators/templates/website-conversion.d.ts +27 -0
- package/dist/generators/templates/website-conversion.d.ts.map +1 -0
- package/dist/generators/templates/website-conversion.js +326 -0
- package/dist/generators/templates/website-conversion.js.map +1 -0
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website-seo.d.ts +76 -0
- package/dist/generators/templates/website-seo.d.ts.map +1 -0
- package/dist/generators/templates/website-seo.js +326 -0
- package/dist/generators/templates/website-seo.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -83
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -875
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +119 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +350 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +5 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +136 -11
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +40 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +263 -0
- package/dist/types/website-strategy.d.ts.map +1 -0
- package/dist/types/website-strategy.js +105 -0
- package/dist/types/website-strategy.js.map +1 -0
- package/dist/types/workflow.d.ts +21 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +8 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +2 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +18 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +4 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +37 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts +89 -0
- package/dist/workflow/overview.d.ts.map +1 -0
- package/dist/workflow/overview.js +358 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +6 -4
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +148 -6
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +79 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +310 -0
- package/dist/workflow/website-strategy.js.map +1 -0
- package/dist/workflow/website-updater.d.ts +17 -0
- package/dist/workflow/website-updater.d.ts.map +1 -0
- package/dist/workflow/website-updater.js +116 -0
- package/dist/workflow/website-updater.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +10 -4
- package/src/adapters/grok.ts +10 -4
- package/src/adapters/openai.ts +38 -6
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +143 -7
- package/src/generators/all.ts +49 -332
- package/src/generators/doc-parser.ts +449 -0
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +8 -0
- package/src/generators/templates/website-components.ts +330 -0
- package/src/generators/templates/website-config.ts +444 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +38 -905
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +493 -0
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +178 -20
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +56 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +21 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +37 -0
- package/src/workflow/overview.ts +475 -0
- package/src/workflow/plan-mode.ts +193 -8
- package/src/workflow/website-strategy.ts +379 -0
- package/src/workflow/website-updater.ts +142 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +331 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/overview.test.ts +392 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- package/tests/workflow/website-strategy.test.ts +246 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable website section generators
|
|
3
|
+
* Pain Points, Value Proposition, How It Works, Stats/Proof Points, FAQ
|
|
4
|
+
* Each section is data-driven with graceful skip when data is missing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WebsiteContentContext } from '../website-context.js';
|
|
8
|
+
import type { WebsiteStrategyDocument } from '../../types/website-strategy.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Escape a string for safe use inside JSX template literals
|
|
12
|
+
*/
|
|
13
|
+
function escapeJsx(str: string): string {
|
|
14
|
+
return str
|
|
15
|
+
.replace(/\\/g, '\\\\')
|
|
16
|
+
.replace(/'/g, "\\'")
|
|
17
|
+
.replace(/`/g, '\\`')
|
|
18
|
+
.replace(/\$/g, '\\$');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Section render metadata for debug tracing
|
|
23
|
+
*/
|
|
24
|
+
export interface SectionRenderInfo {
|
|
25
|
+
name: string;
|
|
26
|
+
dataSource: 'strategy' | 'docs' | 'defaults' | 'skipped';
|
|
27
|
+
itemCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map a feature title to a lucide-react icon name by keyword matching
|
|
32
|
+
*/
|
|
33
|
+
export function mapFeatureIcon(title: string): string {
|
|
34
|
+
const lower = title.toLowerCase();
|
|
35
|
+
const iconMap: Array<[RegExp, string]> = [
|
|
36
|
+
[/secur|auth|permission|access|lock|encrypt/, 'Shield'],
|
|
37
|
+
[/speed|fast|perform|optim/, 'Zap'],
|
|
38
|
+
[/api|integrat|connect|plugin/, 'Plug'],
|
|
39
|
+
[/search|find|discover|retriev/, 'Search'],
|
|
40
|
+
[/analyt|metric|monitor|dashboard/, 'BarChart3'],
|
|
41
|
+
[/data|database|storage|vector/, 'Database'],
|
|
42
|
+
[/team|collaborat|share|user/, 'Users'],
|
|
43
|
+
[/automat|workflow|pipeline/, 'GitBranch'],
|
|
44
|
+
[/cloud|deploy|server|host/, 'Cloud'],
|
|
45
|
+
[/scale|grow|expand/, 'TrendingUp'],
|
|
46
|
+
[/custom|config|setting/, 'Settings'],
|
|
47
|
+
[/document|doc|file|content/, 'FileText'],
|
|
48
|
+
[/test|quality|check|verify/, 'CheckCircle'],
|
|
49
|
+
[/ai|machine|learn|model|neural/, 'Brain'],
|
|
50
|
+
[/code|develop|build|engineer/, 'Code'],
|
|
51
|
+
[/email|message|notif|alert/, 'Bell'],
|
|
52
|
+
[/time|schedule|calendar|clock/, 'Clock'],
|
|
53
|
+
[/money|pay|bill|cost|pric/, 'CreditCard'],
|
|
54
|
+
[/global|world|international/, 'Globe'],
|
|
55
|
+
[/support|help|service/, 'Headphones'],
|
|
56
|
+
];
|
|
57
|
+
for (const [pattern, icon] of iconMap) {
|
|
58
|
+
if (pattern.test(lower)) return icon;
|
|
59
|
+
}
|
|
60
|
+
return 'Star';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if a proof point string contains a numeric metric
|
|
65
|
+
* Numeric metrics are safe to display as stats; qualitative ones become badges
|
|
66
|
+
*/
|
|
67
|
+
export function isNumericMetric(point: string): boolean {
|
|
68
|
+
return /\d+[%+KMB]|\d{2,}/.test(point);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate Pain Points section
|
|
73
|
+
* Data: strategy.icp.painPoints
|
|
74
|
+
* Skip: if painPoints is empty
|
|
75
|
+
*/
|
|
76
|
+
export function generatePainPointsSection(
|
|
77
|
+
strategy?: WebsiteStrategyDocument
|
|
78
|
+
): { jsx: string; info: SectionRenderInfo } {
|
|
79
|
+
const painPoints = strategy?.icp.painPoints || [];
|
|
80
|
+
if (painPoints.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
jsx: '',
|
|
83
|
+
info: { name: 'PainPoints', dataSource: 'skipped', itemCount: 0 },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const items = painPoints.slice(0, 3);
|
|
88
|
+
const icons = ['AlertTriangle', 'XCircle', 'AlertOctagon'];
|
|
89
|
+
const itemsStr = items
|
|
90
|
+
.map(
|
|
91
|
+
(point, i) =>
|
|
92
|
+
` <div key="${i}" className="rounded-2xl bg-card p-8 text-center">
|
|
93
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
|
94
|
+
<${icons[i % icons.length]} className="h-6 w-6 text-red-600" />
|
|
95
|
+
</div>
|
|
96
|
+
<p className="text-foreground font-medium">${escapeJsx(point)}</p>
|
|
97
|
+
</div>`
|
|
98
|
+
)
|
|
99
|
+
.join('\n');
|
|
100
|
+
|
|
101
|
+
const jsx = `
|
|
102
|
+
{/* Pain Points */}
|
|
103
|
+
<section className="bg-muted/50 py-20 sm:py-28">
|
|
104
|
+
<div className="container">
|
|
105
|
+
<h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
106
|
+
Sound familiar?
|
|
107
|
+
</h2>
|
|
108
|
+
<p className="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">
|
|
109
|
+
Common challenges that hold teams back
|
|
110
|
+
</p>
|
|
111
|
+
<div className="mx-auto mt-12 grid max-w-5xl grid-cols-1 gap-8 md:grid-cols-3">
|
|
112
|
+
${itemsStr}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
jsx,
|
|
120
|
+
info: { name: 'PainPoints', dataSource: 'strategy', itemCount: items.length },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generate Value Proposition / Differentiators section
|
|
126
|
+
* Data: strategy.positioning.differentiators + valueProposition
|
|
127
|
+
* Skip: if no differentiators
|
|
128
|
+
*/
|
|
129
|
+
export function generateDifferentiatorsSection(
|
|
130
|
+
strategy?: WebsiteStrategyDocument
|
|
131
|
+
): { jsx: string; info: SectionRenderInfo } {
|
|
132
|
+
const differentiators = strategy?.positioning.differentiators || [];
|
|
133
|
+
const valueProp = strategy?.positioning.valueProposition;
|
|
134
|
+
if (differentiators.length === 0 && !valueProp) {
|
|
135
|
+
return {
|
|
136
|
+
jsx: '',
|
|
137
|
+
info: { name: 'Differentiators', dataSource: 'skipped', itemCount: 0 },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const heading = valueProp
|
|
142
|
+
? escapeJsx(valueProp)
|
|
143
|
+
: 'Why choose us';
|
|
144
|
+
|
|
145
|
+
const itemsStr = differentiators
|
|
146
|
+
.slice(0, 6)
|
|
147
|
+
.map(
|
|
148
|
+
(diff) =>
|
|
149
|
+
` <div className="flex items-start gap-3">
|
|
150
|
+
<CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-primary-600" />
|
|
151
|
+
<p className="text-foreground">${escapeJsx(diff)}</p>
|
|
152
|
+
</div>`
|
|
153
|
+
)
|
|
154
|
+
.join('\n');
|
|
155
|
+
|
|
156
|
+
const jsx = `
|
|
157
|
+
{/* Value Proposition */}
|
|
158
|
+
<section className="py-20 sm:py-28">
|
|
159
|
+
<div className="container">
|
|
160
|
+
<div className="mx-auto max-w-3xl text-center">
|
|
161
|
+
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
162
|
+
${heading}
|
|
163
|
+
</h2>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="mx-auto mt-12 grid max-w-3xl grid-cols-1 gap-6 sm:grid-cols-2">
|
|
166
|
+
${itemsStr}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</section>
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
jsx,
|
|
174
|
+
info: { name: 'Differentiators', dataSource: 'strategy', itemCount: differentiators.length },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generate How It Works section
|
|
180
|
+
* Data: strategy siteArchitecture.pages[0].sections or defaults
|
|
181
|
+
* Always rendered (with defaults if no strategy)
|
|
182
|
+
*/
|
|
183
|
+
export function generateHowItWorksSection(
|
|
184
|
+
strategy?: WebsiteStrategyDocument
|
|
185
|
+
): { jsx: string; info: SectionRenderInfo } {
|
|
186
|
+
const defaultSteps = [
|
|
187
|
+
{ title: 'Sign Up', description: 'Create your account in seconds' },
|
|
188
|
+
{ title: 'Configure', description: 'Set up your workspace to match your needs' },
|
|
189
|
+
{ title: 'Deploy', description: 'Go live and start seeing results' },
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const hasSections = strategy?.siteArchitecture.pages[0]?.sections;
|
|
193
|
+
const steps = hasSections && hasSections.length >= 3
|
|
194
|
+
? hasSections.slice(0, 3).map((s, i) => ({
|
|
195
|
+
title: s.replace(/^(hero|features|cta|pricing|faq|testimonials)/i, '').trim() || defaultSteps[i].title,
|
|
196
|
+
description: defaultSteps[i].description,
|
|
197
|
+
}))
|
|
198
|
+
: defaultSteps;
|
|
199
|
+
|
|
200
|
+
const dataSource = hasSections ? 'strategy' : 'defaults';
|
|
201
|
+
|
|
202
|
+
const stepsStr = steps
|
|
203
|
+
.map(
|
|
204
|
+
(step, i) =>
|
|
205
|
+
` <div className="relative text-center">
|
|
206
|
+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary-600 text-lg font-bold text-white">
|
|
207
|
+
${i + 1}
|
|
208
|
+
</div>
|
|
209
|
+
<h3 className="mt-4 text-lg font-semibold text-foreground">${escapeJsx(step.title)}</h3>
|
|
210
|
+
<p className="mt-2 text-muted-foreground">${escapeJsx(step.description)}</p>
|
|
211
|
+
</div>`
|
|
212
|
+
)
|
|
213
|
+
.join('\n');
|
|
214
|
+
|
|
215
|
+
const jsx = `
|
|
216
|
+
{/* How It Works */}
|
|
217
|
+
<section className="bg-muted/50 py-20 sm:py-28">
|
|
218
|
+
<div className="container">
|
|
219
|
+
<h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
220
|
+
How it works
|
|
221
|
+
</h2>
|
|
222
|
+
<div className="mx-auto mt-16 grid max-w-4xl grid-cols-1 gap-12 md:grid-cols-3">
|
|
223
|
+
${stepsStr}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</section>
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
jsx,
|
|
231
|
+
info: { name: 'HowItWorks', dataSource: dataSource as 'strategy' | 'defaults', itemCount: steps.length },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Generate Stats / Proof Points section
|
|
237
|
+
* CRITICAL: Only show numeric metrics if they appear literally in docs/strategy.
|
|
238
|
+
* Qualitative points render as badges, NOT fake numbers.
|
|
239
|
+
*/
|
|
240
|
+
export function generateStatsSection(
|
|
241
|
+
strategy?: WebsiteStrategyDocument
|
|
242
|
+
): { jsx: string; info: SectionRenderInfo } {
|
|
243
|
+
const proofPoints = strategy?.positioning.proofPoints || [];
|
|
244
|
+
if (proofPoints.length === 0) {
|
|
245
|
+
return {
|
|
246
|
+
jsx: '',
|
|
247
|
+
info: { name: 'Stats', dataSource: 'skipped', itemCount: 0 },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const numericPoints = proofPoints.filter(isNumericMetric);
|
|
252
|
+
const qualitativePoints = proofPoints.filter((p) => !isNumericMetric(p));
|
|
253
|
+
|
|
254
|
+
let statsContent = '';
|
|
255
|
+
|
|
256
|
+
if (numericPoints.length > 0) {
|
|
257
|
+
const statsStr = numericPoints
|
|
258
|
+
.slice(0, 4)
|
|
259
|
+
.map(
|
|
260
|
+
(point) =>
|
|
261
|
+
` <div className="text-center">
|
|
262
|
+
<p className="text-4xl font-bold text-primary-600">${escapeJsx(point)}</p>
|
|
263
|
+
</div>`
|
|
264
|
+
)
|
|
265
|
+
.join('\n');
|
|
266
|
+
|
|
267
|
+
statsContent += ` <div className="mx-auto mt-12 grid max-w-4xl grid-cols-2 gap-8 md:grid-cols-${Math.min(numericPoints.length, 4)}">
|
|
268
|
+
${statsStr}
|
|
269
|
+
</div>\n`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (qualitativePoints.length > 0) {
|
|
273
|
+
const badgesStr = qualitativePoints
|
|
274
|
+
.slice(0, 6)
|
|
275
|
+
.map(
|
|
276
|
+
(point) =>
|
|
277
|
+
` <span className="inline-flex items-center gap-1.5 rounded-full bg-primary-50 px-4 py-2 text-sm font-medium text-primary-700">
|
|
278
|
+
<CheckCircle className="h-4 w-4" />
|
|
279
|
+
${escapeJsx(point)}
|
|
280
|
+
</span>`
|
|
281
|
+
)
|
|
282
|
+
.join('\n');
|
|
283
|
+
|
|
284
|
+
statsContent += ` <div className="mx-auto mt-8 flex max-w-4xl flex-wrap items-center justify-center gap-3">
|
|
285
|
+
${badgesStr}
|
|
286
|
+
</div>\n`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const jsx = `
|
|
290
|
+
{/* Proof Points */}
|
|
291
|
+
<section className="py-20 sm:py-28">
|
|
292
|
+
<div className="container">
|
|
293
|
+
<h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
294
|
+
Built for production
|
|
295
|
+
</h2>
|
|
296
|
+
${statsContent}
|
|
297
|
+
</div>
|
|
298
|
+
</section>
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
jsx,
|
|
303
|
+
info: { name: 'Stats', dataSource: 'strategy', itemCount: proofPoints.length },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Generate Social Proof section
|
|
309
|
+
* Data: strategy.conversionStrategy.socialProof
|
|
310
|
+
* Skip: if empty
|
|
311
|
+
*/
|
|
312
|
+
export function generateSocialProofSection(
|
|
313
|
+
strategy?: WebsiteStrategyDocument
|
|
314
|
+
): { jsx: string; info: SectionRenderInfo } {
|
|
315
|
+
const socialProof = strategy?.conversionStrategy.socialProof || [];
|
|
316
|
+
if (socialProof.length === 0) {
|
|
317
|
+
return {
|
|
318
|
+
jsx: '',
|
|
319
|
+
info: { name: 'SocialProof', dataSource: 'skipped', itemCount: 0 },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const quotesStr = socialProof
|
|
324
|
+
.slice(0, 4)
|
|
325
|
+
.map(
|
|
326
|
+
(quote, i) =>
|
|
327
|
+
` <blockquote key={${i}} className="rounded-2xl border border-border bg-card p-6 shadow-sm">
|
|
328
|
+
<div className="mb-4 text-4xl text-primary-300">“</div>
|
|
329
|
+
<p className="text-foreground">${escapeJsx(quote)}</p>
|
|
330
|
+
<div className="mt-4 flex items-center gap-3">
|
|
331
|
+
<div className="h-10 w-10 rounded-full bg-muted" />
|
|
332
|
+
<div className="text-sm text-muted-foreground">Verified User</div>
|
|
333
|
+
</div>
|
|
334
|
+
</blockquote>`
|
|
335
|
+
)
|
|
336
|
+
.join('\n');
|
|
337
|
+
|
|
338
|
+
const jsx = `
|
|
339
|
+
{/* Social Proof */}
|
|
340
|
+
<section className="py-20 sm:py-28">
|
|
341
|
+
<div className="container">
|
|
342
|
+
<h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
343
|
+
Trusted by teams everywhere
|
|
344
|
+
</h2>
|
|
345
|
+
<div className="mx-auto mt-12 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-2">
|
|
346
|
+
${quotesStr}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</section>
|
|
350
|
+
`;
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
jsx,
|
|
354
|
+
info: { name: 'SocialProof', dataSource: 'strategy', itemCount: socialProof.length },
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Generate Pricing Teaser section for landing page
|
|
360
|
+
* Data: context.pricing (tier names + starting prices)
|
|
361
|
+
* Skip: if no pricing tiers
|
|
362
|
+
*/
|
|
363
|
+
export function generatePricingTeaserSection(
|
|
364
|
+
context?: WebsiteContentContext
|
|
365
|
+
): { jsx: string; info: SectionRenderInfo } {
|
|
366
|
+
const tiers = context?.pricing;
|
|
367
|
+
if (!tiers || tiers.length === 0) {
|
|
368
|
+
return {
|
|
369
|
+
jsx: '',
|
|
370
|
+
info: { name: 'PricingTeaser', dataSource: 'skipped', itemCount: 0 },
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const tiersStr = tiers
|
|
375
|
+
.slice(0, 3)
|
|
376
|
+
.map(
|
|
377
|
+
(tier) =>
|
|
378
|
+
` <div className="rounded-2xl border ${tier.featured ? 'border-primary-600 ring-2 ring-primary-600' : 'border-border'} bg-card p-6 text-center">
|
|
379
|
+
<h3 className="text-lg font-semibold text-foreground">${escapeJsx(tier.name)}</h3>
|
|
380
|
+
<p className="mt-2 text-3xl font-bold text-foreground">${escapeJsx(tier.price)}</p>
|
|
381
|
+
${tier.period ? `<p className="text-sm text-muted-foreground">${escapeJsx(tier.period)}</p>` : ''}
|
|
382
|
+
<p className="mt-2 text-sm text-muted-foreground">${escapeJsx(tier.description)}</p>
|
|
383
|
+
</div>`
|
|
384
|
+
)
|
|
385
|
+
.join('\n');
|
|
386
|
+
|
|
387
|
+
const jsx = `
|
|
388
|
+
{/* Pricing Teaser */}
|
|
389
|
+
<section className="bg-muted/50 py-20 sm:py-28">
|
|
390
|
+
<div className="container">
|
|
391
|
+
<h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
392
|
+
Simple, transparent pricing
|
|
393
|
+
</h2>
|
|
394
|
+
<div className="mx-auto mt-12 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-3">
|
|
395
|
+
${tiersStr}
|
|
396
|
+
</div>
|
|
397
|
+
<div className="mt-8 text-center">
|
|
398
|
+
<Link
|
|
399
|
+
href="/pricing"
|
|
400
|
+
className="text-sm font-semibold text-primary-600 hover:text-primary-500"
|
|
401
|
+
>
|
|
402
|
+
View full pricing <span aria-hidden="true">→</span>
|
|
403
|
+
</Link>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</section>
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
jsx,
|
|
411
|
+
info: { name: 'PricingTeaser', dataSource: 'docs', itemCount: tiers.length },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Generate FAQ section from strategy objections
|
|
417
|
+
* Data: strategy.icp.objections rephrased as Q&A
|
|
418
|
+
* Skip: if no objections
|
|
419
|
+
* Note: Renders as client component with useState accordion
|
|
420
|
+
*/
|
|
421
|
+
export function generateFaqSection(
|
|
422
|
+
strategy?: WebsiteStrategyDocument
|
|
423
|
+
): { jsx: string; info: SectionRenderInfo; needsClientDirective: boolean } {
|
|
424
|
+
const objections = strategy?.icp.objections || [];
|
|
425
|
+
if (objections.length === 0) {
|
|
426
|
+
return {
|
|
427
|
+
jsx: '',
|
|
428
|
+
info: { name: 'FAQ', dataSource: 'skipped', itemCount: 0 },
|
|
429
|
+
needsClientDirective: false,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const faqItems = objections.slice(0, 6).map((obj) => {
|
|
434
|
+
// Convert objection to Q&A format
|
|
435
|
+
const question = obj.endsWith('?') ? obj : `${obj}?`;
|
|
436
|
+
return { question, answer: `We take this seriously. ${obj.replace(/\?$/, '')} is addressed through our robust platform design and industry best practices.` };
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const jsx = `
|
|
440
|
+
{/* FAQ */}
|
|
441
|
+
<section id="faq" className="py-20 sm:py-28">
|
|
442
|
+
<div className="container">
|
|
443
|
+
<h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
444
|
+
Frequently asked questions
|
|
445
|
+
</h2>
|
|
446
|
+
<div className="mx-auto mt-12 max-w-3xl divide-y divide-border">
|
|
447
|
+
{faqItems.map((item, index) => (
|
|
448
|
+
<FaqItem key={index} question={item.question} answer={item.answer} />
|
|
449
|
+
))}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
</section>
|
|
453
|
+
`;
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
jsx,
|
|
457
|
+
info: { name: 'FAQ', dataSource: 'strategy', itemCount: faqItems.length },
|
|
458
|
+
needsClientDirective: true,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Build FAQ items array declaration for the page component
|
|
464
|
+
*/
|
|
465
|
+
export function buildFaqItemsDeclaration(
|
|
466
|
+
strategy?: WebsiteStrategyDocument
|
|
467
|
+
): string {
|
|
468
|
+
const objections = strategy?.icp.objections || [];
|
|
469
|
+
if (objections.length === 0) return '';
|
|
470
|
+
|
|
471
|
+
const faqItems = objections.slice(0, 6).map((obj) => {
|
|
472
|
+
const question = obj.endsWith('?') ? obj : `${obj}?`;
|
|
473
|
+
return { question, answer: `We take this seriously. ${obj.replace(/\?$/, '')} is addressed through our robust platform design and industry best practices.` };
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const itemsStr = faqItems
|
|
477
|
+
.map(
|
|
478
|
+
(item) =>
|
|
479
|
+
` { question: '${escapeJsx(item.question)}', answer: '${escapeJsx(item.answer)}' }`
|
|
480
|
+
)
|
|
481
|
+
.join(',\n');
|
|
482
|
+
|
|
483
|
+
return `const faqItems = [\n${itemsStr}\n];\n`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Generate the FaqItem client component code
|
|
488
|
+
*/
|
|
489
|
+
export function generateFaqItemComponent(): string {
|
|
490
|
+
return `function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
491
|
+
const [open, setOpen] = useState(false);
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<div className="py-4">
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
className="flex w-full items-center justify-between text-left"
|
|
498
|
+
onClick={() => setOpen(!open)}
|
|
499
|
+
aria-expanded={open}
|
|
500
|
+
>
|
|
501
|
+
<span className="text-base font-medium text-foreground">{question}</span>
|
|
502
|
+
<ChevronDown className={\`h-5 w-5 text-muted-foreground transition-transform \${open ? 'rotate-180' : ''}\`} />
|
|
503
|
+
</button>
|
|
504
|
+
{open && (
|
|
505
|
+
<p className="mt-3 text-muted-foreground">{answer}</p>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Build JSON-LD FAQ schema for SEO
|
|
514
|
+
*/
|
|
515
|
+
export function buildFaqSchema(
|
|
516
|
+
strategy?: WebsiteStrategyDocument
|
|
517
|
+
): string {
|
|
518
|
+
const objections = strategy?.icp.objections || [];
|
|
519
|
+
if (objections.length === 0) return '';
|
|
520
|
+
|
|
521
|
+
const faqItems = objections.slice(0, 6).map((obj) => {
|
|
522
|
+
const question = obj.endsWith('?') ? obj : `${obj}?`;
|
|
523
|
+
const answer = `We take this seriously. ${obj.replace(/\?$/, '')} is addressed through our robust platform design and industry best practices.`;
|
|
524
|
+
return { question, answer };
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return `const FAQ_SCHEMA = {
|
|
528
|
+
'@context': 'https://schema.org',
|
|
529
|
+
'@type': 'FAQPage',
|
|
530
|
+
mainEntity: [
|
|
531
|
+
${faqItems.map(item => ` {
|
|
532
|
+
'@type': 'Question',
|
|
533
|
+
name: '${escapeJsx(item.question)}',
|
|
534
|
+
acceptedAnswer: {
|
|
535
|
+
'@type': 'Answer',
|
|
536
|
+
text: '${escapeJsx(item.answer)}',
|
|
537
|
+
},
|
|
538
|
+
}`).join(',\n')}
|
|
539
|
+
],
|
|
540
|
+
};`;
|
|
541
|
+
}
|