--- name: dom-to-jsx description: Rules for converting captured DOM fragments into Next.js App Router JSX components. Covers component naming, when to split, Server vs Client classification, hydration-safe patterns, image handling (next/image vs raw img), and prop extraction for repeated patterns. Load this in every generation run from Stage 1 onward. user-invocable: false --- # DOM → JSX The generate agent receives a DOM fragment per section. Convert it into idiomatic Next.js 14+ App Router JSX. ## Component naming - PascalCase. Section-scoped: `HeroSection`, `FeatureGrid`, `TestimonialCard`, `PricingTable`, `CTAStrip`. - File path: `src/components/sections/.tsx` for top-level sections, `src/components/cards/.tsx` for repeated card components, `src/components/ui/.tsx` for primitives (button, badge). - Default export matches the file name. Named exports are fine for sub-components. ## When to split Split a component when any of these is true: - File exceeds 150 lines of JSX - JSX nesting exceeds 3 levels - A repeated pattern appears 3+ times within the section When splitting for a repeat, extract a component with a `data` prop (array of items) or a single-item prop shape matching the manifest's `candidate_component` proposal. Example — manifest says a `FeatureCard` appears 6 times with `{title, description, icon}`: ```tsx // src/components/cards/FeatureCard.tsx type Props = { title: string; description: string; icon: string }; export default function FeatureCard({ title, description, icon }: Props) { ... } // src/components/sections/Features.tsx import FeatureCard from "@/components/cards/FeatureCard"; const ITEMS = [{ title: "...", description: "...", icon: "..." }, ...]; export default function Features() { return
{ITEMS.map(i => )}
; } ``` Extract `ITEMS` as a local `const` in the section file for Stage 1. If the data is clearly content-managed (12+ items, or visually heterogeneous), move to `src/content/
.ts` as a typed export. ## Server vs Client Default: **Server Component**. Add `"use client"` at the top of a file only if any of these is true: - Uses `useState`, `useEffect`, `useRef`, or any other hook - Uses event handlers (`onClick`, `onChange`, etc.) — interactive buttons that do more than ``-style navigation - Uses `framer-motion`'s `motion.*` components - Uses `lottie-react` or any other client-only lib - Uses browser APIs (`window`, `document`, `localStorage`) Do not mark parents client unnecessarily. Keep `"use client"` at the leaf — a client leaf can live inside a server parent, not vice versa. ## Hydration safety - **No** `Date.now()` / `Math.random()` / `new Date()` at render. If needed, generate in `useEffect` and store in state. At render time the first pass must be deterministic. - **No** reading `window` / `document` at module scope. Wrap in `useEffect` or guard with `typeof window !== "undefined"`. - `suppressHydrationWarning` is a last resort — only on a single element where the server/client divergence is intentional and minimal (e.g. a ``. - Anchor in page: plain `` is fine — `` with a hash works too. ## Accessibility Keep what the source had — do not invent. If the source has `aria-label` or `alt=""`, preserve it. If the source is inaccessible, the clone is inaccessible. This is a fidelity tool, not a correction tool. That said — do not _remove_ accessibility attributes the source had. ## Third-party widgets (skip per `clone-staging`) When the manifest flags a subtree as a third-party widget, replace with: ```tsx
{/* CLONE: skipped third-party widget — */}
``` Pull `W` and `H` from the manifest's bounding box for the widget's container. This preserves layout so later sections land at the right Y offset. ## Forms The clone is a static marketing-page mirror. Forms should render visually but the `onSubmit` handler is `(e) => e.preventDefault()` — no backend wiring. If the source had client-side validation that is material to the layout (error states, spinners), keep it as a visual-only `useState` example. Do not call real APIs. ## Attribute translation - `class` → `className` - `for` → `htmlFor` - `tabindex` → `tabIndex` - Hyphenated attrs on SVG: most become camelCase (`stroke-width` → `strokeWidth`), but `data-*` and `aria-*` stay hyphenated. - `style="..."` → `style={{ ... }}` with camelCase keys. Only inline style that is computed / dynamic; static style always goes to Tailwind per `css-to-tailwind`. ## Content - Copy text verbatim from the DOM fragment. Do not paraphrase. Do not translate. Do not "improve" grammar. - Preserve whitespace-sensitive content (`
`, ``) exactly.
- Keep HTML entities as-is (`—`, ` `, `…`) but JSX renders them when placed inside string literals — use `{"—"}` or `{"\u00a0"}` for non-breaking spaces when adjacency matters.