@createcms/core 0.1.1 → 0.2.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.
@@ -68,12 +68,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
68
68
  allowChildren?: false;
69
69
  } | {
70
70
  allowChildren: true;
71
- allowedChildBlocks?: string[];
72
71
  });
73
72
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
74
73
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
75
74
  properties: TProps;
76
75
  };
76
+ /**
77
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
78
+ * — declares which child block types that parent (or the literal `'root'`) may
79
+ * contain. There are three mutually-exclusive modes, enforced by the type:
80
+ *
81
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
82
+ * entry at all; `'*'` is just an explicit, readable form.)
83
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
84
+ * a block added to the collection later is rejected until listed. `excludes`
85
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
86
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
87
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
88
+ *
89
+ * Whether a parent accepts children AT ALL is the separate, coarser
90
+ * `allowChildren` gate on the block (the root always accepts children); these
91
+ * rules only refine WHICH children an accepting parent may hold.
92
+ */
93
+ type BlockStructureEntry<TBlockName extends string> = {
94
+ /** `'*'` = open base (optional, for readability). */
95
+ accepts?: '*';
96
+ /** Holds anything except these. */
97
+ excludes?: readonly TBlockName[];
98
+ } | {
99
+ /** Holds ONLY these block types. */
100
+ accepts: readonly TBlockName[];
101
+ /**
102
+ * Forbidden alongside a concrete `accepts` list — the list already names
103
+ * exactly what is allowed, so `excludes` would be ignored.
104
+ */
105
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
106
+ };
107
+ /**
108
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
109
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
110
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
111
+ * the collection's block names and are checked at compile time by
112
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
113
+ *
114
+ * This is the single source of truth that the visual editor (drop-zone gating)
115
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
116
+ * alongside each block's `allowChildren` flag, so they can never diverge.
117
+ */
118
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
119
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
120
+ };
77
121
  type SlugConfig = {
78
122
  enabled: false;
79
123
  } | {
@@ -97,6 +141,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
97
141
  * regardless of this flag). Any collection can still be a reference target.
98
142
  */
99
143
  reusableBlock?: boolean;
144
+ /**
145
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
146
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
147
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
148
+ * together with each block's `allowChildren` flag. Open by default; block
149
+ * names are checked at compile time by the field type itself, so a typo is a
150
+ * compile error at the `defineCollection` call site.
151
+ */
152
+ structure?: CollectionStructure<TBlocks>;
100
153
  };
101
154
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
102
155
  type RevalidateEvent<TCollections extends Record<string, AnyCollectionDefinition> = Record<string, AnyCollectionDefinition>> = {
@@ -2455,6 +2455,10 @@ const CMS_ERRORS = {
2455
2455
  status: 404,
2456
2456
  message: 'Parent block not found'
2457
2457
  },
2458
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
2459
+ status: 400,
2460
+ message: 'This block type is not allowed inside the target parent'
2461
+ },
2458
2462
  ROOT_NOT_FOUND: {
2459
2463
  status: 404,
2460
2464
  message: 'Root block not found in snapshot'
@@ -329,12 +329,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
329
329
  allowChildren?: false;
330
330
  } | {
331
331
  allowChildren: true;
332
- allowedChildBlocks?: string[];
333
332
  });
334
333
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
335
334
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
336
335
  properties: TProps;
337
336
  };
337
+ /**
338
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
339
+ * — declares which child block types that parent (or the literal `'root'`) may
340
+ * contain. There are three mutually-exclusive modes, enforced by the type:
341
+ *
342
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
343
+ * entry at all; `'*'` is just an explicit, readable form.)
344
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
345
+ * a block added to the collection later is rejected until listed. `excludes`
346
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
347
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
348
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
349
+ *
350
+ * Whether a parent accepts children AT ALL is the separate, coarser
351
+ * `allowChildren` gate on the block (the root always accepts children); these
352
+ * rules only refine WHICH children an accepting parent may hold.
353
+ */
354
+ type BlockStructureEntry<TBlockName extends string> = {
355
+ /** `'*'` = open base (optional, for readability). */
356
+ accepts?: '*';
357
+ /** Holds anything except these. */
358
+ excludes?: readonly TBlockName[];
359
+ } | {
360
+ /** Holds ONLY these block types. */
361
+ accepts: readonly TBlockName[];
362
+ /**
363
+ * Forbidden alongside a concrete `accepts` list — the list already names
364
+ * exactly what is allowed, so `excludes` would be ignored.
365
+ */
366
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
367
+ };
368
+ /**
369
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
370
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
371
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
372
+ * the collection's block names and are checked at compile time by
373
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
374
+ *
375
+ * This is the single source of truth that the visual editor (drop-zone gating)
376
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
377
+ * alongside each block's `allowChildren` flag, so they can never diverge.
378
+ */
379
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
380
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
381
+ };
338
382
  type SlugConfig = {
339
383
  enabled: false;
340
384
  } | {
@@ -358,6 +402,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
358
402
  * regardless of this flag). Any collection can still be a reference target.
359
403
  */
360
404
  reusableBlock?: boolean;
405
+ /**
406
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
407
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
408
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
409
+ * together with each block's `allowChildren` flag. Open by default; block
410
+ * names are checked at compile time by the field type itself, so a typo is a
411
+ * compile error at the `defineCollection` call site.
412
+ */
413
+ structure?: CollectionStructure<TBlocks>;
361
414
  };
362
415
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
363
416
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -329,12 +329,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
329
329
  allowChildren?: false;
330
330
  } | {
331
331
  allowChildren: true;
332
- allowedChildBlocks?: string[];
333
332
  });
334
333
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
335
334
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
336
335
  properties: TProps;
337
336
  };
337
+ /**
338
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
339
+ * — declares which child block types that parent (or the literal `'root'`) may
340
+ * contain. There are three mutually-exclusive modes, enforced by the type:
341
+ *
342
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
343
+ * entry at all; `'*'` is just an explicit, readable form.)
344
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
345
+ * a block added to the collection later is rejected until listed. `excludes`
346
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
347
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
348
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
349
+ *
350
+ * Whether a parent accepts children AT ALL is the separate, coarser
351
+ * `allowChildren` gate on the block (the root always accepts children); these
352
+ * rules only refine WHICH children an accepting parent may hold.
353
+ */
354
+ type BlockStructureEntry<TBlockName extends string> = {
355
+ /** `'*'` = open base (optional, for readability). */
356
+ accepts?: '*';
357
+ /** Holds anything except these. */
358
+ excludes?: readonly TBlockName[];
359
+ } | {
360
+ /** Holds ONLY these block types. */
361
+ accepts: readonly TBlockName[];
362
+ /**
363
+ * Forbidden alongside a concrete `accepts` list — the list already names
364
+ * exactly what is allowed, so `excludes` would be ignored.
365
+ */
366
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
367
+ };
368
+ /**
369
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
370
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
371
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
372
+ * the collection's block names and are checked at compile time by
373
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
374
+ *
375
+ * This is the single source of truth that the visual editor (drop-zone gating)
376
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
377
+ * alongside each block's `allowChildren` flag, so they can never diverge.
378
+ */
379
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
380
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
381
+ };
338
382
  type SlugConfig = {
339
383
  enabled: false;
340
384
  } | {
@@ -358,6 +402,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
358
402
  * regardless of this flag). Any collection can still be a reference target.
359
403
  */
360
404
  reusableBlock?: boolean;
405
+ /**
406
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
407
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
408
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
409
+ * together with each block's `allowChildren` flag. Open by default; block
410
+ * names are checked at compile time by the field type itself, so a typo is a
411
+ * compile error at the `defineCollection` call site.
412
+ */
413
+ structure?: CollectionStructure<TBlocks>;
361
414
  };
362
415
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
363
416
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -2430,6 +2430,10 @@ const CMS_ERRORS = {
2430
2430
  status: 404,
2431
2431
  message: 'Parent block not found'
2432
2432
  },
2433
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
2434
+ status: 400,
2435
+ message: 'This block type is not allowed inside the target parent'
2436
+ },
2433
2437
  ROOT_NOT_FOUND: {
2434
2438
  status: 404,
2435
2439
  message: 'Root block not found in snapshot'
@@ -232,12 +232,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
232
232
  allowChildren?: false;
233
233
  } | {
234
234
  allowChildren: true;
235
- allowedChildBlocks?: string[];
236
235
  });
237
236
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
238
237
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
239
238
  properties: TProps;
240
239
  };
240
+ /**
241
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
242
+ * — declares which child block types that parent (or the literal `'root'`) may
243
+ * contain. There are three mutually-exclusive modes, enforced by the type:
244
+ *
245
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
246
+ * entry at all; `'*'` is just an explicit, readable form.)
247
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
248
+ * a block added to the collection later is rejected until listed. `excludes`
249
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
250
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
251
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
252
+ *
253
+ * Whether a parent accepts children AT ALL is the separate, coarser
254
+ * `allowChildren` gate on the block (the root always accepts children); these
255
+ * rules only refine WHICH children an accepting parent may hold.
256
+ */
257
+ type BlockStructureEntry<TBlockName extends string> = {
258
+ /** `'*'` = open base (optional, for readability). */
259
+ accepts?: '*';
260
+ /** Holds anything except these. */
261
+ excludes?: readonly TBlockName[];
262
+ } | {
263
+ /** Holds ONLY these block types. */
264
+ accepts: readonly TBlockName[];
265
+ /**
266
+ * Forbidden alongside a concrete `accepts` list — the list already names
267
+ * exactly what is allowed, so `excludes` would be ignored.
268
+ */
269
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
270
+ };
271
+ /**
272
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
273
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
274
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
275
+ * the collection's block names and are checked at compile time by
276
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
277
+ *
278
+ * This is the single source of truth that the visual editor (drop-zone gating)
279
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
280
+ * alongside each block's `allowChildren` flag, so they can never diverge.
281
+ */
282
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
283
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
284
+ };
241
285
  type SlugConfig = {
242
286
  enabled: false;
243
287
  } | {
@@ -261,6 +305,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
261
305
  * regardless of this flag). Any collection can still be a reference target.
262
306
  */
263
307
  reusableBlock?: boolean;
308
+ /**
309
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
310
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
311
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
312
+ * together with each block's `allowChildren` flag. Open by default; block
313
+ * names are checked at compile time by the field type itself, so a typo is a
314
+ * compile error at the `defineCollection` call site.
315
+ */
316
+ structure?: CollectionStructure<TBlocks>;
264
317
  };
265
318
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
266
319
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -232,12 +232,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
232
232
  allowChildren?: false;
233
233
  } | {
234
234
  allowChildren: true;
235
- allowedChildBlocks?: string[];
236
235
  });
237
236
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
238
237
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
239
238
  properties: TProps;
240
239
  };
240
+ /**
241
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
242
+ * — declares which child block types that parent (or the literal `'root'`) may
243
+ * contain. There are three mutually-exclusive modes, enforced by the type:
244
+ *
245
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
246
+ * entry at all; `'*'` is just an explicit, readable form.)
247
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
248
+ * a block added to the collection later is rejected until listed. `excludes`
249
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
250
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
251
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
252
+ *
253
+ * Whether a parent accepts children AT ALL is the separate, coarser
254
+ * `allowChildren` gate on the block (the root always accepts children); these
255
+ * rules only refine WHICH children an accepting parent may hold.
256
+ */
257
+ type BlockStructureEntry<TBlockName extends string> = {
258
+ /** `'*'` = open base (optional, for readability). */
259
+ accepts?: '*';
260
+ /** Holds anything except these. */
261
+ excludes?: readonly TBlockName[];
262
+ } | {
263
+ /** Holds ONLY these block types. */
264
+ accepts: readonly TBlockName[];
265
+ /**
266
+ * Forbidden alongside a concrete `accepts` list — the list already names
267
+ * exactly what is allowed, so `excludes` would be ignored.
268
+ */
269
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
270
+ };
271
+ /**
272
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
273
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
274
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
275
+ * the collection's block names and are checked at compile time by
276
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
277
+ *
278
+ * This is the single source of truth that the visual editor (drop-zone gating)
279
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
280
+ * alongside each block's `allowChildren` flag, so they can never diverge.
281
+ */
282
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
283
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
284
+ };
241
285
  type SlugConfig = {
242
286
  enabled: false;
243
287
  } | {
@@ -261,6 +305,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
261
305
  * regardless of this flag). Any collection can still be a reference target.
262
306
  */
263
307
  reusableBlock?: boolean;
308
+ /**
309
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
310
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
311
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
312
+ * together with each block's `allowChildren` flag. Open by default; block
313
+ * names are checked at compile time by the field type itself, so a typo is a
314
+ * compile error at the `defineCollection` call site.
315
+ */
316
+ structure?: CollectionStructure<TBlocks>;
264
317
  };
265
318
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
266
319
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -950,6 +950,10 @@ const CMS_ERRORS = {
950
950
  status: 404,
951
951
  message: 'Parent block not found'
952
952
  },
953
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
954
+ status: 400,
955
+ message: 'This block type is not allowed inside the target parent'
956
+ },
953
957
  ROOT_NOT_FOUND: {
954
958
  status: 404,
955
959
  message: 'Root block not found in snapshot'
@@ -233,12 +233,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
233
233
  allowChildren?: false;
234
234
  } | {
235
235
  allowChildren: true;
236
- allowedChildBlocks?: string[];
237
236
  });
238
237
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
239
238
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
240
239
  properties: TProps;
241
240
  };
241
+ /**
242
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
243
+ * — declares which child block types that parent (or the literal `'root'`) may
244
+ * contain. There are three mutually-exclusive modes, enforced by the type:
245
+ *
246
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
247
+ * entry at all; `'*'` is just an explicit, readable form.)
248
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
249
+ * a block added to the collection later is rejected until listed. `excludes`
250
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
251
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
252
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
253
+ *
254
+ * Whether a parent accepts children AT ALL is the separate, coarser
255
+ * `allowChildren` gate on the block (the root always accepts children); these
256
+ * rules only refine WHICH children an accepting parent may hold.
257
+ */
258
+ type BlockStructureEntry<TBlockName extends string> = {
259
+ /** `'*'` = open base (optional, for readability). */
260
+ accepts?: '*';
261
+ /** Holds anything except these. */
262
+ excludes?: readonly TBlockName[];
263
+ } | {
264
+ /** Holds ONLY these block types. */
265
+ accepts: readonly TBlockName[];
266
+ /**
267
+ * Forbidden alongside a concrete `accepts` list — the list already names
268
+ * exactly what is allowed, so `excludes` would be ignored.
269
+ */
270
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
271
+ };
272
+ /**
273
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
274
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
275
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
276
+ * the collection's block names and are checked at compile time by
277
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
278
+ *
279
+ * This is the single source of truth that the visual editor (drop-zone gating)
280
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
281
+ * alongside each block's `allowChildren` flag, so they can never diverge.
282
+ */
283
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
284
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
285
+ };
242
286
  type SlugConfig = {
243
287
  enabled: false;
244
288
  } | {
@@ -262,6 +306,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
262
306
  * regardless of this flag). Any collection can still be a reference target.
263
307
  */
264
308
  reusableBlock?: boolean;
309
+ /**
310
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
311
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
312
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
313
+ * together with each block's `allowChildren` flag. Open by default; block
314
+ * names are checked at compile time by the field type itself, so a typo is a
315
+ * compile error at the `defineCollection` call site.
316
+ */
317
+ structure?: CollectionStructure<TBlocks>;
265
318
  };
266
319
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
267
320
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -233,12 +233,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
233
233
  allowChildren?: false;
234
234
  } | {
235
235
  allowChildren: true;
236
- allowedChildBlocks?: string[];
237
236
  });
238
237
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
239
238
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
240
239
  properties: TProps;
241
240
  };
241
+ /**
242
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
243
+ * — declares which child block types that parent (or the literal `'root'`) may
244
+ * contain. There are three mutually-exclusive modes, enforced by the type:
245
+ *
246
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
247
+ * entry at all; `'*'` is just an explicit, readable form.)
248
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
249
+ * a block added to the collection later is rejected until listed. `excludes`
250
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
251
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
252
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
253
+ *
254
+ * Whether a parent accepts children AT ALL is the separate, coarser
255
+ * `allowChildren` gate on the block (the root always accepts children); these
256
+ * rules only refine WHICH children an accepting parent may hold.
257
+ */
258
+ type BlockStructureEntry<TBlockName extends string> = {
259
+ /** `'*'` = open base (optional, for readability). */
260
+ accepts?: '*';
261
+ /** Holds anything except these. */
262
+ excludes?: readonly TBlockName[];
263
+ } | {
264
+ /** Holds ONLY these block types. */
265
+ accepts: readonly TBlockName[];
266
+ /**
267
+ * Forbidden alongside a concrete `accepts` list — the list already names
268
+ * exactly what is allowed, so `excludes` would be ignored.
269
+ */
270
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
271
+ };
272
+ /**
273
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
274
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
275
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
276
+ * the collection's block names and are checked at compile time by
277
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
278
+ *
279
+ * This is the single source of truth that the visual editor (drop-zone gating)
280
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
281
+ * alongside each block's `allowChildren` flag, so they can never diverge.
282
+ */
283
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
284
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
285
+ };
242
286
  type SlugConfig = {
243
287
  enabled: false;
244
288
  } | {
@@ -262,6 +306,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
262
306
  * regardless of this flag). Any collection can still be a reference target.
263
307
  */
264
308
  reusableBlock?: boolean;
309
+ /**
310
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
311
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
312
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
313
+ * together with each block's `allowChildren` flag. Open by default; block
314
+ * names are checked at compile time by the field type itself, so a typo is a
315
+ * compile error at the `defineCollection` call site.
316
+ */
317
+ structure?: CollectionStructure<TBlocks>;
265
318
  };
266
319
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
267
320
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -925,6 +925,10 @@ const CMS_ERRORS = {
925
925
  status: 404,
926
926
  message: 'Parent block not found'
927
927
  },
928
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
929
+ status: 400,
930
+ message: 'This block type is not allowed inside the target parent'
931
+ },
928
932
  ROOT_NOT_FOUND: {
929
933
  status: 404,
930
934
  message: 'Root block not found in snapshot'
@@ -231,12 +231,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
231
231
  allowChildren?: false;
232
232
  } | {
233
233
  allowChildren: true;
234
- allowedChildBlocks?: string[];
235
234
  });
236
235
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
237
236
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
238
237
  properties: TProps;
239
238
  };
239
+ /**
240
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
241
+ * — declares which child block types that parent (or the literal `'root'`) may
242
+ * contain. There are three mutually-exclusive modes, enforced by the type:
243
+ *
244
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
245
+ * entry at all; `'*'` is just an explicit, readable form.)
246
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
247
+ * a block added to the collection later is rejected until listed. `excludes`
248
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
249
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
250
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
251
+ *
252
+ * Whether a parent accepts children AT ALL is the separate, coarser
253
+ * `allowChildren` gate on the block (the root always accepts children); these
254
+ * rules only refine WHICH children an accepting parent may hold.
255
+ */
256
+ type BlockStructureEntry<TBlockName extends string> = {
257
+ /** `'*'` = open base (optional, for readability). */
258
+ accepts?: '*';
259
+ /** Holds anything except these. */
260
+ excludes?: readonly TBlockName[];
261
+ } | {
262
+ /** Holds ONLY these block types. */
263
+ accepts: readonly TBlockName[];
264
+ /**
265
+ * Forbidden alongside a concrete `accepts` list — the list already names
266
+ * exactly what is allowed, so `excludes` would be ignored.
267
+ */
268
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
269
+ };
270
+ /**
271
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
272
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
273
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
274
+ * the collection's block names and are checked at compile time by
275
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
276
+ *
277
+ * This is the single source of truth that the visual editor (drop-zone gating)
278
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
279
+ * alongside each block's `allowChildren` flag, so they can never diverge.
280
+ */
281
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
282
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
283
+ };
240
284
  type SlugConfig = {
241
285
  enabled: false;
242
286
  } | {
@@ -260,6 +304,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
260
304
  * regardless of this flag). Any collection can still be a reference target.
261
305
  */
262
306
  reusableBlock?: boolean;
307
+ /**
308
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
309
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
310
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
311
+ * together with each block's `allowChildren` flag. Open by default; block
312
+ * names are checked at compile time by the field type itself, so a typo is a
313
+ * compile error at the `defineCollection` call site.
314
+ */
315
+ structure?: CollectionStructure<TBlocks>;
263
316
  };
264
317
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
265
318
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {