@xenterprises/nuxt-x-marketing 1.2.2 → 1.3.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.
@@ -1,64 +1,60 @@
1
1
  <template>
2
- <div class="pb-8 max-w-screen-2xl px-8 mx-auto">
3
- <UPageHero :title="blog?.title" :description="blog?.description" />
2
+ <div class="pb-8 max-w-screen-2xl px-4 sm:px-6 lg:px-8 mx-auto">
3
+ <UPageHero
4
+ :title="blog?.title"
5
+ :description="blog?.description"
6
+ />
4
7
 
5
8
  <UPageBody>
6
- <UBlogPosts>
7
- <UBlogPost
8
- v-for="(post, index) in blogPosts?.results"
9
- :key="index"
10
- :to="`/blog/${post?.data?.slug}`"
11
- :title="post?.name"
12
- :description="post?.data?.summary"
13
- :image="{ src: post?.data?.thumbnail, alt: post?.name }"
14
- :orientation="index === 0 ? 'horizontal' : 'vertical'"
15
- :class="[index === 0 && 'col-span-full']"
16
- :ui="{
17
- description: 'line-clamp-3',
18
- }"
19
- >
20
- <div class="mt-4">
21
- <UButton
22
- :to="`/blog/${post?.data?.slug}`"
23
- variant="outline"
24
- color="gray"
25
- label="Read More"
26
- />
27
- </div>
28
- </UBlogPost>
29
- </UBlogPosts>
9
+ <XMarkBlogList
10
+ v-if="posts?.length"
11
+ :posts="posts"
12
+ :columns="3"
13
+ :has-filters="true"
14
+ :has-categories="hasCategories"
15
+ :has-authors="hasAuthors"
16
+ />
17
+ <div
18
+ v-else
19
+ class="text-center py-16 text-neutral-600 dark:text-neutral-400"
20
+ >
21
+ <UIcon
22
+ name="i-lucide-file-text"
23
+ class="w-12 h-12 text-neutral-300 dark:text-neutral-700 mx-auto mb-4"
24
+ />
25
+ <p>No posts yet. Add markdown under <code>content/blog/</code>.</p>
26
+ </div>
30
27
  </UPageBody>
31
28
  </div>
32
29
  </template>
33
30
 
34
- <script setup>
35
- const appConfig = useAppConfig()?.xMarketing;
36
- const blog = appConfig?.blog;
37
- const route = useRoute();
38
- const config = useRuntimeConfig();
31
+ <script setup lang="ts">
32
+ const appConfig = useAppConfig()?.xMarketing
33
+ const blog = appConfig?.blog
34
+
35
+ // Opt-out: xMarketing.blog.active === false disables the shipped blog pages.
36
+ if (blog?.active === false) {
37
+ throw createError({ statusCode: 404, statusMessage: 'Page not found' })
38
+ }
39
+
40
+ const { getPosts } = useXBlog()
41
+
42
+ const { data: posts } = await useAsyncData('marketing-blog-posts', () => getPosts())
43
+
44
+ const hasCategories = computed(() =>
45
+ (posts.value ?? []).some(p => Boolean(p.category)),
46
+ )
47
+ const hasAuthors = computed(() =>
48
+ (posts.value ?? []).some(p => Boolean(p.author)),
49
+ )
50
+
51
+ const title = blog?.meta?.title ?? blog?.title ?? 'Blog'
52
+ const description = blog?.meta?.description ?? blog?.description ?? ''
39
53
 
40
- const blogPosts = await $fetch("https://cdn.builder.io/api/v3/content/posts", {
41
- query: {
42
- apiKey: config?.public?.BUILDERIO_KEY,
43
- offset: 0,
44
- fields: "id,name,data.slug,data.summary,data.thumbnail",
45
- },
46
- });
47
- const title = blog?.meta?.title;
48
- const description = blog?.meta?.description;
49
54
  useSeoMeta({
50
55
  title,
51
56
  ogTitle: title,
52
57
  description,
53
58
  ogDescription: description,
54
- });
55
-
56
- // useHead({
57
- // link: [
58
- // {
59
- // rel: "canonical",
60
- // href: appConfig?.url + "/blog",
61
- // },
62
- // ],
63
- // });
59
+ })
64
60
  </script>
@@ -46,7 +46,7 @@
46
46
  </h2>
47
47
  </header>
48
48
 
49
- <XMarkTestimonials :testimonials="testimonials" layout="grid" />
49
+ <XMarkSocialProofTestimonials :testimonials="testimonials" layout="grid" />
50
50
  </XMarkSection>
51
51
 
52
52
  <!-- Pricing Section -->
@@ -63,7 +63,7 @@
63
63
  </p>
64
64
  </header>
65
65
 
66
- <XMarkPricing :plans="pricingPlans" />
66
+ <XMarkPricingPlans :plans="pricingPlans" />
67
67
  </XMarkSection>
68
68
 
69
69
  <!-- CTA Section -->
@@ -80,8 +80,8 @@
80
80
  </UButton>
81
81
  </div>
82
82
  <div class="mt-6 flex flex-wrap justify-center gap-4">
83
- <XMarkBadge preset="no-credit-card" variant="subtle" />
84
- <XMarkBadge preset="cancel-anytime" variant="subtle" />
83
+ <XMarkSocialProofBadge preset="no-credit-card" variant="subtle" />
84
+ <XMarkSocialProofBadge preset="cancel-anytime" variant="subtle" />
85
85
  </div>
86
86
  </div>
87
87
  </XMarkSection>
@@ -198,10 +198,13 @@
198
198
  </template>
199
199
 
200
200
  <script setup>
201
+ const appConfig = useAppConfig();
202
+
201
203
  useSeoMeta({
202
204
  title: "Modern Platform for Teams",
203
205
  description:
204
206
  "Build, launch, and grow your product with our powerful platform.",
207
+ ogUrl: appConfig.xMarketing?.url,
205
208
  });
206
209
 
207
210
  // Features data
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Config types for the `xMarketing` app.config.ts namespace.
3
+ *
4
+ * Exported so consumers and sibling layers can reference the shape
5
+ * (config-typing convention, plan 21 §3). This is the *input* contract —
6
+ * every field is optional so site overrides can set any subset.
7
+ */
8
+
9
+ export interface XMarketingNavLink {
10
+ label: string;
11
+ to?: string;
12
+ target?: string;
13
+ children?: XMarketingNavLink[];
14
+ }
15
+
16
+ export interface XMarketingTrackingScript {
17
+ /** Stable ID — used to dedupe re-injection. */
18
+ id: string;
19
+ /** External script URL (e.g. "https://www.googletagmanager.com/gtag/js?id=G-XXX"). */
20
+ src?: string;
21
+ /** Inline script body (no `src` required). */
22
+ inline?: string;
23
+ /** Consent category gate. "necessary" fires immediately; "analytics" / "marketing" gated. */
24
+ category?: "necessary" | "analytics" | "marketing";
25
+ /** Extra attributes applied to the injected <script>. */
26
+ attrs?: Record<string, string>;
27
+ }
28
+
29
+ /**
30
+ * Tracking entry — use the convenience IDs for the common tools
31
+ * (GTM, GA4, Clarity) or `scripts[]` for everything else. Scripts
32
+ * are injected only after the visitor grants consent for the
33
+ * matching `category` (default: "analytics").
34
+ */
35
+ export interface XMarketingTracking {
36
+ /** Google Tag Manager container ID, e.g. "GTM-XXXXXXX". */
37
+ gtmId?: string;
38
+ /** Google Analytics 4 measurement ID, e.g. "G-XXXXXXXX". */
39
+ ga4Id?: string;
40
+ /** Microsoft Clarity project ID, e.g. "abc123def4". */
41
+ clarityId?: string;
42
+ /**
43
+ * Arbitrary scripts (Meta Pixel, Hotjar, Segment, LinkedIn Insight,
44
+ * Pinterest, TikTok, etc.). Each fires once the matching consent
45
+ * category is granted. Use `inline` for hand-written snippets like
46
+ * `gtag('js', new Date())`.
47
+ */
48
+ scripts?: XMarketingTrackingScript[];
49
+ /** Auto-inject configured scripts on consent. Default: true. */
50
+ autoInject?: boolean;
51
+ }
52
+
53
+ export interface XMarketingConfig {
54
+ /** Project name */
55
+ name?: string;
56
+ /** Project URL */
57
+ url?: string;
58
+ config?: {
59
+ markerProjectId?: string;
60
+ emailMarketingNewsletters?: {
61
+ organizationId?: string;
62
+ };
63
+ };
64
+ header?: {
65
+ /** Set false to opt out of the default navbar in the shipped shell. */
66
+ active?: boolean;
67
+ logo?: {
68
+ src?: string;
69
+ srcDark?: string;
70
+ alt?: string;
71
+ };
72
+ nav?: {
73
+ buttons?: {
74
+ label: string;
75
+ to: string;
76
+ icon?: string;
77
+ target?: string;
78
+ variant?: string;
79
+ square?: boolean;
80
+ color?: string;
81
+ }[];
82
+ links?: XMarketingNavLink[];
83
+ };
84
+ };
85
+ footer?: {
86
+ /** Set false to opt out of the default footer in the shipped shell. */
87
+ active?: boolean;
88
+ body?: string;
89
+ bg?: {
90
+ color?: string;
91
+ img?: {
92
+ src?: string;
93
+ alt?: string;
94
+ };
95
+ };
96
+ logo?: {
97
+ src?: string;
98
+ alt?: string;
99
+ };
100
+ socials?: {
101
+ name: string;
102
+ url: string;
103
+ icon: string;
104
+ }[];
105
+ columns?: {
106
+ headerLabel: string;
107
+ links: {
108
+ label: string;
109
+ to: string;
110
+ target?: string;
111
+ }[];
112
+ }[];
113
+ };
114
+ /** Cookie-consent banner in the shipped shell; gates tracking injection. */
115
+ consent?: {
116
+ active?: boolean;
117
+ };
118
+ /** Blog defaults; `active: false` disables the shipped /blog pages (404). */
119
+ blog?: {
120
+ active?: boolean;
121
+ title?: string;
122
+ description?: string;
123
+ meta?: {
124
+ title?: string;
125
+ description?: string;
126
+ };
127
+ };
128
+ /**
129
+ * Tracking + analytics config. Read by
130
+ * `<XMarkPrivacyCookieConsent>` and `useConsentTracking()`.
131
+ * Only fires after the matching consent category is granted.
132
+ */
133
+ tracking?: XMarketingTracking;
134
+ }
@@ -0,0 +1,35 @@
1
+ import { defineContentConfig, defineCollection } from '@nuxt/content'
2
+ import { z } from 'zod'
3
+
4
+ /**
5
+ * Default `blog` collection for marketing sites.
6
+ * Consumers can extend/override this config; markdown lives in the
7
+ * consumer (or playground) under `content/blog/*.md`.
8
+ */
9
+ export default defineContentConfig({
10
+ collections: {
11
+ blog: defineCollection({
12
+ type: 'page',
13
+ source: 'blog/**/*.md',
14
+ schema: z.object({
15
+ title: z.string(),
16
+ description: z.string().optional(),
17
+ date: z.date().optional(),
18
+ author: z.union([
19
+ z.string(),
20
+ z.object({
21
+ name: z.string(),
22
+ avatar: z.string().optional(),
23
+ title: z.string().optional(),
24
+ bio: z.string().optional(),
25
+ }),
26
+ ]).optional(),
27
+ image: z.string().optional(),
28
+ category: z.string().optional(),
29
+ tags: z.array(z.string()).optional(),
30
+ published: z.boolean().default(true),
31
+ readingTime: z.number().optional(),
32
+ }),
33
+ }),
34
+ },
35
+ })
package/nuxt.config.ts CHANGED
@@ -5,10 +5,23 @@ export default defineNuxtConfig({
5
5
  ssr: true,
6
6
  devtools: { enabled: process.env.NODE_ENV === "development" },
7
7
  runtimeConfig: {
8
- public: {
9
-
10
- },
8
+ public: {},
11
9
  },
12
- modules: ["@nuxt/ui"],
10
+ // Blog pages use Nuxt Content (`queryCollection('blog')`). Consumers must
11
+ // install `@nuxt/content` (peer) and provide `content/blog/**/*.md`.
12
+ // Builder.io is no longer used.
13
+ modules: ["@nuxt/ui", "@nuxt/content"],
13
14
  css: [resolve(__dirname, "app/assets/css/x-marketing.css")],
14
- });
15
+ content: {
16
+ build: {
17
+ markdown: {
18
+ highlight: {
19
+ theme: {
20
+ default: "github-light",
21
+ dark: "github-dark",
22
+ },
23
+ },
24
+ },
25
+ },
26
+ },
27
+ });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@xenterprises/nuxt-x-marketing",
3
3
  "license": "SEE LICENSE IN LICENSE",
4
4
  "type": "module",
5
- "version": "1.2.2",
5
+ "version": "1.3.0",
6
6
  "main": "./nuxt.config.ts",
7
7
  "scripts": {
8
8
  "dev": "nuxi dev .playground",
@@ -10,13 +10,18 @@
10
10
  "build": "nuxt build .playground",
11
11
  "generate": "nuxt generate .playground",
12
12
  "preview": "nuxt preview .playground",
13
+ "typecheck": "nuxt typecheck .playground",
14
+ "lint": "eslint .",
13
15
  "test": "npm run test:unit && npm run test:e2e",
14
16
  "test:unit": "vitest run --config test/vitest.config.js",
15
17
  "test:unit:watch": "vitest --config test/vitest.config.js",
16
18
  "test:unit:coverage": "vitest run --config test/vitest.config.js --coverage",
17
- "test:e2e": "playwright test --config test/playwright.config.js",
18
- "test:e2e:ui": "playwright test --config test/playwright.config.js --ui",
19
- "test:e2e:headed": "playwright test --config test/playwright.config.js --headed",
19
+ "test:run": "vitest run --config test/vitest.config.js",
20
+ "test:coverage": "vitest run --config test/vitest.config.js --coverage",
21
+ "test:e2e": "playwright test --config test/playwright.smoke.config.js",
22
+ "test:e2e:ui": "playwright test --config test/playwright.smoke.config.js --ui",
23
+ "test:e2e:headed": "playwright test --config test/playwright.smoke.config.js --headed",
24
+ "test:e2e:full": "playwright test --config test/playwright.config.js",
20
25
  "test:visual": "playwright test --config test/playwright.config.js visual-regression",
21
26
  "test:visual:update": "playwright test --config test/playwright.config.js visual-regression --update-snapshots"
22
27
  },
@@ -25,27 +30,43 @@
25
30
  "zod": "^3.23.8"
26
31
  },
27
32
  "peerDependencies": {
28
- "@iconify-json/lucide": ">=1.2.82",
29
- "@nuxt/ui": ">=4.6.1",
30
- "@tailwindcss/typography": ">=0.5.0",
31
- "nuxt": ">=4.0.0",
32
- "vue": ">=3.0.0"
33
+ "@iconify-json/lucide": "^1.2.82",
34
+ "@nuxt/content": "^3.0.0",
35
+ "@nuxt/ui": "^4.6.1",
36
+ "@tailwindcss/typography": "^0.5.0",
37
+ "better-sqlite3": "^11.0.0 || ^12.0.0",
38
+ "nuxt": "^4.0.0",
39
+ "vue": "^3.0.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@nuxt/content": {
43
+ "optional": false
44
+ },
45
+ "better-sqlite3": {
46
+ "optional": false
47
+ }
33
48
  },
34
49
  "devDependencies": {
35
50
  "@axe-core/playwright": "^4.8.4",
36
51
  "@iconify-json/lucide": "^1.2.82",
52
+ "@nuxt/content": "^3.8.2",
53
+ "@nuxt/eslint": "^1.15.2",
37
54
  "@nuxt/ui": "^4.6.1",
38
55
  "@playwright/test": "^1.41.1",
39
56
  "@tailwindcss/typography": "^0.5.19",
57
+ "@types/node": "^22.14.1",
40
58
  "@vitejs/plugin-vue": "^5.0.3",
41
59
  "@vitest/coverage-v8": "^2.1.8",
42
60
  "@vue/test-utils": "^2.4.4",
61
+ "better-sqlite3": "^12.5.0",
62
+ "eslint": "^9.39.0",
43
63
  "happy-dom": "^13.3.8",
44
64
  "nuxt": "^4.4.2",
45
65
  "nuxt-shiki": "^0.3.2",
46
66
  "typescript": "^5.6.3",
47
67
  "vitest": "^2.1.8",
48
- "vue": "latest"
68
+ "vue": "^3.5.39",
69
+ "vue-tsc": "^2.2.8"
49
70
  },
50
71
  "publishConfig": {
51
72
  "access": "public"