@founderhq/next-blog 0.9.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.
@@ -0,0 +1,1983 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, relative, sep } from "node:path";
3
+ import { createEditableBlogTemplates } from "../registry.js";
4
+ export function choiceFromValue(value, name, choices) {
5
+ if (!value)
6
+ return undefined;
7
+ if (choices.includes(value))
8
+ return value;
9
+ throw new Error(`Use ${name} with one of: ${choices.join(", ")}.`);
10
+ }
11
+ function normalizeInstallRoutePrefix(prefix = "/blog") {
12
+ const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
13
+ if (!trimmed) {
14
+ throw new Error("Blog route prefix cannot be the site root in v1. Use /blog or another non-root prefix.");
15
+ }
16
+ return `/${trimmed}`;
17
+ }
18
+ function normalizeInstallAdminPath(adminPath = "/cms") {
19
+ const trimmed = adminPath.trim().replace(/^\/+|\/+$/g, "");
20
+ if (!trimmed) {
21
+ throw new Error("Payload admin path cannot be the site root. Use /cms or another non-root path.");
22
+ }
23
+ return `/${trimmed}`;
24
+ }
25
+ const OPTIONAL_MIGRATION_ENV = new Set([
26
+ "AZURE_STORAGE_CONNECTION_STRING",
27
+ "AZURE_STORAGE_CONTAINER_NAME",
28
+ "BLOB_READ_WRITE_TOKEN",
29
+ "GCS_BUCKET",
30
+ "GCS_PROJECT_ID",
31
+ "NEXT_PUBLIC_BLOG_ROUTE_PREFIX",
32
+ "NEXT_PUBLIC_SITE_NAME",
33
+ "NEXT_PUBLIC_SITE_URL",
34
+ "PAYLOAD_DATABASE_SCHEMA",
35
+ "PAYLOAD_MEDIA_PREFIX",
36
+ "R2_ACCOUNT_ID",
37
+ "R2_ACCESS_KEY_ID",
38
+ "R2_BUCKET",
39
+ "R2_SECRET_ACCESS_KEY",
40
+ "S3_ACCESS_KEY_ID",
41
+ "S3_BUCKET",
42
+ "S3_REGION",
43
+ "S3_SECRET_ACCESS_KEY",
44
+ "UPLOADTHING_TOKEN",
45
+ ]);
46
+ const COMMON_DEPENDENCIES = {
47
+ "@founderhq/next-blog": "^0.9.0",
48
+ "@founderhq/payload-cms-kit": "^0.9.0",
49
+ "@founderhq/payload-plugin": "^0.1.0",
50
+ "@payloadcms/richtext-lexical": "^3.85.0",
51
+ graphql: "^16.8.1",
52
+ payload: "^3.85.0",
53
+ sharp: "^0.34.0",
54
+ };
55
+ const NEXT_DEPENDENCIES = Object.assign(Object.assign({}, COMMON_DEPENDENCIES), { "@payloadcms/next": "^3.85.0" });
56
+ const DB_DEPENDENCIES = {
57
+ postgres: { "@payloadcms/db-postgres": "^3.85.0" },
58
+ "sqlite-local": { "@payloadcms/db-sqlite": "^3.85.0" },
59
+ turso: { "@payloadcms/db-sqlite": "^3.85.0" },
60
+ };
61
+ export const STORAGE_DEPENDENCIES = {
62
+ local: {},
63
+ s3: { "@payloadcms/storage-s3": "^3.85.0" },
64
+ r2: { "@payloadcms/storage-r2": "^3.85.0" },
65
+ "vercel-blob": { "@payloadcms/storage-vercel-blob": "^3.85.0" },
66
+ azure: { "@payloadcms/storage-azure": "^3.85.0" },
67
+ gcs: { "@payloadcms/storage-gcs": "^3.85.0" },
68
+ uploadthing: { "@payloadcms/storage-uploadthing": "^3.85.0" },
69
+ custom: {},
70
+ };
71
+ function readJson(path) {
72
+ try {
73
+ return JSON.parse(readFileSync(path, "utf8"));
74
+ }
75
+ catch (_a) {
76
+ return null;
77
+ }
78
+ }
79
+ function isUnsetEnvValue(value) {
80
+ if (!(value === null || value === void 0 ? void 0 : value.trim()))
81
+ return true;
82
+ return /replace-with|your-db|user:password@host/i.test(value);
83
+ }
84
+ function writeJson(value) {
85
+ return `${JSON.stringify(value, null, 2)}\n`;
86
+ }
87
+ function hasDependency(pkg, name) {
88
+ var _a;
89
+ const deps = pkg.dependencies;
90
+ const devDeps = pkg.devDependencies;
91
+ return Boolean((_a = deps === null || deps === void 0 ? void 0 : deps[name]) !== null && _a !== void 0 ? _a : devDeps === null || devDeps === void 0 ? void 0 : devDeps[name]);
92
+ }
93
+ function packageManagerFromPackageJson(pkg) {
94
+ const packageManager = typeof pkg.packageManager === "string" ? pkg.packageManager : null;
95
+ if (packageManager === null || packageManager === void 0 ? void 0 : packageManager.startsWith("pnpm@"))
96
+ return "pnpm";
97
+ if (packageManager === null || packageManager === void 0 ? void 0 : packageManager.startsWith("yarn@"))
98
+ return "yarn";
99
+ if (packageManager === null || packageManager === void 0 ? void 0 : packageManager.startsWith("bun@"))
100
+ return "bun";
101
+ if (packageManager === null || packageManager === void 0 ? void 0 : packageManager.startsWith("npm@"))
102
+ return "npm";
103
+ return null;
104
+ }
105
+ function detectPackageManager(projectRoot, pkg) {
106
+ var _a, _b, _c, _d;
107
+ return ((_d = (_c = (_b = (_a = packageManagerFromPackageJson(pkg)) !== null && _a !== void 0 ? _a : (existsSync(join(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : null)) !== null && _b !== void 0 ? _b : (existsSync(join(projectRoot, "yarn.lock")) ? "yarn" : null)) !== null && _c !== void 0 ? _c : (existsSync(join(projectRoot, "bun.lock")) ||
108
+ existsSync(join(projectRoot, "bun.lockb"))
109
+ ? "bun"
110
+ : null)) !== null && _d !== void 0 ? _d : "npm");
111
+ }
112
+ function packageManagerRunScript(projectRoot, pkg, script) {
113
+ const packageManager = detectPackageManager(projectRoot, pkg);
114
+ return `${packageManager} run ${script}`;
115
+ }
116
+ function packageManagerInstallCommand(projectRoot, pkg) {
117
+ const packageManager = detectPackageManager(projectRoot, pkg);
118
+ return `${packageManager} install`;
119
+ }
120
+ function needsPackageInstall(pkg, dependencies, devDependencies) {
121
+ return (Object.keys(dependencies).some((name) => !hasDependency(pkg, name)) ||
122
+ Object.keys(devDependencies).some((name) => !hasDependency(pkg, name)));
123
+ }
124
+ export function mergeMissingPackageEntries(pkg, dependencies, devDependencies) {
125
+ var _a, _b;
126
+ const next = Object.assign({}, pkg);
127
+ const nextDependencies = Object.assign({}, ((_a = pkg.dependencies) !== null && _a !== void 0 ? _a : {}));
128
+ const nextDevDependencies = Object.assign({}, ((_b = pkg.devDependencies) !== null && _b !== void 0 ? _b : {}));
129
+ const hasPackage = (name) => nextDependencies[name] !== undefined || nextDevDependencies[name] !== undefined;
130
+ for (const [name, version] of Object.entries(dependencies)) {
131
+ if (!hasPackage(name))
132
+ nextDependencies[name] = version;
133
+ }
134
+ for (const [name, version] of Object.entries(devDependencies)) {
135
+ if (!hasPackage(name))
136
+ nextDevDependencies[name] = version;
137
+ }
138
+ if (Object.keys(nextDependencies).length > 0) {
139
+ next.dependencies = nextDependencies;
140
+ }
141
+ else {
142
+ delete next.dependencies;
143
+ }
144
+ if (Object.keys(nextDevDependencies).length > 0) {
145
+ next.devDependencies = nextDevDependencies;
146
+ }
147
+ else {
148
+ delete next.devDependencies;
149
+ }
150
+ return next;
151
+ }
152
+ function installCommandForPlan(projectRoot, pkg, dependencies, devDependencies) {
153
+ return needsPackageInstall(pkg, dependencies, devDependencies)
154
+ ? packageManagerInstallCommand(projectRoot, pkg)
155
+ : null;
156
+ }
157
+ export function missingMigrationEnv(plan, env = process.env) {
158
+ return Object.keys(plan.env).filter((key) => !OPTIONAL_MIGRATION_ENV.has(key) && isUnsetEnvValue(env[key]));
159
+ }
160
+ export function detectAppDir(projectRoot) {
161
+ const srcApp = join(projectRoot, "src", "app");
162
+ if (existsSync(srcApp))
163
+ return srcApp;
164
+ const app = join(projectRoot, "app");
165
+ if (existsSync(app))
166
+ return app;
167
+ return null;
168
+ }
169
+ function dbImport(choice) {
170
+ if (choice === "postgres") {
171
+ return "import { postgresAdapter } from '@payloadcms/db-postgres'";
172
+ }
173
+ return "import { sqliteAdapter } from '@payloadcms/db-sqlite'";
174
+ }
175
+ function dbConfig(choice, schemaName) {
176
+ if (choice === "postgres") {
177
+ return `postgresAdapter({
178
+ pool: {
179
+ connectionString: process.env.PAYLOAD_DATABASE_URL || '',
180
+ },
181
+ schemaName: process.env.PAYLOAD_DATABASE_SCHEMA || '${schemaName}',
182
+ })`;
183
+ }
184
+ return `sqliteAdapter({
185
+ client: {
186
+ url: process.env.PAYLOAD_DATABASE_URL || 'file:./payload.db',
187
+ authToken: process.env.PAYLOAD_DATABASE_AUTH_TOKEN,
188
+ },
189
+ })`;
190
+ }
191
+ function storageImport(choice) {
192
+ switch (choice) {
193
+ case "s3":
194
+ return "import { s3Storage } from '@payloadcms/storage-s3'";
195
+ case "r2":
196
+ return "import { r2Storage } from '@payloadcms/storage-r2'";
197
+ case "vercel-blob":
198
+ return "import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'";
199
+ case "azure":
200
+ return "import { azureStorage } from '@payloadcms/storage-azure'";
201
+ case "gcs":
202
+ return "import { gcsStorage } from '@payloadcms/storage-gcs'";
203
+ case "uploadthing":
204
+ return "import { uploadthingStorage } from '@payloadcms/storage-uploadthing'";
205
+ default:
206
+ return null;
207
+ }
208
+ }
209
+ function storagePlugin(choice) {
210
+ switch (choice) {
211
+ case "s3":
212
+ return `s3Storage({
213
+ collections: { media: { prefix: process.env.PAYLOAD_MEDIA_PREFIX || 'blog' } },
214
+ bucket: process.env.S3_BUCKET || '',
215
+ config: {
216
+ region: process.env.S3_REGION,
217
+ credentials: {
218
+ accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
219
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
220
+ },
221
+ },
222
+ })`;
223
+ case "r2":
224
+ return `r2Storage({
225
+ collections: { media: { prefix: process.env.PAYLOAD_MEDIA_PREFIX || 'blog' } },
226
+ bucket: process.env.R2_BUCKET || '',
227
+ config: {
228
+ accountId: process.env.R2_ACCOUNT_ID || '',
229
+ accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
230
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
231
+ },
232
+ })`;
233
+ case "vercel-blob":
234
+ return `vercelBlobStorage({
235
+ collections: { media: { prefix: process.env.PAYLOAD_MEDIA_PREFIX || 'blog' } },
236
+ token: process.env.BLOB_READ_WRITE_TOKEN || '',
237
+ })`;
238
+ case "azure":
239
+ return `azureStorage({
240
+ collections: { media: true },
241
+ connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING || '',
242
+ containerName: process.env.AZURE_STORAGE_CONTAINER_NAME || '',
243
+ })`;
244
+ case "gcs":
245
+ return `gcsStorage({
246
+ collections: { media: true },
247
+ bucket: process.env.GCS_BUCKET || '',
248
+ options: {
249
+ projectId: process.env.GCS_PROJECT_ID,
250
+ },
251
+ })`;
252
+ case "uploadthing":
253
+ return `uploadthingStorage({
254
+ collections: { media: true },
255
+ token: process.env.UPLOADTHING_TOKEN || '',
256
+ })`;
257
+ case "custom":
258
+ return null;
259
+ case "local":
260
+ return null;
261
+ }
262
+ }
263
+ function storageFile(choice) {
264
+ if (choice === "custom") {
265
+ return `export function founderHQBlogStoragePlugins() {
266
+ // Add your custom Payload storage adapter plugin here before production uploads.
267
+ return []
268
+ }
269
+ `;
270
+ }
271
+ const importLine = storageImport(choice);
272
+ const plugin = storagePlugin(choice);
273
+ if (!plugin) {
274
+ return `export function founderHQBlogStoragePlugins() {
275
+ return []
276
+ }
277
+ `;
278
+ }
279
+ return `${importLine !== null && importLine !== void 0 ? importLine : ""}
280
+ export function founderHQBlogStoragePlugins() {
281
+ return [
282
+ ${plugin},
283
+ ]
284
+ }
285
+ `;
286
+ }
287
+ function payloadConfig(options) {
288
+ var _a, _b;
289
+ const schemaName = (_a = options.postgresSchema) !== null && _a !== void 0 ? _a : "payload";
290
+ return `import { lexicalEditor } from '@payloadcms/richtext-lexical'
291
+ ${dbImport(options.db)}
292
+ import { buildConfig } from 'payload'
293
+ import sharp from 'sharp'
294
+ import {
295
+ createFounderHQBlogCollections,
296
+ createFounderHQBlogGlobals,
297
+ founderHQBlogRichTextFeatures,
298
+ } from '@founderhq/payload-cms-kit'
299
+ import { founderHQBlogStoragePlugins } from './founderhq-blog.storage'
300
+
301
+ export default buildConfig({
302
+ admin: {
303
+ user: 'cms-users',
304
+ routes: {
305
+ admin: ${JSON.stringify((_b = options.adminPath) !== null && _b !== void 0 ? _b : "/cms")},
306
+ },
307
+ },
308
+ collections: [
309
+ {
310
+ slug: 'cms-users',
311
+ auth: true,
312
+ fields: [],
313
+ },
314
+ ...createFounderHQBlogCollections(),
315
+ ],
316
+ globals: createFounderHQBlogGlobals(),
317
+ editor: lexicalEditor({
318
+ features: founderHQBlogRichTextFeatures(),
319
+ }),
320
+ db: ${dbConfig(options.db, schemaName)},
321
+ plugins: founderHQBlogStoragePlugins(),
322
+ secret: process.env.PAYLOAD_SECRET || '',
323
+ sharp,
324
+ })
325
+ `;
326
+ }
327
+ function payloadRestRoute() {
328
+ return `import config from '@payload-config'
329
+ import {
330
+ REST_DELETE,
331
+ REST_GET,
332
+ REST_OPTIONS,
333
+ REST_PATCH,
334
+ REST_POST,
335
+ REST_PUT,
336
+ } from '@payloadcms/next/routes'
337
+
338
+ export const GET = REST_GET(config)
339
+ export const POST = REST_POST(config)
340
+ export const DELETE = REST_DELETE(config)
341
+ export const PATCH = REST_PATCH(config)
342
+ export const PUT = REST_PUT(config)
343
+ export const OPTIONS = REST_OPTIONS(config)
344
+ `;
345
+ }
346
+ function payloadImportMap() {
347
+ return `/** @type {import('payload').ImportMap} */
348
+ export const importMap = {}
349
+ `;
350
+ }
351
+ function payloadAdminPage() {
352
+ return `import type { Metadata } from 'next'
353
+ import config from '@payload-config'
354
+ import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
355
+ import { importMap } from '../importMap.js'
356
+
357
+ type Args = {
358
+ params: Promise<{ segments: string[] }>
359
+ searchParams: Promise<{ [key: string]: string | string[] }>
360
+ }
361
+
362
+ export const generateMetadata = ({
363
+ params,
364
+ searchParams,
365
+ }: Args): Promise<Metadata> =>
366
+ generatePageMetadata({ config, params, searchParams })
367
+
368
+ const Page = ({ params, searchParams }: Args) =>
369
+ RootPage({ config, params, searchParams, importMap })
370
+
371
+ export default Page
372
+ `;
373
+ }
374
+ function payloadLayout(adminSegment) {
375
+ return `import config from '@payload-config'
376
+ import '@payloadcms/next/css'
377
+ import type { ServerFunctionClient } from 'payload'
378
+ import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
379
+ import React from 'react'
380
+ import { importMap } from './${adminSegment}/importMap.js'
381
+ import './custom.css'
382
+
383
+ type Args = {
384
+ children: React.ReactNode
385
+ }
386
+
387
+ const serverFunction: ServerFunctionClient = async function (args) {
388
+ 'use server'
389
+ return handleServerFunctions({
390
+ ...args,
391
+ config,
392
+ importMap,
393
+ })
394
+ }
395
+
396
+ const Layout = ({ children }: Args) => (
397
+ <RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
398
+ {children}
399
+ </RootLayout>
400
+ )
401
+
402
+ export default Layout
403
+ `;
404
+ }
405
+ function patchNextConfig(content) {
406
+ if (content.includes("withPayload("))
407
+ return content;
408
+ if (!content.includes("export default"))
409
+ return null;
410
+ const importLine = "import { withPayload } from '@payloadcms/next/withPayload'\n";
411
+ const withImport = content.includes("@payloadcms/next/withPayload")
412
+ ? content
413
+ : `${importLine}${content}`;
414
+ return `${withImport.replace(/export\s+default\s+/, "const founderHQNextConfig = ")}
415
+
416
+ export default withPayload(founderHQNextConfig)
417
+ `;
418
+ }
419
+ function nextConfigChange(projectRoot) {
420
+ const candidates = ["next.config.ts", "next.config.mjs", "next.config.js"];
421
+ for (const candidate of candidates) {
422
+ const path = join(projectRoot, candidate);
423
+ if (!existsSync(path))
424
+ continue;
425
+ const content = readFileSync(path, "utf8");
426
+ const patched = patchNextConfig(content);
427
+ if (!patched)
428
+ return null;
429
+ return { kind: "update", path: candidate, content: patched };
430
+ }
431
+ const unsupported = ["next.config.cjs", "next.config.mts", "next.config.cts"].find((candidate) => existsSync(join(projectRoot, candidate)));
432
+ if (unsupported) {
433
+ throw new Error(`Existing ${unsupported} was found. The v1 installer only patches ESM export-default Next configs. Wrap your config with withPayload manually or convert it before running the installer.`);
434
+ }
435
+ return {
436
+ kind: "create",
437
+ path: "next.config.mjs",
438
+ content: `import { withPayload } from '@payloadcms/next/withPayload'
439
+
440
+ /** @type {import('next').NextConfig} */
441
+ const nextConfig = {}
442
+
443
+ export default withPayload(nextConfig)
444
+ `,
445
+ };
446
+ }
447
+ function tsconfigChange(projectRoot) {
448
+ const path = join(projectRoot, "tsconfig.json");
449
+ const existing = readJson(path);
450
+ const next = existing !== null && existing !== void 0 ? existing : { compilerOptions: {} };
451
+ const compilerOptions = next.compilerOptions && typeof next.compilerOptions === "object"
452
+ ? next.compilerOptions
453
+ : {};
454
+ const paths = compilerOptions.paths && typeof compilerOptions.paths === "object"
455
+ ? compilerOptions.paths
456
+ : {};
457
+ paths["@payload-config"] = ["./payload.config.ts"];
458
+ compilerOptions.paths = paths;
459
+ next.compilerOptions = compilerOptions;
460
+ return {
461
+ kind: existing ? "update" : "create",
462
+ path: "tsconfig.json",
463
+ content: writeJson(next),
464
+ };
465
+ }
466
+ function existingPayloadConfig(projectRoot) {
467
+ return [
468
+ "payload.config.ts",
469
+ "payload.config.mts",
470
+ "payload.config.js",
471
+ "payload.config.mjs",
472
+ "payload.config.cjs",
473
+ ].find((candidate) => existsSync(join(projectRoot, candidate)));
474
+ }
475
+ function blogDataFile() {
476
+ return `import {
477
+ filterBlogArticlesByAuthor,
478
+ filterBlogArticlesByCategory,
479
+ filterBlogArticlesByTag,
480
+ fetchAllPayloadBlogPosts,
481
+ fetchPayloadBlogAuthorBySlug,
482
+ fetchPayloadBlogAuthors,
483
+ fetchPayloadBlogCategories,
484
+ fetchPayloadBlogCategoryBySlug,
485
+ fetchPayloadBlogPostBySlug,
486
+ fetchPayloadBlogPostByPreviousSlug,
487
+ fetchPayloadBlogPosts,
488
+ fetchPayloadBlogRedirect,
489
+ fetchPayloadBlogTagBySlug,
490
+ fetchPayloadBlogTags,
491
+ paginateBlogArticles,
492
+ } from '@founderhq/next-blog/server'
493
+
494
+ export const founderHQBlogSite = {
495
+ siteName: process.env.NEXT_PUBLIC_SITE_NAME || 'Blog',
496
+ siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',
497
+ routePrefix: process.env.NEXT_PUBLIC_BLOG_ROUTE_PREFIX || '/blog',
498
+ }
499
+
500
+ const payloadBaseUrl =
501
+ process.env.PAYLOAD_PUBLIC_SERVER_URL ||
502
+ process.env.NEXT_PUBLIC_SITE_URL ||
503
+ 'http://localhost:3000'
504
+
505
+ export const founderHQBlogPageSize = 12
506
+
507
+ export function listBlogPosts(page = 1) {
508
+ return fetchPayloadBlogPosts({
509
+ baseUrl: payloadBaseUrl,
510
+ limit: founderHQBlogPageSize,
511
+ page,
512
+ })
513
+ }
514
+
515
+ export function listAllBlogPosts() {
516
+ return fetchAllPayloadBlogPosts({ baseUrl: payloadBaseUrl, limit: 100 })
517
+ }
518
+
519
+ export function listBlogAuthors() {
520
+ return fetchPayloadBlogAuthors({ baseUrl: payloadBaseUrl, limit: 100 })
521
+ }
522
+
523
+ export function listBlogCategories() {
524
+ return fetchPayloadBlogCategories({ baseUrl: payloadBaseUrl, limit: 100 })
525
+ }
526
+
527
+ export function listBlogTags() {
528
+ return fetchPayloadBlogTags({ baseUrl: payloadBaseUrl, limit: 100 })
529
+ }
530
+
531
+ export function getBlogPostBySlug(slug: string) {
532
+ return fetchPayloadBlogPostBySlug({ baseUrl: payloadBaseUrl, slug })
533
+ }
534
+
535
+ export function getBlogAuthorBySlug(slug: string) {
536
+ return fetchPayloadBlogAuthorBySlug({ baseUrl: payloadBaseUrl, slug })
537
+ }
538
+
539
+ export function getBlogCategoryBySlug(slug: string) {
540
+ return fetchPayloadBlogCategoryBySlug({ baseUrl: payloadBaseUrl, slug })
541
+ }
542
+
543
+ export function getBlogTagBySlug(slug: string) {
544
+ return fetchPayloadBlogTagBySlug({ baseUrl: payloadBaseUrl, slug })
545
+ }
546
+
547
+ export async function listBlogPostsByAuthor(slug: string, page = 1) {
548
+ const author = await getBlogAuthorBySlug(slug)
549
+ if (!author) return null
550
+ const posts = await listAllBlogPosts()
551
+ return {
552
+ author,
553
+ posts: paginateBlogArticles(
554
+ filterBlogArticlesByAuthor(posts.docs, author),
555
+ page,
556
+ founderHQBlogPageSize,
557
+ ),
558
+ }
559
+ }
560
+
561
+ export async function listBlogPostsByCategory(slug: string, page = 1) {
562
+ const category = await getBlogCategoryBySlug(slug)
563
+ if (!category) return null
564
+ const posts = await listAllBlogPosts()
565
+ return {
566
+ category,
567
+ posts: paginateBlogArticles(
568
+ filterBlogArticlesByCategory(posts.docs, category),
569
+ page,
570
+ founderHQBlogPageSize,
571
+ ),
572
+ }
573
+ }
574
+
575
+ export async function listBlogPostsByTag(slug: string, page = 1) {
576
+ const tag = await getBlogTagBySlug(slug)
577
+ if (!tag) return null
578
+ const posts = await listAllBlogPosts()
579
+ return {
580
+ tag,
581
+ posts: paginateBlogArticles(
582
+ filterBlogArticlesByTag(posts.docs, tag),
583
+ page,
584
+ founderHQBlogPageSize,
585
+ ),
586
+ }
587
+ }
588
+
589
+ export function getBlogPostByPreviousSlug(slug: string) {
590
+ return fetchPayloadBlogPostByPreviousSlug({ baseUrl: payloadBaseUrl, slug })
591
+ }
592
+
593
+ export function getBlogRedirect(path: string) {
594
+ return fetchPayloadBlogRedirect({ baseUrl: payloadBaseUrl, path })
595
+ }
596
+ `;
597
+ }
598
+ function importPath(fromFile, toFile) {
599
+ const raw = relative(dirname(fromFile), toFile)
600
+ .replaceAll(sep, "/")
601
+ .replace(/\.(ts|tsx)$/, "");
602
+ return raw.startsWith(".") ? raw : `./${raw}`;
603
+ }
604
+ function blogIndexPage(blogDataImport, blogComponentsImport, blogStylesImport) {
605
+ return `import {
606
+ absoluteUrl,
607
+ buildBlogPagination,
608
+ normalizeRoutePrefix,
609
+ } from '@founderhq/next-blog/server'
610
+ import { BlogIndex } from '${blogComponentsImport}'
611
+ import { founderHQBlogSite, listBlogPosts } from '${blogDataImport}'
612
+ import '${blogStylesImport}'
613
+
614
+ export function generateMetadata() {
615
+ const canonical = absoluteUrl(
616
+ founderHQBlogSite.siteUrl,
617
+ normalizeRoutePrefix(founderHQBlogSite.routePrefix),
618
+ )
619
+ const title = \`\${founderHQBlogSite.siteName} Blog\`
620
+ const description = \`\${founderHQBlogSite.siteName} articles\`
621
+
622
+ return {
623
+ title,
624
+ description,
625
+ alternates: { canonical },
626
+ openGraph: {
627
+ type: 'website',
628
+ url: canonical,
629
+ siteName: founderHQBlogSite.siteName,
630
+ title,
631
+ description,
632
+ },
633
+ twitter: {
634
+ card: 'summary_large_image',
635
+ title,
636
+ description,
637
+ },
638
+ }
639
+ }
640
+
641
+ export default async function BlogPage() {
642
+ const posts = await listBlogPosts()
643
+ return (
644
+ <BlogIndex
645
+ articles={posts.docs}
646
+ site={founderHQBlogSite}
647
+ pagination={buildBlogPagination({
648
+ site: founderHQBlogSite,
649
+ page: posts.page ?? 1,
650
+ totalPages: posts.totalPages ?? 1,
651
+ totalDocs: posts.totalDocs,
652
+ })}
653
+ />
654
+ )
655
+ }
656
+ `;
657
+ }
658
+ function blogPaginatedIndexPage(blogDataImport, blogComponentsImport, blogStylesImport) {
659
+ return `import {
660
+ absoluteUrl,
661
+ buildBlogPagination,
662
+ normalizeRoutePrefix,
663
+ } from '@founderhq/next-blog/server'
664
+ import { BlogIndex } from '${blogComponentsImport}'
665
+ import { founderHQBlogSite, listBlogPosts } from '${blogDataImport}'
666
+ import { notFound, permanentRedirect } from 'next/navigation'
667
+ import '${blogStylesImport}'
668
+
669
+ function pageNumber(value: string) {
670
+ const page = Number(value)
671
+ return Number.isInteger(page) && page > 0 ? page : null
672
+ }
673
+
674
+ export async function generateMetadata({
675
+ params,
676
+ }: {
677
+ params: Promise<{ page: string }>
678
+ }) {
679
+ const { page: rawPage } = await params
680
+ const page = pageNumber(rawPage)
681
+ if (!page) return {}
682
+ const canonical = absoluteUrl(
683
+ founderHQBlogSite.siteUrl,
684
+ \`\${normalizeRoutePrefix(founderHQBlogSite.routePrefix)}/page/\${page}\`,
685
+ )
686
+ const title = \`\${founderHQBlogSite.siteName} Blog - Page \${page}\`
687
+ const description = \`\${founderHQBlogSite.siteName} articles, page \${page}\`
688
+
689
+ return {
690
+ title,
691
+ description,
692
+ alternates: { canonical },
693
+ openGraph: {
694
+ type: 'website',
695
+ url: canonical,
696
+ siteName: founderHQBlogSite.siteName,
697
+ title,
698
+ description,
699
+ },
700
+ twitter: {
701
+ card: 'summary_large_image',
702
+ title,
703
+ description,
704
+ },
705
+ }
706
+ }
707
+
708
+ export default async function BlogPaginatedPage({
709
+ params,
710
+ }: {
711
+ params: Promise<{ page: string }>
712
+ }) {
713
+ const { page: rawPage } = await params
714
+ const page = pageNumber(rawPage)
715
+ if (!page) notFound()
716
+ if (page === 1) permanentRedirect(normalizeRoutePrefix(founderHQBlogSite.routePrefix) || '/')
717
+ const posts = await listBlogPosts(page)
718
+ if (page > (posts.totalPages ?? 1)) notFound()
719
+
720
+ return (
721
+ <BlogIndex
722
+ articles={posts.docs}
723
+ site={founderHQBlogSite}
724
+ title={\`\${founderHQBlogSite.siteName} Blog - Page \${page}\`}
725
+ pagination={buildBlogPagination({
726
+ site: founderHQBlogSite,
727
+ page,
728
+ totalPages: posts.totalPages ?? 1,
729
+ totalDocs: posts.totalDocs,
730
+ })}
731
+ />
732
+ )
733
+ }
734
+ `;
735
+ }
736
+ function blogArticlePage(blogDataImport, blogComponentsImport, blogStylesImport) {
737
+ return `import { articlePath, normalizeRoutePrefix } from '@founderhq/next-blog'
738
+ import { BlogArticleLayout } from '${blogComponentsImport}'
739
+ import {
740
+ buildArticleJsonLd,
741
+ buildArticleMetadata,
742
+ } from '@founderhq/next-blog/server'
743
+ import { notFound, permanentRedirect, redirect } from 'next/navigation'
744
+ import {
745
+ founderHQBlogSite,
746
+ getBlogPostByPreviousSlug,
747
+ getBlogPostBySlug,
748
+ getBlogRedirect,
749
+ } from '${blogDataImport}'
750
+ import '${blogStylesImport}'
751
+
752
+ async function redirectMissingPost(slug: string) {
753
+ const legacyPath = \`\${normalizeRoutePrefix(founderHQBlogSite.routePrefix)}/\${slug}\`
754
+ const manualRedirect = await getBlogRedirect(legacyPath)
755
+ if (manualRedirect?.to) {
756
+ const statusCode = Number(manualRedirect.statusCode ?? 301)
757
+ if (statusCode === 301 || statusCode === 308) {
758
+ permanentRedirect(String(manualRedirect.to))
759
+ }
760
+ redirect(String(manualRedirect.to))
761
+ }
762
+
763
+ const movedPost = await getBlogPostByPreviousSlug(slug)
764
+ if (movedPost) {
765
+ permanentRedirect(articlePath(movedPost, founderHQBlogSite))
766
+ }
767
+ }
768
+
769
+ export async function generateMetadata({
770
+ params,
771
+ }: {
772
+ params: Promise<{ slug: string }>
773
+ }) {
774
+ const { slug } = await params
775
+ const post = await getBlogPostBySlug(slug)
776
+ if (!post) return {}
777
+ const metadata = buildArticleMetadata(post, founderHQBlogSite)
778
+ return {
779
+ title: metadata.title,
780
+ description: metadata.description,
781
+ alternates: { canonical: metadata.canonical },
782
+ robots: metadata.robots,
783
+ openGraph: metadata.openGraph,
784
+ twitter: metadata.twitter,
785
+ }
786
+ }
787
+
788
+ export default async function BlogArticlePage({
789
+ params,
790
+ }: {
791
+ params: Promise<{ slug: string }>
792
+ }) {
793
+ const { slug } = await params
794
+ const post = await getBlogPostBySlug(slug)
795
+ if (!post) {
796
+ await redirectMissingPost(slug)
797
+ notFound()
798
+ }
799
+ return (
800
+ <>
801
+ <script
802
+ type="application/ld+json"
803
+ dangerouslySetInnerHTML={{
804
+ __html: JSON.stringify(buildArticleJsonLd(post, founderHQBlogSite)).replace(
805
+ /</g,
806
+ '\\\\u003c',
807
+ ),
808
+ }}
809
+ />
810
+ <BlogArticleLayout article={post} site={founderHQBlogSite} />
811
+ </>
812
+ )
813
+ }
814
+ `;
815
+ }
816
+ function blogSearchPage(blogDataImport, blogComponentsImport, blogStylesImport) {
817
+ return `import { absoluteUrl, normalizeRoutePrefix } from '@founderhq/next-blog/server'
818
+ import { BlogIndex } from '${blogComponentsImport}'
819
+ import { searchBlogArticles } from '@founderhq/next-blog/server'
820
+ import { founderHQBlogSite, listAllBlogPosts } from '${blogDataImport}'
821
+ import '${blogStylesImport}'
822
+
823
+ export function generateMetadata() {
824
+ const canonical = absoluteUrl(
825
+ founderHQBlogSite.siteUrl,
826
+ \`\${normalizeRoutePrefix(founderHQBlogSite.routePrefix)}/search\`,
827
+ )
828
+
829
+ return {
830
+ title: \`Search \${founderHQBlogSite.siteName} Blog\`,
831
+ alternates: { canonical },
832
+ robots: {
833
+ index: false,
834
+ follow: true,
835
+ googleBot: {
836
+ index: false,
837
+ follow: true,
838
+ },
839
+ },
840
+ }
841
+ }
842
+
843
+ export default async function BlogSearchPage({
844
+ searchParams,
845
+ }: {
846
+ searchParams: Promise<{ q?: string | string[] }>
847
+ }) {
848
+ const { q: rawQuery = '' } = await searchParams
849
+ const q = Array.isArray(rawQuery) ? rawQuery[0] ?? '' : rawQuery
850
+ const posts = await listAllBlogPosts()
851
+ const results = q ? searchBlogArticles(posts.docs, q) : []
852
+ return <BlogIndex articles={results} site={founderHQBlogSite} searchInitialQuery={q} />
853
+ }
854
+ `;
855
+ }
856
+ function blogDirectoryPage(type, blogDataImport, blogComponentsImport, blogStylesImport) {
857
+ const listFn = type === "authors"
858
+ ? "listBlogAuthors"
859
+ : type === "categories"
860
+ ? "listBlogCategories"
861
+ : "listBlogTags";
862
+ const pathFn = type === "authors" ? "authorPath" : type === "categories" ? "categoryPath" : "tagPath";
863
+ const title = type === "authors" ? "Authors" : type === "categories" ? "Categories" : "Tags";
864
+ const itemTitle = type === "authors" ? "item.name" : "item.title";
865
+ const itemDescription = type === "authors" ? "item.bio ?? item.title" : "item.description";
866
+ const itemImage = type === "authors" ? "image: item.avatar," : "";
867
+ return `import { absoluteUrl, normalizeRoutePrefix, ${pathFn} } from '@founderhq/next-blog/server'
868
+ import { BlogDirectory } from '${blogComponentsImport}'
869
+ import { founderHQBlogSite, ${listFn} } from '${blogDataImport}'
870
+ import '${blogStylesImport}'
871
+
872
+ export function generateMetadata() {
873
+ const canonical = absoluteUrl(
874
+ founderHQBlogSite.siteUrl,
875
+ \`\${normalizeRoutePrefix(founderHQBlogSite.routePrefix)}/${type}\`,
876
+ )
877
+ const title = \`\${founderHQBlogSite.siteName} Blog ${title}\`
878
+ return {
879
+ title,
880
+ alternates: { canonical },
881
+ openGraph: {
882
+ type: 'website',
883
+ url: canonical,
884
+ siteName: founderHQBlogSite.siteName,
885
+ title,
886
+ },
887
+ }
888
+ }
889
+
890
+ export default async function BlogDirectoryPage() {
891
+ const items = await ${listFn}()
892
+ return (
893
+ <BlogDirectory
894
+ title="${title}"
895
+ items={items.docs.map((item) => ({
896
+ title: ${itemTitle},
897
+ href: ${pathFn}(item, founderHQBlogSite),
898
+ description: ${itemDescription},
899
+ ${itemImage}
900
+ }))}
901
+ />
902
+ )
903
+ }
904
+ `;
905
+ }
906
+ function blogArchivePage(type, paginated, blogDataImport, blogComponentsImport, blogStylesImport) {
907
+ const listFn = type === "author"
908
+ ? "listBlogPostsByAuthor"
909
+ : type === "category"
910
+ ? "listBlogPostsByCategory"
911
+ : "listBlogPostsByTag";
912
+ const pathFn = type === "author" ? "authorPath" : type === "category" ? "categoryPath" : "tagPath";
913
+ const entity = type;
914
+ const titleExpr = type === "author"
915
+ ? "archive.author.name"
916
+ : type === "category"
917
+ ? "archive.category.title"
918
+ : "archive.tag.title";
919
+ const descriptionExpr = type === "author"
920
+ ? "archive.author.bio"
921
+ : type === "category"
922
+ ? "archive.category.description"
923
+ : "archive.tag.description";
924
+ const entityExpr = type === "author"
925
+ ? "archive.author"
926
+ : type === "category"
927
+ ? "archive.category"
928
+ : "archive.tag";
929
+ const pageParam = paginated ? ", page" : "";
930
+ const routeParams = paginated
931
+ ? "params: Promise<{ slug: string; page: string }>"
932
+ : "params: Promise<{ slug: string }>";
933
+ const pageParser = paginated
934
+ ? `
935
+ function pageNumber(value: string) {
936
+ const page = Number(value)
937
+ return Number.isInteger(page) && page > 0 ? page : null
938
+ }
939
+ `
940
+ : "";
941
+ const metadataPage = paginated
942
+ ? `
943
+ const page = pageNumber(rawPage)
944
+ if (!page) return {}
945
+ `
946
+ : "";
947
+ const pageRead = paginated
948
+ ? " const { slug, page: rawPage } = await params"
949
+ : " const { slug } = await params";
950
+ const pageRuntime = paginated
951
+ ? `
952
+ const page = pageNumber(rawPage)
953
+ if (!page) notFound()
954
+ `
955
+ : " const page = 1\n";
956
+ const redirectPageOne = paginated
957
+ ? `
958
+ if (page === 1) permanentRedirect(${pathFn}(${entityExpr}, founderHQBlogSite))
959
+ `
960
+ : "";
961
+ const pageTitle = paginated
962
+ ? `\`\${${titleExpr}} articles - Page \${page}\``
963
+ : `\`\${${titleExpr}} articles\``;
964
+ return `import {
965
+ absoluteUrl,
966
+ buildBlogPagination,
967
+ ${pathFn},
968
+ } from '@founderhq/next-blog/server'
969
+ import { BlogIndex } from '${blogComponentsImport}'
970
+ import { founderHQBlogSite, ${listFn} } from '${blogDataImport}'
971
+ import { notFound${paginated ? ", permanentRedirect" : ""} } from 'next/navigation'
972
+ import '${blogStylesImport}'
973
+ ${pageParser}
974
+ export async function generateMetadata({
975
+ params,
976
+ }: {
977
+ ${routeParams}
978
+ }) {
979
+ ${pageRead}
980
+ ${metadataPage} const archive = await ${listFn}(slug${pageParam})
981
+ if (!archive) return {}
982
+ const canonical = absoluteUrl(
983
+ founderHQBlogSite.siteUrl,
984
+ ${paginated ? `\`\${${pathFn}(${entityExpr}, founderHQBlogSite)}/page/\${page}\`` : `${pathFn}(${entityExpr}, founderHQBlogSite)`},
985
+ )
986
+ const title = ${pageTitle}
987
+ const description = ${descriptionExpr} ?? title
988
+
989
+ return {
990
+ title,
991
+ description,
992
+ alternates: { canonical },
993
+ openGraph: {
994
+ type: 'website',
995
+ url: canonical,
996
+ siteName: founderHQBlogSite.siteName,
997
+ title,
998
+ description,
999
+ },
1000
+ }
1001
+ }
1002
+
1003
+ export default async function BlogArchivePage({
1004
+ params,
1005
+ }: {
1006
+ ${routeParams}
1007
+ }) {
1008
+ ${pageRead}
1009
+ ${pageRuntime} const archive = await ${listFn}(slug${pageParam})
1010
+ if (!archive) notFound()
1011
+ ${redirectPageOne} if (page > (archive.posts.totalPages ?? 1)) notFound()
1012
+ const basePath = ${pathFn}(${entityExpr}, founderHQBlogSite)
1013
+
1014
+ return (
1015
+ <BlogIndex
1016
+ articles={archive.posts.docs}
1017
+ site={founderHQBlogSite}
1018
+ title={${pageTitle}}
1019
+ description={${descriptionExpr}}
1020
+ pagination={buildBlogPagination({
1021
+ site: founderHQBlogSite,
1022
+ page,
1023
+ totalPages: archive.posts.totalPages ?? 1,
1024
+ totalDocs: archive.posts.totalDocs,
1025
+ basePath,
1026
+ })}
1027
+ />
1028
+ )
1029
+ }
1030
+ `;
1031
+ }
1032
+ function rssRoute(blogDataImport) {
1033
+ return `import { buildRssXml } from '@founderhq/next-blog/server'
1034
+ import { founderHQBlogSite, listAllBlogPosts } from '${blogDataImport}'
1035
+
1036
+ export async function GET() {
1037
+ const posts = await listAllBlogPosts()
1038
+ return new Response(
1039
+ buildRssXml({ site: founderHQBlogSite, articles: posts.docs }),
1040
+ {
1041
+ headers: {
1042
+ 'content-type': 'application/rss+xml; charset=utf-8',
1043
+ },
1044
+ },
1045
+ )
1046
+ }
1047
+ `;
1048
+ }
1049
+ function blogPostsSitemapRoute(blogDataImport) {
1050
+ return `import {
1051
+ buildBlogIndexSitemapEntries,
1052
+ buildBlogSitemapEntries,
1053
+ buildSitemapUrlsetXml,
1054
+ } from '@founderhq/next-blog/server'
1055
+ import {
1056
+ founderHQBlogPageSize,
1057
+ founderHQBlogSite,
1058
+ listAllBlogPosts,
1059
+ } from '${blogDataImport}'
1060
+
1061
+ export const dynamic = 'force-dynamic'
1062
+
1063
+ export async function GET() {
1064
+ const posts = await listAllBlogPosts()
1065
+ const totalPages = Math.max(
1066
+ 1,
1067
+ Math.ceil((posts.totalDocs ?? posts.docs.length) / founderHQBlogPageSize),
1068
+ )
1069
+ const entries = [
1070
+ ...buildBlogIndexSitemapEntries(founderHQBlogSite, totalPages),
1071
+ ...buildBlogSitemapEntries(founderHQBlogSite, posts.docs),
1072
+ ]
1073
+
1074
+ return new Response(buildSitemapUrlsetXml(entries), {
1075
+ headers: {
1076
+ 'content-type': 'application/xml; charset=utf-8',
1077
+ 'cache-control': 's-maxage=3600, stale-while-revalidate=86400',
1078
+ },
1079
+ })
1080
+ }
1081
+ `;
1082
+ }
1083
+ function blogTaxonomySitemapRoute(type, blogDataImport) {
1084
+ const listFn = type === "authors"
1085
+ ? "listBlogAuthors"
1086
+ : type === "categories"
1087
+ ? "listBlogCategories"
1088
+ : "listBlogTags";
1089
+ const buildFn = type === "authors"
1090
+ ? "buildBlogAuthorSitemapEntries"
1091
+ : type === "categories"
1092
+ ? "buildBlogCategorySitemapEntries"
1093
+ : "buildBlogTagSitemapEntries";
1094
+ const filterFn = type === "authors"
1095
+ ? "filterBlogArticlesByAuthor"
1096
+ : type === "categories"
1097
+ ? "filterBlogArticlesByCategory"
1098
+ : "filterBlogArticlesByTag";
1099
+ const pathFn = type === "authors" ? "authorPath" : type === "categories" ? "categoryPath" : "tagPath";
1100
+ const directoryPath = `\`\${normalizeRoutePrefix(founderHQBlogSite.routePrefix)}/${type}\``;
1101
+ return `import {
1102
+ absoluteUrl,
1103
+ ${buildFn},
1104
+ buildBlogPaginatedSitemapEntries,
1105
+ buildSitemapUrlsetXml,
1106
+ ${filterFn},
1107
+ normalizeRoutePrefix,
1108
+ ${pathFn},
1109
+ } from '@founderhq/next-blog/server'
1110
+ import {
1111
+ founderHQBlogPageSize,
1112
+ founderHQBlogSite,
1113
+ listAllBlogPosts,
1114
+ ${listFn},
1115
+ } from '${blogDataImport}'
1116
+
1117
+ export const dynamic = 'force-dynamic'
1118
+
1119
+ export async function GET() {
1120
+ const [items, posts] = await Promise.all([${listFn}(), listAllBlogPosts()])
1121
+ const entries = [
1122
+ { url: absoluteUrl(founderHQBlogSite.siteUrl, ${directoryPath}) },
1123
+ ...${buildFn}(founderHQBlogSite, items.docs),
1124
+ ...items.docs.flatMap((item) => {
1125
+ if (!item.slug) return []
1126
+ return buildBlogPaginatedSitemapEntries(
1127
+ founderHQBlogSite,
1128
+ ${pathFn}(item, founderHQBlogSite),
1129
+ ${filterFn}(posts.docs, item).length,
1130
+ founderHQBlogPageSize,
1131
+ ).slice(1)
1132
+ }),
1133
+ ]
1134
+
1135
+ return new Response(buildSitemapUrlsetXml(entries), {
1136
+ headers: {
1137
+ 'content-type': 'application/xml; charset=utf-8',
1138
+ 'cache-control': 's-maxage=3600, stale-while-revalidate=86400',
1139
+ },
1140
+ })
1141
+ }
1142
+ `;
1143
+ }
1144
+ function rootSitemapIndexRoute(blogDataImport) {
1145
+ return `import {
1146
+ absoluteUrl,
1147
+ buildSitemapIndexXml,
1148
+ } from '@founderhq/next-blog/server'
1149
+ import { founderHQBlogSite } from '${blogDataImport}'
1150
+
1151
+ export async function GET() {
1152
+ return new Response(
1153
+ buildSitemapIndexXml([
1154
+ {
1155
+ url: absoluteUrl(founderHQBlogSite.siteUrl, '/sitemaps/blog-posts.xml'),
1156
+ },
1157
+ {
1158
+ url: absoluteUrl(founderHQBlogSite.siteUrl, '/sitemaps/blog-authors.xml'),
1159
+ },
1160
+ {
1161
+ url: absoluteUrl(founderHQBlogSite.siteUrl, '/sitemaps/blog-categories.xml'),
1162
+ },
1163
+ {
1164
+ url: absoluteUrl(founderHQBlogSite.siteUrl, '/sitemaps/blog-tags.xml'),
1165
+ },
1166
+ ]),
1167
+ {
1168
+ headers: {
1169
+ 'content-type': 'application/xml; charset=utf-8',
1170
+ 'cache-control': 's-maxage=3600, stale-while-revalidate=86400',
1171
+ },
1172
+ },
1173
+ )
1174
+ }
1175
+ `;
1176
+ }
1177
+ function robotsMetadataRoute(blogDataImport) {
1178
+ return `import { buildBlogRobots } from '@founderhq/next-blog/server'
1179
+ import { founderHQBlogSite } from '${blogDataImport}'
1180
+
1181
+ export default function robots() {
1182
+ return buildBlogRobots(founderHQBlogSite)
1183
+ }
1184
+ `;
1185
+ }
1186
+ function absoluteFromSite(siteUrl, path) {
1187
+ return new URL(path, `${siteUrl.replace(/\/$/, "")}/`).toString();
1188
+ }
1189
+ function escapeXml(value) {
1190
+ return value
1191
+ .replace(/&/g, "&amp;")
1192
+ .replace(/</g, "&lt;")
1193
+ .replace(/>/g, "&gt;")
1194
+ .replace(/"/g, "&quot;")
1195
+ .replace(/'/g, "&apos;");
1196
+ }
1197
+ function patchStaticRobots(content, sitemapUrl) {
1198
+ if (/^\s*sitemap\s*:/im.test(content))
1199
+ return content;
1200
+ return `${content.trimEnd()}\nSitemap: ${sitemapUrl}\n`;
1201
+ }
1202
+ function patchMetadataRobots(content, sitemapUrl) {
1203
+ if (content.includes("sitemap"))
1204
+ return content;
1205
+ const functionMatch = /export\s+default\s+(?:async\s+)?function\s+robots\s*\(/.exec(content);
1206
+ if (!functionMatch)
1207
+ return null;
1208
+ const openBrace = content.indexOf("{", functionMatch.index);
1209
+ if (openBrace === -1)
1210
+ return null;
1211
+ const closeBrace = findMatching(content, openBrace, "{", "}");
1212
+ if (closeBrace === -1)
1213
+ return null;
1214
+ const body = content.slice(openBrace + 1, closeBrace);
1215
+ const returnIndexInBody = body.indexOf("return");
1216
+ if (returnIndexInBody === -1)
1217
+ return null;
1218
+ const absoluteReturnIndex = openBrace + 1 + returnIndexInBody;
1219
+ const objectStart = content.indexOf("{", absoluteReturnIndex);
1220
+ if (objectStart === -1 || objectStart > closeBrace)
1221
+ return null;
1222
+ const objectEnd = findMatching(content, objectStart, "{", "}");
1223
+ if (objectEnd === -1 || objectEnd > closeBrace)
1224
+ return null;
1225
+ if (content.slice(objectEnd + 1, closeBrace).replace(/[);]|\s/g, "")) {
1226
+ return null;
1227
+ }
1228
+ const objectBody = content.slice(objectStart + 1, objectEnd);
1229
+ const needsComma = objectBody.trim().length > 0 && !objectBody.trimEnd().endsWith(",");
1230
+ return `${content.slice(0, objectEnd)}${needsComma ? "," : ""}\n sitemap: ${JSON.stringify(sitemapUrl)},${content.slice(objectEnd)}`;
1231
+ }
1232
+ function patchStaticSitemapIndex(content, urls) {
1233
+ if (!/<sitemapindex\b/i.test(content))
1234
+ return null;
1235
+ const additions = urls
1236
+ .filter((url) => !content.includes(url))
1237
+ .map((url) => ` <sitemap><loc>${escapeXml(url)}</loc></sitemap>`);
1238
+ if (additions.length === 0)
1239
+ return content;
1240
+ const patched = content.replace(/<\/sitemapindex>\s*$/i, `${additions.join("\n")}\n</sitemapindex>\n`);
1241
+ return patched === content ? null : patched;
1242
+ }
1243
+ function findMatching(source, openIndex, open, close) {
1244
+ let depth = 0;
1245
+ let quote = null;
1246
+ let escaped = false;
1247
+ let lineComment = false;
1248
+ let blockComment = false;
1249
+ for (let index = openIndex; index < source.length; index += 1) {
1250
+ const char = source[index];
1251
+ const next = source[index + 1];
1252
+ if (lineComment) {
1253
+ if (char === "\n")
1254
+ lineComment = false;
1255
+ continue;
1256
+ }
1257
+ if (blockComment) {
1258
+ if (char === "*" && next === "/") {
1259
+ blockComment = false;
1260
+ index += 1;
1261
+ }
1262
+ continue;
1263
+ }
1264
+ if (quote) {
1265
+ if (escaped) {
1266
+ escaped = false;
1267
+ continue;
1268
+ }
1269
+ if (char === "\\") {
1270
+ escaped = true;
1271
+ continue;
1272
+ }
1273
+ if (char === quote)
1274
+ quote = null;
1275
+ continue;
1276
+ }
1277
+ if (char === "/" && next === "/") {
1278
+ lineComment = true;
1279
+ index += 1;
1280
+ continue;
1281
+ }
1282
+ if (char === "/" && next === "*") {
1283
+ blockComment = true;
1284
+ index += 1;
1285
+ continue;
1286
+ }
1287
+ if (char === "'" || char === '"' || char === "`") {
1288
+ quote = char;
1289
+ continue;
1290
+ }
1291
+ if (char === open)
1292
+ depth += 1;
1293
+ if (char === close) {
1294
+ depth -= 1;
1295
+ if (depth === 0)
1296
+ return index;
1297
+ }
1298
+ }
1299
+ return -1;
1300
+ }
1301
+ function patchMetadataSitemap(content, blogDataImport) {
1302
+ if (content.includes("founderHQBlogSitemapMetadata"))
1303
+ return content;
1304
+ const functionMatch = /export\s+default\s+(?:async\s+)?function\s+sitemap\s*\(/.exec(content);
1305
+ if (!functionMatch)
1306
+ return null;
1307
+ const openBrace = content.indexOf("{", functionMatch.index);
1308
+ if (openBrace === -1)
1309
+ return null;
1310
+ const closeBrace = findMatching(content, openBrace, "{", "}");
1311
+ if (closeBrace === -1)
1312
+ return null;
1313
+ const body = content.slice(openBrace + 1, closeBrace);
1314
+ const returnIndexInBody = body.indexOf("return");
1315
+ if (returnIndexInBody === -1)
1316
+ return null;
1317
+ if (body.slice(0, returnIndexInBody).trim())
1318
+ return null;
1319
+ const absoluteReturnIndex = openBrace + 1 + returnIndexInBody;
1320
+ const arrayStart = content.indexOf("[", absoluteReturnIndex);
1321
+ if (arrayStart === -1 || arrayStart > closeBrace)
1322
+ return null;
1323
+ const arrayEnd = findMatching(content, arrayStart, "[", "]");
1324
+ if (arrayEnd === -1 || arrayEnd > closeBrace)
1325
+ return null;
1326
+ if (content.slice(arrayEnd + 1, closeBrace).replace(/;|\s/g, ""))
1327
+ return null;
1328
+ const existingArray = content.slice(arrayStart, arrayEnd + 1);
1329
+ const beforeFunction = content.slice(0, functionMatch.index).trimEnd();
1330
+ const afterFunction = content.slice(closeBrace + 1).trimStart();
1331
+ const metadataImport = content.includes("MetadataRoute")
1332
+ ? ""
1333
+ : "import type { MetadataRoute } from 'next'\n";
1334
+ const blogImports = `import {
1335
+ authorPath,
1336
+ buildBlogAuthorSitemapEntries,
1337
+ buildBlogCategorySitemapEntries,
1338
+ buildBlogDirectorySitemapEntries,
1339
+ buildBlogIndexSitemapEntries,
1340
+ buildBlogPaginatedSitemapEntries,
1341
+ buildBlogSitemapEntries,
1342
+ buildBlogTagSitemapEntries,
1343
+ categoryPath,
1344
+ filterBlogArticlesByAuthor,
1345
+ filterBlogArticlesByCategory,
1346
+ filterBlogArticlesByTag,
1347
+ tagPath,
1348
+ type BlogSitemapEntry,
1349
+ } from '@founderhq/next-blog/server'
1350
+ import {
1351
+ founderHQBlogPageSize,
1352
+ founderHQBlogSite,
1353
+ listAllBlogPosts,
1354
+ listBlogAuthors,
1355
+ listBlogCategories,
1356
+ listBlogTags,
1357
+ } from '${blogDataImport}'
1358
+ `;
1359
+ const helper = `function founderHQBlogSitemapMetadata(
1360
+ entries: BlogSitemapEntry[],
1361
+ ): MetadataRoute.Sitemap {
1362
+ return entries.map((entry) => ({
1363
+ url: entry.url,
1364
+ ...(entry.lastModified ? { lastModified: new Date(entry.lastModified) } : {}),
1365
+ }))
1366
+ }
1367
+ `;
1368
+ const nextFunction = `export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
1369
+ const founderHQExistingSitemap: MetadataRoute.Sitemap = ${existingArray}
1370
+ const [
1371
+ founderHQBlogPosts,
1372
+ founderHQBlogAuthors,
1373
+ founderHQBlogCategories,
1374
+ founderHQBlogTags,
1375
+ ] = await Promise.all([
1376
+ listAllBlogPosts(),
1377
+ listBlogAuthors(),
1378
+ listBlogCategories(),
1379
+ listBlogTags(),
1380
+ ])
1381
+ const founderHQBlogIndexPages = Math.max(
1382
+ 1,
1383
+ Math.ceil(
1384
+ (founderHQBlogPosts.totalDocs ?? founderHQBlogPosts.docs.length) /
1385
+ founderHQBlogPageSize,
1386
+ ),
1387
+ )
1388
+
1389
+ return [
1390
+ ...founderHQExistingSitemap,
1391
+ ...founderHQBlogSitemapMetadata(
1392
+ buildBlogIndexSitemapEntries(founderHQBlogSite, founderHQBlogIndexPages),
1393
+ ),
1394
+ ...founderHQBlogSitemapMetadata(
1395
+ buildBlogDirectorySitemapEntries(founderHQBlogSite),
1396
+ ),
1397
+ ...founderHQBlogSitemapMetadata(
1398
+ buildBlogSitemapEntries(founderHQBlogSite, founderHQBlogPosts.docs),
1399
+ ),
1400
+ ...founderHQBlogSitemapMetadata(
1401
+ buildBlogAuthorSitemapEntries(founderHQBlogSite, founderHQBlogAuthors.docs),
1402
+ ),
1403
+ ...founderHQBlogSitemapMetadata(
1404
+ founderHQBlogAuthors.docs.flatMap((author) => {
1405
+ if (!author.slug) return []
1406
+ return buildBlogPaginatedSitemapEntries(
1407
+ founderHQBlogSite,
1408
+ authorPath(author, founderHQBlogSite),
1409
+ filterBlogArticlesByAuthor(founderHQBlogPosts.docs, author).length,
1410
+ founderHQBlogPageSize,
1411
+ ).slice(1)
1412
+ }),
1413
+ ),
1414
+ ...founderHQBlogSitemapMetadata(
1415
+ buildBlogCategorySitemapEntries(
1416
+ founderHQBlogSite,
1417
+ founderHQBlogCategories.docs,
1418
+ ),
1419
+ ),
1420
+ ...founderHQBlogSitemapMetadata(
1421
+ founderHQBlogCategories.docs.flatMap((category) =>
1422
+ buildBlogPaginatedSitemapEntries(
1423
+ founderHQBlogSite,
1424
+ categoryPath(category, founderHQBlogSite),
1425
+ filterBlogArticlesByCategory(founderHQBlogPosts.docs, category).length,
1426
+ founderHQBlogPageSize,
1427
+ ).slice(1),
1428
+ ),
1429
+ ),
1430
+ ...founderHQBlogSitemapMetadata(
1431
+ buildBlogTagSitemapEntries(founderHQBlogSite, founderHQBlogTags.docs),
1432
+ ),
1433
+ ...founderHQBlogSitemapMetadata(
1434
+ founderHQBlogTags.docs.flatMap((tag) =>
1435
+ buildBlogPaginatedSitemapEntries(
1436
+ founderHQBlogSite,
1437
+ tagPath(tag, founderHQBlogSite),
1438
+ filterBlogArticlesByTag(founderHQBlogPosts.docs, tag).length,
1439
+ founderHQBlogPageSize,
1440
+ ).slice(1),
1441
+ ),
1442
+ ),
1443
+ ]
1444
+ }
1445
+ `;
1446
+ return [
1447
+ metadataImport,
1448
+ blogImports,
1449
+ beforeFunction,
1450
+ helper,
1451
+ nextFunction,
1452
+ afterFunction,
1453
+ ]
1454
+ .filter((part) => part.trim().length > 0)
1455
+ .join("\n\n")
1456
+ .replace(/\n{3,}/g, "\n\n")
1457
+ .trimEnd()
1458
+ .concat("\n");
1459
+ }
1460
+ function envEntries(options) {
1461
+ var _a, _b, _c;
1462
+ const routePrefix = normalizeInstallRoutePrefix((_a = options.routePrefix) !== null && _a !== void 0 ? _a : "/blog");
1463
+ const base = {
1464
+ PAYLOAD_SECRET: "replace-with-a-long-random-secret",
1465
+ PAYLOAD_DATABASE_URL: options.db === "postgres"
1466
+ ? "postgres://user:password@host:5432/database"
1467
+ : options.db === "turso"
1468
+ ? "libsql://your-db.turso.io"
1469
+ : "file:./payload.db",
1470
+ NEXT_PUBLIC_SITE_NAME: "Blog",
1471
+ NEXT_PUBLIC_SITE_URL: "http://localhost:3000",
1472
+ NEXT_PUBLIC_BLOG_ROUTE_PREFIX: routePrefix,
1473
+ };
1474
+ if (options.db === "turso") {
1475
+ base.PAYLOAD_DATABASE_AUTH_TOKEN = "replace-with-turso-token";
1476
+ }
1477
+ if (options.db === "postgres") {
1478
+ base.PAYLOAD_DATABASE_SCHEMA = (_b = options.postgresSchema) !== null && _b !== void 0 ? _b : "payload";
1479
+ }
1480
+ if (options.storage === "s3") {
1481
+ Object.assign(base, {
1482
+ S3_BUCKET: "",
1483
+ S3_REGION: "",
1484
+ S3_ACCESS_KEY_ID: "",
1485
+ S3_SECRET_ACCESS_KEY: "",
1486
+ PAYLOAD_MEDIA_PREFIX: "blog",
1487
+ });
1488
+ }
1489
+ if (options.storage === "r2") {
1490
+ Object.assign(base, {
1491
+ R2_BUCKET: "",
1492
+ R2_ACCOUNT_ID: "",
1493
+ R2_ACCESS_KEY_ID: "",
1494
+ R2_SECRET_ACCESS_KEY: "",
1495
+ PAYLOAD_MEDIA_PREFIX: "blog",
1496
+ });
1497
+ }
1498
+ if (options.storage === "vercel-blob") {
1499
+ base.BLOB_READ_WRITE_TOKEN = "";
1500
+ }
1501
+ if (options.storage === "azure") {
1502
+ base.AZURE_STORAGE_CONNECTION_STRING = "";
1503
+ base.AZURE_STORAGE_CONTAINER_NAME = "";
1504
+ }
1505
+ if (options.storage === "gcs") {
1506
+ base.GCS_BUCKET = "";
1507
+ base.GCS_PROJECT_ID = "";
1508
+ }
1509
+ if (options.storage === "uploadthing") {
1510
+ base.UPLOADTHING_TOKEN = "";
1511
+ }
1512
+ for (const [key, value] of Object.entries((_c = options.env) !== null && _c !== void 0 ? _c : {})) {
1513
+ if (typeof value !== "string")
1514
+ continue;
1515
+ base[key] =
1516
+ key === "NEXT_PUBLIC_BLOG_ROUTE_PREFIX"
1517
+ ? normalizeInstallRoutePrefix(value)
1518
+ : value;
1519
+ }
1520
+ return base;
1521
+ }
1522
+ function installScripts() {
1523
+ return {
1524
+ "payload:migrate": "payload migrate",
1525
+ "payload:migrate:create": "payload migrate:create",
1526
+ "payload:generate-types": "payload generate:types",
1527
+ };
1528
+ }
1529
+ function assertNoGeneratedFileCollisions(projectRoot, changes) {
1530
+ const collisions = changes.filter((change) => change.kind === "create" && existsSync(join(projectRoot, change.path)));
1531
+ if (collisions.length > 0) {
1532
+ throw new Error(`Refusing to install because generated files already exist: ${collisions
1533
+ .map((change) => change.path)
1534
+ .join(", ")}. Move those files or choose different installer paths before applying.`);
1535
+ }
1536
+ }
1537
+ function nonNextIntegrationReadme(options) {
1538
+ var _a, _b;
1539
+ const routePrefix = normalizeInstallRoutePrefix((_a = options.routePrefix) !== null && _a !== void 0 ? _a : "/blog");
1540
+ const adminPath = normalizeInstallAdminPath((_b = options.adminPath) !== null && _b !== void 0 ? _b : "/cms");
1541
+ return `# FounderHQ Payload Blog Kit
1542
+
1543
+ This project is not a Next.js App Router app, so the installer generated the
1544
+ Payload CMS and framework-neutral blog data helpers only.
1545
+
1546
+ ## Generated files
1547
+
1548
+ - \`payload.config.ts\`: Payload CMS config with FounderHQ blog collections.
1549
+ - \`founderhq-blog.storage.ts\`: selected Payload upload storage adapter.
1550
+ - \`founderhq-blog.ts\`: framework-neutral fetch helpers and site tokens.
1551
+
1552
+ ## Routes to wire in your framework
1553
+
1554
+ - Payload REST API: \`/api/*\`
1555
+ - Payload admin: \`${adminPath}\`
1556
+ - Blog index: \`${routePrefix}\`
1557
+ - Blog index pagination: \`${routePrefix}/page/:page\`
1558
+ - Blog article: \`${routePrefix}/:slug\`
1559
+ - Blog search: \`${routePrefix}/search?q=term\`
1560
+ - Author directory: \`${routePrefix}/authors\`
1561
+ - Author archive: \`${routePrefix}/authors/:slug\`
1562
+ - Author archive pagination: \`${routePrefix}/authors/:slug/page/:page\`
1563
+ - Category directory: \`${routePrefix}/categories\`
1564
+ - Category archive: \`${routePrefix}/categories/:slug\`
1565
+ - Category archive pagination: \`${routePrefix}/categories/:slug/page/:page\`
1566
+ - Tag directory: \`${routePrefix}/tags\`
1567
+ - Tag archive: \`${routePrefix}/tags/:slug\`
1568
+ - Tag archive pagination: \`${routePrefix}/tags/:slug/page/:page\`
1569
+ - RSS feed: \`${routePrefix}/rss.xml\`
1570
+ - Sitemap index: \`/sitemap.xml\`
1571
+ - Blog sitemap URL sets:
1572
+ - \`/sitemaps/blog-posts.xml\`
1573
+ - \`/sitemaps/blog-authors.xml\`
1574
+ - \`/sitemaps/blog-categories.xml\`
1575
+ - \`/sitemaps/blog-tags.xml\`
1576
+
1577
+ Use \`@founderhq/next-blog/server\` for URL, metadata, JSON-LD, RSS, sitemap,
1578
+ search, and Payload REST helpers. The React templates are intentionally not
1579
+ installed because this project is not a supported Next App Router surface.
1580
+ `;
1581
+ }
1582
+ function createManualInstallPlan(options, pkg) {
1583
+ var _a, _b;
1584
+ const routePrefix = normalizeInstallRoutePrefix((_a = options.routePrefix) !== null && _a !== void 0 ? _a : "/blog");
1585
+ const adminPath = normalizeInstallAdminPath((_b = options.adminPath) !== null && _b !== void 0 ? _b : "/cms");
1586
+ const resolvedOptions = Object.assign(Object.assign({}, options), { adminPath, routePrefix });
1587
+ const existingPayloadConfigPath = existingPayloadConfig(options.projectRoot);
1588
+ if (existingPayloadConfigPath) {
1589
+ throw new Error(`Existing ${existingPayloadConfigPath} was found. The installer will not overwrite an existing Payload config. Add @founderhq/payload-cms-kit manually with createFounderHQBlogCollections(), or move the existing config before running the installer.`);
1590
+ }
1591
+ const env = envEntries(resolvedOptions);
1592
+ const changes = [
1593
+ {
1594
+ kind: "create",
1595
+ path: "payload.config.ts",
1596
+ content: payloadConfig(resolvedOptions),
1597
+ },
1598
+ {
1599
+ kind: "create",
1600
+ path: "founderhq-blog.storage.ts",
1601
+ content: storageFile(resolvedOptions.storage),
1602
+ },
1603
+ tsconfigChange(options.projectRoot),
1604
+ {
1605
+ kind: "create",
1606
+ path: "founderhq-blog.ts",
1607
+ content: blogDataFile(),
1608
+ },
1609
+ {
1610
+ kind: "create",
1611
+ path: "FOUNDERHQ_BLOG_INTEGRATION.md",
1612
+ content: nonNextIntegrationReadme(resolvedOptions),
1613
+ },
1614
+ ];
1615
+ assertNoGeneratedFileCollisions(options.projectRoot, changes);
1616
+ const dependencies = Object.assign(Object.assign(Object.assign({}, COMMON_DEPENDENCIES), DB_DEPENDENCIES[resolvedOptions.db]), STORAGE_DEPENDENCIES[resolvedOptions.storage]);
1617
+ const devDependencies = {};
1618
+ const warnings = [
1619
+ "This project is not a Next.js App Router app. The installer generated Payload and framework-neutral helpers only; wire the public routes in your framework.",
1620
+ "The installer uses PAYLOAD_DATABASE_URL and never reuses DATABASE_URL automatically.",
1621
+ "Review generated Payload files before running migrations against production data.",
1622
+ ...(resolvedOptions.storage === "custom"
1623
+ ? [
1624
+ "Custom/manual storage selected. founderhq-blog.storage.ts starts as a no-op so migrations and admin boot; add your storage adapter before production uploads.",
1625
+ ]
1626
+ : []),
1627
+ ];
1628
+ return {
1629
+ appDir: "manual",
1630
+ dependencies,
1631
+ devDependencies,
1632
+ env,
1633
+ changes,
1634
+ scripts: installScripts(),
1635
+ installCommand: installCommandForPlan(options.projectRoot, pkg, dependencies, devDependencies),
1636
+ migrationCommand: resolvedOptions.migration === "run"
1637
+ ? packageManagerRunScript(options.projectRoot, pkg, "payload:migrate")
1638
+ : null,
1639
+ warnings,
1640
+ };
1641
+ }
1642
+ export function createInstallPlan(options) {
1643
+ var _a, _b;
1644
+ const pkgPath = join(options.projectRoot, "package.json");
1645
+ const pkg = readJson(pkgPath);
1646
+ if (!pkg)
1647
+ throw new Error("package.json was not found or is invalid.");
1648
+ if (!hasDependency(pkg, "next")) {
1649
+ return createManualInstallPlan(options, pkg);
1650
+ }
1651
+ const existingPayloadConfigPath = existingPayloadConfig(options.projectRoot);
1652
+ if (existingPayloadConfigPath) {
1653
+ throw new Error(`Existing ${existingPayloadConfigPath} was found. The v1 installer will not overwrite an existing Payload config. Add @founderhq/payload-cms-kit manually with founderHQBlogPlugin(), or move the existing config before running the installer.`);
1654
+ }
1655
+ const appDir = detectAppDir(options.projectRoot);
1656
+ if (!appDir) {
1657
+ throw new Error("No app/ or src/app/ directory found.");
1658
+ }
1659
+ const routePrefix = normalizeInstallRoutePrefix((_a = options.routePrefix) !== null && _a !== void 0 ? _a : "/blog");
1660
+ const adminPath = normalizeInstallAdminPath((_b = options.adminPath) !== null && _b !== void 0 ? _b : "/cms");
1661
+ const resolvedOptions = Object.assign(Object.assign({}, options), { adminPath, routePrefix });
1662
+ const routeSegment = routePrefix.replace(/^\/+/, "").replace(/\/+$/, "") || "blog";
1663
+ const adminSegment = adminPath.replace(/^\/+/, "").replace(/\/+$/, "") || "cms";
1664
+ const env = envEntries(resolvedOptions);
1665
+ const sourceRoot = appDir.endsWith(join("src", "app"))
1666
+ ? join(options.projectRoot, "src")
1667
+ : options.projectRoot;
1668
+ const libDir = join(sourceRoot, "lib");
1669
+ const blogComponentsPath = join(sourceRoot, "components", "blog", "blog-components.tsx");
1670
+ const blogStylesPath = join(sourceRoot, "components", "blog", "blog.css");
1671
+ const blogDataPath = join(libDir, "founderhq-blog.ts");
1672
+ const indexPagePath = join(appDir, routeSegment, "page.tsx");
1673
+ const paginatedIndexPagePath = join(appDir, routeSegment, "page", "[page]", "page.tsx");
1674
+ const articlePagePath = join(appDir, routeSegment, "[slug]", "page.tsx");
1675
+ const searchPagePath = join(appDir, routeSegment, "search", "page.tsx");
1676
+ const authorsPagePath = join(appDir, routeSegment, "authors", "page.tsx");
1677
+ const authorArchivePagePath = join(appDir, routeSegment, "authors", "[slug]", "page.tsx");
1678
+ const authorPaginatedArchivePagePath = join(appDir, routeSegment, "authors", "[slug]", "page", "[page]", "page.tsx");
1679
+ const categoriesPagePath = join(appDir, routeSegment, "categories", "page.tsx");
1680
+ const categoryArchivePagePath = join(appDir, routeSegment, "categories", "[slug]", "page.tsx");
1681
+ const categoryPaginatedArchivePagePath = join(appDir, routeSegment, "categories", "[slug]", "page", "[page]", "page.tsx");
1682
+ const tagsPagePath = join(appDir, routeSegment, "tags", "page.tsx");
1683
+ const tagArchivePagePath = join(appDir, routeSegment, "tags", "[slug]", "page.tsx");
1684
+ const tagPaginatedArchivePagePath = join(appDir, routeSegment, "tags", "[slug]", "page", "[page]", "page.tsx");
1685
+ const rssRoutePath = join(appDir, routeSegment, "rss.xml", "route.ts");
1686
+ const blogPostsSitemapRoutePath = join(appDir, "sitemaps", "blog-posts.xml", "route.ts");
1687
+ const blogAuthorsSitemapRoutePath = join(appDir, "sitemaps", "blog-authors.xml", "route.ts");
1688
+ const blogCategoriesSitemapRoutePath = join(appDir, "sitemaps", "blog-categories.xml", "route.ts");
1689
+ const blogTagsSitemapRoutePath = join(appDir, "sitemaps", "blog-tags.xml", "route.ts");
1690
+ const rootSitemapRoutePath = join(appDir, "sitemap.xml", "route.ts");
1691
+ const metadataSitemapPath = [
1692
+ join(appDir, "sitemap.ts"),
1693
+ join(appDir, "sitemap.tsx"),
1694
+ join(appDir, "sitemap.js"),
1695
+ ].find((path) => existsSync(path));
1696
+ const robotsPath = [
1697
+ join(appDir, "robots.ts"),
1698
+ join(appDir, "robots.tsx"),
1699
+ join(appDir, "robots.js"),
1700
+ ].find((path) => existsSync(path));
1701
+ const rootRobotsPath = join(appDir, "robots.ts");
1702
+ const staticRobotsPath = join(options.projectRoot, "public", "robots.txt");
1703
+ const staticSitemapPath = join(options.projectRoot, "public", "sitemap.xml");
1704
+ const childSitemapUrls = [
1705
+ "/sitemaps/blog-posts.xml",
1706
+ "/sitemaps/blog-authors.xml",
1707
+ "/sitemaps/blog-categories.xml",
1708
+ "/sitemaps/blog-tags.xml",
1709
+ ].map((path) => absoluteFromSite(env.NEXT_PUBLIC_SITE_URL, path));
1710
+ const payloadGroupDir = join(appDir, "(payload)");
1711
+ const payloadAdminDir = join(payloadGroupDir, adminSegment);
1712
+ const relativeApp = (path) => relative(options.projectRoot, path);
1713
+ const configPatch = nextConfigChange(options.projectRoot);
1714
+ const blogTemplates = createEditableBlogTemplates();
1715
+ const blogComponents = blogTemplates.find((template) => template.name === "components");
1716
+ const blogStyles = blogTemplates.find((template) => template.name === "styles");
1717
+ if (!blogComponents || !blogStyles) {
1718
+ throw new Error("FounderHQ editable blog templates are incomplete.");
1719
+ }
1720
+ const robotsChanges = [];
1721
+ const robotsWarnings = [];
1722
+ if (robotsPath) {
1723
+ const existing = readFileSync(robotsPath, "utf8");
1724
+ if (!existing.includes("sitemap")) {
1725
+ const patched = patchMetadataRobots(existing, absoluteFromSite(env.NEXT_PUBLIC_SITE_URL, "/sitemap.xml"));
1726
+ if (patched) {
1727
+ robotsChanges.push({
1728
+ kind: "update",
1729
+ path: relativeApp(robotsPath),
1730
+ content: patched,
1731
+ });
1732
+ }
1733
+ else {
1734
+ robotsWarnings.push("Existing app/robots metadata file could not be patched safely and does not appear to define a sitemap. Add your root /sitemap.xml URL to robots manually.");
1735
+ }
1736
+ }
1737
+ }
1738
+ else if (existsSync(staticRobotsPath)) {
1739
+ const existing = readFileSync(staticRobotsPath, "utf8");
1740
+ if (!/sitemap\s*:/i.test(existing)) {
1741
+ robotsChanges.push({
1742
+ kind: "update",
1743
+ path: relativeApp(staticRobotsPath),
1744
+ content: patchStaticRobots(existing, absoluteFromSite(env.NEXT_PUBLIC_SITE_URL, "/sitemap.xml")),
1745
+ });
1746
+ }
1747
+ }
1748
+ else {
1749
+ robotsChanges.push({
1750
+ kind: "create",
1751
+ path: relativeApp(rootRobotsPath),
1752
+ content: robotsMetadataRoute(importPath(rootRobotsPath, blogDataPath)),
1753
+ });
1754
+ }
1755
+ const sitemapChanges = [
1756
+ {
1757
+ kind: "create",
1758
+ path: relativeApp(blogPostsSitemapRoutePath),
1759
+ content: blogPostsSitemapRoute(importPath(blogPostsSitemapRoutePath, blogDataPath)),
1760
+ },
1761
+ {
1762
+ kind: "create",
1763
+ path: relativeApp(blogAuthorsSitemapRoutePath),
1764
+ content: blogTaxonomySitemapRoute("authors", importPath(blogAuthorsSitemapRoutePath, blogDataPath)),
1765
+ },
1766
+ {
1767
+ kind: "create",
1768
+ path: relativeApp(blogCategoriesSitemapRoutePath),
1769
+ content: blogTaxonomySitemapRoute("categories", importPath(blogCategoriesSitemapRoutePath, blogDataPath)),
1770
+ },
1771
+ {
1772
+ kind: "create",
1773
+ path: relativeApp(blogTagsSitemapRoutePath),
1774
+ content: blogTaxonomySitemapRoute("tags", importPath(blogTagsSitemapRoutePath, blogDataPath)),
1775
+ },
1776
+ ];
1777
+ const sitemapWarnings = [];
1778
+ if (metadataSitemapPath) {
1779
+ const existing = readFileSync(metadataSitemapPath, "utf8");
1780
+ const patched = patchMetadataSitemap(existing, importPath(metadataSitemapPath, blogDataPath));
1781
+ if (patched) {
1782
+ sitemapChanges.push({
1783
+ kind: "update",
1784
+ path: relativeApp(metadataSitemapPath),
1785
+ content: patched,
1786
+ });
1787
+ }
1788
+ else {
1789
+ sitemapWarnings.push("Existing app/sitemap metadata file could not be patched safely. The blog child sitemaps were generated at /sitemaps/blog-*.xml; add their indexable blog URLs to your existing sitemap manually.");
1790
+ }
1791
+ }
1792
+ else if (existsSync(rootSitemapRoutePath)) {
1793
+ const existing = readFileSync(rootSitemapRoutePath, "utf8");
1794
+ if (!childSitemapUrls.every((url) => existing.includes(url)) && ![
1795
+ "/sitemaps/blog-posts.xml",
1796
+ "/sitemaps/blog-authors.xml",
1797
+ "/sitemaps/blog-categories.xml",
1798
+ "/sitemaps/blog-tags.xml",
1799
+ ].every((url) => existing.includes(url))) {
1800
+ sitemapWarnings.push("Existing app/sitemap.xml/route.ts was left unchanged because it could not be patched safely. Add the generated /sitemaps/blog-*.xml URLs to that sitemap index manually.");
1801
+ }
1802
+ }
1803
+ else if (existsSync(staticSitemapPath)) {
1804
+ const existing = readFileSync(staticSitemapPath, "utf8");
1805
+ const patched = patchStaticSitemapIndex(existing, childSitemapUrls);
1806
+ if (patched) {
1807
+ sitemapChanges.push({
1808
+ kind: "update",
1809
+ path: relativeApp(staticSitemapPath),
1810
+ content: patched,
1811
+ });
1812
+ }
1813
+ else {
1814
+ sitemapWarnings.push("Existing public/sitemap.xml could not be patched safely. The blog child sitemaps were generated at /sitemaps/blog-*.xml; add them to the sitemap index manually, or convert a static URL set into a sitemap index.");
1815
+ }
1816
+ }
1817
+ else {
1818
+ sitemapChanges.push({
1819
+ kind: "create",
1820
+ path: relativeApp(rootSitemapRoutePath),
1821
+ content: rootSitemapIndexRoute(importPath(rootSitemapRoutePath, blogDataPath)),
1822
+ });
1823
+ }
1824
+ const changes = [
1825
+ {
1826
+ kind: "create",
1827
+ path: "payload.config.ts",
1828
+ content: payloadConfig(resolvedOptions),
1829
+ },
1830
+ {
1831
+ kind: "create",
1832
+ path: "founderhq-blog.storage.ts",
1833
+ content: storageFile(resolvedOptions.storage),
1834
+ },
1835
+ tsconfigChange(options.projectRoot),
1836
+ ...(configPatch ? [configPatch] : []),
1837
+ {
1838
+ kind: "create",
1839
+ path: relativeApp(join(payloadGroupDir, "api", "[...slug]", "route.ts")),
1840
+ content: payloadRestRoute(),
1841
+ },
1842
+ {
1843
+ kind: "create",
1844
+ path: relativeApp(join(payloadGroupDir, "layout.tsx")),
1845
+ content: payloadLayout(adminSegment),
1846
+ },
1847
+ {
1848
+ kind: "create",
1849
+ path: relativeApp(join(payloadGroupDir, "custom.css")),
1850
+ content: "",
1851
+ },
1852
+ {
1853
+ kind: "create",
1854
+ path: relativeApp(join(payloadAdminDir, "importMap.js")),
1855
+ content: payloadImportMap(),
1856
+ },
1857
+ {
1858
+ kind: "create",
1859
+ path: relativeApp(join(payloadAdminDir, "[[...segments]]", "page.tsx")),
1860
+ content: payloadAdminPage(),
1861
+ },
1862
+ {
1863
+ kind: "create",
1864
+ path: relativeApp(blogDataPath),
1865
+ content: blogDataFile(),
1866
+ },
1867
+ {
1868
+ kind: "create",
1869
+ path: relativeApp(blogComponentsPath),
1870
+ content: blogComponents.content,
1871
+ },
1872
+ {
1873
+ kind: "create",
1874
+ path: relativeApp(blogStylesPath),
1875
+ content: blogStyles.content,
1876
+ },
1877
+ {
1878
+ kind: "create",
1879
+ path: relativeApp(indexPagePath),
1880
+ content: blogIndexPage(importPath(indexPagePath, blogDataPath), importPath(indexPagePath, blogComponentsPath), importPath(indexPagePath, blogStylesPath)),
1881
+ },
1882
+ {
1883
+ kind: "create",
1884
+ path: relativeApp(paginatedIndexPagePath),
1885
+ content: blogPaginatedIndexPage(importPath(paginatedIndexPagePath, blogDataPath), importPath(paginatedIndexPagePath, blogComponentsPath), importPath(paginatedIndexPagePath, blogStylesPath)),
1886
+ },
1887
+ {
1888
+ kind: "create",
1889
+ path: relativeApp(articlePagePath),
1890
+ content: blogArticlePage(importPath(articlePagePath, blogDataPath), importPath(articlePagePath, blogComponentsPath), importPath(articlePagePath, blogStylesPath)),
1891
+ },
1892
+ {
1893
+ kind: "create",
1894
+ path: relativeApp(searchPagePath),
1895
+ content: blogSearchPage(importPath(searchPagePath, blogDataPath), importPath(searchPagePath, blogComponentsPath), importPath(searchPagePath, blogStylesPath)),
1896
+ },
1897
+ {
1898
+ kind: "create",
1899
+ path: relativeApp(authorsPagePath),
1900
+ content: blogDirectoryPage("authors", importPath(authorsPagePath, blogDataPath), importPath(authorsPagePath, blogComponentsPath), importPath(authorsPagePath, blogStylesPath)),
1901
+ },
1902
+ {
1903
+ kind: "create",
1904
+ path: relativeApp(authorArchivePagePath),
1905
+ content: blogArchivePage("author", false, importPath(authorArchivePagePath, blogDataPath), importPath(authorArchivePagePath, blogComponentsPath), importPath(authorArchivePagePath, blogStylesPath)),
1906
+ },
1907
+ {
1908
+ kind: "create",
1909
+ path: relativeApp(authorPaginatedArchivePagePath),
1910
+ content: blogArchivePage("author", true, importPath(authorPaginatedArchivePagePath, blogDataPath), importPath(authorPaginatedArchivePagePath, blogComponentsPath), importPath(authorPaginatedArchivePagePath, blogStylesPath)),
1911
+ },
1912
+ {
1913
+ kind: "create",
1914
+ path: relativeApp(categoriesPagePath),
1915
+ content: blogDirectoryPage("categories", importPath(categoriesPagePath, blogDataPath), importPath(categoriesPagePath, blogComponentsPath), importPath(categoriesPagePath, blogStylesPath)),
1916
+ },
1917
+ {
1918
+ kind: "create",
1919
+ path: relativeApp(categoryArchivePagePath),
1920
+ content: blogArchivePage("category", false, importPath(categoryArchivePagePath, blogDataPath), importPath(categoryArchivePagePath, blogComponentsPath), importPath(categoryArchivePagePath, blogStylesPath)),
1921
+ },
1922
+ {
1923
+ kind: "create",
1924
+ path: relativeApp(categoryPaginatedArchivePagePath),
1925
+ content: blogArchivePage("category", true, importPath(categoryPaginatedArchivePagePath, blogDataPath), importPath(categoryPaginatedArchivePagePath, blogComponentsPath), importPath(categoryPaginatedArchivePagePath, blogStylesPath)),
1926
+ },
1927
+ {
1928
+ kind: "create",
1929
+ path: relativeApp(tagsPagePath),
1930
+ content: blogDirectoryPage("tags", importPath(tagsPagePath, blogDataPath), importPath(tagsPagePath, blogComponentsPath), importPath(tagsPagePath, blogStylesPath)),
1931
+ },
1932
+ {
1933
+ kind: "create",
1934
+ path: relativeApp(tagArchivePagePath),
1935
+ content: blogArchivePage("tag", false, importPath(tagArchivePagePath, blogDataPath), importPath(tagArchivePagePath, blogComponentsPath), importPath(tagArchivePagePath, blogStylesPath)),
1936
+ },
1937
+ {
1938
+ kind: "create",
1939
+ path: relativeApp(tagPaginatedArchivePagePath),
1940
+ content: blogArchivePage("tag", true, importPath(tagPaginatedArchivePagePath, blogDataPath), importPath(tagPaginatedArchivePagePath, blogComponentsPath), importPath(tagPaginatedArchivePagePath, blogStylesPath)),
1941
+ },
1942
+ {
1943
+ kind: "create",
1944
+ path: relativeApp(rssRoutePath),
1945
+ content: rssRoute(importPath(rssRoutePath, blogDataPath)),
1946
+ },
1947
+ ...sitemapChanges,
1948
+ ...robotsChanges,
1949
+ ];
1950
+ assertNoGeneratedFileCollisions(options.projectRoot, changes);
1951
+ const dependencies = Object.assign(Object.assign(Object.assign({}, NEXT_DEPENDENCIES), DB_DEPENDENCIES[resolvedOptions.db]), STORAGE_DEPENDENCIES[resolvedOptions.storage]);
1952
+ const devDependencies = {};
1953
+ const warnings = [
1954
+ "The installer uses PAYLOAD_DATABASE_URL and never reuses DATABASE_URL automatically.",
1955
+ "Review generated Payload files before running migrations against production data.",
1956
+ ...(configPatch
1957
+ ? []
1958
+ : [
1959
+ "next.config could not be patched automatically because it is not an ESM export-default config. Wrap it with withPayload manually.",
1960
+ ]),
1961
+ ...(resolvedOptions.storage === "custom"
1962
+ ? [
1963
+ "Custom/manual storage selected. founderhq-blog.storage.ts starts as a no-op so migrations and admin boot; add your storage adapter before production uploads.",
1964
+ ]
1965
+ : []),
1966
+ ...sitemapWarnings,
1967
+ ...robotsWarnings,
1968
+ ];
1969
+ return {
1970
+ appDir: relativeApp(appDir),
1971
+ dependencies,
1972
+ devDependencies,
1973
+ env,
1974
+ changes,
1975
+ scripts: installScripts(),
1976
+ installCommand: installCommandForPlan(options.projectRoot, pkg, dependencies, devDependencies),
1977
+ migrationCommand: resolvedOptions.migration === "run"
1978
+ ? packageManagerRunScript(options.projectRoot, pkg, "payload:migrate")
1979
+ : null,
1980
+ warnings,
1981
+ };
1982
+ }
1983
+ //# sourceMappingURL=core.js.map