Skip to content
HomePagesHomePages template kit
Menu

template-kit/group-ancestry

The template-kit/group-ancestry authoring rule — what it enforces, why, and how to fix it.

A group is a DOM ancestry relationship, not a naming convention. If a member’s marker is not inside its group’s element, the editor will never find the group.

When meta.groups declares a group, every slot listed in its members must be marked on an element that is a descendant of the element carrying that group’s data-slot-group. This check server-renders every fixture and verifies the ancestry on each marked element it finds.

Every marked instance must satisfy it, not just one. Sections routinely render the same slot twice — a desktop tree and a mobile tree, a header and a footer. The editor resolves the group from whichever element the user actually clicked, so one instance inside the group and one outside is a real defect: clicking the outside copy lands in no group at all.

An unmarked copy is not an instance. If the second rendering is a static mirror rather than a second editable surface, leave it unmarked and this rule ignores it.

The editor resolves a slot’s group by walking up the DOM from the selected element with closest("[data-slot-group]"). That is the entire mechanism. A data-slot-group element that is a sibling of its members — or a member rendered outside the container it was declared under — is invisible to that walk, no matter how correct schema.ts looks.

The failure is quiet, which is why it needs a check rather than care. The section renders, the group’s card appears in the sidebar, and the slot fills normally. Only when someone clicks that particular copy on the canvas does the editor come up with no group — and the sidebar selection does not follow the click. Nothing throws, so this survives review by looking exactly like working code.

Move the marked element inside the group’s container, or, if the copy outside is a mirror rather than a second editable instance, stop marking it.

phone and email are declared as members of the contact group, but the mobile menu renders its own copy of phone outside the grouped container:

// sections/header/Renderer.tsx — template-kit/group-ancestry
export default function Header({ slots }: Props): ReactNode {
const { slot, group } = markers;
return (
<Section schema={schema}>
<div className="hidden md:flex flex-col" {...group.contact}>
<a {...slot.phone}>{slots.phone}</a>
<a {...slot.email}>{slots.email}</a>
</div>
{/* The mobile mirror repeats the marker OUTSIDE the group element. */}
<nav className="md:hidden">
<a {...slot.phone}>{slots.phone}</a>
</nav>
</Section>
);
}

The mobile copy gets its own grouped container, so both instances resolve to contact:

// sections/header/Renderer.tsx — template-kit/group-ancestry
export default function Header({ slots }: Props): ReactNode {
const { slot, group } = markers;
return (
<Section schema={schema}>
<div className="hidden md:flex flex-col" {...group.contact}>
<a {...slot.phone}>{slots.phone}</a>
<a {...slot.email}>{slots.email}</a>
</div>
<nav className="md:hidden" {...group.contact}>
<a {...slot.phone}>{slots.phone}</a>
</nav>
</Section>
);
}

If the mirror is presentational — a duplicate the user should not be able to click and edit — drop its marker instead, and the rule stops applying to it:

<nav className="md:hidden">
<a href={`tel:${slots.phone}`}>{slots.phone}</a>
</nav>