group-ancestry
"A grouped slot must be marked inside its group's data-slot-group element."
Venue: check — A grouped slot must be marked inside its group’s data-slot-group element.
When the schema’s layout declares a group(...) card, 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 the schema 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-ancestryimport { bindGroups, bindSlots, Section, Slot, SlotGroup } from "@homepages/template-kit";
import { schema } from "./schema.js";import type { Props } from "./schema.js";
export default function Header({ slots }: Props) { const slot = bindSlots(schema, slots); const group = bindGroups(schema);
return ( <Section> <SlotGroup slot={group.contact} as="div" className="hidden md:flex flex-col"> <Slot.Text slot={slot.phone} as="a" /> <Slot.Text slot={slot.email} as="a" /> </SlotGroup>
{/* The mobile mirror marks `phone` again, OUTSIDE any group element. */} <nav className="md:hidden"> <Slot.Text slot={slot.phone} as="a" /> </nav> </Section> );}The mobile copy gets its own grouped container, so both instances resolve to contact:
// sections/header/Renderer.tsx — template-kit/group-ancestryimport { bindGroups, bindSlots, defineSchema, group, Section, Slot, SlotGroup, text } from "@homepages/template-kit";import type { SectionProps } from "@homepages/template-kit";
const schema = defineSchema({ label: "Header", anchor: "header", slots: { phone: text.short({ label: "Phone" }), email: text.short({ label: "Email" }), }, layout: [group("contact", "Contact", ["phone", "email"])],});
type Props = SectionProps<typeof schema>;
export default function Header({ slots }: Props) { const slot = bindSlots(schema, slots); const group = bindGroups(schema);
return ( <Section> <SlotGroup slot={group.contact} as="div" className="hidden md:flex flex-col"> <Slot.Text slot={slot.phone} as="a" /> <Slot.Text slot={slot.email} as="a" /> </SlotGroup>
<SlotGroup slot={group.contact} as="nav" className="md:hidden"> <Slot.Text slot={slot.phone} as="a" /> </SlotGroup> </Section> );}One group id, two containers: bindGroups returns a binding per declared group, not
per rendered element, so marking both containers with group.contact is exactly right —
each click resolves up to the nearest one.
If the mirror is presentational — a duplicate the user should not be able to click and
edit — render it with selectable={false}, and the rule stops applying to it:
<nav className="md:hidden"> <Slot.Text slot={slot.phone} as="a" selectable={false} /></nav>The flagged copy keeps its full marker identity and still receives live patches — it never lags the copy the user is editing — but a click on it falls through as if it were unmarked, so it is not an instance this rule judges.