no-client-directive-in-contract
"No \"use client\" in a section's contract files."
Venue: eslint — No “use client” in a section’s contract files.
The rule runs only on a section’s contract files — Renderer.tsx, section.ts and
fixtures.ts (see
Sections and their declarations) — 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 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. The
schema is loaded to learn what slots exist and how AI fills them; the fixtures 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 "./section.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 { defineSchema, Section, text } from "@homepages/template-kit";import type { SectionProps } from "@homepages/template-kit";
declare function Lightbox(props: { headline: string }): null;
const schema = defineSchema({ label: "Gallery", anchor: "gallery", slots: { headline: text.medium({ label: "Headline" }) },});
type Props = SectionProps<typeof schema>;
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.
- Sections and their declarations — the three contract files, and what each declaration is for.