template-kit/no-raw-element
The template-kit/no-raw-element authoring rule — what it enforces, why, and how to fix it.
Exactly three raw elements are banned:
<section>,<img>and<video>. Every other tag is yours.
Three tags, and no others:
| Raw tag | Use instead |
|---|---|
<section> |
<Section> |
<img> |
<Image> |
<video> |
<Video> |
That is the entire rule. <a>, <h1>–<h6>, <div>, <ul>, <button>, <picture>,
<figure> — every other element is yours to write. Presentation is the template’s own
JSX; the kit is not trying to own your markup. If you have concluded “the kit bans raw
HTML”, that is exactly backwards.
There is no island exemption: the three tags are banned in a "use client" file too.
Reason
Section titled “Reason”Each of the three banned tags has a contract-bearing primitive that must emit it, and writing the raw tag skips the contract.
<Section>emits<section class="tr-section">, which is full-bleed by contract: 100% width, zero padding, zero margin, so consecutive sections butt edge-to-edge and a page composes as a clean vertical stack. A raw<section>lacks that class, so the page grows gaps the template never asked for.<Image>owns the responsive<picture>(per-format AVIF/WebPsrcset, the mobile crop,sizes), the aspect-ratio box, and the neutral fallback rect that keeps the layout box whensrcis empty. A raw<img>with a nullsrcis a collapsed box or a broken-image icon on a real customer’s page — and a missing image is a supported state, not a bug: it happens whenever a listing is thin.<Image slotId>also emits the slot marker itself, so the image stays selectable in the editor.<Video>owns the poster frame, the author-declared playback config, and the same fallback rect — a video slot may be empty (a thin listing) or still transcoding, and neither may collapse the layout. A raw<video>also skips the slot marker, so the video stops being selectable in the editor.
The three tags are banned because they are the three the platform has to be able to trust. Everything else carries no contract, so nothing is gained by policing it.
Import the primitive from the package root and use it:
import { Section, Image } from "@homepages/template-kit";Before
Section titled “Before”import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { return ( <section> <h2>{slots.headline}</h2> <img src={slots.hero_image.url ?? ""} alt={slots.hero_image.alt} /> </section> );}import { Image, Section } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { return ( <Section> <h2>{slots.headline}</h2> <Image slotId="hero_image" src={slots.hero_image.url} alt={slots.hero_image.alt} responsive={slots.hero_image.responsive} aspectRatio="5 / 3" /> </Section> );}The <h2> stays exactly as it was. Image takes src/alt directly — a null or empty
src is a supported state it renders a fallback rect for, so no ?? "" guard is needed.
See also
Section titled “See also”- Contract primitives — every primitive and the markers they emit.
image-bare-needs-reason— the escape hatch that drops<Image>’s frame, and what it costs.