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.
Reason
Section titled “Reason”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.
Before
Section titled “Before”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-ancestryexport 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-ancestryexport 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>