@stackonward/cli 1.0.0 → 1.1.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/dist/index.js +1 -1
- package/dist/resources/components/nuxt/site-source/app/components/ui/CheckoutSummary.vue +8 -5
- package/dist/resources/components/nuxt/site-source/app/components/ui/GoogleSignInButton.vue +13 -5
- package/dist/resources/components/nuxt/site-source/app/composables/useConsent.ts +45 -5
- package/dist/resources/components/nuxt/site-source/app/plugins/google-identity.client.ts +5 -2
- package/dist/resources/components/registry.json +0 -8
- package/dist/resources/registry.json +24 -6
- package/dist/resources/templates/go-backend/scripts/test-migrations.sh +0 -0
- package/dist/resources/templates/nuxt/.claude/docs/stack.md +12 -12
- package/dist/resources/templates/nuxt/.env.example +0 -2
- package/dist/resources/templates/nuxt/.husky/commit-msg +0 -0
- package/dist/resources/templates/nuxt/.husky/pre-commit +0 -0
- package/dist/resources/templates/nuxt/README.md +8 -1
- package/dist/resources/templates/nuxt/app/pages/ui/index.vue +3 -2
- package/dist/resources/templates/nuxt/nuxt.config.ts +22 -11
- package/dist/resources/templates/nuxt/package.json +3 -2
- package/package.json +12 -13
- package/dist/resources/components/nuxt/site-source/app/composables/useAnalytics.ts +0 -24
- package/dist/resources/components/nuxt/site-source/app/plugins/analytics.client.ts +0 -121
package/dist/index.js
CHANGED
|
@@ -5415,7 +5415,7 @@ async function runApple(mode, options) {
|
|
|
5415
5415
|
|
|
5416
5416
|
// src/index.ts
|
|
5417
5417
|
var program = new Command();
|
|
5418
|
-
program.name("stackonward").description("Create projects and install reusable components and snippets").version("1.
|
|
5418
|
+
program.name("stackonward").description("Create projects and install reusable components and snippets").version("1.1.0");
|
|
5419
5419
|
program.command("init [name]").description("Create a new project from template").option("-t, --template <template>", "Template to use").option("-d, --dir <directory>", "Target directory").option("--skip-install", "Skip dependency installation").option("--skip-git", "Skip git initialization").option("--module <module>", "Go module path (required for go-backend)").option("--author <author>", "Author name").option("-y, --yes", "Use defaults without interactive prompts").action(init);
|
|
5420
5420
|
program.command("add [items...]").description("Add components or snippets to your project").option("-c, --component", "Add component(s)").option("-s, --snippet", "Add snippet(s)").option("-a, --all", "Add all items in category").option("-y, --yes", "Skip confirmation prompts").option("-o, --overwrite", "Overwrite existing files").option("-p, --path <path>", "Custom install path").action(add);
|
|
5421
5421
|
program.command("list [type]").description("List available templates, components, or snippets").option("-t, --templates", "List templates").option("-c, --components", "List components").option("-s, --snippets", "List snippets").action(list);
|
|
@@ -65,11 +65,14 @@ function openCheckout(): void {
|
|
|
65
65
|
customer: props.customer,
|
|
66
66
|
metadata: props.metadata,
|
|
67
67
|
});
|
|
68
|
-
analytics.track(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
void analytics.track({
|
|
69
|
+
name: "checkout_started",
|
|
70
|
+
properties: {
|
|
71
|
+
attempt_id: props.attemptId,
|
|
72
|
+
currency: currency.value,
|
|
73
|
+
value_minor: totalAmountMinor.value,
|
|
74
|
+
item_count: props.lineItems.length,
|
|
75
|
+
},
|
|
73
76
|
});
|
|
74
77
|
emit("checkout", url);
|
|
75
78
|
if (import.meta.client) {
|
|
@@ -15,7 +15,12 @@ const props = withDefaults(
|
|
|
15
15
|
{
|
|
16
16
|
label: "Continue with Google",
|
|
17
17
|
disabledLabel: "Google sign-in is not configured",
|
|
18
|
-
options: () => ({
|
|
18
|
+
options: () => ({
|
|
19
|
+
theme: "outline",
|
|
20
|
+
size: "large",
|
|
21
|
+
text: "continue_with",
|
|
22
|
+
shape: "rectangular",
|
|
23
|
+
}),
|
|
19
24
|
autoExchange: false,
|
|
20
25
|
oneTap: false,
|
|
21
26
|
},
|
|
@@ -35,8 +40,9 @@ const loading = ref(false);
|
|
|
35
40
|
|
|
36
41
|
async function handleCredential(response: GoogleCredentialResponse) {
|
|
37
42
|
emit("credential", response);
|
|
38
|
-
analytics.track(
|
|
39
|
-
|
|
43
|
+
void analytics.track({
|
|
44
|
+
name: "identity_google_credential",
|
|
45
|
+
...(response.select_by === undefined ? {} : { properties: { select_by: response.select_by } }),
|
|
40
46
|
});
|
|
41
47
|
|
|
42
48
|
if (!props.autoExchange || !response.credential) {
|
|
@@ -48,7 +54,8 @@ async function handleCredential(response: GoogleCredentialResponse) {
|
|
|
48
54
|
const payload = await identity.exchangeCredential(response.credential);
|
|
49
55
|
emit("authenticated", payload);
|
|
50
56
|
} catch (error) {
|
|
51
|
-
const nextError =
|
|
57
|
+
const nextError =
|
|
58
|
+
error instanceof Error ? error : new Error("Google credential exchange failed");
|
|
52
59
|
errorMessage.value = nextError.message;
|
|
53
60
|
emit("error", nextError);
|
|
54
61
|
} finally {
|
|
@@ -68,7 +75,8 @@ async function renderGoogleButton() {
|
|
|
68
75
|
await identity.promptOneTap(handleCredential);
|
|
69
76
|
}
|
|
70
77
|
} catch (error) {
|
|
71
|
-
const nextError =
|
|
78
|
+
const nextError =
|
|
79
|
+
error instanceof Error ? error : new Error("Google sign-in failed to initialize");
|
|
72
80
|
errorMessage.value = nextError.message;
|
|
73
81
|
emit("error", nextError);
|
|
74
82
|
}
|
|
@@ -2,23 +2,63 @@ export type ConsentValue = "accepted" | "rejected";
|
|
|
2
2
|
|
|
3
3
|
export function useConsent(storageKey?: string) {
|
|
4
4
|
const site = useSiteShell();
|
|
5
|
+
const analytics = useAnalytics();
|
|
6
|
+
const analyticsConsent = useAnalyticsConsent();
|
|
5
7
|
const resolvedStorageKey = computed(() => storageKey ?? site.value.consent.storageKey);
|
|
6
8
|
const value = ref<ConsentValue | null>(null);
|
|
7
9
|
const ready = ref(false);
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
const stored = window.localStorage.getItem(resolvedStorageKey.value);
|
|
11
|
+
function applyStoredValue(stored: string | null): void {
|
|
11
12
|
value.value = stored === "accepted" || stored === "rejected" ? stored : null;
|
|
13
|
+
if (value.value === "accepted") {
|
|
14
|
+
analyticsConsent.grant();
|
|
15
|
+
} else if (value.value === "rejected") {
|
|
16
|
+
analyticsConsent.deny();
|
|
17
|
+
} else {
|
|
18
|
+
analyticsConsent.reset();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleStorage(event: StorageEvent): void {
|
|
23
|
+
if (event.key === resolvedStorageKey.value) {
|
|
24
|
+
applyStoredValue(event.newValue);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onMounted(() => {
|
|
29
|
+
try {
|
|
30
|
+
applyStoredValue(window.localStorage.getItem(resolvedStorageKey.value));
|
|
31
|
+
} catch {
|
|
32
|
+
value.value = null;
|
|
33
|
+
analyticsConsent.reset();
|
|
34
|
+
}
|
|
35
|
+
window.addEventListener("storage", handleStorage);
|
|
12
36
|
ready.value = true;
|
|
13
37
|
});
|
|
14
38
|
|
|
15
|
-
|
|
39
|
+
onBeforeUnmount(() => window.removeEventListener("storage", handleStorage));
|
|
40
|
+
|
|
41
|
+
const visible = computed(
|
|
42
|
+
() =>
|
|
43
|
+
site.value.consent.enabled &&
|
|
44
|
+
analytics.status.value !== "disabled" &&
|
|
45
|
+
ready.value &&
|
|
46
|
+
value.value === null,
|
|
47
|
+
);
|
|
16
48
|
|
|
17
49
|
function setConsent(next: ConsentValue): void {
|
|
18
50
|
value.value = next;
|
|
19
51
|
if (import.meta.client) {
|
|
20
|
-
|
|
21
|
-
|
|
52
|
+
try {
|
|
53
|
+
window.localStorage.setItem(resolvedStorageKey.value, next);
|
|
54
|
+
} catch {
|
|
55
|
+
// 浏览器拒绝持久化时,本次会话的明确选择仍然生效。
|
|
56
|
+
}
|
|
57
|
+
if (next === "accepted") {
|
|
58
|
+
analyticsConsent.grant();
|
|
59
|
+
} else {
|
|
60
|
+
analyticsConsent.deny();
|
|
61
|
+
}
|
|
22
62
|
}
|
|
23
63
|
}
|
|
24
64
|
|
|
@@ -12,8 +12,11 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
12
12
|
identity
|
|
13
13
|
.promptOneTap((response) => {
|
|
14
14
|
window.dispatchEvent(new CustomEvent("site:google-credential", { detail: response }));
|
|
15
|
-
analytics.track(
|
|
16
|
-
|
|
15
|
+
void analytics.track({
|
|
16
|
+
name: "identity_google_credential",
|
|
17
|
+
...(response.select_by === undefined
|
|
18
|
+
? {}
|
|
19
|
+
: { properties: { select_by: response.select_by } }),
|
|
17
20
|
});
|
|
18
21
|
})
|
|
19
22
|
.catch((error) => {
|
|
@@ -241,10 +241,6 @@
|
|
|
241
241
|
"path": "site-source/app/components/ui/YoutubeEmbed.vue",
|
|
242
242
|
"target": "app/components/ui/YoutubeEmbed.vue"
|
|
243
243
|
},
|
|
244
|
-
{
|
|
245
|
-
"path": "site-source/app/composables/useAnalytics.ts",
|
|
246
|
-
"target": "app/composables/useAnalytics.ts"
|
|
247
|
-
},
|
|
248
244
|
{
|
|
249
245
|
"path": "site-source/app/composables/useCheckout.ts",
|
|
250
246
|
"target": "app/composables/useCheckout.ts"
|
|
@@ -281,10 +277,6 @@
|
|
|
281
277
|
"path": "site-source/app/layouts/default.vue",
|
|
282
278
|
"target": "app/layouts/default.vue"
|
|
283
279
|
},
|
|
284
|
-
{
|
|
285
|
-
"path": "site-source/app/plugins/analytics.client.ts",
|
|
286
|
-
"target": "app/plugins/analytics.client.ts"
|
|
287
|
-
},
|
|
288
280
|
{
|
|
289
281
|
"path": "site-source/app/plugins/google-identity.client.ts",
|
|
290
282
|
"target": "app/plugins/google-identity.client.ts"
|
|
@@ -5,23 +5,35 @@
|
|
|
5
5
|
"type": "npm",
|
|
6
6
|
"version": "^0.0.2"
|
|
7
7
|
},
|
|
8
|
+
"@stackonward/analytics-core": {
|
|
9
|
+
"name": "@stackonward/analytics-core",
|
|
10
|
+
"description": "Provider-neutral analytics pipeline and browser autocapture",
|
|
11
|
+
"type": "npm",
|
|
12
|
+
"version": "^0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"@stackonward/analytics-nuxt": {
|
|
15
|
+
"name": "@stackonward/analytics-nuxt",
|
|
16
|
+
"description": "Provider-neutral Nuxt analytics lifecycle",
|
|
17
|
+
"type": "npm",
|
|
18
|
+
"version": "^0.1.0"
|
|
19
|
+
},
|
|
8
20
|
"@stackonward/cli": {
|
|
9
21
|
"name": "@stackonward/cli",
|
|
10
22
|
"description": "StackOnward project and resource CLI",
|
|
11
23
|
"type": "npm",
|
|
12
|
-
"version": "^1.
|
|
24
|
+
"version": "^1.1.0"
|
|
13
25
|
},
|
|
14
26
|
"@stackonward/cms-client": {
|
|
15
27
|
"name": "@stackonward/cms-client",
|
|
16
28
|
"description": "Typed CMS delivery client",
|
|
17
29
|
"type": "npm",
|
|
18
|
-
"version": "^0.0.
|
|
30
|
+
"version": "^0.0.3"
|
|
19
31
|
},
|
|
20
32
|
"@stackonward/cms-nuxt": {
|
|
21
33
|
"name": "@stackonward/cms-nuxt",
|
|
22
34
|
"description": "Generic CMS Nuxt runtime",
|
|
23
35
|
"type": "npm",
|
|
24
|
-
"version": "^0.0.
|
|
36
|
+
"version": "^0.0.3"
|
|
25
37
|
},
|
|
26
38
|
"@stackonward/commitlint-config": {
|
|
27
39
|
"name": "@stackonward/commitlint-config",
|
|
@@ -53,6 +65,12 @@
|
|
|
53
65
|
"type": "npm",
|
|
54
66
|
"version": "^0.0.1"
|
|
55
67
|
},
|
|
68
|
+
"@stackonward/google-tag-manager-nuxt": {
|
|
69
|
+
"name": "@stackonward/google-tag-manager-nuxt",
|
|
70
|
+
"description": "Consent-aware Google Tag Manager analytics integration for Nuxt",
|
|
71
|
+
"type": "npm",
|
|
72
|
+
"version": "^0.1.0"
|
|
73
|
+
},
|
|
56
74
|
"@stackonward/identity-browser": {
|
|
57
75
|
"name": "@stackonward/identity-browser",
|
|
58
76
|
"description": "Browser client for opaque identity sessions",
|
|
@@ -81,19 +99,19 @@
|
|
|
81
99
|
"name": "@stackonward/nuxt-layer-base",
|
|
82
100
|
"description": "Nuxt transport, session, SEO, and i18n foundation",
|
|
83
101
|
"type": "layer",
|
|
84
|
-
"version": "^0.0.
|
|
102
|
+
"version": "^0.0.3"
|
|
85
103
|
},
|
|
86
104
|
"@stackonward/nuxt-site-shell": {
|
|
87
105
|
"name": "@stackonward/nuxt-site-shell",
|
|
88
106
|
"description": "Brand-free Nuxt product-site shell",
|
|
89
107
|
"type": "layer",
|
|
90
|
-
"version": "^0.0
|
|
108
|
+
"version": "^0.1.0"
|
|
91
109
|
},
|
|
92
110
|
"@stackonward/onex-cms-client": {
|
|
93
111
|
"name": "@stackonward/onex-cms-client",
|
|
94
112
|
"description": "OneX CMS HTTP contract adapter",
|
|
95
113
|
"type": "npm",
|
|
96
|
-
"version": "^0.0.
|
|
114
|
+
"version": "^0.0.3"
|
|
97
115
|
},
|
|
98
116
|
"@stackonward/onex-device-context": {
|
|
99
117
|
"name": "@stackonward/onex-device-context",
|
|
File without changes
|
|
@@ -30,18 +30,18 @@ ESLint composes the Nuxt-generated flat configuration with `@stackonward/eslint-
|
|
|
30
30
|
|
|
31
31
|
`nuxt.config.ts` owns public runtime mapping; `.env.example` documents available values. Public means readable by browser users.
|
|
32
32
|
|
|
33
|
-
| Variable family | Consumer and meaning
|
|
34
|
-
| -------------------------------------------------------------------------------- |
|
|
35
|
-
| `NUXT_PUBLIC_API_BASE_URL`, `NUXT_PUBLIC_API_TIMEOUT` | Public API URL and timeout configuration
|
|
36
|
-
| `NUXT_PUBLIC_SITE_URL`, `SITE_ENV`, `SITE_NAME`, `SITE_DESCRIPTION` | Canonical site identity and environment under the same prefix
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED`, `GOOGLE_CLIENT_ID` | Google client-side identity configuration
|
|
41
|
-
| `NUXT_PUBLIC_GOOGLE_CREDENTIAL_ENDPOINT`, `GOOGLE_ONE_TAP`, `GOOGLE_AUTO_PROMPT` | Configured credential exchange endpoint and prompting options
|
|
42
|
-
| `NUXT_PUBLIC_CHECKOUT_URL`, `CHECKOUT_RESULT_PARAM` | Checkout destination and return parameter
|
|
43
|
-
| `NUXT_PUBLIC_YOUTUBE_PRIVACY_ENHANCED` | Video privacy configuration
|
|
44
|
-
| `NUXT_PUBLIC_OG_IMAGE_ENABLED` | SEO layer option; verify the installed layer consumer before changing behavior
|
|
33
|
+
| Variable family | Consumer and meaning |
|
|
34
|
+
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
35
|
+
| `NUXT_PUBLIC_API_BASE_URL`, `NUXT_PUBLIC_API_TIMEOUT` | Public API URL and timeout configuration |
|
|
36
|
+
| `NUXT_PUBLIC_SITE_URL`, `SITE_ENV`, `SITE_NAME`, `SITE_DESCRIPTION` | Canonical site identity and environment under the same prefix |
|
|
37
|
+
| `NUXT_PUBLIC_ANALYTICS_DEBUG`, `NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT` | Analytics diagnostics and consent policy |
|
|
38
|
+
| `NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING`, `NUXT_PUBLIC_ANALYTICS_DATA_LAYER` | SPA page capture and GTM data-layer configuration |
|
|
39
|
+
| `NUXT_PUBLIC_GTM_ID` | Public Google Tag Manager container ID; an empty value disables the integration |
|
|
40
|
+
| `NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED`, `GOOGLE_CLIENT_ID` | Google client-side identity configuration |
|
|
41
|
+
| `NUXT_PUBLIC_GOOGLE_CREDENTIAL_ENDPOINT`, `GOOGLE_ONE_TAP`, `GOOGLE_AUTO_PROMPT` | Configured credential exchange endpoint and prompting options |
|
|
42
|
+
| `NUXT_PUBLIC_CHECKOUT_URL`, `CHECKOUT_RESULT_PARAM` | Checkout destination and return parameter |
|
|
43
|
+
| `NUXT_PUBLIC_YOUTUBE_PRIVACY_ENHANCED` | Video privacy configuration |
|
|
44
|
+
| `NUXT_PUBLIC_OG_IMAGE_ENABLED` | SEO layer option; verify the installed layer consumer before changing behavior |
|
|
45
45
|
|
|
46
46
|
Never place OAuth secrets, service credentials or provider API keys here. An enabled client feature does not prove backend credentials or permission are configured. Validate numeric input at its actual consumer rather than relying on `Number.parseInt` alone.
|
|
47
47
|
|
|
@@ -4,12 +4,10 @@ NUXT_PUBLIC_SITE_URL=
|
|
|
4
4
|
NUXT_PUBLIC_SITE_ENV=development
|
|
5
5
|
NUXT_PUBLIC_SITE_NAME="{{projectName}}"
|
|
6
6
|
NUXT_PUBLIC_SITE_DESCRIPTION="Reusable Nuxt product-site foundation."
|
|
7
|
-
NUXT_PUBLIC_ANALYTICS_ENABLED=false
|
|
8
7
|
NUXT_PUBLIC_ANALYTICS_DEBUG=false
|
|
9
8
|
NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT=true
|
|
10
9
|
NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING=true
|
|
11
10
|
NUXT_PUBLIC_ANALYTICS_DATA_LAYER=dataLayer
|
|
12
|
-
NUXT_PUBLIC_GA_ID=
|
|
13
11
|
NUXT_PUBLIC_GTM_ID=
|
|
14
12
|
NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED=false
|
|
15
13
|
NUXT_PUBLIC_GOOGLE_CLIENT_ID=
|
|
File without changes
|
|
File without changes
|
|
@@ -5,12 +5,19 @@ This template extends `@stackonward/nuxt-site-shell` for Nuxt module wiring and
|
|
|
5
5
|
- Nuxt UI and Tailwind theme wiring.
|
|
6
6
|
- Local source files for marketing, app, content and static page shells under `app/layouts` and `app/components`.
|
|
7
7
|
- Editable base components such as `BaseButton`, `BaseModal`, `BaseText`, `BasePopover`, `GoogleSignInButton`, `YoutubeEmbed`, `InfiniteScroll`, `CheckoutSummary` and `CheckoutStatus`.
|
|
8
|
-
- Editable composables and plugins for Google Identity,
|
|
8
|
+
- Editable composables and plugins for Google Identity, consent presentation, privacy-enhanced YouTube embeds and provider-free checkout redirects.
|
|
9
|
+
- Versioned global analytics through `@stackonward/google-tag-manager-nuxt`, including consent gating, SPA page views, element/link autocapture, manual events and product-configurable capture rules.
|
|
9
10
|
- Local Nuxt UI theme overrides under `app/theme` and CSS tokens under `app/assets/css`.
|
|
10
11
|
- A generated `/ui` page showing how to use the included theme and components.
|
|
11
12
|
|
|
12
13
|
Product-specific copy, assets, auth, analytics, CMS providers and business workflows belong in the generated application. Source is copied into the project so product teams can edit it directly.
|
|
13
14
|
|
|
15
|
+
## Analytics
|
|
16
|
+
|
|
17
|
+
Set `NUXT_PUBLIC_GTM_ID` to a published GTM container such as `GTM-ABC123`. The integration remains disabled when the ID is empty. The included consent UI restores the product-owned local decision and starts analytics only after acceptance.
|
|
18
|
+
|
|
19
|
+
Global capture defaults and product overrides live under `googleTagManager.analytics` in `nuxt.config.ts`. Products can configure selectors, internal domains, tracked link types, module naming/rules, event names, device breakpoints and privacy-sensitive field capture. Business event names remain typed in product source. The remote GTM container owns GA4 tags, triggers and consent checks.
|
|
20
|
+
|
|
14
21
|
## Engineering workflow
|
|
15
22
|
|
|
16
23
|
```bash
|
|
@@ -42,8 +42,9 @@ const visibleInfiniteItems = computed(() => infiniteItems.slice(0, visibleItems.
|
|
|
42
42
|
const hasMoreInfiniteItems = computed(() => visibleItems.value < infiniteItems.length);
|
|
43
43
|
|
|
44
44
|
function trackPreviewEvent() {
|
|
45
|
-
analytics.track(
|
|
46
|
-
|
|
45
|
+
void analytics.track({
|
|
46
|
+
name: "creative_system_action",
|
|
47
|
+
properties: { surface: "design_system" },
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { defineNuxtConfig } from "nuxt/config";
|
|
2
2
|
|
|
3
|
+
const siteEnv = process.env.NUXT_PUBLIC_SITE_ENV || "development";
|
|
4
|
+
const gtmId = process.env.NUXT_PUBLIC_GTM_ID || "";
|
|
5
|
+
|
|
3
6
|
export default defineNuxtConfig({
|
|
4
7
|
extends: ["@stackonward/nuxt-site-shell"],
|
|
5
|
-
modules: ["@nuxt/eslint"],
|
|
8
|
+
modules: ["@nuxt/eslint", "@stackonward/google-tag-manager-nuxt"],
|
|
6
9
|
compatibilityDate: "2025-07-15",
|
|
7
10
|
css: ["~/assets/css/site-shell.css"],
|
|
8
11
|
components: [
|
|
@@ -14,6 +17,23 @@ export default defineNuxtConfig({
|
|
|
14
17
|
imports: {
|
|
15
18
|
dirs: ["~/composables"],
|
|
16
19
|
},
|
|
20
|
+
googleTagManager: {
|
|
21
|
+
id: gtmId,
|
|
22
|
+
dataLayerName: process.env.NUXT_PUBLIC_ANALYTICS_DATA_LAYER || "dataLayer",
|
|
23
|
+
analytics: {
|
|
24
|
+
applicationId: "{{projectName}}",
|
|
25
|
+
environment: siteEnv,
|
|
26
|
+
debug: process.env.NUXT_PUBLIC_ANALYTICS_DEBUG === "true",
|
|
27
|
+
consent: {
|
|
28
|
+
required: process.env.NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT !== "false",
|
|
29
|
+
},
|
|
30
|
+
autoCapture: {
|
|
31
|
+
pageViews: process.env.NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING !== "false",
|
|
32
|
+
modulePrefix: "",
|
|
33
|
+
moduleRules: [],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
17
37
|
icon: {
|
|
18
38
|
customCollections: [
|
|
19
39
|
{
|
|
@@ -34,20 +54,11 @@ export default defineNuxtConfig({
|
|
|
34
54
|
},
|
|
35
55
|
site: {
|
|
36
56
|
url: process.env.NUXT_PUBLIC_SITE_URL || "",
|
|
37
|
-
env:
|
|
57
|
+
env: siteEnv,
|
|
38
58
|
name: process.env.NUXT_PUBLIC_SITE_NAME || "{{projectName}}",
|
|
39
59
|
description:
|
|
40
60
|
process.env.NUXT_PUBLIC_SITE_DESCRIPTION || "Premium product media production workspace.",
|
|
41
61
|
},
|
|
42
|
-
analytics: {
|
|
43
|
-
enabled: process.env.NUXT_PUBLIC_ANALYTICS_ENABLED === "true",
|
|
44
|
-
debug: process.env.NUXT_PUBLIC_ANALYTICS_DEBUG === "true",
|
|
45
|
-
requireConsent: process.env.NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT !== "false",
|
|
46
|
-
pageTracking: process.env.NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING !== "false",
|
|
47
|
-
dataLayerName: process.env.NUXT_PUBLIC_ANALYTICS_DATA_LAYER || "dataLayer",
|
|
48
|
-
gaId: process.env.NUXT_PUBLIC_GA_ID || "",
|
|
49
|
-
gtmId: process.env.NUXT_PUBLIC_GTM_ID || "",
|
|
50
|
-
},
|
|
51
62
|
identity: {
|
|
52
63
|
google: {
|
|
53
64
|
enabled: process.env.NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED === "true",
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@stackonward/api-client": "^0.0.2",
|
|
32
|
-
"@stackonward/
|
|
33
|
-
"@stackonward/nuxt-
|
|
32
|
+
"@stackonward/google-tag-manager-nuxt": "^0.1.0",
|
|
33
|
+
"@stackonward/nuxt-layer-base": "^0.0.3",
|
|
34
|
+
"@stackonward/nuxt-site-shell": "^0.1.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@commitlint/cli": "^21.2.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackonward/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "StackOnward CLI for creating projects and installing reusable components and snippets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,15 +10,6 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"dev": "tsup --watch",
|
|
15
|
-
"build": "tsup",
|
|
16
|
-
"prepack": "pnpm build",
|
|
17
|
-
"clean": "rm -rf dist",
|
|
18
|
-
"typecheck": "tsc --noEmit",
|
|
19
|
-
"test": "vitest run",
|
|
20
|
-
"check:package": "node scripts/check-package.mjs"
|
|
21
|
-
},
|
|
22
13
|
"keywords": [
|
|
23
14
|
"cli",
|
|
24
15
|
"stackonward",
|
|
@@ -37,7 +28,6 @@
|
|
|
37
28
|
"directory": "packages/cli"
|
|
38
29
|
},
|
|
39
30
|
"dependencies": {
|
|
40
|
-
"@stackonward/api-client": "workspace:*",
|
|
41
31
|
"commander": "^12.0.0",
|
|
42
32
|
"execa": "^9.0.0",
|
|
43
33
|
"fs-extra": "^11.2.0",
|
|
@@ -45,7 +35,8 @@
|
|
|
45
35
|
"kolorist": "^1.8.0",
|
|
46
36
|
"ora": "^8.0.0",
|
|
47
37
|
"prompts": "^2.4.2",
|
|
48
|
-
"zod": "4"
|
|
38
|
+
"zod": "4",
|
|
39
|
+
"@stackonward/api-client": "0.0.2"
|
|
49
40
|
},
|
|
50
41
|
"devDependencies": {
|
|
51
42
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -56,5 +47,13 @@
|
|
|
56
47
|
},
|
|
57
48
|
"engines": {
|
|
58
49
|
"node": ">=20.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"dev": "tsup --watch",
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"clean": "rm -rf dist",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"check:package": "node scripts/check-package.mjs"
|
|
59
58
|
}
|
|
60
|
-
}
|
|
59
|
+
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export interface AnalyticsEvent {
|
|
2
|
-
name: string;
|
|
3
|
-
properties?: Record<string, unknown>;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function useAnalytics() {
|
|
7
|
-
function track(event: AnalyticsEvent | string, properties?: Record<string, unknown>): void {
|
|
8
|
-
if (!import.meta.client) return;
|
|
9
|
-
const detail = typeof event === "string" ? { name: event, properties } : event;
|
|
10
|
-
window.dispatchEvent(new CustomEvent("site:analytics", { detail }));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function pageView(path: string, title?: string): void {
|
|
14
|
-
track({
|
|
15
|
-
name: "page_view",
|
|
16
|
-
properties: {
|
|
17
|
-
page_path: path,
|
|
18
|
-
page_title: title,
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return { track, pageView };
|
|
24
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
type DataLayerEvent = Record<string, unknown> & {
|
|
2
|
-
event?: string;
|
|
3
|
-
};
|
|
4
|
-
|
|
5
|
-
declare global {
|
|
6
|
-
interface Window {
|
|
7
|
-
dataLayer?: DataLayerEvent[];
|
|
8
|
-
gtag?: (...args: unknown[]) => void;
|
|
9
|
-
__siteAnalyticsInstalled?: boolean;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function appendScript(id: string, src: string): void {
|
|
14
|
-
if (document.getElementById(id)) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const script = document.createElement("script");
|
|
19
|
-
script.id = id;
|
|
20
|
-
script.async = true;
|
|
21
|
-
script.src = src;
|
|
22
|
-
document.head.appendChild(script);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default defineNuxtPlugin((nuxtApp) => {
|
|
26
|
-
const config = useRuntimeConfig();
|
|
27
|
-
const site = useSiteShell();
|
|
28
|
-
const analytics = config.public.analytics;
|
|
29
|
-
|
|
30
|
-
if (!analytics?.enabled || window.__siteAnalyticsInstalled) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const dataLayerName = analytics.dataLayerName || "dataLayer";
|
|
35
|
-
const win = window as unknown as Window & Record<string, unknown>;
|
|
36
|
-
|
|
37
|
-
function dataLayer(): DataLayerEvent[] {
|
|
38
|
-
const existing = win[dataLayerName];
|
|
39
|
-
if (Array.isArray(existing)) {
|
|
40
|
-
return existing as DataLayerEvent[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const next: DataLayerEvent[] = [];
|
|
44
|
-
win[dataLayerName] = next;
|
|
45
|
-
if (dataLayerName === "dataLayer") {
|
|
46
|
-
window.dataLayer = next;
|
|
47
|
-
}
|
|
48
|
-
return next;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function push(event: DataLayerEvent): void {
|
|
52
|
-
dataLayer().push(event);
|
|
53
|
-
if (analytics.debug) {
|
|
54
|
-
window.dispatchEvent(new CustomEvent("site:analytics-debug", { detail: event }));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function hasConsent(): boolean {
|
|
59
|
-
if (!analytics.requireConsent) {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
return window.localStorage.getItem(site.value.consent.storageKey) === "accepted";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function install(): void {
|
|
66
|
-
if (window.__siteAnalyticsInstalled || !hasConsent()) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
window.__siteAnalyticsInstalled = true;
|
|
71
|
-
|
|
72
|
-
if (analytics.gtmId) {
|
|
73
|
-
push({ "gtm.start": Date.now(), event: "gtm.js" });
|
|
74
|
-
appendScript(
|
|
75
|
-
"site-gtm",
|
|
76
|
-
`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(analytics.gtmId)}`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (analytics.gaId) {
|
|
81
|
-
window.gtag = (...args: unknown[]) => {
|
|
82
|
-
dataLayer().push(args as unknown as DataLayerEvent);
|
|
83
|
-
};
|
|
84
|
-
appendScript(
|
|
85
|
-
"site-gtag",
|
|
86
|
-
`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(analytics.gaId)}`,
|
|
87
|
-
);
|
|
88
|
-
window.gtag("js", new Date());
|
|
89
|
-
window.gtag("config", analytics.gaId, { send_page_view: false });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
window.addEventListener("site:consent", (event) => {
|
|
94
|
-
const detail = (event as CustomEvent<{ value?: string }>).detail;
|
|
95
|
-
if (detail?.value === "accepted") {
|
|
96
|
-
install();
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
window.addEventListener("site:analytics", (event) => {
|
|
101
|
-
const detail = (event as CustomEvent<{ name?: string; properties?: Record<string, unknown> }>)
|
|
102
|
-
.detail;
|
|
103
|
-
if (!detail?.name) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
push({ event: detail.name, ...(detail.properties || {}) });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
if (analytics.pageTracking) {
|
|
110
|
-
const router = useRouter();
|
|
111
|
-
router.afterEach((to) => {
|
|
112
|
-
push({
|
|
113
|
-
event: "page_view",
|
|
114
|
-
page_path: to.fullPath,
|
|
115
|
-
page_title: document.title,
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
nuxtApp.hook("app:mounted", install);
|
|
121
|
-
});
|