@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,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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"}
|