@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.
Files changed (2) hide show
  1. package/dist/cli.js +544 -0
  2. 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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chigisoft-web/cms-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "SDK client for ChigiSoft CMS",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",