Gate content behind the lead form
Hold back a section's content until the visitor has submitted your lead form.
Hold back a section’s content until the visitor has submitted your lead form — the gallery opens once they have introduced themselves.
Prerequisites
Section titled “Prerequisites”- A lead form to unlock it — Add a lead form builds one.
- A section holding the content to gate, with a
components/folder for the island — Organize a section’s files adds one.
What this gate is, and what it is not
Section titled “What this gate is, and what it is not”A published page is a static document on a CDN, and the site prefetches its own other pages so navigation feels instant. So the bytes are already in the visitor’s browser before any gate runs: an island’s props travel in the page as JSON, and anyone who opens devtools can read them.
That makes this a gate over what a visitor sees, not over what they can fetch. It is the right tool for shaping a visit — see the gallery after you introduce yourself — and the wrong tool for anything that must stay secret. Nothing on a published page can be kept from a determined reader.
Record the unlock
Section titled “Record the unlock”The unlock is a key in localStorage, written by your form island when the submission
is accepted:
const result = await submitLead({ email, name });if (result.ok) { localStorage.setItem("unlocked:gallery", "1");}Branch on whether the submission was accepted, and never on whether it was simulated.
Wherever there is no site
config — the editor canvas, template-kit dev, any local preview — submitLead sends
nothing and reports the submission as simulated (Add a lead
form), so a gate keyed on a real send is one nobody can open on
those surfaces: the visitor submits the form and stays locked out, and if the gated page
also redirects locked visitors back to the form the two make a loop with no way through.
Withholding the key buys no secrecy either — this gate is a localStorage convenience,
not a security control, so refusing to write it protects nothing. Where a simulated
submission deserves to be visible, say so in the confirmation copy
(Add a lead form) and leave the unlock alone.
Storage is scoped to the origin, and a published site is its own origin — so the key you write is readable by that site’s pages and by nothing else.
Withhold the content
Section titled “Withhold the content”The gated content belongs to an island, and the island renders nothing until it has checked the key. Withholding beats hiding: content that was never rendered cannot flash on a slow connection, and there is no CSS for a reader to defeat.
// sections/gallery/components/GatedGallery.tsx — a new island"use client";
import type { IslandEditor } from "@homepages/template-kit";import { useEditor } from "@homepages/template-kit";import { type ReactNode, useEffect, useState } from "react";
export default function GatedGallery({ photos }: { photos: string[] }): ReactNode { const { inCanvas } = useEditor(); const [unlocked, setUnlocked] = useState(false);
useEffect(() => { setUnlocked(localStorage.getItem("unlocked:gallery") === "1"); }, []);
if (!inCanvas && !unlocked) { return <p>Share your details above to see the full gallery.</p>; }
return ( <ul> {photos.map((src) => ( <li key={src}> <img src={src} alt="" /> </li> ))} </ul> );}
// Live, so the canvas hydrates it and the gate can stand down there.export const editor = { live: true } satisfies IslandEditor;The unlocked state starts out false so that the server render and the first browser
render agree — localStorage does not exist during the server render, and reading it
anywhere but an effect would make the two disagree and break hydration. The effect then
flips it, on the client only.
The island receives photos as an ordinary prop; nothing about the gate changes how
the section passes its content in. Render it from Renderer.tsx like any other
component.
Keep the canvas editable
Section titled “Keep the canvas editable”!inCanvas is not optional. Without it, the site’s owner opens the page in the editor,
the gate finds no key, and the section renders its locked state — so the content they
came to edit is not on the screen and there is no way to reach it.
useEditor() reports inCanvas, and editor = { live: true } is what makes the island
hydrate in the canvas at all; a live: false island stays as its server-rendered
output there, which for this island is the locked state. The two go together. See
Islands for the hook and the pattern.
What inCanvas holds back here is a render, and that is the limit of what it can
hold back. The bridge it reads is installed by the host, so an island whose first render
lands before that install reads inCanvas: false inside a canvas — a render that will
run again corrects itself; a step that cannot be taken back does not. Gating a whole
page is that case.
Gating a whole page
Section titled “Gating a whole page”A section can only withhold its own markup, so a page is fully gated only when every section on it withholds. In practice that means keeping the gated page’s content in one section, and sending a locked visitor away from it.
A redirect inside the canvas navigates the editor away from the page being edited, and
that is not something inCanvas can be trusted to prevent — see Keep the canvas
editable. Hold it out structurally: declare the island
live: false, which is what a canvas consults before it hydrates anything, and let the
island’s server-rendered output be the content.
export default function GatedPage({ photos }: { photos: string[] }): ReactNode { const [locked, setLocked] = useState(false);
useEffect(() => { if (localStorage.getItem("unlocked:gallery") === "1") return; setLocked(true); location.replace("/"); }, []);
return locked ? <LockedNotice /> : <Gallery photos={photos} />;}
// NOT live: no canvas hydrates this island, so nothing above can run there and the// author always gets the server-rendered gallery — the thing they came to edit.export const editor = { live: false } satisfies IslandEditor;The trade is deliberate: the content is in the page before the redirect fires, so a
locked visitor sees it for a moment. That costs nothing this gate was protecting — the
bytes travel in the page either way (What this gate is, and what it is
not) — and it buys a canvas no timing accident
can navigate away from. Note the other half of the bargain: a live: false island must
server-render something (island-server-frame), and
live: false beside serverFrame = false is the one illegal pairing, because it leaves
a permanent blank hole in the canvas. Here that something is the content, which is
exactly what an author needs to see.
Send them to the page that carries the form, and never put a redirecting gate on that
page: a gate that redirects to itself is a loop. Under template-kit dev a root-relative
path leaves the harness’s own /template/<template> mount, so expect that while
authoring.
The locked state still renders an ordinary link to the form, and the form’s success state an ordinary link onward. A redirect is an optimization over a link, not a substitute for one: it fires only once the island has hydrated, and only when the destination is known — so a gate that offers nothing else strands every visitor it cannot redirect on a dead end.
Complete diff
Section titled “Complete diff”The island file under Steps, its import and one render line in
Renderer.tsx, and the three lines added to your form island’s submit handler.
section.ts and fixtures.ts are untouched — a gate changes when content renders, not
what the section declares.
Verify
Section titled “Verify”Run the author loop, then check both states in
dev:
/template/<template>— the plain render, whereinCanvasis false and the gate is live. You should see the locked state./canvas/<template>— the canvas mirror. The content is always visible here, by design. If it is not: a withholding gate is missing its!inCanvasguard or is notlive; a page gate is the reverse — it must not be live, and what shows here is its server render, so a locked state on this surface means the server render is the wrong branch.
Submitting the form writes the key while authoring exactly as it does live, so the
round trip is walkable in dev. To re-lock, or to reach either state without the form,
drive the key from the browser console:
localStorage.setItem("unlocked:gallery", "1"); // then reload — unlockedlocalStorage.removeItem("unlocked:gallery"); // then reload — lockedRules that can fire
Section titled “Rules that can fire”no-client-runtime-in-server— thelocalStorageread or theuseState/useEffectpair stays inRenderer.tsxinstead of moving into the island.require-island-editor— the gate island has a default export but no literaleditordeclaration.no-client-directive-in-contract— the"use client"directive lands inRenderer.tsxinstead of the island’s own file.missing-slot-marker— the island renders a slot’s content outside its primitive, so the marker never reaches the DOM.
See also
Section titled “See also”- Add a lead form — the form whose submission opens this gate, and the result shape the unlock branches on.
- Islands —
editor.live,useEditor(), and theinCanvasescape hatch this guide leans on. - Make a section interactive — the island shape this one follows.