Skip to content
HomePagesHomePages template kit
Menu

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.

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/WebP srcset, the mobile crop, sizes), the aspect-ratio box, and the neutral fallback rect that keeps the layout box when src is empty. A raw <img> with a null src is 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";
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.