@endora-commerce/mod-seo 0.100.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/admin/index.d.ts +31 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +35 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/SeoPage.d.ts +4 -0
  8. package/dist/admin/pages/SeoPage.d.ts.map +1 -0
  9. package/dist/admin/pages/SeoPage.js +189 -0
  10. package/dist/admin/pages/SeoPage.js.map +1 -0
  11. package/dist/backend/entities/seo-meta-override.entity.d.ts +21 -0
  12. package/dist/backend/entities/seo-meta-override.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/seo-meta-override.entity.js +84 -0
  14. package/dist/backend/entities/seo-meta-override.entity.js.map +1 -0
  15. package/dist/backend/entities/sitemap-cache.entity.d.ts +17 -0
  16. package/dist/backend/entities/sitemap-cache.entity.d.ts.map +1 -0
  17. package/dist/backend/entities/sitemap-cache.entity.js +52 -0
  18. package/dist/backend/entities/sitemap-cache.entity.js.map +1 -0
  19. package/dist/backend/index.d.ts +57 -0
  20. package/dist/backend/index.d.ts.map +1 -0
  21. package/dist/backend/index.js +58 -0
  22. package/dist/backend/index.js.map +1 -0
  23. package/dist/backend/routes.d.ts +11 -0
  24. package/dist/backend/routes.d.ts.map +1 -0
  25. package/dist/backend/routes.js +158 -0
  26. package/dist/backend/routes.js.map +1 -0
  27. package/dist/backend/services/meta-tag-resolver.service.d.ts +54 -0
  28. package/dist/backend/services/meta-tag-resolver.service.d.ts.map +1 -0
  29. package/dist/backend/services/meta-tag-resolver.service.js +208 -0
  30. package/dist/backend/services/meta-tag-resolver.service.js.map +1 -0
  31. package/dist/backend/services/sitemap-generator.service.d.ts +79 -0
  32. package/dist/backend/services/sitemap-generator.service.d.ts.map +1 -0
  33. package/dist/backend/services/sitemap-generator.service.js +293 -0
  34. package/dist/backend/services/sitemap-generator.service.js.map +1 -0
  35. package/dist/manifest.d.ts +169 -0
  36. package/dist/manifest.d.ts.map +1 -0
  37. package/dist/manifest.js +49 -0
  38. package/dist/manifest.js.map +1 -0
  39. package/dist/migrations/20260425T154404_seo_init.d.ts +11 -0
  40. package/dist/migrations/20260425T154404_seo_init.d.ts.map +1 -0
  41. package/dist/migrations/20260425T154404_seo_init.js +45 -0
  42. package/dist/migrations/20260425T154404_seo_init.js.map +1 -0
  43. package/dist/migrations/index.d.ts +23 -0
  44. package/dist/migrations/index.d.ts.map +1 -0
  45. package/dist/migrations/index.js +23 -0
  46. package/dist/migrations/index.js.map +1 -0
  47. package/docs/seo.md +74 -0
  48. package/i18n/en.json +3 -0
  49. package/i18n/pl.json +3 -0
  50. package/package.json +89 -0
  51. package/tailwind.css +14 -0
@@ -0,0 +1,79 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { z } from 'zod';
3
+ import { type CatalogCategoryReadPort, type CatalogProductReadPort, type CmsPageReadPort } from '@endora-commerce/contracts';
4
+ import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
5
+ export interface SitemapGeneratorOptions {
6
+ /** Public origin used when no per-channel URL is configured. Defaults to env or localhost. */
7
+ baseUrl?: string;
8
+ /** How old the cache row may be before serve triggers a regenerate. */
9
+ staleAfterMs?: number;
10
+ }
11
+ export interface SitemapStatus {
12
+ generatedAt: Date | null;
13
+ byteSize: number | null;
14
+ urlCount: number | null;
15
+ }
16
+ export interface SitemapChannelStatus extends SitemapStatus {
17
+ salesChannelCode: string;
18
+ salesChannelName: string;
19
+ storefrontUrl: string;
20
+ storefrontUrlSource: 'setting' | 'env' | 'fallback';
21
+ }
22
+ /** Minimal port for the SettingsService — only the read we need. */
23
+ export interface SitemapSettingsPort {
24
+ get<T>(code: string, salesChannelId: string, schema: z.ZodType<T>): Promise<T>;
25
+ }
26
+ export declare class SitemapGeneratorService {
27
+ private readonly emFactory;
28
+ private readonly settings;
29
+ /** What this channel contains, and what those rows are (feature 075, Phase C). */
30
+ private readonly membership;
31
+ private readonly catalogProducts;
32
+ private readonly catalogCategories;
33
+ private readonly cmsPages;
34
+ private readonly fallbackBaseUrl;
35
+ private readonly staleAfterMs;
36
+ constructor(emFactory: () => EntityManager, settings: SitemapSettingsPort | null,
37
+ /** What this channel contains, and what those rows are (feature 075, Phase C). */
38
+ membership: SalesChannelMembershipPort, catalogProducts: CatalogProductReadPort, catalogCategories: CatalogCategoryReadPort, cmsPages: CmsPageReadPort, options?: SitemapGeneratorOptions);
39
+ /**
40
+ * Serve the sitemap for a single channel; regenerates inline when stale
41
+ * or when no cache row exists yet.
42
+ */
43
+ getOrGenerateForChannel(salesChannelCode: string): Promise<{
44
+ payload: string;
45
+ generatedAt: Date;
46
+ }>;
47
+ regenerateForChannel(salesChannelCode: string): Promise<{
48
+ payload: string;
49
+ generatedAt: Date;
50
+ }>;
51
+ getStatusForChannel(salesChannelCode: string): Promise<SitemapStatus>;
52
+ /**
53
+ * Per-channel admin overview: one row per active channel, with the
54
+ * cache stats (or NULLs when never generated) and the resolved
55
+ * storefront URL.
56
+ */
57
+ listChannelStatuses(): Promise<SitemapChannelStatus[]>;
58
+ private resolveStorefrontUrl;
59
+ private resolveStorefrontUrlSource;
60
+ /**
61
+ * What belongs to this channel, through the sanctioned bridge accessor
62
+ * (Constitution XII). This was three hand-written `SELECT`s against
63
+ * `sales_channel_products`, `sales_channel_categories` and
64
+ * `sales_channel_cms_pages` — the tables `SalesChannelMembershipPort`'s own
65
+ * doc comment says a module must not reach for, and the one cross-module
66
+ * read in this file that `check-module-boundary` could never have found,
67
+ * because raw SQL names no import specifier.
68
+ */
69
+ private collectChannelMemberIds;
70
+ /**
71
+ * Every member id, not the first page of them. `listEntityIdsForChannel` is
72
+ * paginated and defaults to 100; a sitemap wants the channel's whole
73
+ * catalogue, so the pages are walked to `total`. `total` is re-read on each
74
+ * call and the loop is bounded by it, so a concurrent membership write
75
+ * cannot spin it.
76
+ */
77
+ private allMemberIds;
78
+ }
79
+ //# sourceMappingURL=sitemap-generator.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-generator.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/sitemap-generator.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAGL,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAE3B,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AA2CnF,MAAM,WAAW,uBAAuB;IACtC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,SAAS,GAAG,KAAK,GAAG,UAAU,CAAC;CACrD;AAED,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAChF;AAED,qBAAa,uBAAuB;IAKhC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAV3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGnB,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IACrD,kFAAkF;IACjE,UAAU,EAAE,0BAA0B,EACtC,eAAe,EAAE,sBAAsB,EACvC,iBAAiB,EAAE,uBAAuB,EAC1C,QAAQ,EAAE,eAAe,EAC1C,OAAO,GAAE,uBAA4B;IASvC;;;OAGG;IACG,uBAAuB,CAC3B,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,IAAI,CAAA;KAAE,CAAC;IAS5C,oBAAoB,CACxB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,IAAI,CAAA;KAAE,CAAC;IA8F5C,mBAAmB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAW3E;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;YA0B9C,oBAAoB;YAKpB,0BAA0B;IAyBxC;;;;;;;;OAQG;YACW,uBAAuB;IAarC;;;;;;OAMG;YACW,YAAY;CAgB3B"}
@@ -0,0 +1,293 @@
1
+ import { z } from 'zod';
2
+ import { ANONYMOUS_PRODUCT_AUDIENCE, isProductVisibleTo, } from '@endora-commerce/contracts';
3
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
4
+ import { SitemapCache } from '../entities/sitemap-cache.entity.js';
5
+ /**
6
+ * SitemapGeneratorService — per-channel sitemap.
7
+ *
8
+ * One sitemap is produced per active Sales Channel. Membership-bridge
9
+ * tables (`sales_channel_products`, `sales_channel_categories`,
10
+ * `sales_channel_cms_pages`) decide what each channel's sitemap contains
11
+ * so a crawler hitting `https://channel-a.example.com/sitemap.xml` does
12
+ * not see URLs that only belong to channel B.
13
+ *
14
+ * Storefront origin used when stamping URLs is read from the per-channel
15
+ * setting `sales_channels.storefront_url` (see the sales_channels module
16
+ * manifest); empty value falls back to the `STOREFRONT_BASE_URL` env, then
17
+ * to `http://localhost:3000`.
18
+ *
19
+ * Cache: one row per channel in `sitemap_cache`, keyed by the channel
20
+ * `code` (varchar(32)). Regenerate is per-channel; the public route
21
+ * resolves the channel from the request scope and serves that row.
22
+ *
23
+ * Anonymous-visible only: products with `visibility != 'public'` and
24
+ * archived/deleted rows are excluded so we don't leak internal SKUs into
25
+ * crawlers.
26
+ *
27
+ * Feature 075, Phase C — the three entity reads go through their owners'
28
+ * published read ports and the membership lookup through
29
+ * `salesChannelMembershipPort`, which is the sanctioned bridge accessor of
30
+ * Constitution XII. Both were the same defect in two disguises: the entity
31
+ * reads were imports no gate can reach, and the membership read was three
32
+ * hand-written `SELECT`s against `sales_channel_products`,
33
+ * `sales_channel_categories` and `sales_channel_cms_pages` — which
34
+ * `check-module-boundary` cannot see at all, because raw SQL names no
35
+ * specifier. `catalog` and `cms` are binding dependencies: a sitemap that
36
+ * silently drops every product URL still parses as a sitemap, which is exactly
37
+ * why it must not be produced.
38
+ */
39
+ const DEFAULT_STALE_AFTER_MS = 6 * 60 * 60 * 1000; // 6 hours
40
+ const STOREFRONT_URL_SETTING = 'sales_channels.storefront_url';
41
+ /** How many member ids one bridge read fetches. The port's own default is 100. */
42
+ const MEMBERSHIP_PAGE_SIZE = 500;
43
+ export class SitemapGeneratorService {
44
+ emFactory;
45
+ settings;
46
+ membership;
47
+ catalogProducts;
48
+ catalogCategories;
49
+ cmsPages;
50
+ fallbackBaseUrl;
51
+ staleAfterMs;
52
+ constructor(emFactory, settings,
53
+ /** What this channel contains, and what those rows are (feature 075, Phase C). */
54
+ membership, catalogProducts, catalogCategories, cmsPages, options = {}) {
55
+ this.emFactory = emFactory;
56
+ this.settings = settings;
57
+ this.membership = membership;
58
+ this.catalogProducts = catalogProducts;
59
+ this.catalogCategories = catalogCategories;
60
+ this.cmsPages = cmsPages;
61
+ this.fallbackBaseUrl =
62
+ options.baseUrl ??
63
+ process.env['STOREFRONT_BASE_URL'] ??
64
+ 'http://localhost:3000';
65
+ this.staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
66
+ }
67
+ /**
68
+ * Serve the sitemap for a single channel; regenerates inline when stale
69
+ * or when no cache row exists yet.
70
+ */
71
+ async getOrGenerateForChannel(salesChannelCode) {
72
+ const em = this.emFactory();
73
+ const cache = await em.findOne(SitemapCache, { key: salesChannelCode });
74
+ if (cache && Date.now() - cache.generatedAt.getTime() < this.staleAfterMs) {
75
+ return { payload: cache.payload, generatedAt: cache.generatedAt };
76
+ }
77
+ return this.regenerateForChannel(salesChannelCode);
78
+ }
79
+ async regenerateForChannel(salesChannelCode) {
80
+ // command-coverage-ignore: idempotent lifecycle reconciler/seed — a system-
81
+ // invariant repair, not an operator-initiated audited write.
82
+ const em = this.emFactory();
83
+ const channel = await em.findOne(SalesChannel, { code: salesChannelCode });
84
+ if (channel === null) {
85
+ throw new Error(`Unknown sales channel code "${salesChannelCode}".`);
86
+ }
87
+ const baseUrl = await this.resolveStorefrontUrl(channel);
88
+ const memberIds = await this.collectChannelMemberIds(channel.id);
89
+ // Each read keeps the filter and the ordering the SQL spelled. `liveOnly`
90
+ // and `activeOnly` are the port's names for the `deleted_at is null` and
91
+ // `status = 'active'` halves; `visibility`, `archivedAt`, `isActive` and
92
+ // `active` are columns on the records, so those predicates stay here — the
93
+ // same conditions, evaluated one layer up.
94
+ const products = (await this.catalogProducts.findByIds([...memberIds.products], {
95
+ liveOnly: true,
96
+ activeOnly: true,
97
+ }))
98
+ // Issue #227 — `visibility === 'public'` was here, and it is not the
99
+ // whole answer: an operator can save `public` **with** a non-empty
100
+ // `allowed_organization_ids`, which restricts the row to the
101
+ // organisations it names whatever the visibility column says. A sitemap
102
+ // is read by crawlers, so the audience is the anonymous one, and
103
+ // `isProductVisibleTo` answers both columns for it — the `public` half
104
+ // unchanged, the allow-list half for the first time.
105
+ .filter((p) => isProductVisibleTo(p, ANONYMOUS_PRODUCT_AUDIENCE) && p.archivedAt === null)
106
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
107
+ // Feature 068 — a deactivated category is not a customer-reachable URL, so
108
+ // it must not be advertised to crawlers.
109
+ const categories = (await this.catalogCategories.findByIds([...memberIds.categories], { liveOnly: true }))
110
+ .filter((c) => c.isActive)
111
+ .sort((a, b) => a.sortOrder - b.sortOrder);
112
+ const cmsPages = (await this.cmsPages.findByIds([...memberIds.cmsPages]))
113
+ .filter((page) => page.status === 'published' && page.active)
114
+ .sort((a, b) => a.slug.localeCompare(b.slug));
115
+ const urls = [];
116
+ for (const c of categories) {
117
+ urls.push({
118
+ loc: `${baseUrl}/c/${c.slug}`,
119
+ lastmod: c.updatedAt,
120
+ changefreq: 'weekly',
121
+ priority: 0.7,
122
+ });
123
+ }
124
+ for (const page of cmsPages) {
125
+ urls.push({
126
+ loc: `${baseUrl}/${page.slug}`,
127
+ lastmod: page.publishedAt ?? page.updatedAt,
128
+ changefreq: 'monthly',
129
+ priority: 0.5,
130
+ });
131
+ }
132
+ for (const p of products) {
133
+ urls.push({
134
+ loc: `${baseUrl}/p/${p.slug}`,
135
+ lastmod: p.updatedAt,
136
+ changefreq: 'daily',
137
+ priority: 0.8,
138
+ });
139
+ }
140
+ const payload = renderSitemap(urls);
141
+ const now = new Date();
142
+ const cache = await em.findOne(SitemapCache, { key: salesChannelCode });
143
+ if (cache) {
144
+ cache.payload = payload;
145
+ cache.urlCount = urls.length;
146
+ cache.byteSize = Buffer.byteLength(payload, 'utf8');
147
+ cache.generatedAt = now;
148
+ }
149
+ else {
150
+ em.create(SitemapCache, {
151
+ key: salesChannelCode,
152
+ payload,
153
+ urlCount: urls.length,
154
+ byteSize: Buffer.byteLength(payload, 'utf8'),
155
+ generatedAt: now,
156
+ });
157
+ }
158
+ await em.flush();
159
+ return { payload, generatedAt: now };
160
+ }
161
+ async getStatusForChannel(salesChannelCode) {
162
+ const em = this.emFactory();
163
+ const cache = await em.findOne(SitemapCache, { key: salesChannelCode });
164
+ if (!cache)
165
+ return { generatedAt: null, byteSize: null, urlCount: null };
166
+ return {
167
+ generatedAt: cache.generatedAt,
168
+ byteSize: cache.byteSize,
169
+ urlCount: cache.urlCount,
170
+ };
171
+ }
172
+ /**
173
+ * Per-channel admin overview: one row per active channel, with the
174
+ * cache stats (or NULLs when never generated) and the resolved
175
+ * storefront URL.
176
+ */
177
+ async listChannelStatuses() {
178
+ const em = this.emFactory();
179
+ const channels = await em.find(SalesChannel, { active: true }, { orderBy: { systemDefault: 'desc', code: 'asc' } });
180
+ const out = [];
181
+ for (const channel of channels) {
182
+ const cache = await em.findOne(SitemapCache, { key: channel.code });
183
+ const { url, source } = await this.resolveStorefrontUrlSource(channel);
184
+ out.push({
185
+ salesChannelCode: channel.code,
186
+ salesChannelName: pickName(channel),
187
+ storefrontUrl: url,
188
+ storefrontUrlSource: source,
189
+ generatedAt: cache?.generatedAt ?? null,
190
+ byteSize: cache?.byteSize ?? null,
191
+ urlCount: cache?.urlCount ?? null,
192
+ });
193
+ }
194
+ return out;
195
+ }
196
+ // --- internals -----------------------------------------------------------
197
+ async resolveStorefrontUrl(channel) {
198
+ const { url } = await this.resolveStorefrontUrlSource(channel);
199
+ return trimTrailingSlash(url);
200
+ }
201
+ async resolveStorefrontUrlSource(channel) {
202
+ if (this.settings) {
203
+ try {
204
+ const fromSetting = await this.settings.get(STOREFRONT_URL_SETTING, channel.id, z.string());
205
+ if (fromSetting && fromSetting.trim() !== '') {
206
+ return { url: fromSetting.trim(), source: 'setting' };
207
+ }
208
+ }
209
+ catch {
210
+ // Not registered or out of scope — fall through to env / fallback.
211
+ }
212
+ }
213
+ const fromEnv = process.env['STOREFRONT_BASE_URL'];
214
+ if (fromEnv && fromEnv.trim() !== '') {
215
+ return { url: fromEnv.trim(), source: 'env' };
216
+ }
217
+ return { url: this.fallbackBaseUrl, source: 'fallback' };
218
+ }
219
+ /**
220
+ * What belongs to this channel, through the sanctioned bridge accessor
221
+ * (Constitution XII). This was three hand-written `SELECT`s against
222
+ * `sales_channel_products`, `sales_channel_categories` and
223
+ * `sales_channel_cms_pages` — the tables `SalesChannelMembershipPort`'s own
224
+ * doc comment says a module must not reach for, and the one cross-module
225
+ * read in this file that `check-module-boundary` could never have found,
226
+ * because raw SQL names no import specifier.
227
+ */
228
+ async collectChannelMemberIds(salesChannelId) {
229
+ const [products, categories, cmsPages] = await Promise.all([
230
+ this.allMemberIds(salesChannelId, 'product'),
231
+ this.allMemberIds(salesChannelId, 'category'),
232
+ this.allMemberIds(salesChannelId, 'cms-page'),
233
+ ]);
234
+ return { products, categories, cmsPages };
235
+ }
236
+ /**
237
+ * Every member id, not the first page of them. `listEntityIdsForChannel` is
238
+ * paginated and defaults to 100; a sitemap wants the channel's whole
239
+ * catalogue, so the pages are walked to `total`. `total` is re-read on each
240
+ * call and the loop is bounded by it, so a concurrent membership write
241
+ * cannot spin it.
242
+ */
243
+ async allMemberIds(salesChannelId, entityType) {
244
+ const ids = new Set();
245
+ for (let page = 0;; page += 1) {
246
+ const { entityIds, total } = await this.membership.listEntityIdsForChannel(salesChannelId, entityType, page, MEMBERSHIP_PAGE_SIZE);
247
+ for (const id of entityIds)
248
+ ids.add(id);
249
+ if (entityIds.length === 0 || ids.size >= total)
250
+ return ids;
251
+ }
252
+ }
253
+ }
254
+ function renderSitemap(urls) {
255
+ const items = urls.map((u) => ` <url><loc>${escapeXml(u.loc)}</loc><lastmod>${u.lastmod.toISOString()}</lastmod>` +
256
+ `<changefreq>${u.changefreq}</changefreq><priority>${u.priority.toFixed(1)}</priority></url>`);
257
+ return [
258
+ '<?xml version="1.0" encoding="UTF-8"?>',
259
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
260
+ ...items,
261
+ '</urlset>',
262
+ '',
263
+ ].join('\n');
264
+ }
265
+ function escapeXml(value) {
266
+ return value
267
+ .replace(/&/g, '&amp;')
268
+ .replace(/</g, '&lt;')
269
+ .replace(/>/g, '&gt;')
270
+ .replace(/"/g, '&quot;')
271
+ .replace(/'/g, '&apos;');
272
+ }
273
+ function trimTrailingSlash(s) {
274
+ return s.endsWith('/') ? s.slice(0, -1) : s;
275
+ }
276
+ function pickName(channel) {
277
+ const map = channel.name ?? {};
278
+ const codes = [];
279
+ if (channel.defaultLanguage)
280
+ codes.push(channel.defaultLanguage);
281
+ codes.push('en', 'en-US', 'pl', 'pl-PL');
282
+ for (const code of codes) {
283
+ const v = map[code];
284
+ if (typeof v === 'string' && v)
285
+ return v;
286
+ }
287
+ for (const v of Object.values(map)) {
288
+ if (typeof v === 'string' && v)
289
+ return v;
290
+ }
291
+ return channel.code;
292
+ }
293
+ //# sourceMappingURL=sitemap-generator.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-generator.service.js","sourceRoot":"","sources":["../../../src/backend/services/sitemap-generator.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * SEO module — manifest backfill (Module Lifecycle, feature 018).
3
+ *
4
+ * Predates the lifecycle system; this manifest is the static record
5
+ * required so the module participates in the registry. No install /
6
+ * uninstall hook today — the module's schema is owned by earlier
7
+ * platform-wide migrations.
8
+ */
9
+ export declare const manifest: {
10
+ id: string;
11
+ name: string;
12
+ version: string;
13
+ dependencies: string[];
14
+ description?: string | undefined;
15
+ acknowledgedDependencies?: {
16
+ moduleId: string;
17
+ port: string;
18
+ reason: string;
19
+ }[] | undefined;
20
+ nonBindingDependencies?: {
21
+ moduleId: string;
22
+ name: string;
23
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
24
+ reason: string;
25
+ whenAbsent?: string | undefined;
26
+ }[] | undefined;
27
+ activation?: {
28
+ settingCode: string;
29
+ default: boolean;
30
+ } | {
31
+ nonDeactivatable: true;
32
+ reason: string;
33
+ } | undefined;
34
+ settings?: {
35
+ moduleCode: string;
36
+ groups: {
37
+ code: string;
38
+ name: string;
39
+ salesChannelCodes?: string[] | undefined;
40
+ isSystemProtected?: boolean | undefined;
41
+ }[];
42
+ settings: {
43
+ code: string;
44
+ name: string;
45
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
46
+ defaultValue: unknown;
47
+ description?: string | undefined;
48
+ groupCode?: string | undefined;
49
+ previousDefaultValues?: unknown[] | undefined;
50
+ salesChannelCodes?: string[] | undefined;
51
+ enumOptions?: string[] | undefined;
52
+ configurationType?: string | undefined;
53
+ hidden?: boolean | undefined;
54
+ }[];
55
+ } | undefined;
56
+ i18n?: {
57
+ bundlesDir: string;
58
+ } | undefined;
59
+ docs?: false | {
60
+ dir: string;
61
+ } | undefined;
62
+ demo?: false | {
63
+ summary: string;
64
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
65
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
66
+ after?: readonly string[] | undefined;
67
+ package?: string | undefined;
68
+ } | undefined;
69
+ actions?: {
70
+ id: string;
71
+ labelKey: string;
72
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
73
+ targetRoute: string;
74
+ keywords: string[];
75
+ weight: number;
76
+ descriptionKey?: string | undefined;
77
+ requiredPermission?: string | undefined;
78
+ }[] | undefined;
79
+ permissions?: {
80
+ code: string;
81
+ label: string;
82
+ module?: string | undefined;
83
+ description?: string | undefined;
84
+ requires?: string[] | undefined;
85
+ }[] | undefined;
86
+ transactionalEmails?: {
87
+ code: string;
88
+ name: string;
89
+ variables: {
90
+ key: string;
91
+ label: string;
92
+ sampleValue?: string | undefined;
93
+ description?: string | undefined;
94
+ }[];
95
+ description?: string | undefined;
96
+ group?: string | undefined;
97
+ }[] | undefined;
98
+ capabilities?: string[] | undefined;
99
+ exclusiveCapabilities?: {
100
+ key: string;
101
+ errorCode: string;
102
+ }[] | undefined;
103
+ errorCodes?: {
104
+ code: string;
105
+ tokens?: string[] | undefined;
106
+ }[] | undefined;
107
+ blocks?: {
108
+ name: string;
109
+ labelKey: string;
110
+ category: string;
111
+ contexts: ("cms" | "invoice" | "email" | "newsletter")[];
112
+ fields: Record<string, {
113
+ type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
114
+ label?: string | undefined;
115
+ required?: boolean | undefined;
116
+ options?: {
117
+ label: string;
118
+ value: string | number;
119
+ }[] | undefined;
120
+ refKind?: string | undefined;
121
+ }>;
122
+ descriptionKey?: string | undefined;
123
+ defaultProps?: Record<string, unknown> | undefined;
124
+ responsiveFields?: string[] | undefined;
125
+ previewIcon?: string | undefined;
126
+ weight?: number | undefined;
127
+ }[] | undefined;
128
+ blockCategories?: {
129
+ key: string;
130
+ titleKey: string;
131
+ contexts: ("cms" | "invoice" | "email" | "newsletter")[];
132
+ weight?: number | undefined;
133
+ visible?: boolean | undefined;
134
+ }[] | undefined;
135
+ env?: {
136
+ name: string;
137
+ describes: {
138
+ en: string;
139
+ pl: string;
140
+ };
141
+ requirement: {
142
+ kind: "required";
143
+ } | {
144
+ kind: "requiredWhen";
145
+ input: string;
146
+ equals: string;
147
+ } | {
148
+ kind: "optional";
149
+ without: {
150
+ en: string;
151
+ pl: string;
152
+ };
153
+ };
154
+ secret: boolean;
155
+ generable: boolean;
156
+ owner: {
157
+ kind: "platform";
158
+ } | {
159
+ kind: "application";
160
+ application: "admin" | "backend" | "storefront";
161
+ } | {
162
+ kind: "module";
163
+ moduleId: string;
164
+ };
165
+ consumers: ("admin" | "backend" | "storefront")[];
166
+ addressOf: "admin" | "backend" | "storefront" | null;
167
+ }[] | undefined;
168
+ };
169
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCnB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { defineModuleManifest } from '@endora-commerce/contracts';
2
+ /**
3
+ * SEO module — manifest backfill (Module Lifecycle, feature 018).
4
+ *
5
+ * Predates the lifecycle system; this manifest is the static record
6
+ * required so the module participates in the registry. No install /
7
+ * uninstall hook today — the module's schema is owned by earlier
8
+ * platform-wide migrations.
9
+ */
10
+ export const manifest = defineModuleManifest({
11
+ id: 'seo',
12
+ name: 'SEO',
13
+ description: 'SEO metadata management for catalog, CMS, and blog surfaces.',
14
+ version: '1.0.0',
15
+ // `auth` owns the `requireAdmin` port the admin routes are gated by. Feature
16
+ // 072 made it a container resolution rather than a constructor argument.
17
+ //
18
+ // `catalog` and `cms` arrived with feature 075 Phase C. Both edges were
19
+ // always real — the meta-tag resolver and the sitemap read their tables
20
+ // directly — and both are **binding**: a sitemap that silently drops every
21
+ // product or page URL still parses as a sitemap, and a crawler cannot tell
22
+ // it from a shop that genuinely sells nothing. Refusing is the honest
23
+ // answer. Meta tags likewise describe a page a customer can reach, which a
24
+ // switched-off `catalog` or `cms` no longer serves.
25
+ dependencies: ['auth', 'catalog', 'cms'],
26
+ settings: {
27
+ moduleCode: 'seo',
28
+ groups: [{ code: 'seo', name: 'SEO' }],
29
+ settings: [
30
+ {
31
+ // Feature 073 — the operator's activation control. Platform-wide.
32
+ code: 'seo.enabled',
33
+ name: 'SEO enabled',
34
+ description: 'Switches the meta-tag override screens, their API and the generated sitemap on or off. Nothing is dropped: overrides stay in the database and the sitemap regenerates from them when you switch it back on.',
35
+ groupCode: 'seo',
36
+ valueType: 'boolean',
37
+ defaultValue: true,
38
+ },
39
+ ],
40
+ },
41
+ // Feature 091 (Phase 4, batch 8) — this module ships a bundle now: its
42
+ // sidebar entry's `labelKey` is module-relative (R8) and resolves in this
43
+ // module's own namespace. Its screen's copy stays in `_i18n`'s `core`
44
+ // scope, which is batch 4's shape and not a new one.
45
+ i18n: { bundlesDir: 'i18n' },
46
+ docs: { dir: 'docs' },
47
+ activation: { settingCode: 'seo.enabled', default: true },
48
+ });
49
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,KAAK;IACT,IAAI,EAAE,KAAK;IACX,WAAW,EACT,8DAA8D;IAChE,OAAO,EAAE,OAAO;IAChB,6EAA6E;IAC7E,yEAAyE;IACzE,EAAE;IACF,wEAAwE;IACxE,wEAAwE;IACxE,2EAA2E;IAC3E,2EAA2E;IAC3E,sEAAsE;IACtE,2EAA2E;IAC3E,oDAAoD;IACpD,YAAY,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC;IACxC,QAAQ,EAAE;QACR,UAAU,EAAE,KAAK;QACjB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACtC,QAAQ,EAAE;YACR;gBACE,kEAAkE;gBAClE,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,aAAa;gBACnB,WAAW,EACT,6MAA6M;gBAC/M,SAAS,EAAE,KAAK;gBAChB,SAAS,EAAE,SAAS;gBACpB,YAAY,EAAE,IAAI;aACnB;SACF;KACF;IACD,uEAAuE;IACvE,0EAA0E;IAC1E,sEAAsE;IACtE,qDAAqD;IACrD,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,UAAU,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE;CAC1D,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * SEO module init (T235). Two tables:
4
+ * - seo_meta_overrides: per-(entity, locale) editorial overrides.
5
+ * - sitemap_cache: singleton cache row for the public XML sitemap.
6
+ */
7
+ export declare class Migration20260425T154404SeoInit extends Migration {
8
+ up(): Promise<void>;
9
+ down(): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=20260425T154404_seo_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T154404_seo_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260425T154404_seo_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;GAIG;AACH,qBAAa,+BAAgC,SAAQ,SAAS;IAC7C,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC"}