@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,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
package/i18n/pl.json
ADDED
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";
|