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