@chigisoft-web/cms-sdk 1.0.3 → 1.0.4
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/dist/cli.js +544 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -80,6 +80,163 @@ function ensureDir(dirPath) {
|
|
|
80
80
|
import_fs.default.mkdirSync(absolutePath, { recursive: true });
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
function configureNextImages() {
|
|
84
|
+
const files = ["next.config.ts", "next.config.js", "next.config.mjs"];
|
|
85
|
+
let foundFile = "";
|
|
86
|
+
let content = "";
|
|
87
|
+
for (const f of files) {
|
|
88
|
+
const p = import_path.default.join(process.cwd(), f);
|
|
89
|
+
if (import_fs.default.existsSync(p)) {
|
|
90
|
+
foundFile = p;
|
|
91
|
+
content = import_fs.default.readFileSync(p, "utf8");
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const spacesPatternStr = `{
|
|
96
|
+
protocol: 'https',
|
|
97
|
+
hostname: '*.ams3.digitaloceanspaces.com',
|
|
98
|
+
}`;
|
|
99
|
+
if (foundFile) {
|
|
100
|
+
if (content.includes("ams3.digitaloceanspaces.com")) {
|
|
101
|
+
console.log(`\u2139\uFE0F DigitalOcean Spaces is already configured in ${import_path.default.basename(foundFile)}.`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (content.includes("remotePatterns: [")) {
|
|
105
|
+
content = content.replace(
|
|
106
|
+
"remotePatterns: [",
|
|
107
|
+
`remotePatterns: [
|
|
108
|
+
${spacesPatternStr},`
|
|
109
|
+
);
|
|
110
|
+
import_fs.default.writeFileSync(foundFile, content, "utf8");
|
|
111
|
+
console.log(`\u2705 Configured DigitalOcean Spaces under remotePatterns in ${import_path.default.basename(foundFile)}.`);
|
|
112
|
+
} else if (content.includes("images: {")) {
|
|
113
|
+
content = content.replace(
|
|
114
|
+
"images: {",
|
|
115
|
+
`images: {
|
|
116
|
+
remotePatterns: [
|
|
117
|
+
${spacesPatternStr},
|
|
118
|
+
],`
|
|
119
|
+
);
|
|
120
|
+
import_fs.default.writeFileSync(foundFile, content, "utf8");
|
|
121
|
+
console.log(`\u2705 Configured DigitalOcean Spaces under images in ${import_path.default.basename(foundFile)}.`);
|
|
122
|
+
} else {
|
|
123
|
+
if (content.includes("const nextConfig: NextConfig = {")) {
|
|
124
|
+
content = content.replace(
|
|
125
|
+
"const nextConfig: NextConfig = {",
|
|
126
|
+
`const nextConfig: NextConfig = {
|
|
127
|
+
images: {
|
|
128
|
+
remotePatterns: [
|
|
129
|
+
${spacesPatternStr},
|
|
130
|
+
],
|
|
131
|
+
},`
|
|
132
|
+
);
|
|
133
|
+
import_fs.default.writeFileSync(foundFile, content, "utf8");
|
|
134
|
+
console.log(`\u2705 Configured DigitalOcean Spaces in ${import_path.default.basename(foundFile)}.`);
|
|
135
|
+
} else if (content.includes("const nextConfig = {")) {
|
|
136
|
+
content = content.replace(
|
|
137
|
+
"const nextConfig = {",
|
|
138
|
+
`const nextConfig = {
|
|
139
|
+
images: {
|
|
140
|
+
remotePatterns: [
|
|
141
|
+
${spacesPatternStr},
|
|
142
|
+
],
|
|
143
|
+
},`
|
|
144
|
+
);
|
|
145
|
+
import_fs.default.writeFileSync(foundFile, content, "utf8");
|
|
146
|
+
console.log(`\u2705 Configured DigitalOcean Spaces in ${import_path.default.basename(foundFile)}.`);
|
|
147
|
+
} else if (content.includes("export default {")) {
|
|
148
|
+
content = content.replace(
|
|
149
|
+
"export default {",
|
|
150
|
+
`export default {
|
|
151
|
+
images: {
|
|
152
|
+
remotePatterns: [
|
|
153
|
+
${spacesPatternStr},
|
|
154
|
+
],
|
|
155
|
+
},`
|
|
156
|
+
);
|
|
157
|
+
import_fs.default.writeFileSync(foundFile, content, "utf8");
|
|
158
|
+
console.log(`\u2705 Configured DigitalOcean Spaces in ${import_path.default.basename(foundFile)}.`);
|
|
159
|
+
} else if (content.includes("module.exports = {")) {
|
|
160
|
+
content = content.replace(
|
|
161
|
+
"module.exports = {",
|
|
162
|
+
`module.exports = {
|
|
163
|
+
images: {
|
|
164
|
+
remotePatterns: [
|
|
165
|
+
${spacesPatternStr},
|
|
166
|
+
],
|
|
167
|
+
},`
|
|
168
|
+
);
|
|
169
|
+
import_fs.default.writeFileSync(foundFile, content, "utf8");
|
|
170
|
+
console.log(`\u2705 Configured DigitalOcean Spaces in ${import_path.default.basename(foundFile)}.`);
|
|
171
|
+
} else {
|
|
172
|
+
console.log(`\u26A0\uFE0F Could not automatically inject images config in ${import_path.default.basename(foundFile)}.`);
|
|
173
|
+
console.log(`Please add the following to your images config:`);
|
|
174
|
+
console.log(`images: { remotePatterns: [ { protocol: 'https', hostname: '*.ams3.digitaloceanspaces.com' } ] }`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const nextConfigContent = `import type { NextConfig } from "next";
|
|
179
|
+
|
|
180
|
+
const nextConfig: NextConfig = {
|
|
181
|
+
images: {
|
|
182
|
+
remotePatterns: [
|
|
183
|
+
{
|
|
184
|
+
protocol: "https",
|
|
185
|
+
hostname: "*.ams3.digitaloceanspaces.com",
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default nextConfig;
|
|
192
|
+
`;
|
|
193
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), "next.config.ts"), nextConfigContent, "utf8");
|
|
194
|
+
console.log(`\u2705 Created next.config.ts configured with DigitalOcean Spaces.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function configureNuxtImages() {
|
|
198
|
+
const p = import_path.default.join(process.cwd(), "nuxt.config.ts");
|
|
199
|
+
if (import_fs.default.existsSync(p)) {
|
|
200
|
+
let content = import_fs.default.readFileSync(p, "utf8");
|
|
201
|
+
if (content.includes("ams3.digitaloceanspaces.com")) {
|
|
202
|
+
console.log("\u2139\uFE0F DigitalOcean Spaces is already configured in nuxt.config.ts.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (content.includes("image: {")) {
|
|
206
|
+
if (content.includes("domains: [")) {
|
|
207
|
+
content = content.replace("domains: [", "domains: ['ams3.digitaloceanspaces.com', ");
|
|
208
|
+
} else {
|
|
209
|
+
content = content.replace("image: {", "image: {\n domains: ['ams3.digitaloceanspaces.com'],");
|
|
210
|
+
}
|
|
211
|
+
import_fs.default.writeFileSync(p, content, "utf8");
|
|
212
|
+
console.log("\u2705 Configured DigitalOcean Spaces in nuxt.config.ts image block.");
|
|
213
|
+
} else if (content.includes("defineNuxtConfig({")) {
|
|
214
|
+
content = content.replace(
|
|
215
|
+
"defineNuxtConfig({",
|
|
216
|
+
`defineNuxtConfig({
|
|
217
|
+
image: {
|
|
218
|
+
domains: ['ams3.digitaloceanspaces.com'],
|
|
219
|
+
},`
|
|
220
|
+
);
|
|
221
|
+
import_fs.default.writeFileSync(p, content, "utf8");
|
|
222
|
+
console.log("\u2705 Injected image configuration into nuxt.config.ts.");
|
|
223
|
+
} else {
|
|
224
|
+
console.log("\u26A0\uFE0F Could not automatically inject images config in nuxt.config.ts.");
|
|
225
|
+
console.log("Please add image config domains: ['ams3.digitaloceanspaces.com'] to your nuxt.config.ts.");
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
const nuxtConfigContent = `// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
229
|
+
export default defineNuxtConfig({
|
|
230
|
+
modules: ['@nuxt/image'],
|
|
231
|
+
image: {
|
|
232
|
+
domains: ['ams3.digitaloceanspaces.com'],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
`;
|
|
236
|
+
import_fs.default.writeFileSync(p, nuxtConfigContent, "utf8");
|
|
237
|
+
console.log("\u2705 Created nuxt.config.ts configured with DigitalOcean Spaces.");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
83
240
|
async function main() {
|
|
84
241
|
console.log("\n===========================================");
|
|
85
242
|
console.log(" chigisoft-cms SDK Setup Wizard \u{1F680} ");
|
|
@@ -109,6 +266,7 @@ async function main() {
|
|
|
109
266
|
if (frameworkChoice === "1") {
|
|
110
267
|
console.log("\n\u{1F527} Setting up Next.js integration...");
|
|
111
268
|
writeEnv(".env.local", projectId, token);
|
|
269
|
+
configureNextImages();
|
|
112
270
|
ensureDir("lib");
|
|
113
271
|
const clientFileContent = `import { createClient } from '@chigisoft-web/cms-sdk';
|
|
114
272
|
|
|
@@ -194,13 +352,221 @@ CHIGISOFT_STUDIO_URL="https://cms.staging.chigisoft.co" (optional)\`}
|
|
|
194
352
|
`;
|
|
195
353
|
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), "app/studio/page.tsx"), studioPageContent, "utf8");
|
|
196
354
|
console.log("\u2705 Created studio page router at app/studio/page.tsx");
|
|
355
|
+
ensureDir("components");
|
|
356
|
+
const blockRendererContent = `import React from 'react';
|
|
357
|
+
import Image from 'next/image';
|
|
358
|
+
import type { CMSBlock } from '@chigisoft-web/cms-sdk';
|
|
359
|
+
|
|
360
|
+
interface CMSBlocksProps {
|
|
361
|
+
blocks: CMSBlock[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export default function CMSBlocks({ blocks }: CMSBlocksProps) {
|
|
365
|
+
if (!blocks || !Array.isArray(blocks)) return null;
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div className="cms-blocks space-y-12 my-8">
|
|
369
|
+
{blocks.map((block) => {
|
|
370
|
+
const { id, type, data, content } = block;
|
|
371
|
+
const blockData = data || content || {};
|
|
372
|
+
|
|
373
|
+
switch (type) {
|
|
374
|
+
case 'heading': {
|
|
375
|
+
const level = blockData.level || 2;
|
|
376
|
+
const Tag = \`h\${level}\` as keyof JSX.IntrinsicElements;
|
|
377
|
+
const alignClass = blockData.align === 'center' ? 'text-center' : blockData.align === 'right' ? 'text-right' : 'text-left';
|
|
378
|
+
const sizeClass = level === 1 ? 'text-4xl font-extrabold' : level === 3 ? 'text-2xl font-bold' : 'text-3xl font-bold';
|
|
379
|
+
return (
|
|
380
|
+
<Tag key={id} className={\`tracking-tight text-gray-900 \${alignClass} \${sizeClass}\`}>
|
|
381
|
+
{blockData.text}
|
|
382
|
+
</Tag>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
case 'richText':
|
|
387
|
+
case 'rich-text':
|
|
388
|
+
return (
|
|
389
|
+
<div
|
|
390
|
+
key={id}
|
|
391
|
+
className="prose max-w-none text-gray-700 leading-relaxed"
|
|
392
|
+
dangerouslySetInnerHTML={{ __html: blockData.content || blockData.html || '' }}
|
|
393
|
+
/>
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
case 'image':
|
|
397
|
+
return (
|
|
398
|
+
<figure key={id} className="my-6">
|
|
399
|
+
{blockData.mediaUrl && (
|
|
400
|
+
<div className="relative w-full h-[400px]">
|
|
401
|
+
<Image
|
|
402
|
+
src={blockData.mediaUrl}
|
|
403
|
+
alt={blockData.alt || ''}
|
|
404
|
+
fill
|
|
405
|
+
sizes="(max-width: 768px) 100vw, 800px"
|
|
406
|
+
className="object-cover rounded-xl shadow-md"
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
{blockData.caption && (
|
|
411
|
+
<figcaption className="text-center text-sm text-gray-500 mt-2">
|
|
412
|
+
{blockData.caption}
|
|
413
|
+
</figcaption>
|
|
414
|
+
)}
|
|
415
|
+
</figure>
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
case 'quote':
|
|
419
|
+
case 'testimonial':
|
|
420
|
+
return (
|
|
421
|
+
<blockquote key={id} className="border-l-4 border-violet-500 pl-4 py-2 my-6 italic text-gray-700 bg-gray-50 rounded-r-lg">
|
|
422
|
+
<p className="text-lg">{blockData.text || blockData.quote}</p>
|
|
423
|
+
{(blockData.attribution || blockData.author) && (
|
|
424
|
+
<cite className="block mt-2 text-sm not-italic font-semibold text-gray-500">
|
|
425
|
+
\u2014 {blockData.attribution || blockData.author} {blockData.role ? \`(\${blockData.role})\` : ''}
|
|
426
|
+
</cite>
|
|
427
|
+
)}
|
|
428
|
+
</blockquote>
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
case 'video':
|
|
432
|
+
case 'embed':
|
|
433
|
+
return (
|
|
434
|
+
<div key={id} className="my-6 aspect-video rounded-xl overflow-hidden shadow-lg">
|
|
435
|
+
<iframe
|
|
436
|
+
src={blockData.url}
|
|
437
|
+
className="w-full h-full"
|
|
438
|
+
allowFullScreen
|
|
439
|
+
title={blockData.caption || 'Video block'}
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
case 'ctaStrip':
|
|
445
|
+
case 'cta':
|
|
446
|
+
return (
|
|
447
|
+
<div key={id} className="p-8 my-8 bg-violet-50 border border-violet-100 rounded-2xl flex flex-col md:flex-row items-center justify-between gap-6">
|
|
448
|
+
<div>
|
|
449
|
+
<h3 className="text-xl font-bold text-gray-900">{blockData.heading}</h3>
|
|
450
|
+
<p className="text-gray-600 mt-1">{blockData.body}</p>
|
|
451
|
+
</div>
|
|
452
|
+
{(blockData.primaryUrl || blockData.buttonUrl) && (
|
|
453
|
+
<a
|
|
454
|
+
href={blockData.primaryUrl || blockData.buttonUrl}
|
|
455
|
+
className="px-6 py-3 bg-violet-600 hover:bg-violet-700 text-white font-semibold rounded-xl transition-colors shrink-0"
|
|
456
|
+
>
|
|
457
|
+
{blockData.primaryLabel || blockData.buttonLabel || 'Learn More'}
|
|
458
|
+
</a>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
default:
|
|
464
|
+
return (
|
|
465
|
+
<div key={id} className="p-4 bg-gray-100 rounded-lg text-xs text-gray-400 font-mono">
|
|
466
|
+
Unknown block type: {type}
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
})}
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
`;
|
|
475
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), "components/CMSBlocks.tsx"), blockRendererContent, "utf8");
|
|
476
|
+
console.log("\u2705 Created block renderer component at components/CMSBlocks.tsx");
|
|
477
|
+
ensureDir("app/[slug]");
|
|
478
|
+
const catchAllPageContent = `import { notFound } from "next/navigation";
|
|
479
|
+
import { chigisoft } from "@/lib/chigisoft";
|
|
480
|
+
import CMSBlocks from "@/components/CMSBlocks";
|
|
481
|
+
|
|
482
|
+
interface PageProps {
|
|
483
|
+
params: {
|
|
484
|
+
slug: string;
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export default async function CatchAllCMSPage({ params }: PageProps) {
|
|
489
|
+
const { slug } = params;
|
|
490
|
+
const page = await chigisoft.getPageBySlug(slug, {
|
|
491
|
+
next: { revalidate: 60 },
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
if (!page) {
|
|
495
|
+
notFound();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<article className="max-w-4xl mx-auto py-16 px-6">
|
|
500
|
+
<header className="mb-10 text-center">
|
|
501
|
+
<h1 className="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl">
|
|
502
|
+
{page.title}
|
|
503
|
+
</h1>
|
|
504
|
+
{page.description && (
|
|
505
|
+
<p className="mt-4 text-xl text-gray-500">
|
|
506
|
+
{page.description}
|
|
507
|
+
</p>
|
|
508
|
+
)}
|
|
509
|
+
</header>
|
|
510
|
+
|
|
511
|
+
<CMSBlocks blocks={page.blocks} />
|
|
512
|
+
</article>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
516
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), "app/[slug]/page.tsx"), catchAllPageContent, "utf8");
|
|
517
|
+
console.log("\u2705 Created catch-all CMS page router at app/[slug]/page.tsx");
|
|
518
|
+
ensureDir("app/blog/[slug]");
|
|
519
|
+
const blogPageContent = `import { notFound } from "next/navigation";
|
|
520
|
+
import { chigisoft } from "@/lib/chigisoft";
|
|
521
|
+
import CMSBlocks from "@/components/CMSBlocks";
|
|
522
|
+
|
|
523
|
+
interface PageProps {
|
|
524
|
+
params: {
|
|
525
|
+
slug: string;
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export default async function BlogPostPage({ params }: PageProps) {
|
|
530
|
+
const { slug } = params;
|
|
531
|
+
const blog = await chigisoft.getBlogBySlug(slug, {
|
|
532
|
+
next: { revalidate: 60 },
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (!blog) {
|
|
536
|
+
notFound();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<article className="max-w-3xl mx-auto py-16 px-6">
|
|
541
|
+
<header className="mb-10">
|
|
542
|
+
<h1 className="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl mb-4">
|
|
543
|
+
{blog.title}
|
|
544
|
+
</h1>
|
|
545
|
+
<div className="flex items-center gap-4 text-sm text-gray-500">
|
|
546
|
+
<time dateTime={blog.createdAt}>
|
|
547
|
+
{new Date(blog.createdAt).toLocaleDateString(undefined, {
|
|
548
|
+
dateStyle: "long",
|
|
549
|
+
})}
|
|
550
|
+
</time>
|
|
551
|
+
</div>
|
|
552
|
+
</header>
|
|
553
|
+
|
|
554
|
+
<CMSBlocks blocks={blog.blocks} />
|
|
555
|
+
</article>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
`;
|
|
559
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), "app/blog/[slug]/page.tsx"), blogPageContent, "utf8");
|
|
560
|
+
console.log("\u2705 Created blog CMS page router at app/blog/[slug]/page.tsx");
|
|
197
561
|
console.log("\n\u{1F389} Next.js Setup Complete!");
|
|
198
562
|
console.log("You can now:");
|
|
199
563
|
console.log('1. Import the client helper in components: `import { chigisoft } from "@/lib/chigisoft";`');
|
|
200
564
|
console.log("2. Access the CMS login/dashboard from the `/studio` route of your site.");
|
|
565
|
+
console.log("3. Render dynamic pages and blogs via catch-all routes and `<CMSBlocks />`!");
|
|
201
566
|
} else if (frameworkChoice === "2") {
|
|
202
567
|
console.log("\n\u{1F527} Setting up Nuxt.js integration...");
|
|
203
568
|
writeEnv(".env", projectId, token);
|
|
569
|
+
configureNuxtImages();
|
|
204
570
|
ensureDir("plugins");
|
|
205
571
|
const pluginContent = `import { createClient } from '@chigisoft-web/cms-sdk';
|
|
206
572
|
|
|
@@ -219,7 +585,9 @@ export default defineNuxtPlugin(() => {
|
|
|
219
585
|
console.log("\u2705 Created Nuxt client plugin at plugins/chigisoft.ts");
|
|
220
586
|
const isNuxt4 = import_fs.default.existsSync(import_path.default.join(process.cwd(), "app"));
|
|
221
587
|
const pagesDir = isNuxt4 ? "app/pages" : "pages";
|
|
588
|
+
const compDir = isNuxt4 ? "app/components" : "components";
|
|
222
589
|
ensureDir(pagesDir);
|
|
590
|
+
ensureDir(compDir);
|
|
223
591
|
const nuxtStudioPageContent = `<script setup lang="ts">
|
|
224
592
|
import { onMounted, ref } from 'vue';
|
|
225
593
|
import { createClient } from '@chigisoft-web/cms-sdk';
|
|
@@ -252,10 +620,186 @@ onMounted(() => {
|
|
|
252
620
|
`;
|
|
253
621
|
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), pagesDir, "studio.vue"), nuxtStudioPageContent, "utf8");
|
|
254
622
|
console.log(`\u2705 Created studio page at ${pagesDir}/studio.vue`);
|
|
623
|
+
const blockRendererVueContent = `<script setup lang="ts">
|
|
624
|
+
import type { CMSBlock } from '@chigisoft-web/cms-sdk';
|
|
625
|
+
|
|
626
|
+
defineProps<{
|
|
627
|
+
blocks: CMSBlock[];
|
|
628
|
+
}>();
|
|
629
|
+
</script>
|
|
630
|
+
|
|
631
|
+
<template>
|
|
632
|
+
<div v-if="blocks && Array.isArray(blocks)" class="cms-blocks space-y-12 my-8">
|
|
633
|
+
<div v-for="block in blocks" :key="block.id">
|
|
634
|
+
<!-- Heading Block -->
|
|
635
|
+
<template v-if="block.type === 'heading'">
|
|
636
|
+
<component
|
|
637
|
+
:is="\`h\${block.data?.level || 2}\`"
|
|
638
|
+
:class="[
|
|
639
|
+
'font-bold tracking-tight text-gray-900',
|
|
640
|
+
block.data?.align === 'center' ? 'text-center' : block.data?.align === 'right' ? 'text-right' : 'text-left',
|
|
641
|
+
block.data?.level === 1 ? 'text-4xl font-extrabold' : block.data?.level === 3 ? 'text-2xl font-bold' : 'text-3xl font-bold'
|
|
642
|
+
]"
|
|
643
|
+
>
|
|
644
|
+
{{ block.data?.text || block.content?.text }}
|
|
645
|
+
</component>
|
|
646
|
+
</template>
|
|
647
|
+
|
|
648
|
+
<!-- Rich Text Block -->
|
|
649
|
+
<template v-else-if="block.type === 'richText' || block.type === 'rich-text'">
|
|
650
|
+
<div
|
|
651
|
+
class="prose max-w-none text-gray-700 leading-relaxed"
|
|
652
|
+
v-html="block.data?.content || block.content?.html || block.content?.text || ''"
|
|
653
|
+
></div>
|
|
654
|
+
</template>
|
|
655
|
+
|
|
656
|
+
<!-- Image Block -->
|
|
657
|
+
<template v-else-if="block.type === 'image'">
|
|
658
|
+
<figure class="my-6">
|
|
659
|
+
<img
|
|
660
|
+
v-if="block.data?.mediaUrl || block.content?.mediaUrl"
|
|
661
|
+
:src="block.data?.mediaUrl || block.content?.mediaUrl"
|
|
662
|
+
:alt="block.data?.alt || block.content?.alt || ''"
|
|
663
|
+
class="w-full h-auto max-h-[500px] object-cover rounded-xl shadow-md"
|
|
664
|
+
/>
|
|
665
|
+
<figcaption
|
|
666
|
+
v-if="block.data?.caption || block.content?.caption"
|
|
667
|
+
class="text-center text-sm text-gray-500 mt-2"
|
|
668
|
+
>
|
|
669
|
+
{{ block.data?.caption || block.content?.caption }}
|
|
670
|
+
</figcaption>
|
|
671
|
+
</figure>
|
|
672
|
+
</template>
|
|
673
|
+
|
|
674
|
+
<!-- Quote Block -->
|
|
675
|
+
<template v-else-if="block.type === 'quote' || block.type === 'testimonial'">
|
|
676
|
+
<blockquote class="border-l-4 border-violet-500 pl-4 py-2 my-6 italic text-gray-700 bg-gray-50 rounded-r-lg">
|
|
677
|
+
<p class="text-lg">{{ block.data?.text || block.data?.quote || block.content?.text }}</p>
|
|
678
|
+
<cite
|
|
679
|
+
v-if="block.data?.attribution || block.data?.author || block.content?.attribution"
|
|
680
|
+
class="block mt-2 text-sm not-italic font-semibold text-gray-500"
|
|
681
|
+
>
|
|
682
|
+
\u2014 {{ block.data?.attribution || block.data?.author || block.content?.attribution }}
|
|
683
|
+
<span v-if="block.data?.role">({{ block.data?.role }})</span>
|
|
684
|
+
</cite>
|
|
685
|
+
</blockquote>
|
|
686
|
+
</template>
|
|
687
|
+
|
|
688
|
+
<!-- Video Block -->
|
|
689
|
+
<template v-else-if="block.type === 'video' || block.type === 'embed'">
|
|
690
|
+
<div class="my-6 aspect-video rounded-xl overflow-hidden shadow-lg">
|
|
691
|
+
<iframe
|
|
692
|
+
:src="block.data?.url || block.content?.url"
|
|
693
|
+
class="w-full h-full"
|
|
694
|
+
allowfullscreen
|
|
695
|
+
:title="block.data?.caption || block.content?.caption || 'Video Block'"
|
|
696
|
+
></iframe>
|
|
697
|
+
</div>
|
|
698
|
+
</template>
|
|
699
|
+
|
|
700
|
+
<!-- CTA Strip Block -->
|
|
701
|
+
<template v-else-if="block.type === 'ctaStrip' || block.type === 'cta'">
|
|
702
|
+
<div class="p-8 my-8 bg-violet-50 border border-violet-100 rounded-2xl flex flex-col md:flex-row items-center justify-between gap-6">
|
|
703
|
+
<div>
|
|
704
|
+
<h3 class="text-xl font-bold text-gray-900">{{ block.data?.heading || block.content?.heading }}</h3>
|
|
705
|
+
<p class="text-gray-600 mt-1">{{ block.data?.body || block.content?.body }}</p>
|
|
706
|
+
</div>
|
|
707
|
+
<a
|
|
708
|
+
v-if="block.data?.primaryUrl || block.data?.buttonUrl || block.content?.primaryUrl"
|
|
709
|
+
:href="block.data?.primaryUrl || block.data?.buttonUrl || block.content?.primaryUrl"
|
|
710
|
+
class="px-6 py-3 bg-violet-600 hover:bg-violet-700 text-white font-semibold rounded-xl transition-colors shrink-0"
|
|
711
|
+
>
|
|
712
|
+
{{ block.data?.primaryLabel || block.data?.buttonLabel || block.content?.primaryLabel || 'Learn More' }}
|
|
713
|
+
</a>
|
|
714
|
+
</div>
|
|
715
|
+
</template>
|
|
716
|
+
|
|
717
|
+
<!-- Unsupported Block -->
|
|
718
|
+
<template v-else>
|
|
719
|
+
<div class="p-4 bg-gray-100 rounded-lg text-xs text-gray-400 font-mono">
|
|
720
|
+
Unknown block type: {{ block.type }}
|
|
721
|
+
</div>
|
|
722
|
+
</template>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
</template>
|
|
726
|
+
`;
|
|
727
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), compDir, "CMSBlocks.vue"), blockRendererVueContent, "utf8");
|
|
728
|
+
console.log(`\u2705 Created block renderer component at ${compDir}/CMSBlocks.vue`);
|
|
729
|
+
const nuxtCatchAllContent = `<script setup lang="ts">
|
|
730
|
+
const route = useRoute();
|
|
731
|
+
const { $chigisoft } = useNuxtApp();
|
|
732
|
+
|
|
733
|
+
const slug = computed(() => route.params.slug as string);
|
|
734
|
+
|
|
735
|
+
const { data: page, error } = await useAsyncData(\`page-\${slug.value}\`, () =>
|
|
736
|
+
$chigisoft.getPageBySlug(slug.value)
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
if (error.value || !page.value) {
|
|
740
|
+
throw createError({ statusCode: 404, message: 'Page not found', fatal: true });
|
|
741
|
+
}
|
|
742
|
+
</script>
|
|
743
|
+
|
|
744
|
+
<template>
|
|
745
|
+
<article v-if="page" class="max-w-4xl mx-auto py-16 px-6">
|
|
746
|
+
<header class="mb-10 text-center">
|
|
747
|
+
<h1 class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl">
|
|
748
|
+
{{ page.title }}
|
|
749
|
+
</h1>
|
|
750
|
+
<p v-if="page.description" class="mt-4 text-xl text-gray-500">
|
|
751
|
+
{{ page.description }}
|
|
752
|
+
</p>
|
|
753
|
+
</header>
|
|
754
|
+
|
|
755
|
+
<CMSBlocks :blocks="page.blocks" />
|
|
756
|
+
</article>
|
|
757
|
+
</template>
|
|
758
|
+
`;
|
|
759
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), pagesDir, "[slug].vue"), nuxtCatchAllContent, "utf8");
|
|
760
|
+
console.log(`\u2705 Created catch-all CMS page router at ${pagesDir}/[slug].vue`);
|
|
761
|
+
ensureDir(import_path.default.join(pagesDir, "blog"));
|
|
762
|
+
const nuxtBlogPageContent = `<script setup lang="ts">
|
|
763
|
+
const route = useRoute();
|
|
764
|
+
const { $chigisoft } = useNuxtApp();
|
|
765
|
+
|
|
766
|
+
const slug = computed(() => route.params.slug as string);
|
|
767
|
+
|
|
768
|
+
const { data: blog, error } = await useAsyncData(\`blog-\${slug.value}\`, () =>
|
|
769
|
+
$chigisoft.getBlogBySlug(slug.value)
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
if (error.value || !blog.value) {
|
|
773
|
+
throw createError({ statusCode: 404, message: 'Blog post not found', fatal: true });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const formatDate = (dateStr: string) => {
|
|
777
|
+
return new Date(dateStr).toLocaleDateString(undefined, { dateStyle: 'long' });
|
|
778
|
+
};
|
|
779
|
+
</script>
|
|
780
|
+
|
|
781
|
+
<template>
|
|
782
|
+
<article v-if="blog" class="max-w-3xl mx-auto py-16 px-6">
|
|
783
|
+
<header class="mb-10">
|
|
784
|
+
<h1 class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl mb-4">
|
|
785
|
+
{{ blog.title }}
|
|
786
|
+
</h1>
|
|
787
|
+
<div class="flex items-center gap-4 text-sm text-gray-500">
|
|
788
|
+
<time :datetime="blog.createdAt">{{ formatDate(blog.createdAt) }}</time>
|
|
789
|
+
</div>
|
|
790
|
+
</header>
|
|
791
|
+
|
|
792
|
+
<CMSBlocks :blocks="blog.blocks" />
|
|
793
|
+
</article>
|
|
794
|
+
</template>
|
|
795
|
+
`;
|
|
796
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), pagesDir, "blog/[slug].vue"), nuxtBlogPageContent, "utf8");
|
|
797
|
+
console.log(`\u2705 Created blog CMS page router at ${pagesDir}/blog/[slug].vue`);
|
|
255
798
|
console.log("\n\u{1F389} Nuxt.js Setup Complete!");
|
|
256
799
|
console.log("You can now:");
|
|
257
800
|
console.log("1. Access the client from Nuxt context: `const { $chigisoft } = useNuxtApp();`");
|
|
258
801
|
console.log("2. Access the CMS login/dashboard from the `/studio` route of your site.");
|
|
802
|
+
console.log("3. Render dynamic pages and blogs via catch-all routes and `<CMSBlocks />`!");
|
|
259
803
|
} else if (frameworkChoice === "3") {
|
|
260
804
|
console.log("\n\u{1F527} Setting up Vanilla HTML/JS demo...");
|
|
261
805
|
const demoHtmlContent = `<!DOCTYPE html>
|