Skip to content

Commit fc3e490

Browse files
KirklandGeeclaude
andcommitted
Prevent Image/Link prop type mismatch in generated components
props.Image returns {src, alt} and props.Link returns {href, target} — not plain strings. Components that typed these as string caused [object Object] rendering at runtime (found in AvatarGroup, Breadcrumbs, CarouselSlider, CmsFilterSearch). Three-layer fix: 1. Prompt (generate_react_component@v1): Added rule 11 with explicit table, rules, and code example showing correct types and JSX usage 2. Deterministic check Webflow-Examples#12: Detects when a prop declared as props.Image or props.Link in the webflow declaration is typed as string in the React component — fails the quality gate and feeds back exact fix instructions 3. types.ts / workflow.ts: Added imageLinkPropsCorrect to DeterministicChecks Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d43d56 commit fc3e490

File tree

4 files changed

+62
-2
lines changed

4 files changed

+62
-2
lines changed

webflow-code-components/src/workflows/component_generator/prompts/generate_react_component@v1.prompt

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,43 @@ text.split(/\\n|\n/)
110110
```
111111
Do NOT use `.split("\n")` alone — it will miss literal `\n` from JSX string attributes and Webflow text fields.
112112

113-
### 11. External API Integrations
113+
### 11. Webflow Prop Type Shapes
114+
Webflow passes certain prop types as **objects**, not plain strings. You MUST type these correctly in the interface and use them correctly in JSX — otherwise they render as `[object Object]` at runtime.
115+
116+
| Webflow prop type | TypeScript type in interface | How to use in JSX |
117+
|---|---|---|
118+
| `props.Image` | `{ src: string; alt?: string }` | `<img src={image.src} alt={image.alt || fallback} />` |
119+
| `props.Link` | `{ href?: string; target?: string }` | `<a href={link?.href || "#"} target={link?.target}>` |
120+
121+
**Rules:**
122+
- NEVER type an Image prop as `string`. Always use `{ src: string; alt?: string }`.
123+
- NEVER type a Link prop as `string`. Always use `{ href?: string; target?: string }`.
124+
- Always use optional chaining (`link?.href`) since the prop may be undefined before the user fills it in.
125+
- Pass `target={link?.target}` to preserve the link target (e.g. `_blank`) the user sets in Webflow.
126+
- When a Link or Image prop feeds into an internal array/object (e.g. a `Slide` or `Item` interface), match the types there too.
127+
128+
**Example — correct Image and Link usage:**
129+
{% raw %}
130+
```tsx
131+
export interface HeroProps {
132+
heroImage?: { src: string; alt?: string };
133+
ctaLink?: { href?: string; target?: string };
134+
}
135+
136+
export default function Hero({ heroImage, ctaLink }: HeroProps) {
137+
return (
138+
<div className="wf-hero">
139+
{heroImage && <img src={heroImage.src} alt={heroImage.alt || ""} className="wf-hero-image" />}
140+
<a href={ctaLink?.href || "#"} target={ctaLink?.target} className="wf-hero-cta">
141+
Get Started
142+
</a>
143+
</div>
144+
);
145+
}
146+
```
147+
{% endraw %}
148+
149+
### 13. External API Integrations
114150
When the component needs to fetch data from an external API:
115151
- Use `useEffect` + `useState` for data fetching (loading → data | error states)
116152
- Use `fetch()` — do NOT import axios or any HTTP library
@@ -121,7 +157,7 @@ When the component needs to fetch data from an external API:
121157
- Show an error message if the fetch fails
122158
- Accept API keys/tokens as props so Webflow designers can configure them in the panel
123159

124-
### 12. Focused and Minimal
160+
### 14. Focused and Minimal
125161
- Keep the component focused on its core purpose
126162
- Don't add unnecessary features beyond what's described
127163
- Provide sensible default values for ALL optional props so it looks complete out of the box

webflow-code-components/src/workflows/component_generator/steps.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,27 @@ export const runDeterministicChecks = step( {
298298
}
299299
}
300300

301+
// 12. Image and Link props typed correctly
302+
// props.Image returns {src, alt} and props.Link returns {href, target} — not plain strings.
303+
// Find every prop declared as props.Image or props.Link in the webflow declaration and verify
304+
// the React component does NOT type that prop as `?: string`.
305+
let imageLinkPropsCorrect = true;
306+
const imagePropNames = [ ...webflowDeclarationCode.matchAll( /(\w+):\s*props\.Image\s*\(/g ) ].map( m => m[1] );
307+
const linkPropNames = [ ...webflowDeclarationCode.matchAll( /(\w+):\s*props\.Link\s*\(/g ) ].map( m => m[1] );
308+
for ( const propName of imagePropNames ) {
309+
// Match `propName?: string` or `propName: string` in the React interface
310+
if ( new RegExp( `${propName}\\??:\\s*string\\b` ).test( reactComponentCode ) ) {
311+
imageLinkPropsCorrect = false;
312+
failures.push( `Prop "${propName}" uses props.Image but is typed as string in the React component — must be typed as { src: string; alt?: string }` );
313+
}
314+
}
315+
for ( const propName of linkPropNames ) {
316+
if ( new RegExp( `${propName}\\??:\\s*string\\b` ).test( reactComponentCode ) ) {
317+
imageLinkPropsCorrect = false;
318+
failures.push( `Prop "${propName}" uses props.Link but is typed as string in the React component — must be typed as { href?: string; target?: string }` );
319+
}
320+
}
321+
301322
const checks = {
302323
classPrefixCorrect,
303324
typographyInherited,
@@ -310,6 +331,7 @@ export const runDeterministicChecks = step( {
310331
propsGrouped,
311332
ssrFlagCorrect,
312333
noCodeFences,
334+
imageLinkPropsCorrect,
313335
};
314336

315337
const allPassed = Object.values( checks ).every( Boolean );

webflow-code-components/src/workflows/component_generator/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const DeterministicChecksSchema = z.object( {
4747
propsGrouped: z.boolean(),
4848
ssrFlagCorrect: z.boolean(),
4949
noCodeFences: z.boolean(),
50+
imageLinkPropsCorrect: z.boolean(),
5051
} );
5152

5253
export const EvaluationSchema = z.object( {

webflow-code-components/src/workflows/component_generator/workflow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default workflow( {
5050
propsGrouped: false,
5151
ssrFlagCorrect: false,
5252
noCodeFences: false,
53+
imageLinkPropsCorrect: false,
5354
};
5455
let llmScore = 0;
5556
let iterations = 0;

0 commit comments

Comments
 (0)