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
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO infrastructure templates for Next.js marketing websites
|
|
3
|
+
* Generates JSON-LD components, enhanced sitemap, robots.txt,
|
|
4
|
+
* error pages, web manifest, and meta helpers
|
|
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
|
+
* Generate reusable JSON-LD component
|
|
23
|
+
*
|
|
24
|
+
* @returns JsonLd component source code
|
|
25
|
+
*/
|
|
26
|
+
export function generateJsonLdComponent(): string {
|
|
27
|
+
return `/**
|
|
28
|
+
* Reusable JSON-LD structured data component
|
|
29
|
+
* Renders schema.org structured data as a script tag
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
interface JsonLdProps {
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
schema: Record<string, any>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function JsonLd({ schema }: JsonLdProps) {
|
|
38
|
+
return (
|
|
39
|
+
<script
|
|
40
|
+
type="application/ld+json"
|
|
41
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate Organization JSON-LD schema data
|
|
50
|
+
*
|
|
51
|
+
* @param projectName - Product name
|
|
52
|
+
* @param context - Optional content context
|
|
53
|
+
* @returns Organization schema as string constant
|
|
54
|
+
*/
|
|
55
|
+
export function generateOrganizationJsonLd(
|
|
56
|
+
projectName: string,
|
|
57
|
+
context?: WebsiteContentContext
|
|
58
|
+
): string {
|
|
59
|
+
const displayName = context?.productName || projectName;
|
|
60
|
+
const description = context?.description || `${displayName} - Modern web application`;
|
|
61
|
+
|
|
62
|
+
return `{
|
|
63
|
+
"@context": "https://schema.org",
|
|
64
|
+
"@type": "Organization",
|
|
65
|
+
"name": "${escapeJsx(displayName)}",
|
|
66
|
+
"description": "${escapeJsx(description)}",
|
|
67
|
+
"url": process.env.NEXT_PUBLIC_SITE_URL || "https://${projectName}.com"
|
|
68
|
+
}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate SoftwareApplication JSON-LD schema data
|
|
73
|
+
*
|
|
74
|
+
* @param projectName - Product name
|
|
75
|
+
* @param context - Optional content context
|
|
76
|
+
* @returns Software application schema as string constant
|
|
77
|
+
*/
|
|
78
|
+
export function generateProductJsonLd(
|
|
79
|
+
projectName: string,
|
|
80
|
+
context?: WebsiteContentContext
|
|
81
|
+
): string {
|
|
82
|
+
const displayName = context?.productName || projectName;
|
|
83
|
+
const description = context?.description || `${displayName} - Modern web application`;
|
|
84
|
+
|
|
85
|
+
return `{
|
|
86
|
+
"@context": "https://schema.org",
|
|
87
|
+
"@type": "SoftwareApplication",
|
|
88
|
+
"name": "${escapeJsx(displayName)}",
|
|
89
|
+
"description": "${escapeJsx(description)}",
|
|
90
|
+
"applicationCategory": "BusinessApplication",
|
|
91
|
+
"operatingSystem": "Web",
|
|
92
|
+
"url": process.env.NEXT_PUBLIC_SITE_URL || "https://${projectName}.com"
|
|
93
|
+
}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate enhanced sitemap with all strategy pages
|
|
98
|
+
*
|
|
99
|
+
* @param projectName - Project name for base URL fallback
|
|
100
|
+
* @param strategy - Optional strategy for page list
|
|
101
|
+
* @returns Enhanced sitemap.ts source code
|
|
102
|
+
*/
|
|
103
|
+
export function generateEnhancedSitemap(
|
|
104
|
+
projectName: string,
|
|
105
|
+
strategy?: WebsiteStrategyDocument
|
|
106
|
+
): string {
|
|
107
|
+
// Build page entries from strategy or defaults
|
|
108
|
+
const pages = strategy?.siteArchitecture.pages || [
|
|
109
|
+
{ path: '/', pageType: 'landing' },
|
|
110
|
+
{ path: '/pricing', pageType: 'pricing' },
|
|
111
|
+
{ path: '/docs', pageType: 'docs' },
|
|
112
|
+
{ path: '/blog', pageType: 'blog' },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const priorityMap: Record<string, number> = {
|
|
116
|
+
landing: 1.0,
|
|
117
|
+
pricing: 0.9,
|
|
118
|
+
solution: 0.8,
|
|
119
|
+
'use-cases': 0.8,
|
|
120
|
+
docs: 0.7,
|
|
121
|
+
blog: 0.7,
|
|
122
|
+
about: 0.6,
|
|
123
|
+
contact: 0.6,
|
|
124
|
+
legal: 0.3,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const frequencyMap: Record<string, string> = {
|
|
128
|
+
landing: 'weekly',
|
|
129
|
+
pricing: 'monthly',
|
|
130
|
+
solution: 'monthly',
|
|
131
|
+
'use-cases': 'monthly',
|
|
132
|
+
docs: 'weekly',
|
|
133
|
+
blog: 'daily',
|
|
134
|
+
about: 'monthly',
|
|
135
|
+
contact: 'monthly',
|
|
136
|
+
legal: 'yearly',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const entries = pages.map(page => {
|
|
140
|
+
const priority = priorityMap[page.pageType] || 0.5;
|
|
141
|
+
const frequency = frequencyMap[page.pageType] || 'monthly';
|
|
142
|
+
const urlPath = page.path === '/' ? '' : page.path;
|
|
143
|
+
return ` {
|
|
144
|
+
url: \`\${baseUrl}${urlPath}\`,
|
|
145
|
+
lastModified: new Date(),
|
|
146
|
+
changeFrequency: '${frequency}' as const,
|
|
147
|
+
priority: ${priority},
|
|
148
|
+
}`;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return `import { MetadataRoute } from 'next';
|
|
152
|
+
|
|
153
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
154
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://${projectName}.com';
|
|
155
|
+
|
|
156
|
+
return [
|
|
157
|
+
${entries.join(',\n')}
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generate enhanced robots.txt
|
|
165
|
+
*
|
|
166
|
+
* @param projectName - Project name for base URL fallback
|
|
167
|
+
* @returns robots.ts source code
|
|
168
|
+
*/
|
|
169
|
+
export function generateEnhancedRobots(projectName: string): string {
|
|
170
|
+
return `import { MetadataRoute } from 'next';
|
|
171
|
+
|
|
172
|
+
export default function robots(): MetadataRoute.Robots {
|
|
173
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://${projectName}.com';
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
rules: [
|
|
177
|
+
{
|
|
178
|
+
userAgent: '*',
|
|
179
|
+
allow: '/',
|
|
180
|
+
disallow: ['/api/', '/admin/', '/_next/'],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
sitemap: \`\${baseUrl}/sitemap.xml\`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate branded 404 Not Found page
|
|
191
|
+
*
|
|
192
|
+
* @param projectName - Product name for branding
|
|
193
|
+
* @param context - Optional content context
|
|
194
|
+
* @returns not-found.tsx source code
|
|
195
|
+
*/
|
|
196
|
+
export function generate404Page(
|
|
197
|
+
projectName: string,
|
|
198
|
+
context?: WebsiteContentContext
|
|
199
|
+
): string {
|
|
200
|
+
const displayName = context?.productName || projectName
|
|
201
|
+
.split('-')
|
|
202
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
203
|
+
.join(' ');
|
|
204
|
+
|
|
205
|
+
return `import Link from 'next/link';
|
|
206
|
+
|
|
207
|
+
export default function NotFound() {
|
|
208
|
+
return (
|
|
209
|
+
<main className="flex min-h-[60vh] flex-col items-center justify-center">
|
|
210
|
+
<div className="text-center">
|
|
211
|
+
<p className="text-7xl font-bold text-primary-600">404</p>
|
|
212
|
+
<h1 className="mt-4 text-3xl font-bold text-gray-900">Page not found</h1>
|
|
213
|
+
<p className="mt-4 text-lg text-gray-600">
|
|
214
|
+
Sorry, we couldn't find the page you're looking for.
|
|
215
|
+
</p>
|
|
216
|
+
<div className="mt-8">
|
|
217
|
+
<Link
|
|
218
|
+
href="/"
|
|
219
|
+
className="rounded-md bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-primary-500"
|
|
220
|
+
>
|
|
221
|
+
Back to ${escapeJsx(displayName)}
|
|
222
|
+
</Link>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</main>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generate error boundary page (500)
|
|
233
|
+
*
|
|
234
|
+
* @param projectName - Product name for branding
|
|
235
|
+
* @returns error.tsx source code
|
|
236
|
+
*/
|
|
237
|
+
export function generate500Page(_projectName: string): string {
|
|
238
|
+
return `'use client';
|
|
239
|
+
|
|
240
|
+
export default function Error({
|
|
241
|
+
reset,
|
|
242
|
+
}: {
|
|
243
|
+
error: Error & { digest?: string };
|
|
244
|
+
reset: () => void;
|
|
245
|
+
}) {
|
|
246
|
+
return (
|
|
247
|
+
<main className="flex min-h-[60vh] flex-col items-center justify-center">
|
|
248
|
+
<div className="text-center">
|
|
249
|
+
<p className="text-7xl font-bold text-red-600">500</p>
|
|
250
|
+
<h1 className="mt-4 text-3xl font-bold text-gray-900">Something went wrong</h1>
|
|
251
|
+
<p className="mt-4 text-lg text-gray-600">
|
|
252
|
+
An unexpected error occurred. Please try again.
|
|
253
|
+
</p>
|
|
254
|
+
<div className="mt-8">
|
|
255
|
+
<button
|
|
256
|
+
onClick={() => reset()}
|
|
257
|
+
className="rounded-md bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-primary-500"
|
|
258
|
+
>
|
|
259
|
+
Try again
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</main>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Generate PWA web manifest
|
|
271
|
+
*
|
|
272
|
+
* @param projectName - Product name
|
|
273
|
+
* @param context - Optional content context
|
|
274
|
+
* @returns manifest.webmanifest JSON content
|
|
275
|
+
*/
|
|
276
|
+
export function generateWebManifest(
|
|
277
|
+
projectName: string,
|
|
278
|
+
context?: WebsiteContentContext
|
|
279
|
+
): string {
|
|
280
|
+
const displayName = context?.productName || projectName
|
|
281
|
+
.split('-')
|
|
282
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
283
|
+
.join(' ');
|
|
284
|
+
|
|
285
|
+
const primaryColor = context?.brand?.primaryColor || '#0ea5e9';
|
|
286
|
+
const description = context?.description || `${displayName} - Modern web application`;
|
|
287
|
+
|
|
288
|
+
return JSON.stringify(
|
|
289
|
+
{
|
|
290
|
+
name: displayName,
|
|
291
|
+
short_name: displayName,
|
|
292
|
+
description,
|
|
293
|
+
start_url: '/',
|
|
294
|
+
display: 'standalone',
|
|
295
|
+
background_color: '#ffffff',
|
|
296
|
+
theme_color: primaryColor,
|
|
297
|
+
icons: [
|
|
298
|
+
{ src: '/favicon.ico', sizes: '48x48', type: 'image/x-icon' },
|
|
299
|
+
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
|
300
|
+
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
null,
|
|
304
|
+
2
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Generate shared metadata helper utility
|
|
310
|
+
*
|
|
311
|
+
* @param projectName - Product name
|
|
312
|
+
* @param strategy - Optional strategy for SEO data
|
|
313
|
+
* @returns Metadata helper source code
|
|
314
|
+
*/
|
|
315
|
+
export function generateMetaHelper(
|
|
316
|
+
projectName: string,
|
|
317
|
+
strategy?: WebsiteStrategyDocument
|
|
318
|
+
): string {
|
|
319
|
+
const primaryKeywords = strategy?.seoStrategy.primaryKeywords || [projectName, 'web app'];
|
|
320
|
+
const keywordsStr = primaryKeywords.map(k => `'${escapeJsx(k)}'`).join(', ');
|
|
321
|
+
|
|
322
|
+
return `import type { Metadata } from 'next';
|
|
323
|
+
|
|
324
|
+
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://${projectName}.com';
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Build page-level metadata with site-wide defaults
|
|
328
|
+
*
|
|
329
|
+
* @param title - Page title (combined with site name via template)
|
|
330
|
+
* @param description - Page meta description
|
|
331
|
+
* @param keywords - Additional page-specific keywords
|
|
332
|
+
* @param path - Page path for canonical URL
|
|
333
|
+
* @returns Next.js Metadata object
|
|
334
|
+
*/
|
|
335
|
+
export function buildMetadata({
|
|
336
|
+
title,
|
|
337
|
+
description,
|
|
338
|
+
keywords = [],
|
|
339
|
+
path = '/',
|
|
340
|
+
}: {
|
|
341
|
+
title: string;
|
|
342
|
+
description: string;
|
|
343
|
+
keywords?: string[];
|
|
344
|
+
path?: string;
|
|
345
|
+
}): Metadata {
|
|
346
|
+
const url = \`\${BASE_URL}\${path}\`;
|
|
347
|
+
const allKeywords = [...new Set([${keywordsStr}, ...keywords])];
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
title,
|
|
351
|
+
description,
|
|
352
|
+
keywords: allKeywords,
|
|
353
|
+
alternates: {
|
|
354
|
+
canonical: url,
|
|
355
|
+
},
|
|
356
|
+
openGraph: {
|
|
357
|
+
title,
|
|
358
|
+
description,
|
|
359
|
+
url,
|
|
360
|
+
type: 'website',
|
|
361
|
+
},
|
|
362
|
+
twitter: {
|
|
363
|
+
card: 'summary_large_image',
|
|
364
|
+
title,
|
|
365
|
+
description,
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
`;
|
|
370
|
+
}
|