Skip to content
HomePagesHomePages template kit

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.

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.

sections/gallery/Renderer.tsx
"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>
);
}
sections/gallery/Lightbox.tsx
"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 again
import { 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>
);
}