template-kit/no-client-directive-in-contract
The template-kit/no-client-directive-in-contract authoring rule — what it enforces, why, and how to fix it.
A section’s four contract files may never carry
"use client". This is the one rule scoped by filename, not by the directive.
The rule runs only on a section’s four contract files — Renderer.tsx, schema.ts,
fill-spec.ts, fixtures.ts — matched by glob. On one of those files, a "use client"
string in the directive prologue is reported.
The prologue is the run of leading string-literal statements (see server vs client). Three consequences worth knowing:
"use strict"; "use client";reports. A second directive is still in the prologue.const x = 1;above"use client"does not report — real code ended the prologue, so that string is not a directive. This is a trap, not a loophole: the file is also not an island, and every server-rendering rule still applies to it. It is a dead string.foo("use client")is not a directive — it is a call argument, and is ignored.
Any other file in the section folder may carry the directive freely. That is exactly where interactivity belongs.
Reason
Section titled “Reason”The four contract files are the platform’s interface to a section. They are read by the
publisher, the editor, and the fill pipeline — none of which is a browser.
schema.ts is loaded to learn what slots exist; fill-spec.ts to learn how AI fills
them; fixtures.ts to render a preview; Renderer.tsx to produce the section’s HTML.
Marking one "use client" would make the whole section an island — and take the
section’s server-rendered HTML with it. The section would ship as an empty shell that
only appears once JavaScript has run, which is precisely what a published property page
must never do.
Put the interactive part in its own "use client" component in the section folder,
and render it from the Renderer. The contract file stays server-only; the island next to
it does the browser work.
Before
Section titled “Before”"use client";
import { useState } from "react";
import { Section } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { const [open, setOpen] = useState(false); return ( <Section> <button onClick={() => setOpen(true)}>Open gallery</button> {open && <p>{slots.headline}</p>} </Section> );}"use client";
import { useState } from "react";
export default function Lightbox({ headline }: { headline: string }) { const [open, setOpen] = useState(false); return ( <div> <button onClick={() => setOpen(true)}>Open gallery</button> {open && <p>{headline}</p>} </div> );}// sections/gallery/Renderer.tsx — no directive; server-rendered againimport { Section } from "@homepages/template-kit";
import Lightbox from "./Lightbox.js";import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { return ( <Section> <Lightbox headline={slots.headline} /> </Section> );}See also
Section titled “See also”- Server vs client — the directive, the prologue, and what an island may do.
- Schema system — what each of the four contract files is for.