@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,57 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
4
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
5
+ import { MetaTagResolverService } from './services/meta-tag-resolver.service.js';
6
+ import { SitemapGeneratorService, type SitemapGeneratorOptions, type SitemapSettingsPort } from './services/sitemap-generator.service.js';
7
+ import { SeoMetaOverride } from './entities/seo-meta-override.entity.js';
8
+ import { SitemapCache } from './entities/sitemap-cache.entity.js';
9
+ /**
10
+ * `seo` — the sitemap that only read its setting in production (feature 072,
11
+ * wave 2, T117).
12
+ *
13
+ * `SitemapGeneratorService` resolves a channel's base URL in three steps:
14
+ * the per-channel `sales_channels.storefront_url` setting, then
15
+ * `STOREFRONT_BASE_URL`, then a fallback. The settings port that makes the
16
+ * first step possible was an optional constructor argument, `composition.ts`
17
+ * passed one and `test-server.ts` did not — so under the harness that step has
18
+ * never existed and every sitemap URL has come from env or fallback.
19
+ *
20
+ * That is the `cms` shape again, in its quieter form: not an endpoint that
21
+ * 500s, but a resolution step that silently is not there, producing a
22
+ * plausible answer from the next rung down. Resolving `settingsReadPort` here
23
+ * gives both compositions the same three steps.
24
+ *
25
+ * `sitemapOptions` stays a contribution point rather than becoming env-derived.
26
+ * It is genuinely composition-specific — the harness pins `staleAfterMs: 0` so
27
+ * regeneration is deterministic and a fixed `baseUrl` so an assertion has
28
+ * something stable to match — and a module should not have to know which of its
29
+ * callers is a test.
30
+ *
31
+ * `auditLog` stops being optional: SEO overrides are admin writes, and an
32
+ * optional audit sink defaults to not recording.
33
+ */
34
+ export interface SeoCradle {
35
+ readonly emFactory: () => EntityManager;
36
+ readonly auditLogService: AuditPort;
37
+ readonly requireAdmin: RequireAdminFactory;
38
+ readonly settingsReadPort: SitemapSettingsPort;
39
+ /** Composition-specific sitemap tuning; `{}` in production. */
40
+ readonly sitemapOptions: SitemapGeneratorOptions;
41
+ readonly seoMetaResolver: MetaTagResolverService;
42
+ readonly seoSitemapService: SitemapGeneratorService;
43
+ }
44
+ export declare function registerModule(ctx: ModuleContext): void;
45
+ /**
46
+ * The module's persisted entity classes, on the `./backend` subpath, as one
47
+ * array and **no named class export** (D-168).
48
+ *
49
+ * This is the shape the platform reads when the package is *installed*: the
50
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
51
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
52
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
53
+ * the package pass of `check-entity-tenant-classification`. A missing array is
54
+ * answered with `[]` — zero entities registered, no error anywhere.
55
+ */
56
+ export declare const entities: (typeof SeoMetaOverride | typeof SitemapCache)[];
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAO3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACjF,OAAO,EACL,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACzB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,CAAC;IAC/C,+DAA+D;IAC/D,QAAQ,CAAC,cAAc,EAAE,uBAAuB,CAAC;IACjD,QAAQ,CAAC,eAAe,EAAE,sBAAsB,CAAC;IACjD,QAAQ,CAAC,iBAAiB,EAAE,uBAAuB,CAAC;CACrD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA6DvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,kDAGpB,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { MetaTagResolverService } from './services/meta-tag-resolver.service.js';
3
+ import { SitemapGeneratorService, } from './services/sitemap-generator.service.js';
4
+ import { registerSeoRoutes } from './routes.js';
5
+ import { SeoMetaOverride } from './entities/seo-meta-override.entity.js';
6
+ import { SitemapCache } from './entities/sitemap-cache.entity.js';
7
+ export function registerModule(ctx) {
8
+ ctx.di.register({
9
+ sitemapOptions: ctx.asFunction(() => ({})).singleton(),
10
+ seoMetaResolver: ctx
11
+ .asFunction(({ emFactory, auditLogService }) => new MetaTagResolverService(emFactory, auditLogService,
12
+ // Feature 075, Phase C. Never captured: the proxies resolve per
13
+ // call, so a switched-off owner answers 503 `MODULE_DISABLED` at
14
+ // the call rather than through a gate frozen at composition time.
15
+ lazyPort(ctx, 'catalogProductReadPort'), lazyPort(ctx, 'catalogCategoryReadPort'), lazyPort(ctx, 'cmsPageReadPort')))
16
+ .singleton(),
17
+ seoSitemapService: ctx
18
+ .asFunction(({ emFactory, sitemapOptions }) => new SitemapGeneratorService(emFactory, lazyPort(ctx, 'settingsReadPort'), lazyPort(ctx, 'salesChannelMembershipPort'), lazyPort(ctx, 'catalogProductReadPort'), lazyPort(ctx, 'catalogCategoryReadPort'), lazyPort(ctx, 'cmsPageReadPort'), sitemapOptions))
19
+ .singleton(),
20
+ });
21
+ ctx.di.providePort('seoMetaWritePort', ctx
22
+ .asFunction(({ seoMetaResolver }) => ({
23
+ async upsertProductMeta(input) {
24
+ await seoMetaResolver.upsertOverride({
25
+ entityType: 'product',
26
+ entityId: input.productId,
27
+ locale: input.locale,
28
+ ...(input.title !== undefined ? { title: input.title } : {}),
29
+ ...(input.description !== undefined ? { description: input.description } : {}),
30
+ });
31
+ },
32
+ }))
33
+ .singleton());
34
+ ctx.routes(async (app) => {
35
+ const { seoMetaResolver, seoSitemapService, requireAdmin } = ctx.cradle();
36
+ await registerSeoRoutes(app, {
37
+ metaResolver: seoMetaResolver,
38
+ sitemap: seoSitemapService,
39
+ requireAdmin,
40
+ });
41
+ });
42
+ }
43
+ /**
44
+ * The module's persisted entity classes, on the `./backend` subpath, as one
45
+ * array and **no named class export** (D-168).
46
+ *
47
+ * This is the shape the platform reads when the package is *installed*: the
48
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
49
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
50
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
51
+ * the package pass of `check-entity-tenant-classification`. A missing array is
52
+ * answered with `[]` — zero entities registered, no error anywhere.
53
+ */
54
+ export const entities = [
55
+ SeoMetaOverride,
56
+ SitemapCache,
57
+ ];
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAG5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACjF,OAAO,EACL,uBAAuB,GAGxB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAuClE,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd,cAAc,EAAE,GAAG,CAAC,UAAU,CAAC,GAA4B,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE;QAE/E,eAAe,EAAE,GAAG;aACjB,UAAU,CACT,CAAC,EAAE,SAAS,EAAE,eAAe,EAAa,EAAE,EAAE,CAC5C,IAAI,sBAAsB,CACxB,SAAS,EACT,eAAe;QACf,gEAAgE;QAChE,iEAAiE;QACjE,kEAAkE;QAClE,QAAQ,CAAyB,GAAG,EAAE,wBAAwB,CAAC,EAC/D,QAAQ,CAA0B,GAAG,EAAE,yBAAyB,CAAC,EACjE,QAAQ,CAAkB,GAAG,EAAE,iBAAiB,CAAC,CAClD,CACJ;aACA,SAAS,EAAE;QAEd,iBAAiB,EAAE,GAAG;aACnB,UAAU,CACT,CAAC,EAAE,SAAS,EAAE,cAAc,EAAa,EAAE,EAAE,CAC3C,IAAI,uBAAuB,CACzB,SAAS,EACT,QAAQ,CAAsB,GAAG,EAAE,kBAAkB,CAAC,EACtD,QAAQ,CAA6B,GAAG,EAAE,4BAA4B,CAAC,EACvE,QAAQ,CAAyB,GAAG,EAAE,wBAAwB,CAAC,EAC/D,QAAQ,CAA0B,GAAG,EAAE,yBAAyB,CAAC,EACjE,QAAQ,CAAkB,GAAG,EAAE,iBAAiB,CAAC,EACjD,cAAc,CACf,CACJ;aACA,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,kBAAkB,EAClB,GAAG;SACA,UAAU,CAAC,CAAC,EAAE,eAAe,EAAa,EAAoB,EAAE,CAAC,CAAC;QACjE,KAAK,CAAC,iBAAiB,CAAC,KAAK;YAC3B,MAAM,eAAe,CAAC,cAAc,CAAC;gBACnC,UAAU,EAAE,SAAS;gBACrB,QAAQ,EAAE,KAAK,CAAC,SAAS;gBACzB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5D,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC/E,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;SACF,SAAS,EAAE,CACf,CAAC;IAEF,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,MAAM,EAAa,CAAC;QACrF,MAAM,iBAAiB,CAAC,GAAG,EAAE;YAC3B,YAAY,EAAE,eAAe;YAC7B,OAAO,EAAE,iBAAiB;YAC1B,YAAY;SACb,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,eAAe;IACf,YAAY;CACb,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { MetaTagResolverService } from './services/meta-tag-resolver.service.js';
3
+ import type { SitemapGeneratorService } from './services/sitemap-generator.service.js';
4
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
5
+ export interface SeoRoutesDeps {
6
+ metaResolver: MetaTagResolverService;
7
+ sitemap: SitemapGeneratorService;
8
+ requireAdmin: RequireAdminFactory;
9
+ }
10
+ export declare function registerSeoRoutes(app: FastifyInstance, deps: SeoRoutesDeps): Promise<void>;
11
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACtF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AACvF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG5E,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,sBAAsB,CAAC;IACrC,OAAO,EAAE,uBAAuB,CAAC;IACjC,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAgNf"}
@@ -0,0 +1,158 @@
1
+ import { seoEntityTypeSchema, upsertSeoMetaOverrideRequestSchema, } from '@endora-commerce/contracts';
2
+ import { currentSalesChannel } from '@endora-commerce/platform/kernel';
3
+ export async function registerSeoRoutes(app, deps) {
4
+ const { metaResolver, sitemap, requireAdmin } = deps;
5
+ // -- Public sitemap -------------------------------------------------------
6
+ //
7
+ // Resolves the channel from the request scope (filled by the
8
+ // SalesChannelResolverMiddleware via header / query / host map / system
9
+ // default). Each channel has its own cache row; if the resolver did not
10
+ // resolve a channel (e.g. middleware bypass in tests), we serve nothing.
11
+ app.get('/api/v1/catalog/sitemap.xml', { config: { streamingResponse: true } }, async (_request, reply) => {
12
+ const channel = currentSalesChannel();
13
+ if (!channel) {
14
+ reply
15
+ .status(503)
16
+ .header('Content-Type', 'application/xml; charset=utf-8')
17
+ .header('Cache-Control', 'no-store');
18
+ return '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>\n';
19
+ }
20
+ const { payload } = await sitemap.getOrGenerateForChannel(channel.code);
21
+ reply
22
+ .header('Content-Type', 'application/xml; charset=utf-8')
23
+ .header('Cache-Control', 'public, max-age=3600');
24
+ return payload;
25
+ });
26
+ // -- Admin: per-channel listing ------------------------------------------
27
+ app.get('/api/v1/admin/seo/sitemap', { preHandler: requireAdmin('catalog:write') }, async () => {
28
+ const rows = await sitemap.listChannelStatuses();
29
+ return {
30
+ data: rows.map((r) => ({
31
+ salesChannelCode: r.salesChannelCode,
32
+ salesChannelName: r.salesChannelName,
33
+ storefrontUrl: r.storefrontUrl,
34
+ storefrontUrlSource: r.storefrontUrlSource,
35
+ generatedAt: r.generatedAt?.toISOString() ?? null,
36
+ urlCount: r.urlCount,
37
+ byteSize: r.byteSize,
38
+ })),
39
+ };
40
+ });
41
+ // -- Admin: per-channel regenerate ---------------------------------------
42
+ app.post('/api/v1/admin/seo/sitemap/:channelCode/regenerate', { preHandler: requireAdmin('catalog:write') }, async (request) => {
43
+ const { channelCode } = request.params;
44
+ const { generatedAt } = await sitemap.regenerateForChannel(channelCode);
45
+ const status = await sitemap.getStatusForChannel(channelCode);
46
+ return {
47
+ data: {
48
+ salesChannelCode: channelCode,
49
+ generatedAt: generatedAt.toISOString(),
50
+ urlCount: status.urlCount ?? 0,
51
+ byteSize: status.byteSize ?? 0,
52
+ },
53
+ };
54
+ });
55
+ // -- Admin: per-channel status -------------------------------------------
56
+ app.get('/api/v1/admin/seo/sitemap/:channelCode/status', { preHandler: requireAdmin('catalog:write') }, async (request) => {
57
+ const status = await sitemap.getStatusForChannel(request.params.channelCode);
58
+ return {
59
+ data: {
60
+ salesChannelCode: request.params.channelCode,
61
+ generatedAt: status.generatedAt?.toISOString() ?? null,
62
+ urlCount: status.urlCount,
63
+ byteSize: status.byteSize,
64
+ },
65
+ };
66
+ });
67
+ // -- Admin: preview / download cached XML --------------------------------
68
+ //
69
+ // `?download=1` flips Content-Disposition to attachment so the browser
70
+ // saves the XML; otherwise the response renders inline (the admin UI
71
+ // opens it in a new tab for preview).
72
+ app.get('/api/v1/admin/seo/sitemap/:channelCode/xml', { preHandler: requireAdmin('catalog:write'), config: { streamingResponse: true } }, async (request, reply) => {
73
+ const { channelCode } = request.params;
74
+ const { payload } = await sitemap.getOrGenerateForChannel(channelCode);
75
+ reply
76
+ .header('Content-Type', 'application/xml; charset=utf-8')
77
+ .header('Cache-Control', 'no-store');
78
+ const isDownload = typeof request.query.download === 'string' &&
79
+ request.query.download !== '' &&
80
+ request.query.download !== '0' &&
81
+ request.query.download.toLowerCase() !== 'false';
82
+ if (isDownload) {
83
+ reply.header('Content-Disposition', `attachment; filename="sitemap-${channelCode}.xml"`);
84
+ }
85
+ else {
86
+ reply.header('Content-Disposition', 'inline');
87
+ }
88
+ return payload;
89
+ });
90
+ // -- Meta editor ----------------------------------------------------------
91
+ app.get('/api/v1/admin/seo/meta/:entityType/:entityId', { preHandler: requireAdmin('catalog:write') }, async (request) => {
92
+ const entityType = seoEntityTypeSchema.parse(request.params.entityType);
93
+ const locale = request.query.locale ?? 'en-US';
94
+ const resolved = await metaResolver.resolve({
95
+ entityType,
96
+ entityId: request.params.entityId,
97
+ locale,
98
+ });
99
+ const override = await metaResolver.getOverride({
100
+ entityType,
101
+ entityId: request.params.entityId,
102
+ locale,
103
+ });
104
+ return {
105
+ data: {
106
+ resolved,
107
+ override: override
108
+ ? {
109
+ title: override.title ?? null,
110
+ description: override.description ?? null,
111
+ ogTitle: override.ogTitle ?? null,
112
+ ogDescription: override.ogDescription ?? null,
113
+ ogImageUrl: override.ogImageUrl ?? null,
114
+ }
115
+ : null,
116
+ },
117
+ };
118
+ });
119
+ app.put('/api/v1/admin/seo/meta/:entityType/:entityId', {
120
+ preHandler: requireAdmin('catalog:write'),
121
+ schema: { body: upsertSeoMetaOverrideRequestSchema },
122
+ }, async (request) => {
123
+ const entityType = seoEntityTypeSchema.parse(request.params.entityType);
124
+ const body = upsertSeoMetaOverrideRequestSchema.parse(request.body);
125
+ const row = await metaResolver.upsertOverride({
126
+ entityType,
127
+ entityId: request.params.entityId,
128
+ locale: body.locale,
129
+ ...(body.title !== undefined ? { title: body.title } : {}),
130
+ ...(body.description !== undefined ? { description: body.description } : {}),
131
+ ...(body.ogTitle !== undefined ? { ogTitle: body.ogTitle } : {}),
132
+ ...(body.ogDescription !== undefined ? { ogDescription: body.ogDescription } : {}),
133
+ ...(body.ogImageUrl !== undefined ? { ogImageUrl: body.ogImageUrl } : {}),
134
+ });
135
+ return {
136
+ data: {
137
+ id: row.id,
138
+ locale: row.locale,
139
+ title: row.title ?? null,
140
+ description: row.description ?? null,
141
+ ogTitle: row.ogTitle ?? null,
142
+ ogDescription: row.ogDescription ?? null,
143
+ ogImageUrl: row.ogImageUrl ?? null,
144
+ },
145
+ };
146
+ });
147
+ app.delete('/api/v1/admin/seo/meta/:entityType/:entityId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
148
+ const entityType = seoEntityTypeSchema.parse(request.params.entityType);
149
+ const locale = request.query.locale ?? 'en-US';
150
+ await metaResolver.deleteOverride({
151
+ entityType,
152
+ entityId: request.params.entityId,
153
+ locale,
154
+ });
155
+ return reply.status(204).send();
156
+ });
157
+ }
158
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CatalogCategoryReadPort, CatalogCategoryRecord, CatalogProductReadPort, CatalogProductRecord, CmsPageReadPort, CmsPageRecord, ResolvedMeta, SeoEntityType } from '@endora-commerce/contracts';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ import { SeoMetaOverride } from '../entities/seo-meta-override.entity.js';
5
+ export interface RuleBuilderInput {
6
+ product?: CatalogProductRecord;
7
+ category?: CatalogCategoryRecord;
8
+ cmsPage?: CmsPageRecord;
9
+ }
10
+ export declare class MetaTagResolverService {
11
+ #private;
12
+ private readonly emFactory;
13
+ private readonly auditLog;
14
+ /** The three rows a rule is derived from, asked of the modules that own them. */
15
+ private readonly catalogProducts;
16
+ private readonly catalogCategories;
17
+ private readonly cmsPages;
18
+ constructor(emFactory: () => EntityManager, auditLog: AuditPort | undefined,
19
+ /** The three rows a rule is derived from, asked of the modules that own them. */
20
+ catalogProducts: CatalogProductReadPort, catalogCategories: CatalogCategoryReadPort, cmsPages: CmsPageReadPort);
21
+ resolve(input: {
22
+ entityType: SeoEntityType;
23
+ entityId: string;
24
+ locale: string;
25
+ }): Promise<ResolvedMeta>;
26
+ getOverride(input: {
27
+ entityType: SeoEntityType;
28
+ entityId: string;
29
+ locale: string;
30
+ }): Promise<SeoMetaOverride | null>;
31
+ upsertOverride(input: {
32
+ entityType: SeoEntityType;
33
+ entityId: string;
34
+ locale: string;
35
+ title?: string | null;
36
+ description?: string | null;
37
+ ogTitle?: string | null;
38
+ ogDescription?: string | null;
39
+ ogImageUrl?: string | null;
40
+ }): Promise<SeoMetaOverride>;
41
+ deleteOverride(input: {
42
+ entityType: SeoEntityType;
43
+ entityId: string;
44
+ locale: string;
45
+ }): Promise<void>;
46
+ private findOverride;
47
+ private loadRuleSource;
48
+ }
49
+ export declare function buildRuleMeta(entityType: SeoEntityType, source: RuleBuilderInput, locale: string): {
50
+ title: string;
51
+ description: string;
52
+ openGraph: ResolvedMeta['openGraph'];
53
+ };
54
+ //# sourceMappingURL=meta-tag-resolver.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta-tag-resolver.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/meta-tag-resolver.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,aAAa,EACd,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAyB1E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,qBAAa,sBAAsB;;IAE/B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBALR,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,SAAS,GAAG,SAAS;IAChD,iFAAiF;IAChE,eAAe,EAAE,sBAAsB,EACvC,iBAAiB,EAAE,uBAAuB,EAC1C,QAAQ,EAAE,eAAe;IAStC,OAAO,CAAC,KAAK,EAAE;QACnB,UAAU,EAAE,aAAa,CAAC;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,YAAY,CAAC;IAuBnB,WAAW,CAAC,KAAK,EAAE;QACvB,UAAU,EAAE,aAAa,CAAC;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAI7B,cAAc,CAAC,KAAK,EAAE;QAC1B,UAAU,EAAE,aAAa,CAAC;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,GAAG,OAAO,CAAC,eAAe,CAAC;IAqCtB,cAAc,CAAC,KAAK,EAAE;QAC1B,UAAU,EAAE,aAAa,CAAC;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC;YASH,YAAY;YAOZ,cAAc;CAgC7B;AAED,wBAAgB,aAAa,CAC3B,UAAU,EAAE,aAAa,EACzB,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,MAAM,GACb;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,CAAA;CAAE,CAgD9E"}
@@ -0,0 +1,208 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
4
+ import { SeoMetaOverride } from '../entities/seo-meta-override.entity.js';
5
+ /**
6
+ * MetaTagResolverService (T235 / FR-101).
7
+ *
8
+ * Resolution order:
9
+ * 1. Look up the per-(entity, locale) override row.
10
+ * 2. If a field is set on the override, use it verbatim.
11
+ * 3. Otherwise fall back to the rule derived from the entity (Product
12
+ * name + description, Category name, etc.).
13
+ *
14
+ * Locale fallback: if no override exists for the requested locale, try
15
+ * `en-US`, then return rule-only.
16
+ *
17
+ * The three rule sources arrive over their owners' published read ports
18
+ * (feature 075, Phase C) rather than out of `catalog`'s and `cms`' tables.
19
+ * Both edges fail closed, which is the answer a meta tag should get: the tags
20
+ * describe a page a customer can reach, and a switched-off `catalog` or `cms`
21
+ * means there is no such page to describe.
22
+ */
23
+ const FALLBACK_LOCALE = 'en-US';
24
+ const TITLE_MAX = 60;
25
+ const DESCRIPTION_MAX = 160;
26
+ export class MetaTagResolverService {
27
+ emFactory;
28
+ auditLog;
29
+ catalogProducts;
30
+ catalogCategories;
31
+ cmsPages;
32
+ constructor(emFactory, auditLog,
33
+ /** The three rows a rule is derived from, asked of the modules that own them. */
34
+ catalogProducts, catalogCategories, cmsPages) {
35
+ this.emFactory = emFactory;
36
+ this.auditLog = auditLog;
37
+ this.catalogProducts = catalogProducts;
38
+ this.catalogCategories = catalogCategories;
39
+ this.cmsPages = cmsPages;
40
+ }
41
+ #audit(em, action, objectId, stateBefore, stateAfter) {
42
+ if (this.auditLog) {
43
+ recordAuditFromContext(this.auditLog, em, { action, objectType: 'seo_meta_override', objectId, stateBefore, stateAfter });
44
+ }
45
+ }
46
+ async resolve(input) {
47
+ const em = this.emFactory();
48
+ const ruleSource = await this.loadRuleSource(input.entityType, input.entityId);
49
+ const rule = buildRuleMeta(input.entityType, ruleSource, input.locale);
50
+ const override = await this.findOverride(em, input);
51
+ if (!override) {
52
+ return { ...rule, source: 'rule', locale: input.locale };
53
+ }
54
+ return {
55
+ title: override.title ?? rule.title,
56
+ description: override.description ?? rule.description,
57
+ openGraph: {
58
+ title: override.ogTitle ?? rule.openGraph.title,
59
+ description: override.ogDescription ?? rule.openGraph.description,
60
+ image: override.ogImageUrl ?? rule.openGraph.image,
61
+ },
62
+ source: 'override',
63
+ locale: input.locale,
64
+ };
65
+ }
66
+ async getOverride(input) {
67
+ return this.findOverride(this.emFactory(), input);
68
+ }
69
+ async upsertOverride(input) {
70
+ const em = this.emFactory();
71
+ // Validate the entity actually exists so an admin typo doesn't write a
72
+ // dangling row.
73
+ await this.loadRuleSource(input.entityType, input.entityId);
74
+ const existing = await em.findOne(SeoMetaOverride, {
75
+ entityType: input.entityType,
76
+ entityId: input.entityId,
77
+ locale: input.locale,
78
+ });
79
+ if (existing) {
80
+ if (input.title !== undefined)
81
+ existing.title = input.title;
82
+ if (input.description !== undefined)
83
+ existing.description = input.description;
84
+ if (input.ogTitle !== undefined)
85
+ existing.ogTitle = input.ogTitle;
86
+ if (input.ogDescription !== undefined)
87
+ existing.ogDescription = input.ogDescription;
88
+ if (input.ogImageUrl !== undefined)
89
+ existing.ogImageUrl = input.ogImageUrl;
90
+ this.#audit(em, 'seo_meta_override.upsert', existing.id, null, { entityType: existing.entityType, entityId: existing.entityId, locale: existing.locale });
91
+ await em.flush();
92
+ return existing;
93
+ }
94
+ const row = em.create(SeoMetaOverride, {
95
+ entityType: input.entityType,
96
+ entityId: input.entityId,
97
+ locale: input.locale,
98
+ ...(input.title !== undefined ? { title: input.title } : {}),
99
+ ...(input.description !== undefined ? { description: input.description } : {}),
100
+ ...(input.ogTitle !== undefined ? { ogTitle: input.ogTitle } : {}),
101
+ ...(input.ogDescription !== undefined ? { ogDescription: input.ogDescription } : {}),
102
+ ...(input.ogImageUrl !== undefined ? { ogImageUrl: input.ogImageUrl } : {}),
103
+ });
104
+ em.persist(row);
105
+ this.#audit(em, 'seo_meta_override.upsert', row.id, null, { entityType: row.entityType, entityId: row.entityId, locale: row.locale });
106
+ await em.flush();
107
+ return row;
108
+ }
109
+ async deleteOverride(input) {
110
+ const em = this.emFactory();
111
+ const row = await em.findOne(SeoMetaOverride, input);
112
+ if (row) {
113
+ this.#audit(em, 'seo_meta_override.delete', row.id, { entityType: row.entityType, entityId: row.entityId, locale: row.locale }, null);
114
+ await em.removeAndFlush(row);
115
+ }
116
+ }
117
+ async findOverride(em, input) {
118
+ return em.findOne(SeoMetaOverride, input);
119
+ }
120
+ async loadRuleSource(entityType, entityId) {
121
+ if (entityType === 'product') {
122
+ const product = await this.catalogProducts.findById(entityId);
123
+ if (!product) {
124
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Product ${entityId} not found.`);
125
+ }
126
+ return { product };
127
+ }
128
+ if (entityType === 'category') {
129
+ // Feature 068 — meta tags describe a page a customer can reach: a
130
+ // deactivated (or deleted, never filtered here before) category has none.
131
+ // `liveOnly` is the port's name for the `deletedAt: null` half; `isActive`
132
+ // is a column on the record, so the other half stays a test here.
133
+ const category = await this.catalogCategories.findById(entityId, { liveOnly: true });
134
+ if (!category || !category.isActive) {
135
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Category ${entityId} not found.`);
136
+ }
137
+ return { category };
138
+ }
139
+ if (entityType === 'cms_page') {
140
+ const cmsPage = await this.cmsPages.findById(entityId);
141
+ if (!cmsPage) {
142
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `CMS page ${entityId} not found.`);
143
+ }
144
+ return { cmsPage };
145
+ }
146
+ // Exhaustiveness — every branch in `seoEntityTypeSchema` is handled above.
147
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Unknown entity type ${String(entityType)}.`);
148
+ }
149
+ }
150
+ export function buildRuleMeta(entityType, source, locale) {
151
+ if (entityType === 'product' && source.product) {
152
+ const name = pickLocale(source.product.name, locale);
153
+ const description = pickLocale(source.product.description, locale);
154
+ return {
155
+ title: trim(name, TITLE_MAX),
156
+ description: trim(description, DESCRIPTION_MAX),
157
+ openGraph: {
158
+ title: trim(name, TITLE_MAX),
159
+ description: trim(description, DESCRIPTION_MAX),
160
+ image: null,
161
+ },
162
+ };
163
+ }
164
+ if (entityType === 'category' && source.category) {
165
+ const name = pickLocale(source.category.name, locale);
166
+ const description = `Browse ${trim(name, 80)}.`;
167
+ return {
168
+ title: trim(name, TITLE_MAX),
169
+ description,
170
+ openGraph: {
171
+ title: trim(name, TITLE_MAX),
172
+ description,
173
+ image: null,
174
+ },
175
+ };
176
+ }
177
+ if (entityType === 'cms_page' && source.cmsPage) {
178
+ // Feature 014 — meta fields are per-language; fall back to the
179
+ // admin-facing `name` for title when no localized meta is set.
180
+ const title = pickLocale(source.cmsPage.metaTitle ?? null, locale) || source.cmsPage.name;
181
+ const description = pickLocale(source.cmsPage.metaDescription ?? null, locale);
182
+ return {
183
+ title: trim(title, TITLE_MAX),
184
+ description: trim(description, DESCRIPTION_MAX),
185
+ openGraph: {
186
+ title: trim(title, TITLE_MAX),
187
+ description: trim(description, DESCRIPTION_MAX),
188
+ image: null,
189
+ },
190
+ };
191
+ }
192
+ return {
193
+ title: '',
194
+ description: '',
195
+ openGraph: { title: '', description: '', image: null },
196
+ };
197
+ }
198
+ function pickLocale(value, locale) {
199
+ if (!value)
200
+ return '';
201
+ return value[locale] ?? value[FALLBACK_LOCALE] ?? Object.values(value)[0] ?? '';
202
+ }
203
+ function trim(value, max) {
204
+ if (value.length <= max)
205
+ return value;
206
+ return value.slice(0, max - 1).replace(/\s+\S*$/, '').trimEnd() + '…';
207
+ }
208
+ //# sourceMappingURL=meta-tag-resolver.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta-tag-resolver.service.js","sourceRoot":"","sources":["../../../src/backend/services/meta-tag-resolver.service.ts"],"names":[],"mappings":"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"}