@endora-commerce/mod-blog 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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/admin/api/blog-client.d.ts +70 -0
  4. package/dist/admin/api/blog-client.d.ts.map +1 -0
  5. package/dist/admin/api/blog-client.js +137 -0
  6. package/dist/admin/api/blog-client.js.map +1 -0
  7. package/dist/admin/components/CategoryTreeNode.d.ts +25 -0
  8. package/dist/admin/components/CategoryTreeNode.d.ts.map +1 -0
  9. package/dist/admin/components/CategoryTreeNode.js +14 -0
  10. package/dist/admin/components/CategoryTreeNode.js.map +1 -0
  11. package/dist/admin/components/PostStatusBadge.d.ts +7 -0
  12. package/dist/admin/components/PostStatusBadge.d.ts.map +1 -0
  13. package/dist/admin/components/PostStatusBadge.js +16 -0
  14. package/dist/admin/components/PostStatusBadge.js.map +1 -0
  15. package/dist/admin/components/RelatedPostsPicker.d.ts +11 -0
  16. package/dist/admin/components/RelatedPostsPicker.d.ts.map +1 -0
  17. package/dist/admin/components/RelatedPostsPicker.js +77 -0
  18. package/dist/admin/components/RelatedPostsPicker.js.map +1 -0
  19. package/dist/admin/components/RelatedProductsPicker.d.ts +9 -0
  20. package/dist/admin/components/RelatedProductsPicker.d.ts.map +1 -0
  21. package/dist/admin/components/RelatedProductsPicker.js +66 -0
  22. package/dist/admin/components/RelatedProductsPicker.js.map +1 -0
  23. package/dist/admin/components/TagPicker.d.ts +9 -0
  24. package/dist/admin/components/TagPicker.d.ts.map +1 -0
  25. package/dist/admin/components/TagPicker.js +66 -0
  26. package/dist/admin/components/TagPicker.js.map +1 -0
  27. package/dist/admin/index.d.ts +33 -0
  28. package/dist/admin/index.d.ts.map +1 -0
  29. package/dist/admin/index.js +114 -0
  30. package/dist/admin/index.js.map +1 -0
  31. package/dist/admin/pages/BlogCategoryEditor.d.ts +9 -0
  32. package/dist/admin/pages/BlogCategoryEditor.d.ts.map +1 -0
  33. package/dist/admin/pages/BlogCategoryEditor.js +196 -0
  34. package/dist/admin/pages/BlogCategoryEditor.js.map +1 -0
  35. package/dist/admin/pages/BlogCategoryTreePage.d.ts +9 -0
  36. package/dist/admin/pages/BlogCategoryTreePage.d.ts.map +1 -0
  37. package/dist/admin/pages/BlogCategoryTreePage.js +122 -0
  38. package/dist/admin/pages/BlogCategoryTreePage.js.map +1 -0
  39. package/dist/admin/pages/BlogPostEditor.d.ts +9 -0
  40. package/dist/admin/pages/BlogPostEditor.d.ts.map +1 -0
  41. package/dist/admin/pages/BlogPostEditor.js +314 -0
  42. package/dist/admin/pages/BlogPostEditor.js.map +1 -0
  43. package/dist/admin/pages/BlogPostListPage.d.ts +9 -0
  44. package/dist/admin/pages/BlogPostListPage.d.ts.map +1 -0
  45. package/dist/admin/pages/BlogPostListPage.js +75 -0
  46. package/dist/admin/pages/BlogPostListPage.js.map +1 -0
  47. package/dist/admin/pages/BlogTagListPage.d.ts +9 -0
  48. package/dist/admin/pages/BlogTagListPage.d.ts.map +1 -0
  49. package/dist/admin/pages/BlogTagListPage.js +145 -0
  50. package/dist/admin/pages/BlogTagListPage.js.map +1 -0
  51. package/dist/backend/entities/blog-category-language.entity.d.ts +11 -0
  52. package/dist/backend/entities/blog-category-language.entity.d.ts.map +1 -0
  53. package/dist/backend/entities/blog-category-language.entity.js +35 -0
  54. package/dist/backend/entities/blog-category-language.entity.js.map +1 -0
  55. package/dist/backend/entities/blog-category-sales-channel.entity.d.ts +17 -0
  56. package/dist/backend/entities/blog-category-sales-channel.entity.d.ts.map +1 -0
  57. package/dist/backend/entities/blog-category-sales-channel.entity.js +48 -0
  58. package/dist/backend/entities/blog-category-sales-channel.entity.js.map +1 -0
  59. package/dist/backend/entities/blog-category.entity.d.ts +32 -0
  60. package/dist/backend/entities/blog-category.entity.d.ts.map +1 -0
  61. package/dist/backend/entities/blog-category.entity.js +113 -0
  62. package/dist/backend/entities/blog-category.entity.js.map +1 -0
  63. package/dist/backend/entities/blog-post-category.entity.d.ts +10 -0
  64. package/dist/backend/entities/blog-post-category.entity.d.ts.map +1 -0
  65. package/dist/backend/entities/blog-post-category.entity.js +34 -0
  66. package/dist/backend/entities/blog-post-category.entity.js.map +1 -0
  67. package/dist/backend/entities/blog-post-language.entity.d.ts +9 -0
  68. package/dist/backend/entities/blog-post-language.entity.d.ts.map +1 -0
  69. package/dist/backend/entities/blog-post-language.entity.js +33 -0
  70. package/dist/backend/entities/blog-post-language.entity.js.map +1 -0
  71. package/dist/backend/entities/blog-post-related-post.entity.d.ts +15 -0
  72. package/dist/backend/entities/blog-post-related-post.entity.d.ts.map +1 -0
  73. package/dist/backend/entities/blog-post-related-post.entity.js +42 -0
  74. package/dist/backend/entities/blog-post-related-post.entity.js.map +1 -0
  75. package/dist/backend/entities/blog-post-related-product.entity.d.ts +14 -0
  76. package/dist/backend/entities/blog-post-related-product.entity.d.ts.map +1 -0
  77. package/dist/backend/entities/blog-post-related-product.entity.js +41 -0
  78. package/dist/backend/entities/blog-post-related-product.entity.js.map +1 -0
  79. package/dist/backend/entities/blog-post-sales-channel.entity.d.ts +16 -0
  80. package/dist/backend/entities/blog-post-sales-channel.entity.d.ts.map +1 -0
  81. package/dist/backend/entities/blog-post-sales-channel.entity.js +47 -0
  82. package/dist/backend/entities/blog-post-sales-channel.entity.js.map +1 -0
  83. package/dist/backend/entities/blog-post-tag.entity.d.ts +14 -0
  84. package/dist/backend/entities/blog-post-tag.entity.d.ts.map +1 -0
  85. package/dist/backend/entities/blog-post-tag.entity.js +41 -0
  86. package/dist/backend/entities/blog-post-tag.entity.js.map +1 -0
  87. package/dist/backend/entities/blog-post.entity.d.ts +31 -0
  88. package/dist/backend/entities/blog-post.entity.d.ts.map +1 -0
  89. package/dist/backend/entities/blog-post.entity.js +106 -0
  90. package/dist/backend/entities/blog-post.entity.js.map +1 -0
  91. package/dist/backend/entities/blog-tag.entity.d.ts +18 -0
  92. package/dist/backend/entities/blog-tag.entity.d.ts.map +1 -0
  93. package/dist/backend/entities/blog-tag.entity.js +66 -0
  94. package/dist/backend/entities/blog-tag.entity.js.map +1 -0
  95. package/dist/backend/index.d.ts +100 -0
  96. package/dist/backend/index.d.ts.map +1 -0
  97. package/dist/backend/index.js +182 -0
  98. package/dist/backend/index.js.map +1 -0
  99. package/dist/backend/routes.admin.d.ts +12 -0
  100. package/dist/backend/routes.admin.d.ts.map +1 -0
  101. package/dist/backend/routes.admin.js +148 -0
  102. package/dist/backend/routes.admin.js.map +1 -0
  103. package/dist/backend/routes.storefront.d.ts +6 -0
  104. package/dist/backend/routes.storefront.d.ts.map +1 -0
  105. package/dist/backend/routes.storefront.js +72 -0
  106. package/dist/backend/routes.storefront.js.map +1 -0
  107. package/dist/backend/services/blog-asset-references.d.ts +4 -0
  108. package/dist/backend/services/blog-asset-references.d.ts.map +1 -0
  109. package/dist/backend/services/blog-asset-references.js +79 -0
  110. package/dist/backend/services/blog-asset-references.js.map +1 -0
  111. package/dist/backend/services/blog-cache.d.ts +55 -0
  112. package/dist/backend/services/blog-cache.d.ts.map +1 -0
  113. package/dist/backend/services/blog-cache.js +111 -0
  114. package/dist/backend/services/blog-cache.js.map +1 -0
  115. package/dist/backend/services/blog-category-service.d.ts +53 -0
  116. package/dist/backend/services/blog-category-service.d.ts.map +1 -0
  117. package/dist/backend/services/blog-category-service.js +446 -0
  118. package/dist/backend/services/blog-category-service.js.map +1 -0
  119. package/dist/backend/services/blog-language-reference.d.ts +18 -0
  120. package/dist/backend/services/blog-language-reference.d.ts.map +1 -0
  121. package/dist/backend/services/blog-language-reference.js +43 -0
  122. package/dist/backend/services/blog-language-reference.js.map +1 -0
  123. package/dist/backend/services/blog-post-service.d.ts +79 -0
  124. package/dist/backend/services/blog-post-service.d.ts.map +1 -0
  125. package/dist/backend/services/blog-post-service.js +539 -0
  126. package/dist/backend/services/blog-post-service.js.map +1 -0
  127. package/dist/backend/services/blog-settings-resolver.d.ts +36 -0
  128. package/dist/backend/services/blog-settings-resolver.d.ts.map +1 -0
  129. package/dist/backend/services/blog-settings-resolver.js +68 -0
  130. package/dist/backend/services/blog-settings-resolver.js.map +1 -0
  131. package/dist/backend/services/blog-slug-collision.d.ts +35 -0
  132. package/dist/backend/services/blog-slug-collision.d.ts.map +1 -0
  133. package/dist/backend/services/blog-slug-collision.js +84 -0
  134. package/dist/backend/services/blog-slug-collision.js.map +1 -0
  135. package/dist/backend/services/blog-storefront-resolver.d.ts +107 -0
  136. package/dist/backend/services/blog-storefront-resolver.d.ts.map +1 -0
  137. package/dist/backend/services/blog-storefront-resolver.js +492 -0
  138. package/dist/backend/services/blog-storefront-resolver.js.map +1 -0
  139. package/dist/backend/services/blog-tag-service.d.ts +45 -0
  140. package/dist/backend/services/blog-tag-service.d.ts.map +1 -0
  141. package/dist/backend/services/blog-tag-service.js +198 -0
  142. package/dist/backend/services/blog-tag-service.js.map +1 -0
  143. package/dist/backend/services/seed-default-category.d.ts +25 -0
  144. package/dist/backend/services/seed-default-category.d.ts.map +1 -0
  145. package/dist/backend/services/seed-default-category.js +48 -0
  146. package/dist/backend/services/seed-default-category.js.map +1 -0
  147. package/dist/backend/services/seed-roles.d.ts +41 -0
  148. package/dist/backend/services/seed-roles.d.ts.map +1 -0
  149. package/dist/backend/services/seed-roles.js +98 -0
  150. package/dist/backend/services/seed-roles.js.map +1 -0
  151. package/dist/manifest.d.ts +226 -0
  152. package/dist/manifest.d.ts.map +1 -0
  153. package/dist/manifest.js +236 -0
  154. package/dist/manifest.js.map +1 -0
  155. package/dist/migrations/20260506T081055_blog_init.d.ts +33 -0
  156. package/dist/migrations/20260506T081055_blog_init.d.ts.map +1 -0
  157. package/dist/migrations/20260506T081055_blog_init.js +260 -0
  158. package/dist/migrations/20260506T081055_blog_init.js.map +1 -0
  159. package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts +6 -0
  160. package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts.map +1 -0
  161. package/dist/migrations/20260903T101744_blog_namespace_block_names.js +58 -0
  162. package/dist/migrations/20260903T101744_blog_namespace_block_names.js.map +1 -0
  163. package/dist/migrations/index.d.ts +34 -0
  164. package/dist/migrations/index.d.ts.map +1 -0
  165. package/dist/migrations/index.js +37 -0
  166. package/dist/migrations/index.js.map +1 -0
  167. package/docs/blog/index.md +199 -0
  168. package/i18n/en.json +166 -0
  169. package/i18n/pl.json +166 -0
  170. package/package.json +108 -0
  171. package/tailwind.css +14 -0
@@ -0,0 +1,492 @@
1
+ import { BlogCacheService } from './blog-cache.js';
2
+ function toChannelRow(channel) {
3
+ return { id: channel.id, code: channel.code, default_language: channel.defaultLanguage };
4
+ }
5
+ /**
6
+ * BlogStorefrontResolver — feature 016 / T051.
7
+ *
8
+ * Three read endpoints behind `/api/v1/blog/**`:
9
+ *
10
+ * getIndex → BlogIndexResponse (latest N + first-level categories)
11
+ * getBySlug → BlogBySlugResponse discriminated by `kind`
12
+ * getTagByCode → BlogTagByCodeResponse paginated
13
+ *
14
+ * Every method:
15
+ * - reads the channel-resolved blog settings via BlogSettingsResolver;
16
+ * - returns null (the route maps to 404) when blog.enabled = false;
17
+ * - applies the platform-wide language-fallback rule (feature 005)
18
+ * via the `pickLanguage` helper;
19
+ * - wraps results in BlogCacheService when Redis is wired.
20
+ */
21
+ export class BlogStorefrontResolver {
22
+ emFactory;
23
+ settingsResolver;
24
+ deps;
25
+ cache;
26
+ constructor(emFactory, settingsResolver, deps = {}, cache) {
27
+ this.emFactory = emFactory;
28
+ this.settingsResolver = settingsResolver;
29
+ this.deps = deps;
30
+ this.cache = cache;
31
+ }
32
+ async getIndex(input) {
33
+ const em = this.emFactory();
34
+ const channel = toChannelRow(input.resolvedChannel);
35
+ const language = input.language ?? channel.default_language;
36
+ const settings = await this.settingsResolver.getResolved(channel.id);
37
+ if (!settings.enabled)
38
+ return null;
39
+ const cacheKey = BlogCacheService.composeIndexKey(channel.code, language);
40
+ if (this.cache) {
41
+ const cached = await this.cache.get(cacheKey);
42
+ if (cached)
43
+ return cached;
44
+ }
45
+ const latestRows = (await em.execute(`select p.*
46
+ from blog_posts p
47
+ join blog_post_sales_channels psc on psc.blog_post_id = p.id
48
+ where p.deleted_at is null
49
+ and p.active = true
50
+ and p.status = 'published'
51
+ and psc.sales_channel_id = ?
52
+ and psc.deleted_at is null
53
+ order by p.published_at desc nulls last, p.created_at desc
54
+ limit ?`, [channel.id, settings.latestCount]));
55
+ const topLevelRows = (await em.execute(`select c.*
56
+ from blog_categories c
57
+ join blog_category_sales_channels csc on csc.blog_category_id = c.id
58
+ where c.deleted_at is null
59
+ and c.enabled = true
60
+ and c.parent_id is null
61
+ and csc.sales_channel_id = ?
62
+ and csc.deleted_at is null
63
+ order by c.position asc, c.slug asc`, [channel.id]));
64
+ const channelDefault = channel.default_language;
65
+ const latestPosts = await Promise.all(latestRows.map((r) => this.toPostCard(em, r, language, channelDefault)));
66
+ const topLevelCategories = await Promise.all(topLevelRows.map((r) => this.toCategoryTile(r, language, channelDefault)));
67
+ const out = {
68
+ urlPrefix: settings.urlPrefix,
69
+ latestPosts,
70
+ topLevelCategories,
71
+ };
72
+ if (this.cache)
73
+ await this.cache.set(cacheKey, out);
74
+ return out;
75
+ }
76
+ async getBySlug(slug, input) {
77
+ const em = this.emFactory();
78
+ const channel = toChannelRow(input.resolvedChannel);
79
+ const language = input.language ?? channel.default_language;
80
+ const settings = await this.settingsResolver.getResolved(channel.id);
81
+ if (!settings.enabled)
82
+ return null;
83
+ const channelDefault = channel.default_language;
84
+ // Probe the post side first — by-(channel, slug) covered by the partial-unique index.
85
+ const postRows = (await em.execute(`select p.*
86
+ from blog_posts p
87
+ join blog_post_sales_channels psc on psc.blog_post_id = p.id
88
+ where p.deleted_at is null
89
+ and p.active = true
90
+ and p.status = 'published'
91
+ and psc.sales_channel_id = ?
92
+ and psc.slug = ?
93
+ and psc.deleted_at is null
94
+ limit 1`, [channel.id, slug]));
95
+ if (postRows[0]) {
96
+ const post = postRows[0];
97
+ const cacheKey = BlogCacheService.composePostKey(channel.code, language, slug);
98
+ if (this.cache) {
99
+ const cached = await this.cache.get(cacheKey);
100
+ if (cached)
101
+ return cached;
102
+ }
103
+ const resolved = await this.toResolvedPost(em, post, channel, language, channelDefault, settings.urlPrefix);
104
+ if (resolved === null)
105
+ return null;
106
+ const payload = { kind: 'post', post: resolved };
107
+ if (this.cache)
108
+ await this.cache.set(cacheKey, payload);
109
+ return payload;
110
+ }
111
+ // Probe the category side.
112
+ const categoryRows = (await em.execute(`select c.*
113
+ from blog_categories c
114
+ join blog_category_sales_channels csc on csc.blog_category_id = c.id
115
+ where c.deleted_at is null
116
+ and c.enabled = true
117
+ and csc.sales_channel_id = ?
118
+ and csc.slug = ?
119
+ and csc.deleted_at is null
120
+ limit 1`, [channel.id, slug]));
121
+ if (categoryRows[0]) {
122
+ const category = categoryRows[0];
123
+ const page = Math.max(1, input.page ?? 1);
124
+ const cacheKey = BlogCacheService.composeCategoryKey(channel.code, language, slug, page);
125
+ if (this.cache) {
126
+ const cached = await this.cache.get(cacheKey);
127
+ if (cached)
128
+ return cached;
129
+ }
130
+ const offset = (page - 1) * settings.postsPerPage;
131
+ const totalRows = (await em.execute(`select count(*)::int as n
132
+ from blog_posts p
133
+ join blog_post_categories pc on pc.blog_post_id = p.id
134
+ join blog_post_sales_channels psc on psc.blog_post_id = p.id
135
+ where p.deleted_at is null
136
+ and p.active = true
137
+ and p.status = 'published'
138
+ and pc.blog_category_id = ?
139
+ and psc.sales_channel_id = ?
140
+ and psc.deleted_at is null`, [category.id, channel.id]));
141
+ const totalItems = totalRows[0]?.n ?? 0;
142
+ const pageRows = (await em.execute(`select p.*
143
+ from blog_posts p
144
+ join blog_post_categories pc on pc.blog_post_id = p.id
145
+ join blog_post_sales_channels psc on psc.blog_post_id = p.id
146
+ where p.deleted_at is null
147
+ and p.active = true
148
+ and p.status = 'published'
149
+ and pc.blog_category_id = ?
150
+ and psc.sales_channel_id = ?
151
+ and psc.deleted_at is null
152
+ order by p.published_at desc nulls last, p.created_at desc
153
+ limit ${settings.postsPerPage} offset ${offset}`, [category.id, channel.id]));
154
+ const childRows = (await em.execute(`select c.*
155
+ from blog_categories c
156
+ join blog_category_sales_channels csc on csc.blog_category_id = c.id
157
+ where c.deleted_at is null
158
+ and c.enabled = true
159
+ and c.parent_id = ?
160
+ and csc.sales_channel_id = ?
161
+ and csc.deleted_at is null
162
+ order by c.position asc, c.slug asc`, [category.id, channel.id]));
163
+ const resolvedCategory = {
164
+ id: category.id,
165
+ name: this.pickName(category.name, language, channelDefault) ?? category.slug,
166
+ slug: category.slug,
167
+ mainImageUrl: await this.maybeAssetUrl(category.main_image_asset_id),
168
+ description: this.normaliseDescription(category.description),
169
+ metaTitle: this.pickLanguage(category.meta_title, language, channelDefault),
170
+ metaDescription: this.pickLanguage(category.meta_description, language, channelDefault),
171
+ metaKeywords: this.pickLanguage(category.meta_keywords, language, channelDefault),
172
+ childCategories: await Promise.all(childRows.map((r) => this.toCategoryTile(r, language, channelDefault))),
173
+ breadcrumb: [
174
+ { name: 'Blog', url: `/${settings.urlPrefix}` },
175
+ { name: this.pickName(category.name, language, channelDefault) ?? category.slug, url: `/${settings.urlPrefix}/${category.slug}` },
176
+ ],
177
+ };
178
+ const payload = {
179
+ kind: 'category',
180
+ category: resolvedCategory,
181
+ posts: {
182
+ data: await Promise.all(pageRows.map((r) => this.toPostCard(em, r, language, channelDefault))),
183
+ pagination: {
184
+ page,
185
+ perPage: settings.postsPerPage,
186
+ totalPages: Math.max(1, Math.ceil(totalItems / settings.postsPerPage)),
187
+ totalItems,
188
+ },
189
+ },
190
+ };
191
+ if (this.cache)
192
+ await this.cache.set(cacheKey, payload);
193
+ return payload;
194
+ }
195
+ return null;
196
+ }
197
+ async getTagByCode(code, input) {
198
+ const em = this.emFactory();
199
+ const channel = toChannelRow(input.resolvedChannel);
200
+ const language = input.language ?? channel.default_language;
201
+ const settings = await this.settingsResolver.getResolved(channel.id);
202
+ if (!settings.enabled)
203
+ return null;
204
+ const channelDefault = channel.default_language;
205
+ const tagRows = (await em.execute(`select id::text as id, code, name, description from blog_tags where code = ? and deleted_at is null limit 1`, [code]));
206
+ if (tagRows.length === 0)
207
+ return null;
208
+ const tag = tagRows[0];
209
+ const page = Math.max(1, input.page ?? 1);
210
+ const cacheKey = BlogCacheService.composeTagKey(channel.code, language, code, page);
211
+ if (this.cache) {
212
+ const cached = await this.cache.get(cacheKey);
213
+ if (cached)
214
+ return cached;
215
+ }
216
+ const offset = (page - 1) * settings.postsPerPage;
217
+ const totalRows = (await em.execute(`select count(*)::int as n
218
+ from blog_posts p
219
+ join blog_post_tags pt on pt.blog_post_id = p.id
220
+ join blog_post_sales_channels psc on psc.blog_post_id = p.id
221
+ where p.deleted_at is null
222
+ and p.active = true
223
+ and p.status = 'published'
224
+ and pt.blog_tag_id = ?
225
+ and psc.sales_channel_id = ?
226
+ and psc.deleted_at is null`, [tag.id, channel.id]));
227
+ const totalItems = totalRows[0]?.n ?? 0;
228
+ const pageRows = (await em.execute(`select p.*
229
+ from blog_posts p
230
+ join blog_post_tags pt on pt.blog_post_id = p.id
231
+ join blog_post_sales_channels psc on psc.blog_post_id = p.id
232
+ where p.deleted_at is null
233
+ and p.active = true
234
+ and p.status = 'published'
235
+ and pt.blog_tag_id = ?
236
+ and psc.sales_channel_id = ?
237
+ and psc.deleted_at is null
238
+ order by p.published_at desc nulls last, p.created_at desc
239
+ limit ${settings.postsPerPage} offset ${offset}`, [tag.id, channel.id]));
240
+ const tagName = this.pickLanguage(tag.name, language, channelDefault) ?? tag.code;
241
+ const payload = {
242
+ tag: {
243
+ id: tag.id,
244
+ code: tag.code,
245
+ name: tagName,
246
+ description: this.pickLanguage(tag.description, language, channelDefault),
247
+ },
248
+ posts: {
249
+ data: await Promise.all(pageRows.map((r) => this.toPostCard(em, r, language, channelDefault))),
250
+ pagination: {
251
+ page,
252
+ perPage: settings.postsPerPage,
253
+ totalPages: Math.max(1, Math.ceil(totalItems / settings.postsPerPage)),
254
+ totalItems,
255
+ },
256
+ },
257
+ breadcrumb: [
258
+ { name: 'Blog', url: `/${settings.urlPrefix}` },
259
+ { name: tagName, url: `/${settings.urlPrefix}/tag/${tag.code}` },
260
+ ],
261
+ };
262
+ if (this.cache)
263
+ await this.cache.set(cacheKey, payload);
264
+ return payload;
265
+ }
266
+ // ────────────────────────────────────────────────────────────────────
267
+ // Internals
268
+ // ────────────────────────────────────────────────────────────────────
269
+ /**
270
+ * Language-fallback rule (FR-019 / R12 / per feature 005): pick the
271
+ * requested language; fall back to the channel's default language; if
272
+ * neither exists, return null. The caller decides whether `null`
273
+ * means "skip this field" or "404 the page" depending on context.
274
+ */
275
+ pickLanguage(map, language, channelDefault) {
276
+ if (!map)
277
+ return null;
278
+ if (map[language])
279
+ return map[language] ?? null;
280
+ if (map[channelDefault])
281
+ return map[channelDefault] ?? null;
282
+ return null;
283
+ }
284
+ /**
285
+ * Resolve a human-readable name for a published entity. Applies the
286
+ * standard language fallback (requested → channel default) and, when that
287
+ * is exhausted, falls back to the first language actually authored on the
288
+ * entity rather than giving up.
289
+ *
290
+ * This keeps a *published* post/category viewable even when its content
291
+ * was authored only in a language other than the channel's default — the
292
+ * alternative (returning null) made the post-detail resolver 404 while the
293
+ * index/category resolvers still listed the post (via their `?? slug`
294
+ * fallback), an inconsistency that surfaced as "the post is in the list but
295
+ * its page 404s". A published entity is always reachable; only its display
296
+ * language degrades.
297
+ */
298
+ pickName(map, language, channelDefault) {
299
+ const resolved = this.pickLanguage(map, language, channelDefault);
300
+ if (resolved !== null)
301
+ return resolved;
302
+ if (!map)
303
+ return null;
304
+ for (const value of Object.values(map)) {
305
+ if (value)
306
+ return value;
307
+ }
308
+ return null;
309
+ }
310
+ async toPostCard(em, row, language, channelDefault) {
311
+ // Pull the primary category slug if any (the first one assigned).
312
+ const catRows = (await em.execute(`select c.slug
313
+ from blog_post_categories pc
314
+ join blog_categories c on c.id = pc.blog_category_id
315
+ where pc.blog_post_id = ?
316
+ and c.deleted_at is null
317
+ order by c.is_system desc, c.position asc, c.slug asc
318
+ limit 1`, [row.id]));
319
+ const primaryCategorySlug = catRows[0]?.slug ?? null;
320
+ const name = this.pickName(row.name, language, channelDefault) ?? row.slug;
321
+ const excerpt = this.pickLanguage(row.meta_description, language, channelDefault) ??
322
+ this.firstParagraph(row.content, language, channelDefault);
323
+ return {
324
+ id: row.id,
325
+ slug: row.slug,
326
+ name,
327
+ publishedAt: this.toIso(row.published_at),
328
+ excerpt,
329
+ mainImageUrl: await this.maybeAssetUrl(row.main_image_asset_id),
330
+ primaryCategorySlug,
331
+ };
332
+ }
333
+ async toCategoryTile(row, language, channelDefault) {
334
+ return {
335
+ id: row.id,
336
+ name: this.pickName(row.name, language, channelDefault) ?? row.slug,
337
+ slug: row.slug,
338
+ mainImageUrl: await this.maybeAssetUrl(row.main_image_asset_id),
339
+ };
340
+ }
341
+ async toResolvedPost(em, post, channel, language, channelDefault, urlPrefix) {
342
+ // A published post is always reachable; fall back to any authored
343
+ // language (then the slug) rather than 404 when the channel default is
344
+ // missing. Keeps the post-detail page consistent with the index/category
345
+ // listings, which already surface the post via their own slug fallback.
346
+ const name = this.pickName(post.name, language, channelDefault) ?? post.slug;
347
+ // Tags (ordered).
348
+ const tagRows = (await em.execute(`select t.id::text as id, t.code, t.name
349
+ from blog_post_tags pt
350
+ join blog_tags t on t.id = pt.blog_tag_id and t.deleted_at is null
351
+ where pt.blog_post_id = ?
352
+ order by pt.position`, [post.id]));
353
+ // Related posts (filtered to available ones in scope).
354
+ const relatedPostRows = (await em.execute(`select p.*
355
+ from blog_post_related_posts r
356
+ join blog_posts p on p.id = r.related_post_id
357
+ join blog_post_sales_channels psc on psc.blog_post_id = p.id
358
+ where r.parent_post_id = ?
359
+ and p.deleted_at is null
360
+ and p.active = true
361
+ and p.status = 'published'
362
+ and psc.sales_channel_id = ?
363
+ and psc.deleted_at is null
364
+ order by r.position`, [post.id, channel.id]));
365
+ const relatedPosts = await Promise.all(relatedPostRows.map((r) => this.toPostCard(em, r, language, channelDefault)));
366
+ // Related products via the optional port.
367
+ const relatedProductIds = (await em.execute(`select product_id::text as id
368
+ from blog_post_related_products
369
+ where blog_post_id = ?
370
+ order by position`, [post.id]));
371
+ const relatedProducts = [];
372
+ if (this.deps.resolveProductCard) {
373
+ for (const r of relatedProductIds) {
374
+ const card = await this.deps.resolveProductCard(r.id, channel.id, language);
375
+ if (card)
376
+ relatedProducts.push(card);
377
+ }
378
+ }
379
+ // Categories (used in breadcrumb + the resolved-post categories list).
380
+ const categoryRows = (await em.execute(`select c.id::text as id, c.slug, c.name
381
+ from blog_post_categories pc
382
+ join blog_categories c on c.id = pc.blog_category_id and c.deleted_at is null
383
+ where pc.blog_post_id = ?
384
+ order by c.is_system desc, c.position asc, c.slug asc`, [post.id]));
385
+ const categories = categoryRows.map((c) => ({
386
+ id: c.id,
387
+ name: this.pickName(c.name, language, channelDefault) ?? c.slug,
388
+ slug: c.slug,
389
+ }));
390
+ const tags = tagRows.map((t) => ({
391
+ id: t.id,
392
+ code: t.code,
393
+ name: this.pickLanguage(t.name, language, channelDefault) ?? t.code,
394
+ }));
395
+ const breadcrumb = [
396
+ { name: 'Blog', url: `/${urlPrefix}` },
397
+ ...(categories[0]
398
+ ? [{ name: categories[0].name, url: `/${urlPrefix}/${categories[0].slug}` }]
399
+ : []),
400
+ { name, url: `/${urlPrefix}/${post.slug}` },
401
+ ];
402
+ return {
403
+ id: post.id,
404
+ name,
405
+ slug: post.slug,
406
+ publishedAt: this.toIso(post.published_at),
407
+ metaTitle: this.pickLanguage(post.meta_title, language, channelDefault),
408
+ metaDescription: this.pickLanguage(post.meta_description, language, channelDefault),
409
+ metaKeywords: this.pickLanguage(post.meta_keywords, language, channelDefault),
410
+ content: this.normaliseContent(post.content),
411
+ tags,
412
+ relatedPosts,
413
+ relatedProducts,
414
+ categories,
415
+ breadcrumb,
416
+ };
417
+ }
418
+ async maybeAssetUrl(assetId) {
419
+ if (!assetId)
420
+ return null;
421
+ if (!this.deps.resolveAssetUrl)
422
+ return null;
423
+ return this.deps.resolveAssetUrl(assetId);
424
+ }
425
+ normaliseDescription(raw) {
426
+ if (!raw || typeof raw !== 'object')
427
+ return null;
428
+ return {
429
+ schema_version: typeof raw.schema_version === 'number' ? raw.schema_version : 1,
430
+ languages: raw.languages ?? {},
431
+ };
432
+ }
433
+ normaliseContent(raw) {
434
+ if (!raw || typeof raw !== 'object')
435
+ return { schema_version: 1, languages: {} };
436
+ return {
437
+ schema_version: typeof raw.schema_version === 'number' ? raw.schema_version : 1,
438
+ languages: raw.languages ?? {},
439
+ };
440
+ }
441
+ /**
442
+ * Best-effort excerpt extraction — picks the first text-bearing node
443
+ * in the resolved language's tree. Falls back to the channel default
444
+ * language. Returns null when nothing usable is found.
445
+ */
446
+ firstParagraph(raw, language, channelDefault) {
447
+ if (!raw || typeof raw !== 'object' || !raw.languages)
448
+ return null;
449
+ const tree = raw.languages[language] ?? raw.languages[channelDefault];
450
+ if (!tree)
451
+ return null;
452
+ return extractText(tree, 160);
453
+ }
454
+ toIso(value) {
455
+ if (value === null || value === undefined)
456
+ return null;
457
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
458
+ }
459
+ }
460
+ /**
461
+ * Recursively walks a Page Builder tree node and collects up to `max`
462
+ * characters of text from `props.text` / `props.value` fields. Used as a
463
+ * fallback excerpt when no `meta_description` is set.
464
+ */
465
+ function extractText(node, max) {
466
+ const acc = [];
467
+ const walk = (n) => {
468
+ if (acc.join(' ').length >= max)
469
+ return;
470
+ if (!n || typeof n !== 'object')
471
+ return;
472
+ const obj = n;
473
+ const props = obj['props'];
474
+ if (props) {
475
+ if (typeof props['text'] === 'string')
476
+ acc.push(props['text']);
477
+ if (typeof props['value'] === 'string')
478
+ acc.push(props['value']);
479
+ }
480
+ const children = obj['children'];
481
+ if (Array.isArray(children)) {
482
+ for (const child of children)
483
+ walk(child);
484
+ }
485
+ };
486
+ walk(node);
487
+ if (acc.length === 0)
488
+ return null;
489
+ const s = acc.join(' ').trim();
490
+ return s.length > max ? `${s.slice(0, max).trimEnd()}…` : s;
491
+ }
492
+ //# sourceMappingURL=blog-storefront-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-storefront-resolver.js","sourceRoot":"","sources":["../../../src/backend/services/blog-storefront-resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type BlogTagDetail, type BlogTagInboundReferencesResponse, type CreateBlogTagRequest, type PatchBlogTagRequest } from '@endora-commerce/contracts';
3
+ import type { BlogCacheService } from './blog-cache.js';
4
+ /**
5
+ * BlogTagService — feature 016 / T065.
6
+ *
7
+ * CRUD over `blog_tags` with global `code` uniqueness (R14) and block-
8
+ * on-delete (FR-018): deletion is refused with 409 BLOG_TAG_IN_USE
9
+ * (listing the first 25 referencing posts) when at least one
10
+ * `blog_post_tags` row references the tag.
11
+ *
12
+ * Tag → Post attachment (`replaceForPost`) is owned by `BlogPostService`
13
+ * already (`setTags` keeps the ordered list per Post + bumps the post's
14
+ * version); this service exposes `getInboundReferences` to drive the
15
+ * admin's confirmation dialog before the operator commits a delete.
16
+ */
17
+ export declare class BlogTagService {
18
+ private readonly emFactory;
19
+ private readonly cache?;
20
+ constructor(emFactory: () => EntityManager, cache?: BlogCacheService | undefined);
21
+ list(filters: {
22
+ q?: string | undefined;
23
+ page?: number | undefined;
24
+ perPage?: number | undefined;
25
+ }): Promise<{
26
+ data: BlogTagDetail[];
27
+ pagination: {
28
+ page: number;
29
+ perPage: number;
30
+ totalPages: number;
31
+ totalItems: number;
32
+ };
33
+ }>;
34
+ getById(id: string): Promise<BlogTagDetail>;
35
+ create(input: CreateBlogTagRequest): Promise<BlogTagDetail>;
36
+ patch(id: string, input: PatchBlogTagRequest): Promise<BlogTagDetail>;
37
+ softDelete(id: string, version: number | undefined): Promise<void>;
38
+ getInboundReferences(id: string): Promise<BlogTagInboundReferencesResponse>;
39
+ private findRow;
40
+ private assertCodeAvailable;
41
+ private assertVersion;
42
+ private toDetail;
43
+ private toIso;
44
+ }
45
+ //# sourceMappingURL=blog-tag-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-tag-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/blog-tag-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,gCAAgC,EACrC,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAYxD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;gBADN,SAAS,EAAE,MAAM,aAAa,EAC9B,KAAK,CAAC,EAAE,gBAAgB,YAAA;IAGrC,IAAI,CAAC,OAAO,EAAE;QAClB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAC9B,GAAG,OAAO,CAAC;QACV,IAAI,EAAE,aAAa,EAAE,CAAC;QACtB,UAAU,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC;KACvF,CAAC;IAsCI,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAQ3C,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAyB3D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,CAAC;IA0CrE,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA2ClE,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gCAAgC,CAAC;YA6BnE,OAAO;YAQP,mBAAmB;IAsBjC,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,KAAK;CAGd"}