popeye-cli 1.4.7 → 1.5.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/README.md +222 -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/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +42 -0
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +4 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +2 -1
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +49 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +336 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/templates/index.d.ts +4 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +4 -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 +278 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +41 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +283 -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-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 +14 -47
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +412 -499
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-context.d.ts +83 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +190 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website.d.ts +3 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +73 -10
- package/dist/generators/website.js.map +1 -1
- package/dist/state/index.d.ts +27 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +30 -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 +15 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +6 -0
- package/dist/types/workflow.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 +3 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +25 -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 +354 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +2 -1
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +83 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +70 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +238 -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 +105 -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/interactive.ts +47 -0
- package/src/generators/all.ts +6 -1
- package/src/generators/doc-parser.ts +372 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +305 -0
- package/src/generators/templates/website-config.ts +291 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +451 -505
- package/src/generators/website-context.ts +265 -0
- package/src/generators/website.ts +109 -19
- package/src/state/index.ts +42 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +15 -0
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +25 -0
- package/src/workflow/overview.ts +469 -0
- package/src/workflow/plan-mode.ts +115 -4
- package/src/workflow/website-strategy.ts +305 -0
- package/src/workflow/website-updater.ts +131 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-context.test.ts +222 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/workflow/overview.test.ts +392 -0
- package/tests/workflow/website-strategy.test.ts +191 -0
|
@@ -1,191 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Website templates for Next.js marketing sites
|
|
3
|
-
* Generates SEO-ready
|
|
2
|
+
* Website content templates for Next.js marketing sites
|
|
3
|
+
* Generates SEO-ready content pages with optional project context
|
|
4
|
+
* and strategy-driven marketing content
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
export function generateWebsitePackageJson(projectName: string): string {
|
|
10
|
-
return `{
|
|
11
|
-
"name": "${projectName}-website",
|
|
12
|
-
"version": "1.0.0",
|
|
13
|
-
"private": true,
|
|
14
|
-
"scripts": {
|
|
15
|
-
"dev": "next dev -p 3001",
|
|
16
|
-
"build": "next build",
|
|
17
|
-
"start": "next start -p 3001",
|
|
18
|
-
"lint": "next lint",
|
|
19
|
-
"test": "vitest run",
|
|
20
|
-
"test:watch": "vitest",
|
|
21
|
-
"typecheck": "tsc --noEmit"
|
|
22
|
-
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"next": "^14.1.0",
|
|
25
|
-
"react": "^18.2.0",
|
|
26
|
-
"react-dom": "^18.2.0",
|
|
27
|
-
"lucide-react": "^0.312.0",
|
|
28
|
-
"clsx": "^2.1.0",
|
|
29
|
-
"tailwind-merge": "^2.2.0"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/node": "^20.11.0",
|
|
33
|
-
"@types/react": "^18.2.0",
|
|
34
|
-
"@types/react-dom": "^18.2.0",
|
|
35
|
-
"autoprefixer": "^10.4.17",
|
|
36
|
-
"postcss": "^8.4.33",
|
|
37
|
-
"tailwindcss": "^3.4.1",
|
|
38
|
-
"typescript": "^5.3.3",
|
|
39
|
-
"@testing-library/react": "^14.1.2",
|
|
40
|
-
"@vitejs/plugin-react": "^4.2.1",
|
|
41
|
-
"vitest": "^1.2.0",
|
|
42
|
-
"jsdom": "^24.0.0"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Generate Next.js config
|
|
50
|
-
*/
|
|
51
|
-
export function generateNextConfig(): string {
|
|
52
|
-
return `/** @type {import('next').NextConfig} */
|
|
53
|
-
const nextConfig = {
|
|
54
|
-
// Enable React Strict Mode for better development
|
|
55
|
-
reactStrictMode: true,
|
|
56
|
-
|
|
57
|
-
// Image optimization
|
|
58
|
-
images: {
|
|
59
|
-
domains: [],
|
|
60
|
-
formats: ['image/avif', 'image/webp'],
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
// Disable x-powered-by header
|
|
64
|
-
poweredByHeader: false,
|
|
65
|
-
|
|
66
|
-
// Trailing slash config
|
|
67
|
-
trailingSlash: false,
|
|
68
|
-
|
|
69
|
-
// Headers for security
|
|
70
|
-
async headers() {
|
|
71
|
-
return [
|
|
72
|
-
{
|
|
73
|
-
source: '/:path*',
|
|
74
|
-
headers: [
|
|
75
|
-
{
|
|
76
|
-
key: 'X-DNS-Prefetch-Control',
|
|
77
|
-
value: 'on',
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
key: 'X-Content-Type-Options',
|
|
81
|
-
value: 'nosniff',
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
];
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
module.exports = nextConfig;
|
|
90
|
-
`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Generate website tsconfig.json
|
|
95
|
-
*/
|
|
96
|
-
export function generateWebsiteTsconfig(): string {
|
|
97
|
-
return `{
|
|
98
|
-
"compilerOptions": {
|
|
99
|
-
"target": "ES2017",
|
|
100
|
-
"lib": ["dom", "dom.iterable", "esnext"],
|
|
101
|
-
"allowJs": true,
|
|
102
|
-
"skipLibCheck": true,
|
|
103
|
-
"strict": true,
|
|
104
|
-
"noEmit": true,
|
|
105
|
-
"esModuleInterop": true,
|
|
106
|
-
"module": "esnext",
|
|
107
|
-
"moduleResolution": "bundler",
|
|
108
|
-
"resolveJsonModule": true,
|
|
109
|
-
"isolatedModules": true,
|
|
110
|
-
"jsx": "preserve",
|
|
111
|
-
"incremental": true,
|
|
112
|
-
"plugins": [
|
|
113
|
-
{
|
|
114
|
-
"name": "next"
|
|
115
|
-
}
|
|
116
|
-
],
|
|
117
|
-
"paths": {
|
|
118
|
-
"@/*": ["./src/*"]
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
122
|
-
"exclude": ["node_modules"]
|
|
123
|
-
}
|
|
124
|
-
`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Generate Tailwind config for website
|
|
129
|
-
*/
|
|
130
|
-
export function generateWebsiteTailwindConfig(): string {
|
|
131
|
-
return `import type { Config } from 'tailwindcss';
|
|
132
|
-
|
|
133
|
-
const config: Config = {
|
|
134
|
-
content: [
|
|
135
|
-
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
136
|
-
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
137
|
-
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
138
|
-
],
|
|
139
|
-
theme: {
|
|
140
|
-
extend: {
|
|
141
|
-
colors: {
|
|
142
|
-
primary: {
|
|
143
|
-
50: '#f0f9ff',
|
|
144
|
-
100: '#e0f2fe',
|
|
145
|
-
200: '#bae6fd',
|
|
146
|
-
300: '#7dd3fc',
|
|
147
|
-
400: '#38bdf8',
|
|
148
|
-
500: '#0ea5e9',
|
|
149
|
-
600: '#0284c7',
|
|
150
|
-
700: '#0369a1',
|
|
151
|
-
800: '#075985',
|
|
152
|
-
900: '#0c4a6e',
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
fontFamily: {
|
|
156
|
-
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
plugins: [],
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
export default config;
|
|
164
|
-
`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Generate PostCSS config for website
|
|
169
|
-
*/
|
|
170
|
-
export function generateWebsitePostcssConfig(): string {
|
|
171
|
-
return `module.exports = {
|
|
172
|
-
plugins: {
|
|
173
|
-
tailwindcss: {},
|
|
174
|
-
autoprefixer: {},
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
`;
|
|
178
|
-
}
|
|
7
|
+
import type { WebsiteContentContext } from '../website-context.js';
|
|
8
|
+
// Strategy data is accessed via context.strategy (WebsiteContentContext includes it)
|
|
179
9
|
|
|
180
10
|
/**
|
|
181
11
|
* Generate root layout.tsx with metadata
|
|
182
12
|
*/
|
|
183
|
-
export function generateWebsiteLayout(
|
|
13
|
+
export function generateWebsiteLayout(
|
|
14
|
+
projectName: string,
|
|
15
|
+
context?: WebsiteContentContext
|
|
16
|
+
): string {
|
|
184
17
|
const title = projectName
|
|
185
18
|
.split('-')
|
|
186
19
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
187
20
|
.join(' ');
|
|
188
21
|
|
|
22
|
+
const strategy = context?.strategy;
|
|
23
|
+
const displayName = context?.productName || title;
|
|
24
|
+
const desc = strategy?.messaging.longDescription
|
|
25
|
+
|| context?.description
|
|
26
|
+
|| `${displayName} - Your modern web application`;
|
|
27
|
+
|
|
28
|
+
// SEO keywords from strategy or defaults
|
|
29
|
+
const keywords = strategy?.seoStrategy.primaryKeywords
|
|
30
|
+
? strategy.seoStrategy.primaryKeywords.map(k => `'${escapeJsx(k)}'`).join(', ')
|
|
31
|
+
: `'${projectName}', 'web app', 'nextjs'`;
|
|
32
|
+
|
|
189
33
|
return `import type { Metadata } from 'next';
|
|
190
34
|
import { Inter } from 'next/font/google';
|
|
191
35
|
import './globals.css';
|
|
@@ -195,27 +39,30 @@ const inter = Inter({
|
|
|
195
39
|
variable: '--font-inter',
|
|
196
40
|
});
|
|
197
41
|
|
|
42
|
+
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://${projectName}.com';
|
|
43
|
+
|
|
198
44
|
export const metadata: Metadata = {
|
|
45
|
+
metadataBase: new URL(BASE_URL),
|
|
199
46
|
title: {
|
|
200
|
-
default: '${
|
|
201
|
-
template: '%s | ${
|
|
47
|
+
default: '${escapeJsx(displayName)}',
|
|
48
|
+
template: '%s | ${escapeJsx(displayName)}',
|
|
202
49
|
},
|
|
203
|
-
description: '${
|
|
204
|
-
keywords: [
|
|
205
|
-
authors: [{ name: '${
|
|
206
|
-
creator: '${
|
|
50
|
+
description: '${escapeJsx(desc)}',
|
|
51
|
+
keywords: [${keywords}],
|
|
52
|
+
authors: [{ name: '${escapeJsx(displayName)} Team' }],
|
|
53
|
+
creator: '${escapeJsx(displayName)}',
|
|
207
54
|
openGraph: {
|
|
208
55
|
type: 'website',
|
|
209
56
|
locale: 'en_US',
|
|
210
|
-
url:
|
|
211
|
-
siteName: '${
|
|
212
|
-
title: '${
|
|
213
|
-
description: '${
|
|
57
|
+
url: BASE_URL,
|
|
58
|
+
siteName: '${escapeJsx(displayName)}',
|
|
59
|
+
title: '${escapeJsx(displayName)}',
|
|
60
|
+
description: '${escapeJsx(desc)}',
|
|
214
61
|
},
|
|
215
62
|
twitter: {
|
|
216
63
|
card: 'summary_large_image',
|
|
217
|
-
title: '${
|
|
218
|
-
description: '${
|
|
64
|
+
title: '${escapeJsx(displayName)}',
|
|
65
|
+
description: '${escapeJsx(desc)}',
|
|
219
66
|
},
|
|
220
67
|
robots: {
|
|
221
68
|
index: true,
|
|
@@ -240,9 +87,16 @@ export default function RootLayout({
|
|
|
240
87
|
}
|
|
241
88
|
|
|
242
89
|
/**
|
|
243
|
-
* Generate globals.css
|
|
90
|
+
* Generate globals.css with optional brand colors
|
|
244
91
|
*/
|
|
245
|
-
export function generateWebsiteGlobalsCss(
|
|
92
|
+
export function generateWebsiteGlobalsCss(
|
|
93
|
+
context?: WebsiteContentContext
|
|
94
|
+
): string {
|
|
95
|
+
// Convert hex to HSL for CSS custom properties if brand color provided
|
|
96
|
+
const primaryHsl = context?.brand?.primaryColor
|
|
97
|
+
? hexToHslString(context.brand.primaryColor)
|
|
98
|
+
: '199 89% 48%';
|
|
99
|
+
|
|
246
100
|
return `@tailwind base;
|
|
247
101
|
@tailwind components;
|
|
248
102
|
@tailwind utilities;
|
|
@@ -251,7 +105,7 @@ export function generateWebsiteGlobalsCss(): string {
|
|
|
251
105
|
:root {
|
|
252
106
|
--background: 0 0% 100%;
|
|
253
107
|
--foreground: 222.2 84% 4.9%;
|
|
254
|
-
--primary:
|
|
108
|
+
--primary: ${primaryHsl};
|
|
255
109
|
--primary-foreground: 210 40% 98%;
|
|
256
110
|
}
|
|
257
111
|
|
|
@@ -269,257 +123,417 @@ export function generateWebsiteGlobalsCss(): string {
|
|
|
269
123
|
}
|
|
270
124
|
|
|
271
125
|
/**
|
|
272
|
-
* Generate landing page.tsx
|
|
126
|
+
* Generate landing page.tsx with optional context-driven content
|
|
127
|
+
* When strategy is available, uses strategy messaging, trust signals, and CTAs
|
|
273
128
|
*/
|
|
274
|
-
export function generateWebsiteLandingPage(
|
|
129
|
+
export function generateWebsiteLandingPage(
|
|
130
|
+
projectName: string,
|
|
131
|
+
context?: WebsiteContentContext
|
|
132
|
+
): string {
|
|
275
133
|
const title = projectName
|
|
276
134
|
.split('-')
|
|
277
135
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
278
136
|
.join(' ');
|
|
279
137
|
|
|
138
|
+
const strategy = context?.strategy;
|
|
139
|
+
const displayName = context?.productName || title;
|
|
140
|
+
|
|
141
|
+
// Strategy-driven or context-driven hero
|
|
142
|
+
const headline = strategy?.messaging.headline || displayName;
|
|
143
|
+
const subheadline = strategy?.messaging.subheadline || '';
|
|
144
|
+
const heroText = strategy?.messaging.longDescription
|
|
145
|
+
? escapeJsx(strategy.messaging.longDescription)
|
|
146
|
+
: context?.description
|
|
147
|
+
? escapeJsx(context.description)
|
|
148
|
+
: null;
|
|
149
|
+
|
|
150
|
+
const features = context?.features && context.features.length > 0
|
|
151
|
+
? context.features.slice(0, 6)
|
|
152
|
+
: null;
|
|
153
|
+
|
|
154
|
+
// CTAs from strategy or defaults
|
|
155
|
+
const primaryCtaText = strategy?.conversionStrategy.primaryCta.text || 'Get started';
|
|
156
|
+
const primaryCtaHref = strategy?.conversionStrategy.primaryCta.href || '/pricing';
|
|
157
|
+
const secondaryCtaText = strategy?.conversionStrategy.secondaryCta.text || 'Learn more';
|
|
158
|
+
const secondaryCtaHref = strategy?.conversionStrategy.secondaryCta.href || '/docs';
|
|
159
|
+
|
|
160
|
+
// Build hero paragraph
|
|
161
|
+
const heroParagraph = heroText
|
|
162
|
+
? ` ${heroText}`
|
|
163
|
+
: ` {/* TODO: populate from project specification */}`;
|
|
164
|
+
|
|
165
|
+
// Build features array
|
|
166
|
+
const featuresBlock = features
|
|
167
|
+
? features.map((f) =>
|
|
168
|
+
` {\n title: '${escapeJsx(f.title)}',\n description: '${escapeJsx(f.description)}',\n }`
|
|
169
|
+
).join(',\n')
|
|
170
|
+
: ` {\n title: 'Feature 1',\n description: '/* TODO: populate from project specification */',\n },\n {\n title: 'Feature 2',\n description: '/* TODO: populate from project specification */',\n },\n {\n title: 'Feature 3',\n description: '/* TODO: populate from project specification */',\n }`;
|
|
171
|
+
|
|
172
|
+
// Trust signals from strategy
|
|
173
|
+
const trustSignals = strategy?.conversionStrategy.trustSignals || [];
|
|
174
|
+
const trustSignalsBlock = trustSignals.length > 0
|
|
175
|
+
? trustSignals.map(s => ` '${escapeJsx(s)}'`).join(',\n')
|
|
176
|
+
: '';
|
|
177
|
+
|
|
178
|
+
// Social proof from strategy
|
|
179
|
+
const socialProof = strategy?.conversionStrategy.socialProof || [];
|
|
180
|
+
const socialProofBlock = socialProof.length > 0
|
|
181
|
+
? socialProof.map(s => ` '${escapeJsx(s)}'`).join(',\n')
|
|
182
|
+
: '';
|
|
183
|
+
|
|
184
|
+
// Build optional sections
|
|
185
|
+
const trustSection = trustSignals.length > 0 ? `
|
|
186
|
+
{/* Trust Signals */}
|
|
187
|
+
<section className="border-y border-gray-100 bg-gray-50 py-12">
|
|
188
|
+
<div className="container">
|
|
189
|
+
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-4">
|
|
190
|
+
{[
|
|
191
|
+
${trustSignalsBlock}
|
|
192
|
+
].map((signal) => (
|
|
193
|
+
<p key={signal} className="text-sm font-medium text-gray-600">{signal}</p>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
` : '';
|
|
199
|
+
|
|
200
|
+
const socialProofSection = socialProof.length > 0 ? `
|
|
201
|
+
{/* Social Proof */}
|
|
202
|
+
<section className="py-16 sm:py-24">
|
|
203
|
+
<div className="container">
|
|
204
|
+
<h2 className="text-center text-3xl font-bold tracking-tight text-gray-900">
|
|
205
|
+
Trusted by teams everywhere
|
|
206
|
+
</h2>
|
|
207
|
+
<div className="mx-auto mt-12 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-2">
|
|
208
|
+
{[
|
|
209
|
+
${socialProofBlock}
|
|
210
|
+
].map((quote, i) => (
|
|
211
|
+
<blockquote key={i} className="rounded-2xl border border-gray-200 p-6">
|
|
212
|
+
<p className="text-gray-700">“{quote}”</p>
|
|
213
|
+
</blockquote>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</section>
|
|
218
|
+
` : '';
|
|
219
|
+
|
|
220
|
+
// Metadata: strategy-driven or default
|
|
221
|
+
const metaTitle = strategy?.seoStrategy.titleTemplates?.home || 'Welcome';
|
|
222
|
+
const metaDesc = strategy?.seoStrategy.metaDescriptions?.home || `Welcome to ${displayName}`;
|
|
223
|
+
|
|
280
224
|
return `import type { Metadata } from 'next';
|
|
281
225
|
import Link from 'next/link';
|
|
226
|
+
import Header from '@/components/Header';
|
|
227
|
+
import Footer from '@/components/Footer';
|
|
228
|
+
import JsonLd from '@/components/JsonLd';
|
|
282
229
|
|
|
283
230
|
export const metadata: Metadata = {
|
|
284
|
-
title: '
|
|
285
|
-
description: '
|
|
231
|
+
title: '${escapeJsx(metaTitle)}',
|
|
232
|
+
description: '${escapeJsx(metaDesc)}',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const ORG_SCHEMA = {
|
|
236
|
+
'@context': 'https://schema.org',
|
|
237
|
+
'@type': 'Organization',
|
|
238
|
+
name: '${escapeJsx(displayName)}',
|
|
239
|
+
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://${projectName}.com',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const PRODUCT_SCHEMA = {
|
|
243
|
+
'@context': 'https://schema.org',
|
|
244
|
+
'@type': 'SoftwareApplication',
|
|
245
|
+
name: '${escapeJsx(displayName)}',
|
|
246
|
+
applicationCategory: 'BusinessApplication',
|
|
247
|
+
operatingSystem: 'Web',
|
|
286
248
|
};
|
|
287
249
|
|
|
288
250
|
export default function HomePage() {
|
|
289
251
|
return (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
252
|
+
<>
|
|
253
|
+
<Header />
|
|
254
|
+
<JsonLd schema={ORG_SCHEMA} />
|
|
255
|
+
<JsonLd schema={PRODUCT_SCHEMA} />
|
|
256
|
+
<main className="flex min-h-screen flex-col">
|
|
257
|
+
{/* Hero Section */}
|
|
258
|
+
<section className="relative overflow-hidden bg-gradient-to-b from-primary-50 to-white py-20 sm:py-32">
|
|
259
|
+
<div className="container">
|
|
260
|
+
<div className="mx-auto max-w-2xl text-center">
|
|
261
|
+
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
|
|
262
|
+
${escapeJsx(headline)}
|
|
263
|
+
</h1>
|
|
264
|
+
${subheadline ? ` <p className="mt-4 text-xl font-medium text-primary-600">\n ${escapeJsx(subheadline)}\n </p>` : ''}
|
|
265
|
+
<p className="mt-6 text-lg leading-8 text-gray-600">
|
|
266
|
+
${heroParagraph}
|
|
267
|
+
</p>
|
|
268
|
+
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
269
|
+
<Link
|
|
270
|
+
href="${escapeJsx(primaryCtaHref)}"
|
|
271
|
+
className="rounded-md bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
|
|
272
|
+
>
|
|
273
|
+
${escapeJsx(primaryCtaText)}
|
|
274
|
+
</Link>
|
|
275
|
+
<Link
|
|
276
|
+
href="${escapeJsx(secondaryCtaHref)}"
|
|
277
|
+
className="text-sm font-semibold leading-6 text-gray-900 hover:text-primary-600"
|
|
278
|
+
>
|
|
279
|
+
${escapeJsx(secondaryCtaText)} <span aria-hidden="true">-></span>
|
|
280
|
+
</Link>
|
|
281
|
+
</div>
|
|
315
282
|
</div>
|
|
316
283
|
</div>
|
|
317
|
-
</
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
284
|
+
</section>
|
|
285
|
+
${trustSection}
|
|
286
|
+
{/* Features Section */}
|
|
287
|
+
<section id="features" className="py-20 sm:py-32">
|
|
288
|
+
<div className="container">
|
|
289
|
+
<div className="mx-auto max-w-2xl text-center">
|
|
290
|
+
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
|
291
|
+
Everything you need
|
|
292
|
+
</h2>
|
|
293
|
+
<p className="mt-4 text-lg text-gray-600">
|
|
294
|
+
{/* TODO: populate section subtitle from project specification */}
|
|
295
|
+
</p>
|
|
296
|
+
</div>
|
|
297
|
+
<div className="mx-auto mt-16 max-w-5xl">
|
|
298
|
+
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
299
|
+
{[
|
|
300
|
+
${featuresBlock}
|
|
301
|
+
].map((feature) => (
|
|
302
|
+
<div
|
|
303
|
+
key={feature.title}
|
|
304
|
+
className="rounded-2xl border border-gray-200 p-8"
|
|
305
|
+
>
|
|
306
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
307
|
+
{feature.title}
|
|
308
|
+
</h3>
|
|
309
|
+
<p className="mt-2 text-gray-600">{feature.description}</p>
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</section>
|
|
316
|
+
${socialProofSection}
|
|
317
|
+
{/* CTA Section */}
|
|
318
|
+
<section className="bg-primary-600 py-16 sm:py-24">
|
|
319
|
+
<div className="container text-center">
|
|
320
|
+
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
|
|
321
|
+
Ready to get started?
|
|
326
322
|
</h2>
|
|
327
|
-
<p className="mt-4 text-lg text-
|
|
328
|
-
|
|
323
|
+
<p className="mt-4 text-lg text-primary-100">
|
|
324
|
+
${strategy?.messaging.elevatorPitch ? escapeJsx(strategy.messaging.elevatorPitch) : 'Start building today.'}
|
|
329
325
|
</p>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
title: 'Secure',
|
|
340
|
-
description: 'Built with security best practices.',
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
title: 'Scalable',
|
|
344
|
-
description: 'Grows with your business needs.',
|
|
345
|
-
},
|
|
346
|
-
].map((feature) => (
|
|
347
|
-
<div
|
|
348
|
-
key={feature.title}
|
|
349
|
-
className="rounded-2xl border border-gray-200 p-8"
|
|
350
|
-
>
|
|
351
|
-
<h3 className="text-lg font-semibold text-gray-900">
|
|
352
|
-
{feature.title}
|
|
353
|
-
</h3>
|
|
354
|
-
<p className="mt-2 text-gray-600">{feature.description}</p>
|
|
355
|
-
</div>
|
|
356
|
-
))}
|
|
326
|
+
<div className="mt-8">
|
|
327
|
+
<Link
|
|
328
|
+
href="${escapeJsx(primaryCtaHref)}"
|
|
329
|
+
className="rounded-md bg-white px-6 py-3 text-sm font-semibold text-primary-600 shadow-sm hover:bg-primary-50"
|
|
330
|
+
>
|
|
331
|
+
${escapeJsx(primaryCtaText)}
|
|
332
|
+
</Link>
|
|
357
333
|
</div>
|
|
358
334
|
</div>
|
|
359
|
-
</
|
|
360
|
-
</
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<footer className="border-t border-gray-200 py-12">
|
|
364
|
-
<div className="container">
|
|
365
|
-
<p className="text-center text-sm text-gray-500">
|
|
366
|
-
© {new Date().getFullYear()} ${title}. All rights reserved.
|
|
367
|
-
</p>
|
|
368
|
-
</div>
|
|
369
|
-
</footer>
|
|
370
|
-
</main>
|
|
335
|
+
</section>
|
|
336
|
+
</main>
|
|
337
|
+
<Footer />
|
|
338
|
+
</>
|
|
371
339
|
);
|
|
372
340
|
}
|
|
373
341
|
`;
|
|
374
342
|
}
|
|
375
343
|
|
|
376
344
|
/**
|
|
377
|
-
* Generate pricing page
|
|
345
|
+
* Generate pricing page with optional context-driven tiers and FAQ
|
|
378
346
|
*/
|
|
379
|
-
export function generateWebsitePricingPage(
|
|
347
|
+
export function generateWebsitePricingPage(
|
|
348
|
+
projectName: string,
|
|
349
|
+
context?: WebsiteContentContext
|
|
350
|
+
): string {
|
|
380
351
|
const title = projectName
|
|
381
352
|
.split('-')
|
|
382
353
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
383
354
|
.join(' ');
|
|
384
355
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
356
|
+
const strategy = context?.strategy;
|
|
357
|
+
const displayName = context?.productName || title;
|
|
358
|
+
const tiers = context?.pricing && context.pricing.length > 0
|
|
359
|
+
? context.pricing
|
|
360
|
+
: null;
|
|
361
|
+
|
|
362
|
+
// Build tiers array
|
|
363
|
+
const tiersBlock = tiers
|
|
364
|
+
? tiers.map((t) => {
|
|
365
|
+
const featuresStr = t.features.map((f) => ` '${escapeJsx(f)}'`).join(',\n');
|
|
366
|
+
return ` {
|
|
367
|
+
name: '${escapeJsx(t.name)}',
|
|
368
|
+
price: '${escapeJsx(t.price)}',
|
|
369
|
+
description: '${escapeJsx(t.description)}',
|
|
370
|
+
features: [
|
|
371
|
+
${featuresStr}
|
|
372
|
+
],
|
|
373
|
+
cta: '${escapeJsx(t.cta)}',
|
|
374
|
+
featured: ${t.featured ? 'true' : 'false'},
|
|
375
|
+
}`;
|
|
376
|
+
}).join(',\n')
|
|
377
|
+
: ` {
|
|
378
|
+
name: '/* TODO: tier name */',
|
|
379
|
+
price: '/* TODO */',
|
|
380
|
+
description: '/* TODO: populate from project specification */',
|
|
381
|
+
features: ['/* TODO: populate from project specification */'],
|
|
398
382
|
cta: 'Get started',
|
|
399
383
|
featured: false,
|
|
400
384
|
},
|
|
401
385
|
{
|
|
402
|
-
name: '
|
|
403
|
-
price: '
|
|
404
|
-
description: '
|
|
405
|
-
features: [
|
|
406
|
-
'Unlimited projects',
|
|
407
|
-
'Priority support',
|
|
408
|
-
'Advanced analytics',
|
|
409
|
-
'Custom integrations',
|
|
410
|
-
],
|
|
386
|
+
name: '/* TODO: tier name */',
|
|
387
|
+
price: '/* TODO */',
|
|
388
|
+
description: '/* TODO: populate from project specification */',
|
|
389
|
+
features: ['/* TODO: populate from project specification */'],
|
|
411
390
|
cta: 'Start free trial',
|
|
412
391
|
featured: true,
|
|
413
392
|
},
|
|
414
393
|
{
|
|
415
|
-
name: '
|
|
416
|
-
price: '
|
|
417
|
-
description: '
|
|
418
|
-
features: [
|
|
419
|
-
'Everything in Pro',
|
|
420
|
-
'Dedicated support',
|
|
421
|
-
'SLA guarantee',
|
|
422
|
-
'Custom contracts',
|
|
423
|
-
],
|
|
394
|
+
name: '/* TODO: tier name */',
|
|
395
|
+
price: '/* TODO */',
|
|
396
|
+
description: '/* TODO: populate from project specification */',
|
|
397
|
+
features: ['/* TODO: populate from project specification */'],
|
|
424
398
|
cta: 'Contact sales',
|
|
425
399
|
featured: false,
|
|
426
|
-
}
|
|
400
|
+
}`;
|
|
401
|
+
|
|
402
|
+
// Pricing metadata from strategy or defaults
|
|
403
|
+
const metaTitle = strategy?.seoStrategy.titleTemplates?.pricing || 'Pricing';
|
|
404
|
+
const metaDesc = strategy?.seoStrategy.metaDescriptions?.pricing || `Choose the perfect plan for your needs - ${displayName}`;
|
|
405
|
+
|
|
406
|
+
// Enterprise CTA from strategy
|
|
407
|
+
const enterpriseCtaText = strategy?.conversionStrategy.primaryCta.text || 'Contact Sales';
|
|
408
|
+
|
|
409
|
+
return `import type { Metadata } from 'next';
|
|
410
|
+
import Link from 'next/link';
|
|
411
|
+
import Header from '@/components/Header';
|
|
412
|
+
import Footer from '@/components/Footer';
|
|
413
|
+
|
|
414
|
+
export const metadata: Metadata = {
|
|
415
|
+
title: '${escapeJsx(metaTitle)}',
|
|
416
|
+
description: '${escapeJsx(metaDesc)}',
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const tiers = [
|
|
420
|
+
${tiersBlock}
|
|
427
421
|
];
|
|
428
422
|
|
|
429
423
|
export default function PricingPage() {
|
|
430
424
|
return (
|
|
431
|
-
|
|
432
|
-
<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
425
|
+
<>
|
|
426
|
+
<Header />
|
|
427
|
+
<main className="py-20 sm:py-32">
|
|
428
|
+
<div className="container">
|
|
429
|
+
<div className="mx-auto max-w-2xl text-center">
|
|
430
|
+
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
|
431
|
+
Simple, transparent pricing
|
|
432
|
+
</h1>
|
|
433
|
+
<p className="mt-6 text-lg text-gray-600">
|
|
434
|
+
Choose the plan that works best for you.
|
|
435
|
+
</p>
|
|
436
|
+
</div>
|
|
441
437
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}\`}
|
|
451
|
-
>
|
|
452
|
-
<h2
|
|
453
|
-
className={\`text-lg font-semibold \${
|
|
454
|
-
tier.featured ? 'text-white' : 'text-gray-900'
|
|
455
|
-
}\`}
|
|
456
|
-
>
|
|
457
|
-
{tier.name}
|
|
458
|
-
</h2>
|
|
459
|
-
<p
|
|
460
|
-
className={\`mt-2 text-sm \${
|
|
461
|
-
tier.featured ? 'text-primary-100' : 'text-gray-600'
|
|
438
|
+
<div className="mx-auto mt-16 grid max-w-lg grid-cols-1 gap-8 lg:max-w-5xl lg:grid-cols-3">
|
|
439
|
+
{tiers.map((tier) => (
|
|
440
|
+
<div
|
|
441
|
+
key={tier.name}
|
|
442
|
+
className={\`rounded-2xl p-8 \${
|
|
443
|
+
tier.featured
|
|
444
|
+
? 'bg-primary-600 text-white ring-2 ring-primary-600'
|
|
445
|
+
: 'border border-gray-200 bg-white'
|
|
462
446
|
}\`}
|
|
463
447
|
>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
<p className="mt-6">
|
|
467
|
-
<span
|
|
468
|
-
className={\`text-4xl font-bold \${
|
|
448
|
+
<h2
|
|
449
|
+
className={\`text-lg font-semibold \${
|
|
469
450
|
tier.featured ? 'text-white' : 'text-gray-900'
|
|
470
451
|
}\`}
|
|
471
452
|
>
|
|
472
|
-
{tier.
|
|
473
|
-
</
|
|
474
|
-
|
|
453
|
+
{tier.name}
|
|
454
|
+
</h2>
|
|
455
|
+
<p
|
|
456
|
+
className={\`mt-2 text-sm \${
|
|
457
|
+
tier.featured ? 'text-primary-100' : 'text-gray-600'
|
|
458
|
+
}\`}
|
|
459
|
+
>
|
|
460
|
+
{tier.description}
|
|
461
|
+
</p>
|
|
462
|
+
<p className="mt-6">
|
|
475
463
|
<span
|
|
476
|
-
className={\`text-
|
|
477
|
-
tier.featured ? 'text-
|
|
464
|
+
className={\`text-4xl font-bold \${
|
|
465
|
+
tier.featured ? 'text-white' : 'text-gray-900'
|
|
478
466
|
}\`}
|
|
479
467
|
>
|
|
480
|
-
|
|
468
|
+
{tier.price}
|
|
481
469
|
</span>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
<li
|
|
487
|
-
key={feature}
|
|
488
|
-
className={\`flex text-sm \${
|
|
489
|
-
tier.featured ? 'text-primary-100' : 'text-gray-600'
|
|
490
|
-
}\`}
|
|
491
|
-
>
|
|
492
|
-
<svg
|
|
493
|
-
className={\`h-5 w-5 flex-shrink-0 \${
|
|
494
|
-
tier.featured ? 'text-white' : 'text-primary-600'
|
|
470
|
+
{tier.price !== 'Custom' && (
|
|
471
|
+
<span
|
|
472
|
+
className={\`text-sm \${
|
|
473
|
+
tier.featured ? 'text-primary-100' : 'text-gray-600'
|
|
495
474
|
}\`}
|
|
496
|
-
viewBox="0 0 20 20"
|
|
497
|
-
fill="currentColor"
|
|
498
475
|
>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
<
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
476
|
+
/month
|
|
477
|
+
</span>
|
|
478
|
+
)}
|
|
479
|
+
</p>
|
|
480
|
+
<ul className="mt-8 space-y-4">
|
|
481
|
+
{tier.features.map((feature) => (
|
|
482
|
+
<li
|
|
483
|
+
key={feature}
|
|
484
|
+
className={\`flex text-sm \${
|
|
485
|
+
tier.featured ? 'text-primary-100' : 'text-gray-600'
|
|
486
|
+
}\`}
|
|
487
|
+
>
|
|
488
|
+
<svg
|
|
489
|
+
className={\`h-5 w-5 flex-shrink-0 \${
|
|
490
|
+
tier.featured ? 'text-white' : 'text-primary-600'
|
|
491
|
+
}\`}
|
|
492
|
+
viewBox="0 0 20 20"
|
|
493
|
+
fill="currentColor"
|
|
494
|
+
>
|
|
495
|
+
<path
|
|
496
|
+
fillRule="evenodd"
|
|
497
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
498
|
+
clipRule="evenodd"
|
|
499
|
+
/>
|
|
500
|
+
</svg>
|
|
501
|
+
<span className="ml-3">{feature}</span>
|
|
502
|
+
</li>
|
|
503
|
+
))}
|
|
504
|
+
</ul>
|
|
505
|
+
<button
|
|
506
|
+
className={\`mt-8 w-full rounded-md px-4 py-2 text-sm font-semibold \${
|
|
507
|
+
tier.featured
|
|
508
|
+
? 'bg-white text-primary-600 hover:bg-primary-50'
|
|
509
|
+
: 'bg-primary-600 text-white hover:bg-primary-500'
|
|
510
|
+
}\`}
|
|
511
|
+
>
|
|
512
|
+
{tier.cta}
|
|
513
|
+
</button>
|
|
514
|
+
</div>
|
|
515
|
+
))}
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
{/* Enterprise CTA */}
|
|
519
|
+
<div className="mx-auto mt-16 max-w-2xl text-center">
|
|
520
|
+
<h2 className="text-2xl font-bold text-gray-900">
|
|
521
|
+
Need a custom plan?
|
|
522
|
+
</h2>
|
|
523
|
+
<p className="mt-4 text-gray-600">
|
|
524
|
+
Contact our sales team for enterprise pricing and custom solutions.
|
|
525
|
+
</p>
|
|
526
|
+
<Link
|
|
527
|
+
href="/contact"
|
|
528
|
+
className="mt-6 inline-block rounded-md border border-primary-600 px-6 py-3 text-sm font-semibold text-primary-600 hover:bg-primary-50"
|
|
529
|
+
>
|
|
530
|
+
${escapeJsx(enterpriseCtaText)}
|
|
531
|
+
</Link>
|
|
532
|
+
</div>
|
|
520
533
|
</div>
|
|
521
|
-
</
|
|
522
|
-
|
|
534
|
+
</main>
|
|
535
|
+
<Footer />
|
|
536
|
+
</>
|
|
523
537
|
);
|
|
524
538
|
}
|
|
525
539
|
`;
|
|
@@ -587,55 +601,6 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
587
601
|
`;
|
|
588
602
|
}
|
|
589
603
|
|
|
590
|
-
/**
|
|
591
|
-
* Generate website Dockerfile
|
|
592
|
-
*/
|
|
593
|
-
export function generateWebsiteDockerfile(): string {
|
|
594
|
-
return `# Build stage
|
|
595
|
-
FROM node:20-alpine AS builder
|
|
596
|
-
|
|
597
|
-
WORKDIR /app
|
|
598
|
-
|
|
599
|
-
# Copy package files
|
|
600
|
-
COPY package*.json ./
|
|
601
|
-
|
|
602
|
-
# Install dependencies
|
|
603
|
-
RUN npm ci
|
|
604
|
-
|
|
605
|
-
# Copy source
|
|
606
|
-
COPY . .
|
|
607
|
-
|
|
608
|
-
# Build
|
|
609
|
-
RUN npm run build
|
|
610
|
-
|
|
611
|
-
# Production stage
|
|
612
|
-
FROM node:20-alpine AS runner
|
|
613
|
-
|
|
614
|
-
WORKDIR /app
|
|
615
|
-
|
|
616
|
-
ENV NODE_ENV=production
|
|
617
|
-
ENV NEXT_TELEMETRY_DISABLED=1
|
|
618
|
-
|
|
619
|
-
# Create non-root user
|
|
620
|
-
RUN addgroup --system --gid 1001 nodejs
|
|
621
|
-
RUN adduser --system --uid 1001 nextjs
|
|
622
|
-
|
|
623
|
-
# Copy built assets
|
|
624
|
-
COPY --from=builder /app/public ./public
|
|
625
|
-
COPY --from=builder /app/.next/standalone ./
|
|
626
|
-
COPY --from=builder /app/.next/static ./.next/static
|
|
627
|
-
|
|
628
|
-
USER nextjs
|
|
629
|
-
|
|
630
|
-
EXPOSE 3000
|
|
631
|
-
|
|
632
|
-
ENV PORT=3000
|
|
633
|
-
ENV HOSTNAME="0.0.0.0"
|
|
634
|
-
|
|
635
|
-
CMD ["node", "server.js"]
|
|
636
|
-
`;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
604
|
/**
|
|
640
605
|
* Generate website README
|
|
641
606
|
*/
|
|
@@ -700,22 +665,29 @@ content/
|
|
|
700
665
|
}
|
|
701
666
|
|
|
702
667
|
/**
|
|
703
|
-
* Generate website spec JSON
|
|
668
|
+
* Generate website spec JSON with optional context
|
|
704
669
|
*/
|
|
705
|
-
export function generateWebsiteSpec(
|
|
670
|
+
export function generateWebsiteSpec(
|
|
671
|
+
projectName: string,
|
|
672
|
+
context?: WebsiteContentContext
|
|
673
|
+
): string {
|
|
706
674
|
const title = projectName
|
|
707
675
|
.split('-')
|
|
708
676
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
709
677
|
.join(' ');
|
|
710
678
|
|
|
679
|
+
const displayName = context?.productName || title;
|
|
680
|
+
const tagline = context?.tagline || context?.description || 'Build something amazing';
|
|
681
|
+
const primaryColor = context?.brand?.primaryColor || '#0ea5e9';
|
|
682
|
+
|
|
711
683
|
return JSON.stringify(
|
|
712
684
|
{
|
|
713
685
|
version: '1.0',
|
|
714
686
|
brand: {
|
|
715
|
-
name:
|
|
716
|
-
tagline
|
|
687
|
+
name: displayName,
|
|
688
|
+
tagline,
|
|
717
689
|
colors: {
|
|
718
|
-
primary:
|
|
690
|
+
primary: primaryColor,
|
|
719
691
|
secondary: '#64748b',
|
|
720
692
|
accent: '#f59e0b',
|
|
721
693
|
background: '#ffffff',
|
|
@@ -727,8 +699,8 @@ export function generateWebsiteSpec(projectName: string): string {
|
|
|
727
699
|
},
|
|
728
700
|
},
|
|
729
701
|
seo: {
|
|
730
|
-
title:
|
|
731
|
-
description: `${
|
|
702
|
+
title: displayName,
|
|
703
|
+
description: context?.description || `${displayName} - Your modern web application`,
|
|
732
704
|
keywords: [projectName, 'web app', 'nextjs', 'saas'],
|
|
733
705
|
locale: 'en_US',
|
|
734
706
|
},
|
|
@@ -754,58 +726,6 @@ export function generateWebsiteSpec(projectName: string): string {
|
|
|
754
726
|
);
|
|
755
727
|
}
|
|
756
728
|
|
|
757
|
-
/**
|
|
758
|
-
* Generate vitest config for website
|
|
759
|
-
*/
|
|
760
|
-
export function generateWebsiteVitestConfig(): string {
|
|
761
|
-
return `import { defineConfig } from 'vitest/config';
|
|
762
|
-
import react from '@vitejs/plugin-react';
|
|
763
|
-
import path from 'path';
|
|
764
|
-
|
|
765
|
-
export default defineConfig({
|
|
766
|
-
plugins: [react()],
|
|
767
|
-
test: {
|
|
768
|
-
environment: 'jsdom',
|
|
769
|
-
include: ['**/*.test.{ts,tsx}'],
|
|
770
|
-
globals: true,
|
|
771
|
-
setupFiles: ['./tests/setup.ts'],
|
|
772
|
-
},
|
|
773
|
-
resolve: {
|
|
774
|
-
alias: {
|
|
775
|
-
'@': path.resolve(__dirname, './src'),
|
|
776
|
-
},
|
|
777
|
-
},
|
|
778
|
-
});
|
|
779
|
-
`;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Generate vitest setup for website
|
|
784
|
-
*/
|
|
785
|
-
export function generateWebsiteVitestSetup(): string {
|
|
786
|
-
return `import '@testing-library/jest-dom';
|
|
787
|
-
|
|
788
|
-
// Mock next/navigation
|
|
789
|
-
vi.mock('next/navigation', () => ({
|
|
790
|
-
useRouter: () => ({
|
|
791
|
-
push: vi.fn(),
|
|
792
|
-
replace: vi.fn(),
|
|
793
|
-
prefetch: vi.fn(),
|
|
794
|
-
}),
|
|
795
|
-
useSearchParams: () => new URLSearchParams(),
|
|
796
|
-
usePathname: () => '/',
|
|
797
|
-
}));
|
|
798
|
-
|
|
799
|
-
// Mock next/image
|
|
800
|
-
vi.mock('next/image', () => ({
|
|
801
|
-
default: (props: Record<string, unknown>) => {
|
|
802
|
-
// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
|
|
803
|
-
return <img {...props} />;
|
|
804
|
-
},
|
|
805
|
-
}));
|
|
806
|
-
`;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
729
|
/**
|
|
810
730
|
* Generate sample test for website
|
|
811
731
|
*/
|
|
@@ -830,13 +750,6 @@ describe('HomePage', () => {
|
|
|
830
750
|
expect(screen.getByRole('link', { name: /get started/i })).toBeInTheDocument();
|
|
831
751
|
expect(screen.getByRole('link', { name: /learn more/i })).toBeInTheDocument();
|
|
832
752
|
});
|
|
833
|
-
|
|
834
|
-
it('renders feature cards', () => {
|
|
835
|
-
render(<HomePage />);
|
|
836
|
-
expect(screen.getByText('Fast')).toBeInTheDocument();
|
|
837
|
-
expect(screen.getByText('Secure')).toBeInTheDocument();
|
|
838
|
-
expect(screen.getByText('Scalable')).toBeInTheDocument();
|
|
839
|
-
});
|
|
840
753
|
});
|
|
841
754
|
`;
|
|
842
755
|
}
|
|
@@ -894,13 +807,46 @@ export default function BlogPage() {
|
|
|
894
807
|
}
|
|
895
808
|
|
|
896
809
|
/**
|
|
897
|
-
*
|
|
810
|
+
* Escape a string for safe use inside JSX template literals
|
|
898
811
|
*/
|
|
899
|
-
|
|
900
|
-
return
|
|
901
|
-
|
|
812
|
+
function escapeJsx(str: string): string {
|
|
813
|
+
return str
|
|
814
|
+
.replace(/\\/g, '\\\\')
|
|
815
|
+
.replace(/'/g, "\\'")
|
|
816
|
+
.replace(/`/g, '\\`')
|
|
817
|
+
.replace(/\$/g, '\\$');
|
|
818
|
+
}
|
|
902
819
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
820
|
+
/**
|
|
821
|
+
* Convert hex color to HSL string for CSS custom properties
|
|
822
|
+
* Returns format: "H S% L%"
|
|
823
|
+
*/
|
|
824
|
+
function hexToHslString(hex: string): string {
|
|
825
|
+
// Remove # prefix
|
|
826
|
+
const h = hex.replace('#', '');
|
|
827
|
+
const r = parseInt(h.substring(0, 2), 16) / 255;
|
|
828
|
+
const g = parseInt(h.substring(2, 4), 16) / 255;
|
|
829
|
+
const b = parseInt(h.substring(4, 6), 16) / 255;
|
|
830
|
+
|
|
831
|
+
const max = Math.max(r, g, b);
|
|
832
|
+
const min = Math.min(r, g, b);
|
|
833
|
+
const l = (max + min) / 2;
|
|
834
|
+
|
|
835
|
+
if (max === min) {
|
|
836
|
+
return `0 0% ${Math.round(l * 100)}%`;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const d = max - min;
|
|
840
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
841
|
+
|
|
842
|
+
let hue = 0;
|
|
843
|
+
if (max === r) {
|
|
844
|
+
hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
845
|
+
} else if (max === g) {
|
|
846
|
+
hue = ((b - r) / d + 2) / 6;
|
|
847
|
+
} else {
|
|
848
|
+
hue = ((r - g) / d + 4) / 6;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return `${Math.round(hue * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
906
852
|
}
|