@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 +38 -0
- package/dist/module.d.mts +12 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +43 -1
- package/dist/runtime/plugins/api.global.js +3 -1
- package/dist/runtime/plugins/auth0.client.js +3 -0
- package/package.json +3 -1
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
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.
|
|
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",
|