@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.
- package/CHANGELOG.md +34 -0
- package/README.md +100 -105
- package/app/app.config.ts +29 -123
- package/app/app.vue +31 -100
- package/app/components/X/Footer/index.vue +1 -1
- package/app/components/X/Mark/Button/BackToTop.vue +2 -2
- package/app/components/X/Mark/Privacy/CookieConsent.vue +15 -16
- package/app/components/X/Mark/Privacy/GDPR.vue +1 -1
- package/app/composables/useXBlog.js +168 -8
- package/app/pages/blog/[...slug].vue +81 -69
- package/app/pages/blog/index.vue +46 -50
- package/app/pages/index.vue +7 -4
- package/app/types/config.ts +134 -0
- package/content.config.ts +35 -0
- package/nuxt.config.ts +18 -5
- package/package.json +31 -10
- package/playwright-report-smoke/index.html +90 -0
package/app/pages/blog/index.vue
CHANGED
|
@@ -1,64 +1,60 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="pb-8 max-w-screen-2xl px-8 mx-auto">
|
|
3
|
-
<UPageHero
|
|
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
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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>
|
package/app/pages/index.vue
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
</h2>
|
|
47
47
|
</header>
|
|
48
48
|
|
|
49
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
84
|
-
<
|
|
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
|
-
|
|
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.
|
|
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:
|
|
18
|
-
"test:
|
|
19
|
-
"test:e2e
|
|
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": "
|
|
29
|
-
"@nuxt/
|
|
30
|
-
"@
|
|
31
|
-
"
|
|
32
|
-
"
|
|
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": "
|
|
68
|
+
"vue": "^3.5.39",
|
|
69
|
+
"vue-tsc": "^2.2.8"
|
|
49
70
|
},
|
|
50
71
|
"publishConfig": {
|
|
51
72
|
"access": "public"
|