@merkaly/nuxt 0.10.1 → 0.10.3

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/README.md CHANGED
@@ -35,6 +35,44 @@ On boot the module restores an existing session (user and token) and then runs t
35
35
  | store | optional | required | `merkaly:tenant` resolves the store from the host (public); a signed-in buyer is restored too; protected pages (account, checkout) ask for login. |
36
36
  | www | optional | not used | No hooks; anonymous or signed-in. |
37
37
 
38
+ ### Session lifetime and Auth0 setup for a new app
39
+
40
+ The SPA keeps the session with **refresh tokens** (`useRefreshTokens`, cached in `localStorage`, login with `offline_access`). The access token for the API lasts **2 hours** (`token_lifetime_for_web` of `https://api.merkaly.io`); before each API call the module asks Auth0 for the current token, which is cached and renewed with the refresh token once it expires. When the refresh token is gone or expired, protected surfaces (`requiresAuth: true`) go back to login instead of calling the API without a token.
41
+
42
+ Every new Nuxt app needs this in Auth0 (done for admin, dashboard, store and www on 2026-11-04):
43
+
44
+ 1. **Application** (type SPA): allowed callback (`/auth`), logout and web origin URLs for each environment.
45
+ 2. **Refresh Token Rotation: on**, **Refresh Token Expiration: on**, with idle lifetime `2592000` (30 days) and maximum lifetime `7776000` (90 days). Never non-rotating or non-expiring.
46
+ 3. **API `https://api.merkaly.io`**: "Allow Offline Access" stays **on**; without it Auth0 issues no refresh tokens and every session ends after 2 hours.
47
+ 4. **Organizations**: apps that work inside a store (admin) require an organization at login; staff-only apps are filtered by the "Enforce surface access" post-login Action (role `MK_*`).
48
+
49
+ These settings can be read and changed through the Management API (`merkaly.us.auth0.com`, the canonical domain, never the custom `auth.merkaly.io`).
50
+
51
+ ## SEO: robots and sitemap
52
+
53
+ Every app gets `@nuxtjs/robots`. Apps are **private by default**: in production
54
+ `robots.txt` answers `Disallow: /` and pages carry the noindex meta and the
55
+ `X-Robots-Tag` header (admin, dashboard). Outside production nothing is indexable.
56
+
57
+ Public apps (www, store) opt in, and can also get `@nuxtjs/sitemap`, with
58
+ `hreflang` built from `merkaly.i18n` when the strategy has a locale prefix:
59
+
60
+ ```ts
61
+ export default defineNuxtConfig({
62
+ merkaly: {
63
+ seo: {
64
+ indexable: true,
65
+ url: 'https://www.merkaly.io',
66
+ disallow: ['/en/auth', '/es/auth', '/pt/auth'], // robots.txt
67
+ sitemap: { exclude: ['/en/auth', '/es/auth', '/pt/auth'] }, // or `true`
68
+ },
69
+ },
70
+ });
71
+ ```
72
+
73
+ The sitemap is only installed for indexable apps and is served at `/sitemap.xml`
74
+ (linked from `robots.txt`).
75
+
38
76
  ## Money formatting
39
77
 
40
78
  Money in Merkaly is stored as **integer cents**. `MKFormatMoney`, `MKInputMoney`,
package/dist/module.d.mts CHANGED
@@ -57,6 +57,18 @@ interface MerkalyModuleOptions {
57
57
  project: string;
58
58
  token: string;
59
59
  };
60
+ seo?: {
61
+ /** robots.txt paths to block, e.g. the Auth0 callback. */
62
+ disallow?: string[];
63
+ /** Private apps (admin, dashboard) stay false: robots.txt Disallow, noindex meta and X-Robots-Tag. */
64
+ indexable?: boolean;
65
+ /** Generate /sitemap.xml from the pages, with hreflang from the i18n locales. */
66
+ sitemap?: boolean | {
67
+ exclude?: string[];
68
+ };
69
+ /** Public site URL, used in robots.txt and the sitemap. */
70
+ url?: string;
71
+ };
60
72
  }
61
73
  declare const _default: _nuxt_schema.NuxtModule<MerkalyModuleOptions, MerkalyModuleOptions, false>;
62
74
 
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.14.0"
6
6
  },
7
- "version": "0.10.1",
7
+ "version": "0.10.3",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, useLogger, createResolver, addPlugin, addServerHandler, addRouteMiddleware, addImportsDir, addComponentsDir, addTypeTemplate, addTemplate } from '@nuxt/kit';
1
+ import { defineNuxtModule, useLogger, createResolver, installModule, addPlugin, addServerHandler, addRouteMiddleware, addImportsDir, addComponentsDir, addTypeTemplate, addTemplate } from '@nuxt/kit';
2
2
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { resolve, dirname } from 'node:path';
4
4
  import { defu } from 'defu';
@@ -42,6 +42,13 @@ const defaultOptions = {
42
42
  dsn: "",
43
43
  project: "",
44
44
  token: ""
45
+ },
46
+ seo: {
47
+ disallow: [],
48
+ indexable: false,
49
+ // private unless the app opts in
50
+ sitemap: false,
51
+ url: ""
45
52
  }
46
53
  };
47
54
  function hasI18nConfig(options) {
@@ -76,6 +83,10 @@ function buildModuleDependencies(nuxt, options) {
76
83
  if (hasPlausibleConfig(options)) {
77
84
  dependencies["@nuxtjs/plausible"] = {};
78
85
  }
86
+ configureSite(nuxt, options);
87
+ dependencies["@nuxtjs/robots"] = {
88
+ overrides: { disallow: options.seo?.disallow ?? [] }
89
+ };
79
90
  const logger = useLogger(MODULE_NAME);
80
91
  Object.keys(dependencies).forEach((dependency) => {
81
92
  logger.info(`Loaded ${dependency} module`);
@@ -126,6 +137,33 @@ function configureI18n(nuxt, options) {
126
137
  vueI18n
127
138
  };
128
139
  }
140
+ function configureSite(nuxt, options) {
141
+ nuxt.options.site = defu({
142
+ indexable: Boolean(options.seo?.indexable),
143
+ url: options.seo?.url || void 0
144
+ }, nuxt.options.site || {});
145
+ }
146
+ function configureSitemap(options) {
147
+ const exclude = typeof options.seo?.sitemap === "object" ? options.seo.sitemap.exclude ?? [] : [];
148
+ const prefixed = hasI18nConfig(options) && options.i18n.strategy && options.i18n.strategy !== "no_prefix";
149
+ if (!prefixed) {
150
+ return { exclude };
151
+ }
152
+ return {
153
+ autoI18n: {
154
+ // locales are registered through a hook, so the sitemap module can't read them on its own
155
+ defaultLocale: options.i18n.defaultLocale,
156
+ locales: options.i18n.locales.map((locale) => ({
157
+ _hreflang: locale.language,
158
+ _sitemap: locale.language,
159
+ code: locale.code,
160
+ language: locale.language
161
+ })),
162
+ strategy: options.i18n.strategy
163
+ },
164
+ exclude
165
+ };
166
+ }
129
167
  function configureRuntimeConfig(nuxt, options) {
130
168
  nuxt.options.runtimeConfig.merkaly = defu({
131
169
  sentry: {
@@ -278,6 +316,10 @@ const module$1 = defineNuxtModule({
278
316
  const resolver = createResolver(import.meta.url);
279
317
  configureNuxtOptions(nuxt, options);
280
318
  await configureExternalIntegrations(nuxt, logger);
319
+ if (options.seo?.indexable && options.seo.sitemap) {
320
+ await installModule("@nuxtjs/sitemap", configureSitemap(options));
321
+ logger.info("Loaded @nuxtjs/sitemap module");
322
+ }
281
323
  registerRuntimeFeatures(options, resolver);
282
324
  }
283
325
  });
@@ -1,4 +1,4 @@
1
- import { defineNuxtPlugin, useRequestURL, useRuntimeConfig } from "#app";
1
+ import { defineNuxtPlugin, useNuxtApp, useRequestURL, useRuntimeConfig } from "#app";
2
2
  import { useAuth, useTenant } from "#imports";
3
3
  const GLOBAL_API_HEADER = "x-merkaly-global";
4
4
  function serverBaseURL(api, global) {
@@ -10,8 +10,10 @@ export default defineNuxtPlugin(({ provide }) => {
10
10
  if (import.meta.server && import.meta.dev && new URL(useRuntimeConfig().public.merkaly.api.url).hostname.endsWith(".test")) {
11
11
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
12
12
  }
13
+ const nuxtApp = useNuxtApp();
13
14
  provide("api", async (url, options = {}) => {
14
15
  const { token } = useAuth();
16
+ if (import.meta.client && token.value) await nuxtApp.$auth0?.getTokenSilently();
15
17
  const { tenant } = useTenant();
16
18
  const { public: { merkaly } } = useRuntimeConfig();
17
19
  const requiresTenant = options.requiresTenant ?? merkaly.auth0.requiresTenant;
@@ -43,6 +43,9 @@ export default defineNuxtPlugin(async ({ callHook, hook }) => {
43
43
  if (user.value && !token.value && tokenError) {
44
44
  user.value = void 0;
45
45
  await self0.logout({ openUrl: false });
46
+ if ($config.merkaly.auth0.requiresAuth) {
47
+ return auth0.loginWithRedirect({ appState: { target: location.pathname + location.search } });
48
+ }
46
49
  }
47
50
  return initTenantContext();
48
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkaly/nuxt",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/merkaly-io/nuxt.git"
@@ -38,6 +38,8 @@
38
38
  "@nuxt/kit": "^4.6.0",
39
39
  "@nuxtjs/i18n": "^10.6.0",
40
40
  "@nuxtjs/plausible": "^4.0.0",
41
+ "@nuxtjs/robots": "^7.0.2",
42
+ "@nuxtjs/sitemap": "^9.1.0",
41
43
  "@sentry/nuxt": "^11.10.0",
42
44
  "@types/node": "latest",
43
45
  "@types/vue-select": "^3.16.8",