@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.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/admin/index.d.ts +31 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +35 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/SeoPage.d.ts +4 -0
- package/dist/admin/pages/SeoPage.d.ts.map +1 -0
- package/dist/admin/pages/SeoPage.js +189 -0
- package/dist/admin/pages/SeoPage.js.map +1 -0
- package/dist/backend/entities/seo-meta-override.entity.d.ts +21 -0
- package/dist/backend/entities/seo-meta-override.entity.d.ts.map +1 -0
- package/dist/backend/entities/seo-meta-override.entity.js +84 -0
- package/dist/backend/entities/seo-meta-override.entity.js.map +1 -0
- package/dist/backend/entities/sitemap-cache.entity.d.ts +17 -0
- package/dist/backend/entities/sitemap-cache.entity.d.ts.map +1 -0
- package/dist/backend/entities/sitemap-cache.entity.js +52 -0
- package/dist/backend/entities/sitemap-cache.entity.js.map +1 -0
- package/dist/backend/index.d.ts +57 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +58 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +11 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +158 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/meta-tag-resolver.service.d.ts +54 -0
- package/dist/backend/services/meta-tag-resolver.service.d.ts.map +1 -0
- package/dist/backend/services/meta-tag-resolver.service.js +208 -0
- package/dist/backend/services/meta-tag-resolver.service.js.map +1 -0
- package/dist/backend/services/sitemap-generator.service.d.ts +79 -0
- package/dist/backend/services/sitemap-generator.service.d.ts.map +1 -0
- package/dist/backend/services/sitemap-generator.service.js +293 -0
- package/dist/backend/services/sitemap-generator.service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +49 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T154404_seo_init.d.ts +11 -0
- package/dist/migrations/20260425T154404_seo_init.d.ts.map +1 -0
- package/dist/migrations/20260425T154404_seo_init.js +45 -0
- package/dist/migrations/20260425T154404_seo_init.js.map +1 -0
- package/dist/migrations/index.d.ts +23 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +23 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/seo.md +74 -0
- package/i18n/en.json +3 -0
- package/i18n/pl.json +3 -0
- package/package.json +89 -0
- 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, '&')
|
|
268
|
+
.replace(/</g, '<')
|
|
269
|
+
.replace(/>/g, '>')
|
|
270
|
+
.replace(/"/g, '"')
|
|
271
|
+
.replace(/'/g, ''');
|
|
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"}
|
package/dist/manifest.js
ADDED
|
@@ -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"}
|