@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 ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@xenterprises/nuxt-x-marketing` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.3.0] - 2026-07-24
9
+
10
+ ### BREAKING
11
+
12
+ - **Blog migrated from Builder.io to Nuxt Content.** Consumers that relied on `NUXT_PUBLIC_BUILDERIO_KEY` and Builder models must migrate posts to `content/blog/*.md` and install the Content peer dependencies.
13
+ - **Default app shell rewritten to a config-driven shell.** The layer's `app/app.vue` (previously a hardcoded demo referencing non-existent `XMarkNavbar`/`XMarkFooter`, rendering no navbar/footer) now renders `XHeaderNav` + `XFooter` from `xMarketing.header`/`footer` config plus the consent banner. Consumers without their own `app/app.vue` will see the rendered shell change; the hardcoded newsletter section, dead demo links, and `/logo-white.svg`/`/logo-dark.svg` fallbacks are gone.
14
+ - **`xMarketing.name`/`url` defaults changed from placeholders to `undefined`** and both keys are now live: `name` drives the shell title template and `og:site_name` (page titles gain a `| <name>` suffix when set), `url` drives the home page `og:url`. Consumers reading the old placeholder defaults from `appConfig` now get `undefined`.
15
+
16
+ ### Added
17
+
18
+ - Config-driven default shell with `xMarketing` opt-out switches for header, footer, and consent banner.
19
+ - `xMarketing.blog.active` opt-out on the shipped blog pages.
20
+ - `xMarketing.name`/`url` wired into shell SEO defaults (title template, `og:site_name`, `og:url`).
21
+ - Playground e2e smoke suite (`test:e2e`, plan 17) and lint quality gate.
22
+ - Consumer-minimal quickstart docs and plan-18 batteries-included audit.
23
+
24
+ ### Changed
25
+
26
+ - Exported named `XMarketingConfig` interface for typed `app.config.ts` (plan-21 config-typing convention).
27
+ - Peer dependency ranges tightened to caret bounds.
28
+
29
+ ### Fixed
30
+
31
+ - Widened `better-sqlite3` peer range to `^11.0.0 || ^12.0.0`.
32
+ - Default home page references real component names; `UToggle` replaced with `USwitch`.
33
+ - Repaired red gates: added `test:run`/`test:coverage` aliases and pinned `vue`.
34
+ - Test suite: fixed stale hero title assertion and scoped `h1` queries to main content.
package/README.md CHANGED
@@ -31,86 +31,56 @@ export default defineNuxtConfig({
31
31
 
32
32
  ## Quick Start
33
33
 
34
- ### Basic App Layout
34
+ ### What the consumer writes
35
35
 
36
- Create your `app.vue` with Navbar, Newsletter, and Footer:
36
+ The layer is batteries-included: it ships a default app shell (navbar + footer + cookie-consent banner) and default pages (`/`, `/blog`, `/blog/[...slug]`). You do **not** write an `app.vue` configure the shell in `app/app.config.ts` under the `xMarketing` namespace (it must live in `app/`, not the project root):
37
37
 
38
- ```vue
39
- <template>
40
- <div class="min-h-screen flex flex-col">
41
- <XMarkNavbar
42
- :links="navLinks"
43
- :logo-light="'/logo-white.svg'"
44
- :logo-dark="'/logo-dark.svg'"
45
- logo-alt="Company"
46
- >
47
- <template #actions>
48
- <UButton variant="ghost" color="neutral">Sign In</UButton>
49
- <UButton color="primary">Get Started</UButton>
50
- </template>
51
- </XMarkNavbar>
52
-
53
- <main class="flex-grow">
54
- <NuxtPage />
55
- </main>
56
-
57
- <XMarkSection bg="subtle" padding="lg">
58
- <XMarkNewsletter
59
- title="Stay in the loop"
60
- description="Get the latest updates delivered to your inbox."
61
- @submit="handleNewsletterSubmit"
62
- />
63
- </XMarkSection>
38
+ ```javascript
39
+ // app/app.config.ts
40
+ export default defineAppConfig({
41
+ xMarketing: {
42
+ name: "Acme Inc",
43
+ url: "https://acme.com",
44
+ header: {
45
+ logo: { src: "/logo.svg", srcDark: "/logo-dark.svg", alt: "Acme" },
46
+ nav: {
47
+ links: [
48
+ { label: "Pricing", to: "/#pricing" },
49
+ { label: "Blog", to: "/blog" },
50
+ ],
51
+ buttons: [{ label: "Get Started", to: "/signup", color: "primary" }],
52
+ },
53
+ },
54
+ footer: {
55
+ logo: { src: "/logo.svg", alt: "Acme" },
56
+ body: "Building the future of modern software.",
57
+ socials: [
58
+ { name: "GitHub", url: "https://github.com/acme", icon: "i-lucide-github" },
59
+ ],
60
+ columns: [
61
+ {
62
+ headerLabel: "Product",
63
+ links: [{ label: "Blog", to: "/blog" }],
64
+ },
65
+ ],
66
+ },
67
+ blog: { active: true, title: "Blog" },
68
+ tracking: { gtmId: "GTM-XXXXXXX" }, // optional; fires only after consent
69
+ },
70
+ });
71
+ ```
64
72
 
65
- <XMarkFooter
66
- :logo="'/logo-dark.svg'"
67
- description="Building the future of modern software."
68
- :social="socialLinks"
69
- :columns="footerColumns"
70
- :legal-links="legalLinks"
71
- />
72
- </div>
73
- </template>
73
+ Blog posts are markdown in `content/blog/*.md` — re-declare the `blog` collection in your own `content.config.ts` so Nuxt Content binds it to your content dir (the layer's `.playground/content.config.ts` is the reference).
74
74
 
75
- <script setup>
76
- const navLinks = [
77
- { label: "Features", to: "/#features" },
78
- { label: "Pricing", to: "/pricing" },
79
- { label: "Blog", to: "/blog" },
80
- ];
81
-
82
- const socialLinks = [
83
- { name: "Twitter", href: "https://twitter.com", icon: "i-lucide-twitter" },
84
- { name: "GitHub", href: "https://github.com", icon: "i-lucide-github" },
85
- ];
86
-
87
- const footerColumns = [
88
- {
89
- title: "Product",
90
- links: [
91
- { label: "Features", to: "/#features" },
92
- { label: "Pricing", to: "/pricing" },
93
- ],
94
- },
95
- {
96
- title: "Company",
97
- links: [
98
- { label: "About", to: "/about" },
99
- { label: "Blog", to: "/blog" },
100
- ],
101
- },
102
- ];
75
+ ### Opting out of the defaults
103
76
 
104
- const legalLinks = [
105
- { label: "Privacy Policy", to: "/privacy" },
106
- { label: "Terms of Service", to: "/terms" },
107
- ];
77
+ Every shipped default has a switch or a standard Nuxt override:
108
78
 
109
- const handleNewsletterSubmit = (email) => {
110
- console.log("Newsletter signup:", email);
111
- };
112
- </script>
113
- ```
79
+ - `xMarketing.header.active: false` removes the default navbar from the shell
80
+ - `xMarketing.footer.active: false` — removes the default footer
81
+ - `xMarketing.consent.active: false` — removes the cookie-consent banner (tracking scripts then never fire)
82
+ - `xMarketing.blog.active: false` — the shipped `/blog` pages 404
83
+ - Replace the whole shell by shipping your own `app/app.vue`; replace any page by shipping the same path under `app/pages/` (standard Nuxt layer overriding)
114
84
 
115
85
  ### Example Landing Page
116
86
 
@@ -177,32 +147,28 @@ const pricingPlans = [
177
147
 
178
148
  ### Core Layout
179
149
 
180
- #### XMarkNavbar
150
+ #### XMarkLayoutNavbar
181
151
 
182
- Fixed navigation header with transparent-to-solid scroll transition.
152
+ Fixed navigation header with transparent-to-solid scroll transition. Falls back to `xMarketing.header` (logo, nav links, nav buttons) when props are omitted.
183
153
 
184
154
  ```vue
185
- <XMarkNavbar
155
+ <XMarkLayoutNavbar
186
156
  :links="[{ label: 'Features', to: '/#features' }]"
187
- logo-light="/logo-white.svg"
188
- logo-dark="/logo-dark.svg"
189
- logo-alt="Company"
157
+ :logo="{ src: '/logo.svg', srcDark: '/logo-dark.svg', alt: 'Company' }"
158
+ :buttons="[
159
+ { label: 'Sign In', to: '/login', variant: 'ghost' },
160
+ { label: 'Get Started', to: '/signup', color: 'primary' },
161
+ ]"
190
162
  :transparent="true"
191
163
  :scroll-threshold="100"
192
- >
193
- <template #actions>
194
- <UButton variant="ghost">Sign In</UButton>
195
- <UButton color="primary">Get Started</UButton>
196
- </template>
197
- </XMarkNavbar>
164
+ />
198
165
  ```
199
166
 
200
167
  | Prop | Type | Default | Description |
201
168
  |------|------|---------|-------------|
202
- | `links` | `Array` | `[]` | Navigation links `{ label, to }` |
203
- | `logoLight` | `String` | `''` | Logo for transparent/dark state |
204
- | `logoDark` | `String` | `''` | Logo for solid/light state |
205
- | `logoAlt` | `String` | `'Logo'` | Logo alt text |
169
+ | `links` | `Array` | `xMarketing.header.nav.links` | Navigation links `{ label, to }` |
170
+ | `logo` | `Object` | `xMarketing.header.logo` | Logo `{ src, srcDark?, alt? }` |
171
+ | `buttons` | `Array` | `xMarketing.header.nav.buttons` | Action buttons `{ label, to, color?, variant?, icon? }` |
206
172
  | `transparent` | `Boolean` | `true` | Start transparent over hero |
207
173
  | `scrollThreshold` | `Number` | `100` | Pixels before transition |
208
174
 
@@ -226,40 +192,39 @@ Section wrapper with background variants and optional patterns.
226
192
  | `bgImage` | `String` | `''` | Background image URL |
227
193
  | `parallax` | `Boolean` | `false` | Enable parallax on bg image |
228
194
 
229
- #### XMarkFooter
195
+ #### XMarkLayoutFooter
230
196
 
231
197
  Full footer with brand, link columns, social icons, and newsletter.
232
198
 
233
199
  ```vue
234
- <XMarkFooter
235
- logo="/logo.svg"
236
- logo-alt="Company"
200
+ <XMarkLayoutFooter
201
+ :logo="{ src: '/logo.svg', alt: 'Company' }"
237
202
  description="Building the future of modern software."
238
203
  :social="[{ name: 'Twitter', href: 'https://twitter.com', icon: 'i-lucide-twitter' }]"
239
204
  :columns="[{ title: 'Product', links: [{ label: 'Features', to: '/features' }] }]"
240
205
  :legal-links="[{ label: 'Privacy', to: '/privacy' }]"
241
- :show-newsletter="true"
206
+ :has-newsletter="true"
242
207
  @newsletter-submit="handleSubmit"
243
208
  />
244
209
  ```
245
210
 
246
211
  | Prop | Type | Default | Description |
247
212
  |------|------|---------|-------------|
248
- | `logo` | `String` | `''` | Logo image URL |
213
+ | `logo` | `Object` | `null` | Logo `{ src, alt? }` |
249
214
  | `description` | `String` | `''` | Brand description |
250
215
  | `social` | `Array` | `[]` | Social links `{ name, href, icon }` |
251
216
  | `columns` | `Array` | `[]` | Link columns `{ title, links: [{ label, to }] }` |
252
217
  | `legalLinks` | `Array` | `[]` | Legal links `{ label, to }` |
253
218
  | `copyright` | `String` | auto | Copyright text |
254
- | `showNewsletter` | `Boolean` | `false` | Show newsletter section |
219
+ | `hasNewsletter` | `Boolean` | `false` | Show newsletter section |
255
220
 
256
- #### XMarkLegalFooter
221
+ #### XMarkLayoutFooterLegal
257
222
 
258
223
  Minimal footer with copyright and legal links only.
259
224
 
260
225
  ```vue
261
- <XMarkLegalFooter
262
- company-name="X Enterprises"
226
+ <XMarkLayoutFooterLegal
227
+ copyright="© 2026 Acme Inc. All rights reserved."
263
228
  :links="[{ label: 'Privacy', to: '/privacy' }]"
264
229
  />
265
230
  ```
@@ -1072,7 +1037,7 @@ npm run test:e2e
1072
1037
  | `useStaggerReveal(selector, delay?)` | Adds incremental `transition-delay` to `[data-reveal]` children inside a container. |
1073
1038
  | `useParallax(options?)` | Parallax scroll effect for `.xParallax[data-parallax-speed]` elements. Uses `requestAnimationFrame` for performance. |
1074
1039
  | `useElementParallax(speed?)` | Individual element parallax via template ref. Returns `{ elementRef }`. |
1075
- | `useXBlog()` | Blog data fetching composable. Reads `runtimeConfig.public.apiURL`. |
1040
+ | `useXBlog()` | Nuxt Content blog helpers (`getPosts`, `getPostByPath`, `getSurround`, `normalizeBlogPost`). Expects a `blog` collection and markdown under `content/blog/`. |
1076
1041
 
1077
1042
  All composables are SSR-safe — they guard lifecycle hooks with `getCurrentInstance()` and `import.meta.client` checks.
1078
1043
 
@@ -1082,11 +1047,10 @@ All composables are SSR-safe — they guard lifecycle hooks with `getCurrentInst
1082
1047
 
1083
1048
  | Name | Required | Description |
1084
1049
  |------|----------|-------------|
1085
- | `NUXT_PUBLIC_API_URL` | No | Base API URL for blog composable (used by `useXBlog`) |
1086
1050
  | Marker.io Project ID | No | Set via `appConfig.xMarketing.config.markerProjectId` (not env var) |
1087
1051
  | Newsletter Org ID | No | Set via `appConfig.xMarketing.config.emailMarketingNewsletters.organizationId` |
1088
1052
 
1089
- The layer itself has no required environment variables. All configuration is done via `app.config.ts`.
1053
+ The layer itself has no required environment variables. Blog content is **file-based via Nuxt Content** (not Builder.io). All site config is done via `app.config.ts`.
1090
1054
 
1091
1055
  ---
1092
1056
 
@@ -1179,7 +1143,8 @@ The layer provides a complete marketing website toolkit built on Nuxt UI v4:
1179
1143
 
1180
1144
  | Path | Purpose |
1181
1145
  |------|---------|
1182
- | `nuxt.config.ts` | Registers `@nuxt/ui`, loads CSS, enables SSR and devtools |
1146
+ | `nuxt.config.ts` | Registers `@nuxt/ui` + `@nuxt/content`, loads CSS, enables SSR |
1147
+ | `content.config.ts` | Default Nuxt Content `blog` collection schema |
1183
1148
  | `app/app.config.ts` | Default xMarketing config + Nuxt UI theme overrides + type augmentation |
1184
1149
  | `app/app.vue` | Default app shell with navbar, newsletter, footer (override in consumer app) |
1185
1150
  | `app/assets/css/x-marketing.css` | Full design system: colors, typography, animations, effects |
@@ -1190,7 +1155,36 @@ The layer provides a complete marketing website toolkit built on Nuxt UI v4:
1190
1155
  | `app/composables/` | Composables: useScrollReveal, useParallax, useXBlog |
1191
1156
  | `app/plugins/marketing.client.ts` | Client plugin: auto-initializes scroll/parallax globally |
1192
1157
  | `app/types/marketing.d.ts` | TypeScript interfaces for all data structures |
1193
- | `app/pages/` | Default blog pages (index + detail with slug routing) |
1158
+ | `app/pages/blog/` | Default blog index + slug detail (Nuxt Content, not Builder.io) |
1159
+
1160
+ ### Blog (Nuxt Content)
1161
+
1162
+ **Breaking (Builder.io removed):** default `/blog` pages no longer call the Builder CDN.
1163
+ Posts come from markdown in the consumer app.
1164
+
1165
+ 1. Peer-install `@nuxt/content` and `better-sqlite3` (already required by Content v3).
1166
+ 2. Ensure the layer is extended so `content.config.ts` defines the `blog` collection (or copy/adapt it).
1167
+ 3. Add posts under `content/blog/*.md`:
1168
+
1169
+ ```md
1170
+ ---
1171
+ title: Getting Started
1172
+ description: First post on the marketing site.
1173
+ date: 2025-01-15
1174
+ author: Tim
1175
+ image: /blog/cover.jpg
1176
+ category: Tutorial
1177
+ tags: [nuxt, marketing]
1178
+ published: true
1179
+ readingTime: 5
1180
+ ---
1181
+
1182
+ ## Hello
1183
+
1184
+ Markdown body rendered via `ContentRenderer`.
1185
+ ```
1186
+
1187
+ 4. `useXBlog().getPosts()` / `getPostByPath()` feed `XMarkBlogList` and `XMarkBlogDetail`.
1194
1188
 
1195
1189
  ### Overriding in Consumer Apps
1196
1190
 
@@ -1198,6 +1192,7 @@ The layer provides a complete marketing website toolkit built on Nuxt UI v4:
1198
1192
  - **app.config.ts**: Deep-merges with layer defaults. Set `xMarketing.header`, `xMarketing.footer`, etc.
1199
1193
  - **app.vue**: Override entirely by creating your own `app.vue` in the consumer app.
1200
1194
  - **Pages**: Consumer pages take precedence. Override `/blog` by creating `pages/blog/index.vue`.
1195
+ - **Content**: Own the markdown under `content/blog/`; do not put secrets in frontmatter.
1201
1196
 
1202
1197
  ---
1203
1198
 
package/app/app.config.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { XMarketingConfig } from "./types/config";
2
+
1
3
  export default defineAppConfig({
2
4
  ui: {
3
5
  primary: "brand",
@@ -25,19 +27,24 @@ export default defineAppConfig({
25
27
  },
26
28
  },
27
29
  xMarketing: {
28
- name: "X Enterprises",
29
- url: "https://x-enterprises.com",
30
+ /** Site name used by the shipped shell for the SEO title template
31
+ * ("%s | name") and og:site_name. Leave undefined to keep bare titles. */
32
+ name: undefined,
33
+ /** Canonical site URL — used as og:url on the shipped home page. */
34
+ url: undefined,
30
35
  config: {
31
- markerProjectId: "",
36
+ markerProjectId: undefined,
32
37
  emailMarketingNewsletters: {
33
- organizationId: "",
38
+ organizationId: undefined,
34
39
  },
35
40
  },
36
41
  header: {
42
+ /** Set false to opt out of the default navbar in the shipped shell. */
43
+ active: true as boolean,
37
44
  logo: {
38
- src: "",
39
- srcDark: "",
40
- alt: "",
45
+ src: undefined,
46
+ srcDark: undefined,
47
+ alt: undefined,
41
48
  },
42
49
  nav: {
43
50
  buttons: [],
@@ -45,23 +52,29 @@ export default defineAppConfig({
45
52
  },
46
53
  },
47
54
  footer: {
55
+ /** Set false to opt out of the default footer in the shipped shell. */
56
+ active: true as boolean,
48
57
  logo: {
49
- src: "",
50
- alt: "",
58
+ src: undefined,
59
+ alt: undefined,
51
60
  },
52
61
  bg: {
53
- color: "",
62
+ color: undefined,
54
63
  img: {
55
- src: "",
56
- alt: "",
64
+ src: undefined,
65
+ alt: undefined,
57
66
  },
58
67
  },
59
- body: "",
68
+ body: undefined,
60
69
  socials: [],
61
70
  columns: [],
62
71
  },
72
+ /** Cookie-consent banner in the shipped shell; gates tracking injection. */
73
+ consent: {
74
+ active: true as boolean,
75
+ },
63
76
  blog: {
64
- active: true,
77
+ active: true as boolean,
65
78
  title: "Blog",
66
79
  description:
67
80
  "Welcome to our blog. Here you can find the latest news, updates, and articles.",
@@ -94,121 +107,14 @@ export default defineAppConfig({
94
107
  scripts: [],
95
108
  // ---- Behavior ----
96
109
  /** Auto-inject configured scripts on consent. Default: true. */
97
- autoInject: true,
110
+ autoInject: true as boolean,
98
111
  },
99
112
  },
100
113
  });
101
114
 
102
115
  declare module "@nuxt/schema" {
103
116
  interface AppConfigInput {
104
- xMarketing?: {
105
- /** Project name */
106
- name?: string;
107
- /** Project URL */
108
- url?: string;
109
- config?: {
110
- markerProjectId?: string;
111
- emailMarketingNewsletters?: {
112
- organizationId?: string;
113
- };
114
- };
115
- header?: {
116
- logo?: {
117
- src?: string;
118
- srcDark?: string;
119
- alt?: string;
120
- };
121
- nav?: {
122
- buttons?: {
123
- label: string;
124
- to: string;
125
- icon?: string;
126
- target?: string;
127
- variant?: string;
128
- square?: boolean;
129
- color?: string;
130
- }[];
131
- links?: NavLink[];
132
- };
133
- };
134
- footer?: {
135
- body?: string;
136
- bg?: {
137
- color?: string;
138
- img?: {
139
- src?: string;
140
- alt?: string;
141
- };
142
- };
143
- logo?: {
144
- src?: string;
145
- alt?: string;
146
- };
147
- socials?: {
148
- name: string;
149
- url: string;
150
- icon: string;
151
- }[];
152
- columns?: {
153
- headerLabel: string;
154
- links: {
155
- label: string;
156
- to: string;
157
- target?: string;
158
- }[];
159
- }[];
160
- /**
161
- * Tracking + analytics config. Read by
162
- * `<XMarkPrivacyCookieConsent>` and `useConsentTracking()`.
163
- * Only fires after the matching consent category is granted.
164
- */
165
- tracking?: XMarketingTracking;
166
- };
117
+ xMarketing?: XMarketingConfig;
167
118
  }
168
119
  }
169
120
 
170
- /**
171
- * Tracking entry — use the convenience IDs for the common tools
172
- * (GTM, GA4, Clarity) or `scripts[]` for everything else. Scripts
173
- * are injected only after the visitor grants consent for the
174
- * matching `category` (default: "analytics").
175
- */
176
- interface XMarketingTracking {
177
- /** Google Tag Manager container ID, e.g. "GTM-XXXXXXX". */
178
- gtmId?: string;
179
- /** Google Analytics 4 measurement ID, e.g. "G-XXXXXXXX". */
180
- ga4Id?: string;
181
- /** Microsoft Clarity project ID, e.g. "abc123def4". */
182
- clarityId?: string;
183
- /**
184
- * Arbitrary scripts (Meta Pixel, Hotjar, Segment, LinkedIn Insight,
185
- * Pinterest, TikTok, etc.). Each fires once the matching consent
186
- * category is granted. Use `inline` for hand-written snippets like
187
- * `gtag('js', new Date())`.
188
- */
189
- scripts?: XMarketingTrackingScript[];
190
- /** Auto-inject configured scripts on consent. Default: true. */
191
- autoInject?: boolean;
192
- }
193
-
194
- interface XMarketingTrackingScript {
195
- /** Stable ID — used to dedupe re-injection. */
196
- id: string;
197
- /** External script URL (e.g. "https://www.googletagmanager.com/gtag/js?id=G-XXX"). */
198
- src?: string;
199
- /** Inline script body (no `src` required). */
200
- inline?: string;
201
- /** Consent category gate. "necessary" fires immediately; "analytics" / "marketing" gated. */
202
- category?: "necessary" | "analytics" | "marketing";
203
- /** Extra attributes applied to the injected <script>. */
204
- attrs?: Record<string, string>;
205
- }
206
- }
207
-
208
- interface NavLink {
209
- label: string;
210
- to?: string;
211
- target?: string;
212
- children?: NavLink[];
213
- }
214
-