Skip to content
HomePagesHomePages template kit
Menu

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.

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.

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