@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,45 @@
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 class Migration20260425T154404SeoInit extends Migration {
8
+ async up() {
9
+ this.addSql(`
10
+ create table "seo_meta_overrides" (
11
+ "id" uuid not null,
12
+ "entity_type" varchar(32) not null,
13
+ "entity_id" uuid not null,
14
+ "locale" varchar(10) not null,
15
+ "title" varchar(160) null,
16
+ "description" varchar(400) null,
17
+ "og_title" varchar(160) null,
18
+ "og_description" varchar(400) null,
19
+ "og_image_url" varchar(2048) null,
20
+ "created_at" timestamptz not null,
21
+ "updated_at" timestamptz not null,
22
+ constraint "seo_meta_overrides_pkey" primary key ("id"),
23
+ constraint "uniq_seo_meta_overrides_entity_locale"
24
+ unique ("entity_type", "entity_id", "locale")
25
+ );
26
+ `);
27
+ this.addSql('create index "seo_meta_overrides_entity_type_index" on "seo_meta_overrides" ("entity_type");');
28
+ this.addSql('create index "seo_meta_overrides_entity_id_index" on "seo_meta_overrides" ("entity_id");');
29
+ this.addSql(`
30
+ create table "sitemap_cache" (
31
+ "key" varchar(32) not null,
32
+ "payload" text not null,
33
+ "url_count" int not null default 0,
34
+ "byte_size" int not null default 0,
35
+ "generated_at" timestamptz not null,
36
+ constraint "sitemap_cache_pkey" primary key ("key")
37
+ );
38
+ `);
39
+ }
40
+ async down() {
41
+ this.addSql('drop table if exists "sitemap_cache" cascade;');
42
+ this.addSql('drop table if exists "seo_meta_overrides" cascade;');
43
+ }
44
+ }
45
+ //# sourceMappingURL=20260425T154404_seo_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T154404_seo_init.js","sourceRoot":"","sources":["../../src/migrations/20260425T154404_seo_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;GAIG;AACH,MAAM,OAAO,+BAAgC,SAAQ,SAAS;IACnD,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;KAiBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,8FAA8F,CAAC,CAAC;QAC5G,IAAI,CAAC,MAAM,CAAC,0FAA0F,CAAC,CAAC;QAExG,IAAI,CAAC,MAAM,CAAC;;;;;;;;;KASX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,+CAA+C,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,CAAC,oDAAoD,CAAC,CAAC;IACpE,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * **One class**, stamped inside the frozen historical prefix
10
+ * (`BASELINE_THROUGH`, `db/migration-order.ts`), so its position is history and
11
+ * the manifest graph does not move it.
12
+ *
13
+ * The **named** export stays beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports the class by name
16
+ * from this specifier, and a migration class name is contract in a way an
17
+ * entity class name is not: `mikro_orm_migrations` persists it, so it is a
18
+ * string every already-migrated database holds.
19
+ */
20
+ import { Migration20260425T154404SeoInit } from './20260425T154404_seo_init.js';
21
+ export declare const migrations: (typeof Migration20260425T154404SeoInit)[];
22
+ export { Migration20260425T154404SeoInit };
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,+BAA+B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,eAAO,MAAM,UAAU,4CAAoC,CAAC;AAE5D,OAAO,EAAE,+BAA+B,EAAE,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * **One class**, stamped inside the frozen historical prefix
10
+ * (`BASELINE_THROUGH`, `db/migration-order.ts`), so its position is history and
11
+ * the manifest graph does not move it.
12
+ *
13
+ * The **named** export stays beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports the class by name
16
+ * from this specifier, and a migration class name is contract in a way an
17
+ * entity class name is not: `mikro_orm_migrations` persists it, so it is a
18
+ * string every already-migrated database holds.
19
+ */
20
+ import { Migration20260425T154404SeoInit } from './20260425T154404_seo_init.js';
21
+ export const migrations = [Migration20260425T154404SeoInit];
22
+ export { Migration20260425T154404SeoInit };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,+BAA+B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,+BAA+B,CAAC,CAAC;AAE5D,OAAO,EAAE,+BAA+B,EAAE,CAAC"}
package/docs/seo.md ADDED
@@ -0,0 +1,74 @@
1
+ ---
2
+ title: seo
3
+ description: Meta-tag resolver + cached XML sitemap
4
+ ---
5
+
6
+ # `seo`
7
+
8
+ Per-page meta-tag resolution and the public XML sitemap.
9
+
10
+ ## Public surface
11
+
12
+ | Verb + Path | Audience | Purpose |
13
+ | --- | --- | --- |
14
+ | `GET /api/v1/catalog/sitemap.xml` | crawlers | Cached XML sitemap; falls through to inline regenerate when stale |
15
+ | `POST /api/v1/admin/seo/sitemap/regenerate` | admin (`catalog:write`) | Force a fresh build |
16
+ | `GET /api/v1/admin/seo/sitemap/status` | admin | Last-generated timestamp + size + URL count |
17
+ | `GET /api/v1/admin/seo/meta/:entityType/:entityId` | admin | Resolved meta + override row for a (entity, locale) |
18
+ | `PUT /api/v1/admin/seo/meta/:entityType/:entityId` | admin | Upsert override (per locale) |
19
+ | `DELETE /api/v1/admin/seo/meta/:entityType/:entityId` | admin | Drop the override (resolver returns to rule output) |
20
+
21
+ ## Meta-tag resolution
22
+
23
+ `MetaTagResolverService.resolve({ entityType, entityId, locale })` returns
24
+ `{ title, description, openGraph, source, locale }`. The resolution order is:
25
+
26
+ 1. Look up the per-(entity, locale) override row.
27
+ 2. If a field is set on the override, use it verbatim.
28
+ 3. Otherwise fall back to the rule derived from the entity (Product name +
29
+ description, Category name + auto-summary).
30
+ 4. Per-field locale fallback: when a Product has no `pl-PL` text, the rule
31
+ uses `en-US`, then any available locale.
32
+
33
+ Title and description are truncated at 60 / 160 characters with an ellipsis.
34
+ The truncation lives in one helper so changes to the SEO budget happen in
35
+ one place.
36
+
37
+ ## Sitemap
38
+
39
+ `SitemapGeneratorService.regenerate()` walks active, public-visibility,
40
+ non-archived products and non-deleted categories, stamps absolute URLs
41
+ against `STOREFRONT_BASE_URL`, and writes the XML payload to a singleton
42
+ `sitemap_cache` row.
43
+
44
+ The public route (`GET /catalog/sitemap.xml`) returns the cached payload;
45
+ when the row is older than `staleAfterMs` (default 6 h) it regenerates
46
+ inline. A `Cache-Control: public, max-age=3600` header is set on the
47
+ response so well-behaved CDNs hold the payload for an hour.
48
+
49
+ ## Anonymous-only filtering
50
+
51
+ The sitemap excludes:
52
+ - products with `status != 'active'`
53
+ - products with `visibility != 'public'`
54
+ - archived (`archivedAt`) and soft-deleted (`deletedAt`) rows
55
+ - soft-deleted categories
56
+
57
+ The contract is simple: only what an anonymous Customer can see is
58
+ ever exposed to crawlers.
59
+
60
+ ## Entities
61
+
62
+ `SeoMetaOverride` (entityType, entityId, locale, title?, description?,
63
+ ogTitle?, ogDescription?, ogImageUrl?), `SitemapCache` (singleton key,
64
+ payload, urlCount, byteSize, generatedAt).
65
+
66
+ ## Extension points
67
+
68
+ - **CMS pages** — when a CMS module ships, hook its slug + body into both
69
+ `MetaTagResolverService.loadRuleSource()` and
70
+ `SitemapGeneratorService.regenerate()`.
71
+ - **Sitemap index** — for catalogues > 50 000 URLs, split into per-section
72
+ sitemaps and emit a `<sitemapindex>` from this module's route.
73
+ - **Scheduled regenerate** — wire a BullMQ repeatable into the production
74
+ composition root that calls `regenerate()` nightly.
package/i18n/en.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "nav.seo.label": "SEO"
3
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "nav.seo.label": "SEO"
3
+ }
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@endora-commerce/mod-seo",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "SEO metadata management for catalog, CMS, and blog surfaces.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "seo"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/seo"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./admin": {
34
+ "types": "./dist/admin/index.d.ts",
35
+ "default": "./dist/admin/index.js"
36
+ },
37
+ "./tailwind.css": "./tailwind.css",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "i18n",
43
+ "docs",
44
+ "tailwind.css"
45
+ ],
46
+ "engines": {
47
+ "node": ">=22.18.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@mikro-orm/core": "^6",
51
+ "@mikro-orm/migrations": "^6",
52
+ "@mikro-orm/postgresql": "^6",
53
+ "fastify": "^5",
54
+ "react": "^19",
55
+ "zod": "^4",
56
+ "@endora-commerce/admin-kit": "0.100.0",
57
+ "@endora-commerce/contracts": "0.100.0",
58
+ "@endora-commerce/platform": "0.100.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@endora-commerce/admin-kit": {
62
+ "optional": true
63
+ },
64
+ "react": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@mikro-orm/core": "^6.6.13",
70
+ "@mikro-orm/migrations": "^6.6.13",
71
+ "@mikro-orm/postgresql": "^6.6.13",
72
+ "@types/node": "^22.9.0",
73
+ "@types/react": "^19.2.14",
74
+ "fastify": "^5.12.5",
75
+ "react": "^19.2.5",
76
+ "typescript": "^5.9.3",
77
+ "vitest": "^4.1.11",
78
+ "zod": "^4.2.0",
79
+ "@endora-commerce/admin-kit": "0.100.0",
80
+ "@endora-commerce/contracts": "0.100.0",
81
+ "@endora-commerce/platform": "0.100.0"
82
+ },
83
+ "scripts": {
84
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
85
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
86
+ "lint": "eslint src",
87
+ "test": "vitest run"
88
+ }
89
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-seo — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";