@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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/admin/api/blog-client.d.ts +70 -0
- package/dist/admin/api/blog-client.d.ts.map +1 -0
- package/dist/admin/api/blog-client.js +137 -0
- package/dist/admin/api/blog-client.js.map +1 -0
- package/dist/admin/components/CategoryTreeNode.d.ts +25 -0
- package/dist/admin/components/CategoryTreeNode.d.ts.map +1 -0
- package/dist/admin/components/CategoryTreeNode.js +14 -0
- package/dist/admin/components/CategoryTreeNode.js.map +1 -0
- package/dist/admin/components/PostStatusBadge.d.ts +7 -0
- package/dist/admin/components/PostStatusBadge.d.ts.map +1 -0
- package/dist/admin/components/PostStatusBadge.js +16 -0
- package/dist/admin/components/PostStatusBadge.js.map +1 -0
- package/dist/admin/components/RelatedPostsPicker.d.ts +11 -0
- package/dist/admin/components/RelatedPostsPicker.d.ts.map +1 -0
- package/dist/admin/components/RelatedPostsPicker.js +77 -0
- package/dist/admin/components/RelatedPostsPicker.js.map +1 -0
- package/dist/admin/components/RelatedProductsPicker.d.ts +9 -0
- package/dist/admin/components/RelatedProductsPicker.d.ts.map +1 -0
- package/dist/admin/components/RelatedProductsPicker.js +66 -0
- package/dist/admin/components/RelatedProductsPicker.js.map +1 -0
- package/dist/admin/components/TagPicker.d.ts +9 -0
- package/dist/admin/components/TagPicker.d.ts.map +1 -0
- package/dist/admin/components/TagPicker.js +66 -0
- package/dist/admin/components/TagPicker.js.map +1 -0
- package/dist/admin/index.d.ts +33 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +114 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/BlogCategoryEditor.d.ts +9 -0
- package/dist/admin/pages/BlogCategoryEditor.d.ts.map +1 -0
- package/dist/admin/pages/BlogCategoryEditor.js +196 -0
- package/dist/admin/pages/BlogCategoryEditor.js.map +1 -0
- package/dist/admin/pages/BlogCategoryTreePage.d.ts +9 -0
- package/dist/admin/pages/BlogCategoryTreePage.d.ts.map +1 -0
- package/dist/admin/pages/BlogCategoryTreePage.js +122 -0
- package/dist/admin/pages/BlogCategoryTreePage.js.map +1 -0
- package/dist/admin/pages/BlogPostEditor.d.ts +9 -0
- package/dist/admin/pages/BlogPostEditor.d.ts.map +1 -0
- package/dist/admin/pages/BlogPostEditor.js +314 -0
- package/dist/admin/pages/BlogPostEditor.js.map +1 -0
- package/dist/admin/pages/BlogPostListPage.d.ts +9 -0
- package/dist/admin/pages/BlogPostListPage.d.ts.map +1 -0
- package/dist/admin/pages/BlogPostListPage.js +75 -0
- package/dist/admin/pages/BlogPostListPage.js.map +1 -0
- package/dist/admin/pages/BlogTagListPage.d.ts +9 -0
- package/dist/admin/pages/BlogTagListPage.d.ts.map +1 -0
- package/dist/admin/pages/BlogTagListPage.js +145 -0
- package/dist/admin/pages/BlogTagListPage.js.map +1 -0
- package/dist/backend/entities/blog-category-language.entity.d.ts +11 -0
- package/dist/backend/entities/blog-category-language.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-category-language.entity.js +35 -0
- package/dist/backend/entities/blog-category-language.entity.js.map +1 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.d.ts +17 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.js +48 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/blog-category.entity.d.ts +32 -0
- package/dist/backend/entities/blog-category.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-category.entity.js +113 -0
- package/dist/backend/entities/blog-category.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-category.entity.d.ts +10 -0
- package/dist/backend/entities/blog-post-category.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-category.entity.js +34 -0
- package/dist/backend/entities/blog-post-category.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-language.entity.d.ts +9 -0
- package/dist/backend/entities/blog-post-language.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-language.entity.js +33 -0
- package/dist/backend/entities/blog-post-language.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-related-post.entity.d.ts +15 -0
- package/dist/backend/entities/blog-post-related-post.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-related-post.entity.js +42 -0
- package/dist/backend/entities/blog-post-related-post.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-related-product.entity.d.ts +14 -0
- package/dist/backend/entities/blog-post-related-product.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-related-product.entity.js +41 -0
- package/dist/backend/entities/blog-post-related-product.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.d.ts +16 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.js +47 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-tag.entity.d.ts +14 -0
- package/dist/backend/entities/blog-post-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-tag.entity.js +41 -0
- package/dist/backend/entities/blog-post-tag.entity.js.map +1 -0
- package/dist/backend/entities/blog-post.entity.d.ts +31 -0
- package/dist/backend/entities/blog-post.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post.entity.js +106 -0
- package/dist/backend/entities/blog-post.entity.js.map +1 -0
- package/dist/backend/entities/blog-tag.entity.d.ts +18 -0
- package/dist/backend/entities/blog-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-tag.entity.js +66 -0
- package/dist/backend/entities/blog-tag.entity.js.map +1 -0
- package/dist/backend/index.d.ts +100 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +182 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +12 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +148 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +6 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +72 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/blog-asset-references.d.ts +4 -0
- package/dist/backend/services/blog-asset-references.d.ts.map +1 -0
- package/dist/backend/services/blog-asset-references.js +79 -0
- package/dist/backend/services/blog-asset-references.js.map +1 -0
- package/dist/backend/services/blog-cache.d.ts +55 -0
- package/dist/backend/services/blog-cache.d.ts.map +1 -0
- package/dist/backend/services/blog-cache.js +111 -0
- package/dist/backend/services/blog-cache.js.map +1 -0
- package/dist/backend/services/blog-category-service.d.ts +53 -0
- package/dist/backend/services/blog-category-service.d.ts.map +1 -0
- package/dist/backend/services/blog-category-service.js +446 -0
- package/dist/backend/services/blog-category-service.js.map +1 -0
- package/dist/backend/services/blog-language-reference.d.ts +18 -0
- package/dist/backend/services/blog-language-reference.d.ts.map +1 -0
- package/dist/backend/services/blog-language-reference.js +43 -0
- package/dist/backend/services/blog-language-reference.js.map +1 -0
- package/dist/backend/services/blog-post-service.d.ts +79 -0
- package/dist/backend/services/blog-post-service.d.ts.map +1 -0
- package/dist/backend/services/blog-post-service.js +539 -0
- package/dist/backend/services/blog-post-service.js.map +1 -0
- package/dist/backend/services/blog-settings-resolver.d.ts +36 -0
- package/dist/backend/services/blog-settings-resolver.d.ts.map +1 -0
- package/dist/backend/services/blog-settings-resolver.js +68 -0
- package/dist/backend/services/blog-settings-resolver.js.map +1 -0
- package/dist/backend/services/blog-slug-collision.d.ts +35 -0
- package/dist/backend/services/blog-slug-collision.d.ts.map +1 -0
- package/dist/backend/services/blog-slug-collision.js +84 -0
- package/dist/backend/services/blog-slug-collision.js.map +1 -0
- package/dist/backend/services/blog-storefront-resolver.d.ts +107 -0
- package/dist/backend/services/blog-storefront-resolver.d.ts.map +1 -0
- package/dist/backend/services/blog-storefront-resolver.js +492 -0
- package/dist/backend/services/blog-storefront-resolver.js.map +1 -0
- package/dist/backend/services/blog-tag-service.d.ts +45 -0
- package/dist/backend/services/blog-tag-service.d.ts.map +1 -0
- package/dist/backend/services/blog-tag-service.js +198 -0
- package/dist/backend/services/blog-tag-service.js.map +1 -0
- package/dist/backend/services/seed-default-category.d.ts +25 -0
- package/dist/backend/services/seed-default-category.d.ts.map +1 -0
- package/dist/backend/services/seed-default-category.js +48 -0
- package/dist/backend/services/seed-default-category.js.map +1 -0
- package/dist/backend/services/seed-roles.d.ts +41 -0
- package/dist/backend/services/seed-roles.d.ts.map +1 -0
- package/dist/backend/services/seed-roles.js +98 -0
- package/dist/backend/services/seed-roles.js.map +1 -0
- package/dist/manifest.d.ts +226 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +236 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260506T081055_blog_init.d.ts +33 -0
- package/dist/migrations/20260506T081055_blog_init.d.ts.map +1 -0
- package/dist/migrations/20260506T081055_blog_init.js +260 -0
- package/dist/migrations/20260506T081055_blog_init.js.map +1 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts +6 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts.map +1 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.js +58 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.js.map +1 -0
- package/dist/migrations/index.d.ts +34 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +37 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/blog/index.md +199 -0
- package/i18n/en.json +166 -0
- package/i18n/pl.json +166 -0
- package/package.json +108 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Blog module — feature 016 / data-model.md.
|
|
4
|
+
*
|
|
5
|
+
* Schema changes (one atomic migration):
|
|
6
|
+
* - new tables: blog_categories + blog_category_sales_channels +
|
|
7
|
+
* blog_category_languages + blog_posts + blog_post_sales_channels +
|
|
8
|
+
* blog_post_languages + blog_post_categories + blog_post_tags +
|
|
9
|
+
* blog_post_related_posts + blog_post_related_products + blog_tags.
|
|
10
|
+
* - GIN index on blog_posts.content (jsonb_path_ops) so the asset-reference
|
|
11
|
+
* scan stays fast (R13).
|
|
12
|
+
* - btree index on blog_posts(status, published_at DESC) drives the
|
|
13
|
+
* storefront's "newest first" listing.
|
|
14
|
+
* - btree index on blog_categories(parent_id, position) drives the
|
|
15
|
+
* per-tree fetch.
|
|
16
|
+
* - PARTIAL unique indexes on blog_post_sales_channels(sales_channel_id,
|
|
17
|
+
* slug) WHERE deleted_at IS NULL and the analogous one on
|
|
18
|
+
* blog_category_sales_channels — defensive guards for the cross-table
|
|
19
|
+
* uniqueness contract documented in research.md § R3 (the cross-table
|
|
20
|
+
* check itself runs at the service layer with a pg_advisory_xact_lock).
|
|
21
|
+
* - PARTIAL unique on blog_tags(code) WHERE deleted_at IS NULL — global
|
|
22
|
+
* tag-code uniqueness (R14).
|
|
23
|
+
*
|
|
24
|
+
* No seed rows; the seeded `Default` Category and the two seeded admin
|
|
25
|
+
* roles are inserted by runtime reconcilers in `services/seed-default-
|
|
26
|
+
* category.ts` and `services/seed-roles.ts` so admin edits across deploys
|
|
27
|
+
* stay safe (R8 + R11).
|
|
28
|
+
*/
|
|
29
|
+
export class Migration20260506T081055BlogInit extends Migration {
|
|
30
|
+
async up() {
|
|
31
|
+
// ────────────────────────────────────────────────────────────────────
|
|
32
|
+
// 1) blog_categories — adjacency-list tree
|
|
33
|
+
// ────────────────────────────────────────────────────────────────────
|
|
34
|
+
this.addSql(`
|
|
35
|
+
create table "blog_categories" (
|
|
36
|
+
"id" uuid not null,
|
|
37
|
+
"parent_id" uuid null,
|
|
38
|
+
"position" int not null default 0,
|
|
39
|
+
"name" jsonb not null default '{}'::jsonb,
|
|
40
|
+
"slug" varchar(160) not null,
|
|
41
|
+
"enabled" boolean not null default true,
|
|
42
|
+
"description" jsonb null,
|
|
43
|
+
"main_image_asset_id" uuid null,
|
|
44
|
+
"meta_title" jsonb null,
|
|
45
|
+
"meta_description" jsonb null,
|
|
46
|
+
"meta_keywords" jsonb null,
|
|
47
|
+
"is_system" boolean not null default false,
|
|
48
|
+
"version" int not null default 1,
|
|
49
|
+
"created_at" timestamptz not null,
|
|
50
|
+
"updated_at" timestamptz not null,
|
|
51
|
+
"deleted_at" timestamptz null,
|
|
52
|
+
constraint "blog_categories_pkey" primary key ("id"),
|
|
53
|
+
constraint "blog_categories_parent_fk" foreign key ("parent_id")
|
|
54
|
+
references "blog_categories" ("id") on delete no action,
|
|
55
|
+
constraint "blog_categories_main_image_fk" foreign key ("main_image_asset_id")
|
|
56
|
+
references "assets" ("id") on delete no action
|
|
57
|
+
);
|
|
58
|
+
`);
|
|
59
|
+
this.addSql('create index "idx_blog_categories_parent_position" on "blog_categories" ("parent_id", "position");');
|
|
60
|
+
this.addSql('create index "idx_blog_categories_main_image_asset" on "blog_categories" ("main_image_asset_id");');
|
|
61
|
+
this.addSql('create index "idx_blog_categories_is_system" on "blog_categories" ("is_system");');
|
|
62
|
+
this.addSql('create index "idx_blog_categories_deleted_at" on "blog_categories" ("deleted_at");');
|
|
63
|
+
// ────────────────────────────────────────────────────────────────────
|
|
64
|
+
// 2) blog_category_sales_channels (M2M + denormalised slug + deleted_at)
|
|
65
|
+
// ────────────────────────────────────────────────────────────────────
|
|
66
|
+
this.addSql(`
|
|
67
|
+
create table "blog_category_sales_channels" (
|
|
68
|
+
"blog_category_id" uuid not null,
|
|
69
|
+
"sales_channel_id" uuid not null,
|
|
70
|
+
"slug" varchar(160) not null,
|
|
71
|
+
"deleted_at" timestamptz null,
|
|
72
|
+
constraint "blog_category_sales_channels_pkey"
|
|
73
|
+
primary key ("blog_category_id", "sales_channel_id"),
|
|
74
|
+
constraint "blog_category_sales_channels_category_fk" foreign key ("blog_category_id")
|
|
75
|
+
references "blog_categories" ("id") on delete cascade,
|
|
76
|
+
constraint "blog_category_sales_channels_channel_fk" foreign key ("sales_channel_id")
|
|
77
|
+
references "sales_channels" ("id") on delete cascade
|
|
78
|
+
);
|
|
79
|
+
`);
|
|
80
|
+
this.addSql(`create unique index "idx_blog_categories_slug_per_channel_uniq"
|
|
81
|
+
on "blog_category_sales_channels" ("sales_channel_id", "slug")
|
|
82
|
+
where "deleted_at" is null;`);
|
|
83
|
+
// ────────────────────────────────────────────────────────────────────
|
|
84
|
+
// 3) blog_category_languages (M2M)
|
|
85
|
+
// ────────────────────────────────────────────────────────────────────
|
|
86
|
+
this.addSql(`
|
|
87
|
+
create table "blog_category_languages" (
|
|
88
|
+
"blog_category_id" uuid not null,
|
|
89
|
+
"language" varchar(8) not null,
|
|
90
|
+
constraint "blog_category_languages_pkey"
|
|
91
|
+
primary key ("blog_category_id", "language"),
|
|
92
|
+
constraint "blog_category_languages_category_fk" foreign key ("blog_category_id")
|
|
93
|
+
references "blog_categories" ("id") on delete cascade
|
|
94
|
+
);
|
|
95
|
+
`);
|
|
96
|
+
// ────────────────────────────────────────────────────────────────────
|
|
97
|
+
// 4) blog_posts
|
|
98
|
+
// ────────────────────────────────────────────────────────────────────
|
|
99
|
+
this.addSql(`
|
|
100
|
+
create table "blog_posts" (
|
|
101
|
+
"id" uuid not null,
|
|
102
|
+
"name" jsonb not null default '{}'::jsonb,
|
|
103
|
+
"slug" varchar(160) not null,
|
|
104
|
+
"active" boolean not null default true,
|
|
105
|
+
"status" varchar(16) not null default 'draft',
|
|
106
|
+
"published_at" timestamptz null,
|
|
107
|
+
"description" text null,
|
|
108
|
+
"meta_title" jsonb null,
|
|
109
|
+
"meta_description" jsonb null,
|
|
110
|
+
"meta_keywords" jsonb null,
|
|
111
|
+
"content" jsonb not null default '{}'::jsonb,
|
|
112
|
+
"version" int not null default 1,
|
|
113
|
+
"created_at" timestamptz not null,
|
|
114
|
+
"updated_at" timestamptz not null,
|
|
115
|
+
"deleted_at" timestamptz null,
|
|
116
|
+
constraint "blog_posts_pkey" primary key ("id")
|
|
117
|
+
);
|
|
118
|
+
`);
|
|
119
|
+
this.addSql(`create index "idx_blog_posts_status_published_at"
|
|
120
|
+
on "blog_posts" ("status", "published_at" desc);`);
|
|
121
|
+
this.addSql('create index "idx_blog_posts_deleted_at" on "blog_posts" ("deleted_at");');
|
|
122
|
+
this.addSql(`create index "idx_blog_posts_content_refs"
|
|
123
|
+
on "blog_posts" using gin ("content" jsonb_path_ops);`);
|
|
124
|
+
// ────────────────────────────────────────────────────────────────────
|
|
125
|
+
// 5) blog_post_sales_channels (M2M + denormalised slug + deleted_at)
|
|
126
|
+
// ────────────────────────────────────────────────────────────────────
|
|
127
|
+
this.addSql(`
|
|
128
|
+
create table "blog_post_sales_channels" (
|
|
129
|
+
"blog_post_id" uuid not null,
|
|
130
|
+
"sales_channel_id" uuid not null,
|
|
131
|
+
"slug" varchar(160) not null,
|
|
132
|
+
"deleted_at" timestamptz null,
|
|
133
|
+
constraint "blog_post_sales_channels_pkey"
|
|
134
|
+
primary key ("blog_post_id", "sales_channel_id"),
|
|
135
|
+
constraint "blog_post_sales_channels_post_fk" foreign key ("blog_post_id")
|
|
136
|
+
references "blog_posts" ("id") on delete cascade,
|
|
137
|
+
constraint "blog_post_sales_channels_channel_fk" foreign key ("sales_channel_id")
|
|
138
|
+
references "sales_channels" ("id") on delete cascade
|
|
139
|
+
);
|
|
140
|
+
`);
|
|
141
|
+
this.addSql(`create unique index "idx_blog_posts_slug_per_channel_uniq"
|
|
142
|
+
on "blog_post_sales_channels" ("sales_channel_id", "slug")
|
|
143
|
+
where "deleted_at" is null;`);
|
|
144
|
+
// ────────────────────────────────────────────────────────────────────
|
|
145
|
+
// 6) blog_post_languages (M2M)
|
|
146
|
+
// ────────────────────────────────────────────────────────────────────
|
|
147
|
+
this.addSql(`
|
|
148
|
+
create table "blog_post_languages" (
|
|
149
|
+
"blog_post_id" uuid not null,
|
|
150
|
+
"language" varchar(8) not null,
|
|
151
|
+
constraint "blog_post_languages_pkey"
|
|
152
|
+
primary key ("blog_post_id", "language"),
|
|
153
|
+
constraint "blog_post_languages_post_fk" foreign key ("blog_post_id")
|
|
154
|
+
references "blog_posts" ("id") on delete cascade
|
|
155
|
+
);
|
|
156
|
+
`);
|
|
157
|
+
// ────────────────────────────────────────────────────────────────────
|
|
158
|
+
// 7) blog_post_categories (M2M)
|
|
159
|
+
// ────────────────────────────────────────────────────────────────────
|
|
160
|
+
this.addSql(`
|
|
161
|
+
create table "blog_post_categories" (
|
|
162
|
+
"blog_post_id" uuid not null,
|
|
163
|
+
"blog_category_id" uuid not null,
|
|
164
|
+
constraint "blog_post_categories_pkey"
|
|
165
|
+
primary key ("blog_post_id", "blog_category_id"),
|
|
166
|
+
constraint "blog_post_categories_post_fk" foreign key ("blog_post_id")
|
|
167
|
+
references "blog_posts" ("id") on delete cascade,
|
|
168
|
+
constraint "blog_post_categories_category_fk" foreign key ("blog_category_id")
|
|
169
|
+
references "blog_categories" ("id") on delete no action
|
|
170
|
+
);
|
|
171
|
+
`);
|
|
172
|
+
this.addSql(`create index "idx_blog_post_categories_category_id"
|
|
173
|
+
on "blog_post_categories" ("blog_category_id");`);
|
|
174
|
+
// ────────────────────────────────────────────────────────────────────
|
|
175
|
+
// 8) blog_tags
|
|
176
|
+
// ────────────────────────────────────────────────────────────────────
|
|
177
|
+
this.addSql(`
|
|
178
|
+
create table "blog_tags" (
|
|
179
|
+
"id" uuid not null,
|
|
180
|
+
"name" jsonb not null default '{}'::jsonb,
|
|
181
|
+
"description" jsonb null,
|
|
182
|
+
"code" varchar(64) not null,
|
|
183
|
+
"version" int not null default 1,
|
|
184
|
+
"created_at" timestamptz not null,
|
|
185
|
+
"updated_at" timestamptz not null,
|
|
186
|
+
"deleted_at" timestamptz null,
|
|
187
|
+
constraint "blog_tags_pkey" primary key ("id")
|
|
188
|
+
);
|
|
189
|
+
`);
|
|
190
|
+
this.addSql(`create unique index "idx_blog_tags_code_uniq"
|
|
191
|
+
on "blog_tags" ("code")
|
|
192
|
+
where "deleted_at" is null;`);
|
|
193
|
+
// ────────────────────────────────────────────────────────────────────
|
|
194
|
+
// 9) blog_post_tags (M2M, ordered)
|
|
195
|
+
// ────────────────────────────────────────────────────────────────────
|
|
196
|
+
this.addSql(`
|
|
197
|
+
create table "blog_post_tags" (
|
|
198
|
+
"blog_post_id" uuid not null,
|
|
199
|
+
"blog_tag_id" uuid not null,
|
|
200
|
+
"position" int not null default 0,
|
|
201
|
+
constraint "blog_post_tags_pkey"
|
|
202
|
+
primary key ("blog_post_id", "blog_tag_id"),
|
|
203
|
+
constraint "blog_post_tags_post_fk" foreign key ("blog_post_id")
|
|
204
|
+
references "blog_posts" ("id") on delete cascade,
|
|
205
|
+
constraint "blog_post_tags_tag_fk" foreign key ("blog_tag_id")
|
|
206
|
+
references "blog_tags" ("id") on delete no action
|
|
207
|
+
);
|
|
208
|
+
`);
|
|
209
|
+
this.addSql(`create index "idx_blog_post_tags_tag_id"
|
|
210
|
+
on "blog_post_tags" ("blog_tag_id");`);
|
|
211
|
+
// ────────────────────────────────────────────────────────────────────
|
|
212
|
+
// 10) blog_post_related_posts (self-join, ordered)
|
|
213
|
+
// ────────────────────────────────────────────────────────────────────
|
|
214
|
+
this.addSql(`
|
|
215
|
+
create table "blog_post_related_posts" (
|
|
216
|
+
"parent_post_id" uuid not null,
|
|
217
|
+
"related_post_id" uuid not null,
|
|
218
|
+
"position" int not null default 0,
|
|
219
|
+
constraint "blog_post_related_posts_pkey"
|
|
220
|
+
primary key ("parent_post_id", "related_post_id"),
|
|
221
|
+
constraint "blog_post_related_posts_parent_fk" foreign key ("parent_post_id")
|
|
222
|
+
references "blog_posts" ("id") on delete cascade,
|
|
223
|
+
constraint "blog_post_related_posts_related_fk" foreign key ("related_post_id")
|
|
224
|
+
references "blog_posts" ("id") on delete no action
|
|
225
|
+
);
|
|
226
|
+
`);
|
|
227
|
+
this.addSql(`create index "idx_blog_post_related_posts_related"
|
|
228
|
+
on "blog_post_related_posts" ("related_post_id");`);
|
|
229
|
+
// ────────────────────────────────────────────────────────────────────
|
|
230
|
+
// 11) blog_post_related_products (M2M to products, ordered)
|
|
231
|
+
// ────────────────────────────────────────────────────────────────────
|
|
232
|
+
this.addSql(`
|
|
233
|
+
create table "blog_post_related_products" (
|
|
234
|
+
"blog_post_id" uuid not null,
|
|
235
|
+
"product_id" uuid not null,
|
|
236
|
+
"position" int not null default 0,
|
|
237
|
+
constraint "blog_post_related_products_pkey"
|
|
238
|
+
primary key ("blog_post_id", "product_id"),
|
|
239
|
+
constraint "blog_post_related_products_post_fk" foreign key ("blog_post_id")
|
|
240
|
+
references "blog_posts" ("id") on delete cascade,
|
|
241
|
+
constraint "blog_post_related_products_product_fk" foreign key ("product_id")
|
|
242
|
+
references "products" ("id") on delete no action
|
|
243
|
+
);
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
async down() {
|
|
247
|
+
this.addSql('drop table if exists "blog_post_related_products" cascade;');
|
|
248
|
+
this.addSql('drop table if exists "blog_post_related_posts" cascade;');
|
|
249
|
+
this.addSql('drop table if exists "blog_post_tags" cascade;');
|
|
250
|
+
this.addSql('drop table if exists "blog_tags" cascade;');
|
|
251
|
+
this.addSql('drop table if exists "blog_post_categories" cascade;');
|
|
252
|
+
this.addSql('drop table if exists "blog_post_languages" cascade;');
|
|
253
|
+
this.addSql('drop table if exists "blog_post_sales_channels" cascade;');
|
|
254
|
+
this.addSql('drop table if exists "blog_posts" cascade;');
|
|
255
|
+
this.addSql('drop table if exists "blog_category_languages" cascade;');
|
|
256
|
+
this.addSql('drop table if exists "blog_category_sales_channels" cascade;');
|
|
257
|
+
this.addSql('drop table if exists "blog_categories" cascade;');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=20260506T081055_blog_init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260506T081055_blog_init.js","sourceRoot":"","sources":["../../src/migrations/20260506T081055_blog_init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260903T101744_blog_namespace_block_names.d.ts","sourceRoot":"","sources":["../../src/migrations/20260903T101744_blog_namespace_block_names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAmDlD,qBAAa,+CAAgD,SAAQ,SAAS;IAC7D,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAOnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAMrC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
import { FROZEN_BLOCK_RENAMES, FROZEN_BLOCK_RENAMES_INVERSE, applyRenameFunctionSql, createRenameFunctionSql, dropRenameFunctionSql, } from '@endora-commerce/page-builder-core/migration';
|
|
3
|
+
/**
|
|
4
|
+
* Namespace the Page Builder block names stored in `blog`'s 2 `jsonb` columns
|
|
5
|
+
* — feature 096, T405 (`contracts/block-name-migration.md`).
|
|
6
|
+
*
|
|
7
|
+
* **The rewrite is structural, never textual.** Twelve of the 74 renamed names
|
|
8
|
+
* are ordinary English words that occur throughout shop content — `Row`,
|
|
9
|
+
* `Text`, `Image`, `Map`, `Button` among them — so a text substitution over the
|
|
10
|
+
* column would corrupt a `RawHtml` block's markup and every `alt` attribute in
|
|
11
|
+
* the shop. `pg_temp.rename_block_names_blog` descends the document and replaces the value of a
|
|
12
|
+
* `type` property in a **node** position and nothing else; the walk is
|
|
13
|
+
* generated from `FROZEN_BLOCK_RENAMES` by
|
|
14
|
+
* `@endora-commerce/page-builder-core/migration`, so five migrations share one
|
|
15
|
+
* definition of what a node is.
|
|
16
|
+
*
|
|
17
|
+
* **This migration belongs to `blog` because `blog` owns these tables**,
|
|
18
|
+
* not because it owns the new names (`contracts/block-name-migration.md` §2).
|
|
19
|
+
* Some of the names it writes belong to other modules; that creates no
|
|
20
|
+
* obligation on them and **no new manifest `dependencies` edge**, because a
|
|
21
|
+
* block name is a string value inside a JSONB document — not a foreign key and
|
|
22
|
+
* not a table identifier.
|
|
23
|
+
*
|
|
24
|
+
* **It cannot fail on its input.** A name the map does not hold is left
|
|
25
|
+
* byte-identical: an already-namespaced one and an unrecognised one alike,
|
|
26
|
+
* because the map's domain is bare and its codomain is dotted, so the two sets
|
|
27
|
+
* are disjoint. That is also what makes a second run rewrite nothing — FR-013
|
|
28
|
+
* is a property of the map rather than an outcome a branch has to remember to
|
|
29
|
+
* produce, and no dot test appears in this SQL.
|
|
30
|
+
*
|
|
31
|
+
* `down()` applies the inverse over the identical walk and is **exact for the
|
|
32
|
+
* frozen set**. It is partial by construction: a name with no pre-migration
|
|
33
|
+
* form — a block authored after the upgrade, a third party's `acme.Banner` —
|
|
34
|
+
* has nothing to return to and is left alone. That is correct, and it is why
|
|
35
|
+
* the operator pre-flight (§6) is a backup rather than a `down()`.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Named per module: the five rename migrations may run on one pooled session,
|
|
39
|
+
* and a second `create function` over the same name fails. Each drops its own
|
|
40
|
+
* when it is done, and a rollback removes it with everything else — `pg_temp`
|
|
41
|
+
* DDL is transactional.
|
|
42
|
+
*/
|
|
43
|
+
const FN = 'rename_block_names_blog';
|
|
44
|
+
export class Migration20260903T101744BlogNamespaceBlockNames extends Migration {
|
|
45
|
+
async up() {
|
|
46
|
+
this.addSql(createRenameFunctionSql(FN, FROZEN_BLOCK_RENAMES));
|
|
47
|
+
this.addSql(applyRenameFunctionSql(FN, 'blog_posts', 'content'));
|
|
48
|
+
this.addSql(applyRenameFunctionSql(FN, 'blog_categories', 'description'));
|
|
49
|
+
this.addSql(dropRenameFunctionSql(FN));
|
|
50
|
+
}
|
|
51
|
+
async down() {
|
|
52
|
+
this.addSql(createRenameFunctionSql(FN, FROZEN_BLOCK_RENAMES_INVERSE));
|
|
53
|
+
this.addSql(applyRenameFunctionSql(FN, 'blog_posts', 'content'));
|
|
54
|
+
this.addSql(applyRenameFunctionSql(FN, 'blog_categories', 'description'));
|
|
55
|
+
this.addSql(dropRenameFunctionSql(FN));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=20260903T101744_blog_namespace_block_names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260903T101744_blog_namespace_block_names.js","sourceRoot":"","sources":["../../src/migrations/20260903T101744_blog_namespace_block_names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,8CAA8C,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH;;;;;GAKG;AACH,MAAM,EAAE,GAAG,yBAAyB,CAAC;AAErC,MAAM,OAAO,+CAAgD,SAAQ,SAAS;IACnE,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC;QACjE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,iBAAiB,EAAE,aAAa,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC;QACjE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,iBAAiB,EAAE,aAAa,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;CACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
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 — *"the `./migrations` export of
|
|
8
|
+
* @endora-commerce/mod-blog exports no 'migrations' array"*. Until D-168's
|
|
9
|
+
* merge request this file published only `export *`, so an installed `blog`
|
|
10
|
+
* did not merely lose its schema, it stopped the platform from booting; the
|
|
11
|
+
* defect was invisible because a workspace member is not an installed package
|
|
12
|
+
* and the committed host registry names the class directly. That is the same
|
|
13
|
+
* silence D-168 found on `./backend`, one subpath along.
|
|
14
|
+
*
|
|
15
|
+
* The **named** export stays, and the asymmetry with `./backend` is deliberate.
|
|
16
|
+
* `db/migrations-registry.generated.ts` imports each class by name from this
|
|
17
|
+
* specifier and hands it to `migration('blog', …)`, and a migration class name
|
|
18
|
+
* is contract in a way an entity class name is not: `mikro_orm_migrations`
|
|
19
|
+
* persists it, so it is a string every already-migrated database holds. It also
|
|
20
|
+
* carries none of the hazard D-168 removes — no module has a reason to name
|
|
21
|
+
* another module's migration, and doing so buys nothing an entity import buys.
|
|
22
|
+
*
|
|
23
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
24
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
25
|
+
* a query against a table nobody created. The composer refuses a migration file
|
|
26
|
+
* no declared subpath covers for exactly that reason, but it cannot see whether
|
|
27
|
+
* the barrel behind the subpath actually carries the class — that is this
|
|
28
|
+
* file's job.
|
|
29
|
+
*/
|
|
30
|
+
import { Migration20260506T081055BlogInit } from './20260506T081055_blog_init.js';
|
|
31
|
+
import { Migration20260903T101744BlogNamespaceBlockNames } from './20260903T101744_blog_namespace_block_names.js';
|
|
32
|
+
export declare const migrations: (typeof Migration20260506T081055BlogInit)[];
|
|
33
|
+
export { Migration20260506T081055BlogInit, Migration20260903T101744BlogNamespaceBlockNames, };
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,gCAAgC,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,+CAA+C,EAAE,MAAM,iDAAiD,CAAC;AAElH,eAAO,MAAM,UAAU,6CAGtB,CAAC;AAEF,OAAO,EACL,gCAAgC,EAChC,+CAA+C,GAChD,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 — *"the `./migrations` export of
|
|
8
|
+
* @endora-commerce/mod-blog exports no 'migrations' array"*. Until D-168's
|
|
9
|
+
* merge request this file published only `export *`, so an installed `blog`
|
|
10
|
+
* did not merely lose its schema, it stopped the platform from booting; the
|
|
11
|
+
* defect was invisible because a workspace member is not an installed package
|
|
12
|
+
* and the committed host registry names the class directly. That is the same
|
|
13
|
+
* silence D-168 found on `./backend`, one subpath along.
|
|
14
|
+
*
|
|
15
|
+
* The **named** export stays, and the asymmetry with `./backend` is deliberate.
|
|
16
|
+
* `db/migrations-registry.generated.ts` imports each class by name from this
|
|
17
|
+
* specifier and hands it to `migration('blog', …)`, and a migration class name
|
|
18
|
+
* is contract in a way an entity class name is not: `mikro_orm_migrations`
|
|
19
|
+
* persists it, so it is a string every already-migrated database holds. It also
|
|
20
|
+
* carries none of the hazard D-168 removes — no module has a reason to name
|
|
21
|
+
* another module's migration, and doing so buys nothing an entity import buys.
|
|
22
|
+
*
|
|
23
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
24
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
25
|
+
* a query against a table nobody created. The composer refuses a migration file
|
|
26
|
+
* no declared subpath covers for exactly that reason, but it cannot see whether
|
|
27
|
+
* the barrel behind the subpath actually carries the class — that is this
|
|
28
|
+
* file's job.
|
|
29
|
+
*/
|
|
30
|
+
import { Migration20260506T081055BlogInit } from './20260506T081055_blog_init.js';
|
|
31
|
+
import { Migration20260903T101744BlogNamespaceBlockNames } from './20260903T101744_blog_namespace_block_names.js';
|
|
32
|
+
export const migrations = [
|
|
33
|
+
Migration20260506T081055BlogInit,
|
|
34
|
+
Migration20260903T101744BlogNamespaceBlockNames,
|
|
35
|
+
];
|
|
36
|
+
export { Migration20260506T081055BlogInit, Migration20260903T101744BlogNamespaceBlockNames, };
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,gCAAgC,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,+CAA+C,EAAE,MAAM,iDAAiD,CAAC;AAElH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,gCAAgC;IAChC,+CAA+C;CAChD,CAAC;AAEF,OAAO,EACL,gCAAgC,EAChC,+CAA+C,GAChD,CAAC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Blog
|
|
3
|
+
sidebar_position: 16
|
|
4
|
+
description: Editorial Posts with Page Builder bodies, taxonomy (Categories + Tags), and storefront feeds
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Blog
|
|
8
|
+
|
|
9
|
+
The Blog module owns the storefront's editorial blog surface — Posts,
|
|
10
|
+
Categories, and Tags — composed in the same drag-and-drop **Page
|
|
11
|
+
Builder** that ships with the [CMS module](../cms/index.md). Posts
|
|
12
|
+
inherit the CMS Page's field shape (slug, status lifecycle, per-language
|
|
13
|
+
content, SEO meta) and add blog-specific affordances: a parent Category
|
|
14
|
+
tree, free-form Tags, ordered Related Posts, and ordered Related
|
|
15
|
+
Products.
|
|
16
|
+
|
|
17
|
+
| Entity | Identifier | Lifecycle | Embedded by |
|
|
18
|
+
| -------------- | ---------------------- | ------------------------------------ | -------------------------------------------------------- |
|
|
19
|
+
| **Post** | `slug` (per channel) | `draft → published → archived` | URL `<blog-prefix>/<slug>` on the storefront |
|
|
20
|
+
| **Category** | `slug` (per channel) | `enabled` flag, tree-structured | URL `<blog-prefix>/<slug>` (siblings of Posts) and tiles |
|
|
21
|
+
| **Tag** | `code` (global) | block-on-delete | URL `<blog-prefix>/tag/<code>` and per-Post chip strip |
|
|
22
|
+
|
|
23
|
+
## Entities and the reference graph
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
blog_categories ── parent_id ──┐ self-FK tree (NO ACTION)
|
|
27
|
+
│ ▼
|
|
28
|
+
├─── blog_post_categories ───────► blog_posts
|
|
29
|
+
│ │
|
|
30
|
+
│ ┌────────┼─────────┐
|
|
31
|
+
▼ ▼ ▼ ▼
|
|
32
|
+
blog_post_categories blog_post_tags blog_post_related_posts (self-FK)
|
|
33
|
+
│ │
|
|
34
|
+
▼ │
|
|
35
|
+
blog_tags │
|
|
36
|
+
▼
|
|
37
|
+
blog_post_related_products → catalog.products
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The schema lives entirely in `037_blog_init.ts` (eleven new tables, all
|
|
41
|
+
prefixed `blog_*`) and never touches existing tables.
|
|
42
|
+
|
|
43
|
+
## URLs and routing
|
|
44
|
+
|
|
45
|
+
The storefront mounts a single Next.js catch-all under
|
|
46
|
+
`storefront/app/(blog)/[[...slug]]/page.tsx`. The matcher reads the
|
|
47
|
+
channel-resolved `blog.url_prefix` from the [Settings module](../settings/index.md)
|
|
48
|
+
and dispatches:
|
|
49
|
+
|
|
50
|
+
| URL pattern (after channel resolution) | Renders |
|
|
51
|
+
| -------------------------------------- | ------------------------------------ |
|
|
52
|
+
| `/<prefix>` | Blog index — latest N + first-level Categories |
|
|
53
|
+
| `/<prefix>/tag/<code>` | Tag view — paginated cards for the Tag |
|
|
54
|
+
| `/<prefix>/<slug>` | Category page (if the slug matches a `blog_categories.slug`) |
|
|
55
|
+
| `/<prefix>/<slug>` | Post page (if the slug matches a `blog_posts.slug`) |
|
|
56
|
+
|
|
57
|
+
The dispatch resolution happens in **one** backend call — `GET /api/v1/blog/by-slug?slug=…` — which returns a discriminated union (`{ kind: 'category' | 'post', … }`). Two channels MAY use the same slug for unrelated entities (channel context disambiguates).
|
|
58
|
+
|
|
59
|
+
### Slug uniqueness
|
|
60
|
+
|
|
61
|
+
Slug uniqueness is enforced **per `(sales_channel, slug)` across the
|
|
62
|
+
union of `blog_posts` and `blog_categories`**:
|
|
63
|
+
|
|
64
|
+
- DB-level partial unique indexes on each scope row (`*_sales_channels`)
|
|
65
|
+
catch any application bug that bypasses the service-level check.
|
|
66
|
+
- The `BlogSlugCollision.assertSlugAvailable` helper acquires a
|
|
67
|
+
Postgres `pg_advisory_xact_lock` per `(channel, slug)` so a concurrent
|
|
68
|
+
save cannot squeeze a duplicate past the per-table indexes.
|
|
69
|
+
- The literal `tag` is reserved as a slug (would collide with the
|
|
70
|
+
`<prefix>/tag/<code>` URL pattern).
|
|
71
|
+
|
|
72
|
+
## Lifecycle
|
|
73
|
+
|
|
74
|
+
### Posts
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
draft ─── publish ────► published ─── unpublish ───► draft
|
|
78
|
+
│ │
|
|
79
|
+
│ archive │
|
|
80
|
+
▼ ▼
|
|
81
|
+
archived ◄── unarchive ── (admin)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`published_at` is set on the first transition to `published` and
|
|
85
|
+
preserved across subsequent unpublish / archive transitions (so re-
|
|
86
|
+
publishing a post does not reset its publication date). A Post renders
|
|
87
|
+
on the storefront when:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
status = 'published'
|
|
91
|
+
AND active = true
|
|
92
|
+
AND deleted_at IS NULL
|
|
93
|
+
AND requested-channel ∈ post.salesChannels
|
|
94
|
+
AND blog.enabled[channel] = true
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Categories
|
|
98
|
+
|
|
99
|
+
Categories carry a single `enabled` flag (no draft / publish). The seeded
|
|
100
|
+
`Default` row is system-protected — admins can rename it, change its
|
|
101
|
+
metadata, or detach it from channels, but it cannot be deleted.
|
|
102
|
+
|
|
103
|
+
## Reference protection
|
|
104
|
+
|
|
105
|
+
Five guards block destructive operations:
|
|
106
|
+
|
|
107
|
+
1. **Tag delete with referencing posts** → 409 `BLOG_TAG_IN_USE`.
|
|
108
|
+
2. **Category delete with referencing posts** → 409 `BLOG_CATEGORY_IN_USE`.
|
|
109
|
+
3. **Category delete with child rows** → 409 `BLOG_CATEGORY_HAS_CHILDREN`.
|
|
110
|
+
4. **Seeded `Default` Category delete** → 409 `BLOG_CATEGORY_PROTECTED`.
|
|
111
|
+
5. **Library Asset soft-delete while embedded in a Post body or Category description** →
|
|
112
|
+
409 `ASSET_REFERENCED` (descriptors registered with the
|
|
113
|
+
[Assets Library reference registry](../assets-library/index.md#asset-reference-registry)).
|
|
114
|
+
|
|
115
|
+
The Post-as-Related-Post relationship uses a different contract:
|
|
116
|
+
**detach-on-delete**. When a Post is soft-deleted, every parent Post's
|
|
117
|
+
`relatedPostIds` shrinks atomically. The admin sees a confirmation
|
|
118
|
+
dialog listing the affected parents (driven by the
|
|
119
|
+
`/posts/:id/inbound-references` probe).
|
|
120
|
+
|
|
121
|
+
The Post-as-Related-Product relationship uses **soft-delete-+-storefront-filter**: a soft-deleted product is invisible on the next storefront read; the join row remains. A generic `ProductReferenceRegistry` may ship in a follow-up.
|
|
122
|
+
|
|
123
|
+
## Settings
|
|
124
|
+
|
|
125
|
+
| Code | Type | Default | Notes |
|
|
126
|
+
| --------------------- | ------- | ------: | ---------------------------------------------------- |
|
|
127
|
+
| `blog.enabled` | boolean | `true` | Disables the namespace per channel. |
|
|
128
|
+
| `blog.url_prefix` | string | `blog` | Single URL segment, `^[a-z0-9-]+$`. Reserved Next.js segments refused. |
|
|
129
|
+
| `blog.latest_count` | number | `5` | Latest posts on the index. |
|
|
130
|
+
| `blog.posts_per_page` | number | `12` | Page size on Category and Tag views. |
|
|
131
|
+
|
|
132
|
+
A change to any `blog.*` setting drops the platform-wide settings cache at the
|
|
133
|
+
write seam and then fires an `EventBus` event, which wipes the storefront cache
|
|
134
|
+
(see Cache strategy below).
|
|
135
|
+
|
|
136
|
+
## Admin roles
|
|
137
|
+
|
|
138
|
+
Two roles seed at first boot through `services/seed-roles.ts`:
|
|
139
|
+
|
|
140
|
+
| Code | Default name | Permissions |
|
|
141
|
+
| ----------------- | ----------------- | ---------------------------------------------------- |
|
|
142
|
+
| `blog_manager` | Blog Manager | `blog.read`, `blog.write` |
|
|
143
|
+
| `content_manager` | Content Manager | `blog.read`, `blog.write`, `cms.read`, `cms.write` |
|
|
144
|
+
|
|
145
|
+
Both are **system-protected**: `AdminRoleService.remove` refuses delete
|
|
146
|
+
with 409 `ADMIN_ROLE_PROTECTED`. The reconciler preserves admin-edited
|
|
147
|
+
names across reboots and only refreshes the canonical permissions
|
|
148
|
+
array if it has drifted.
|
|
149
|
+
|
|
150
|
+
## Storefront API
|
|
151
|
+
|
|
152
|
+
| Method | Path | Returns |
|
|
153
|
+
| ------ | ------------------------------------- | -------------------------------------------------------- |
|
|
154
|
+
| GET | `/api/v1/blog/by-channel` | `BlogIndexResponse` — latest N + first-level Categories |
|
|
155
|
+
| GET | `/api/v1/blog/by-slug?slug=…` | Discriminated `BlogBySlugResponse` (category / post) |
|
|
156
|
+
| GET | `/api/v1/blog/tag-by-code?code=…` | `BlogTagByCodeResponse` — paginated posts for the Tag |
|
|
157
|
+
|
|
158
|
+
All three:
|
|
159
|
+
|
|
160
|
+
- read the resolved channel + language from request headers (`x-sales-channel`, `x-blog-language` / `accept-language`);
|
|
161
|
+
- read `blog.*` settings from the channel-aware Settings service;
|
|
162
|
+
- return `404 BLOG_DISABLED` (or `BLOG_POST_NOT_FOUND` / `BLOG_TAG_NOT_FOUND`) when the resolved scope has no matching content;
|
|
163
|
+
- ride the [`BlogCacheService`](#cache-strategy) Redis read-through.
|
|
164
|
+
|
|
165
|
+
## Cache strategy
|
|
166
|
+
|
|
167
|
+
Keys live under `blog:v1:<channelCode>:<language>:` with four shapes:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
blog:v1:<channel>:<language>:index
|
|
171
|
+
blog:v1:<channel>:<language>:category:<slug>:p<page>
|
|
172
|
+
blog:v1:<channel>:<language>:post:<slug>
|
|
173
|
+
blog:v1:<channel>:<language>:tag:<code>:p<page>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
TTL: 5 minutes (aligned with the CMS module's cache).
|
|
177
|
+
|
|
178
|
+
Invalidation:
|
|
179
|
+
|
|
180
|
+
| Event | Wipe |
|
|
181
|
+
| -------------------------------------- | ----------------------------------------------- |
|
|
182
|
+
| Post / Category / Tag write | `BlogCacheService.invalidateAll()` |
|
|
183
|
+
| Soft-delete or settings write | `BlogCacheService.invalidateAll()` via EventBus |
|
|
184
|
+
|
|
185
|
+
The granular `invalidatePost(channelCode, slug)` and friends are wired
|
|
186
|
+
on the cache class for future surgical-invalidation work (coarse
|
|
187
|
+
invalidation is correct at v1; rate of writes is low).
|
|
188
|
+
|
|
189
|
+
## Cross-module dependencies
|
|
190
|
+
|
|
191
|
+
| Module | What the blog reads |
|
|
192
|
+
| -------------------------------------------- | ----------------------------------------------------------- |
|
|
193
|
+
| [Settings](../settings/index.md) | Four `blog.*` settings via `settings.service.get` |
|
|
194
|
+
| [Sales Channels](../sales_channels/index.md) | Channel resolution + language fallback |
|
|
195
|
+
| [Assets Library](../assets-library/index.md) | Asset URL signing + reference registry |
|
|
196
|
+
| [CMS](../cms/index.md) | Page Builder envelope (`cmsContentEnvelopeSchema`) |
|
|
197
|
+
| [Catalog](../catalog.md) | Product card resolution for Related Products |
|
|
198
|
+
|
|
199
|
+
The blog module never imports another module's internals — every cross-module read goes through a documented service port.
|