docs/solutions/logic-errors/2026-03-23-layout-invalid-column-groups-should-unwrap-content-instead-of-dropping-it.md
withColumn is supposed to clean up broken column_group trees. That recovery path had two bad behaviors.
First, the code path for a group that only wrapped a paragraph tried to unwrap the paragraph child itself. That leaves invalid content under the group and can explode during the next normalization step.
Second, the fallback branch for groups with no valid columns called removeNodes, which silently dropped the group's content even though the comment said the group should be unwrapped.
The implementation mixed up two different recovery actions:
It also unwrapped the wrong node for the paragraph-only case.
The broken shape looked like this:
if (node.children.length === 1 && firstChild.type === editor.getType(KEYS.p)) {
editor.tf.unwrapNodes({ at: PathApi.child(path, 0) });
}
if (!node.children.some((child) => ElementApi.isElement(child) && child.type === type)) {
editor.tf.removeNodes({ at: path });
}
That sequence can both corrupt the tree and discard the user's block content.
Unwrap the column_group itself in both recovery paths and return immediately:
if (node.children.length === 1 && firstChild.type === editor.getType(KEYS.p)) {
editor.tf.unwrapNodes({ at: path });
return;
}
if (!node.children.some((child) => ElementApi.isElement(child) && child.type === type)) {
editor.tf.unwrapNodes({ at: path });
return;
}
Now invalid column wrappers recover to plain blocks instead of crashing or eating content.
These checks passed:
bun test packages/layout/src
pnpm test:slowest -- --top 20 apps/www/src/__tests__/package-integration/ai-utils apps/www/src/__tests__/package-integration/markdown-rich apps/www/src/__tests__/package-integration/markdown-deserializer packages/layout/src
pnpm turbo build --filter=./packages/layout
pnpm turbo typecheck --filter=./packages/layout
When a normalizer says it will "unwrap" invalid content, add one direct test that proves the content survives.
For wrapper-like nodes such as column_group, cover these recovery shapes explicitly:
That catches the easy bug where cleanup logic removes the container and the content with it.