@siteable/core 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +874 -822
- package/package.json +1 -1
- package/src/blocks/faq/FaqBlock.tsx +4 -6
- package/src/blocks/footer/FooterBlock.tsx +4 -6
- package/src/blocks/gallery/GalleryBlock.tsx +5 -6
- package/src/blocks/image/ImageBlock.tsx +5 -1
- package/src/blocks/logocloud/LogoCloudBlock.tsx +3 -2
- package/src/blocks/pricing/PricingBlock.tsx +4 -27
- package/src/blocks/stats/StatsBlock.tsx +4 -6
- package/src/blocks/team/TeamBlock.tsx +4 -6
- package/src/blocks/testimonials/TestimonialsBlock.tsx +4 -6
- package/src/editor/AgentPanel.tsx +3 -1
- package/src/editor/LayersPanel.tsx +3 -1
- package/src/editor/LeftSidebar.tsx +3 -1
- package/src/lib/block-default-content.ts +110 -0
- package/src/lib/block-metadata.ts +28 -11
- package/src/lib/generate-site.ts +15 -2
- package/src/lib/generation-prompt.ts +4 -3
- package/src/lib/prop-normalization.ts +121 -0
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { ChevronDown } from 'lucide-react'
|
|
3
3
|
import type { BlockConfig } from '../types'
|
|
4
|
+
import { defaultFaqItems } from '@/lib/block-default-content'
|
|
4
5
|
|
|
5
6
|
interface FaqItem {
|
|
6
7
|
question: string
|
|
@@ -13,12 +14,9 @@ interface FaqProps {
|
|
|
13
14
|
items?: FaqItem[]
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ question: 'Can I use my own components?', answer: 'Yes! OpenPage supports custom components. You can build your own blocks following our component schema and register them in the block registry.' },
|
|
20
|
-
{ question: 'Is it free to use?', answer: 'OpenPage offers a free tier for personal projects with up to 5 blocks. Pro and Team plans unlock unlimited blocks, custom domains, and priority support.' },
|
|
21
|
-
]
|
|
17
|
+
// ISS-005: fallback FAQs are the block-default-content contract, shared with
|
|
18
|
+
// the prop normalizer (was an inline literal, value-identical).
|
|
19
|
+
const defaultFaqs = defaultFaqItems as FaqItem[]
|
|
22
20
|
|
|
23
21
|
function AccordionItem({ item, isOpen, onToggle }: { item: FaqItem; isOpen: boolean; onToggle: () => void }) {
|
|
24
22
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlockConfig } from '../types'
|
|
2
|
+
import { defaultFooterColumns } from '@/lib/block-default-content'
|
|
2
3
|
|
|
3
4
|
interface FooterProps {
|
|
4
5
|
logo: string
|
|
@@ -41,12 +42,9 @@ function FooterSimple({ props }: { props: FooterProps }) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function FooterMultiColumn({ props }: { props: FooterProps }) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ title: 'Resources', links: ['Documentation', 'API Reference', 'Guides', 'Community'] },
|
|
48
|
-
{ title: 'Legal', links: ['Privacy', 'Terms', 'Security', 'Cookie Policy'] },
|
|
49
|
-
]
|
|
45
|
+
// ISS-005: fallback columns are the block-metadata contract (defaultFooterColumns),
|
|
46
|
+
// shared with the prop normalizer so shape and render default can never drift.
|
|
47
|
+
const columns = props.columns || defaultFooterColumns
|
|
50
48
|
|
|
51
49
|
return (
|
|
52
50
|
<footer className="px-6 @md:px-10 py-12 border-t border-border-subtle">
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ImageIcon } from 'lucide-react'
|
|
2
2
|
import type { BlockConfig } from '../types'
|
|
3
|
+
// ISS-005: fallback images are the block-default-content contract, shared with
|
|
4
|
+
// the prop normalizer (was an inline literal; src/caption:'' additions are
|
|
5
|
+
// falsy → placeholder icon + no caption div, byte-identical render).
|
|
6
|
+
import { defaultGalleryImages } from '@/lib/block-default-content'
|
|
3
7
|
|
|
4
8
|
interface GalleryImage {
|
|
5
9
|
src?: string
|
|
@@ -7,11 +11,6 @@ interface GalleryImage {
|
|
|
7
11
|
caption?: string
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
const defaultImages: GalleryImage[] = [
|
|
11
|
-
{ alt: 'Image 1' }, { alt: 'Image 2' }, { alt: 'Image 3' },
|
|
12
|
-
{ alt: 'Image 4' }, { alt: 'Image 5' }, { alt: 'Image 6' },
|
|
13
|
-
]
|
|
14
|
-
|
|
15
14
|
function ImageCard({ image, tall }: { image: GalleryImage; tall?: boolean }) {
|
|
16
15
|
return (
|
|
17
16
|
<div className={`rounded-lg overflow-hidden border border-border-default group ${tall ? 'row-span-2' : ''}`}>
|
|
@@ -34,7 +33,7 @@ export function GalleryBlock({ block }: { block: BlockConfig }) {
|
|
|
34
33
|
const title = props.title as string | undefined
|
|
35
34
|
const images = ((props.images as GalleryImage[]) || []).length > 0
|
|
36
35
|
? (props.images as GalleryImage[])
|
|
37
|
-
:
|
|
36
|
+
: defaultGalleryImages
|
|
38
37
|
|
|
39
38
|
if (variant === 'masonry') {
|
|
40
39
|
return (
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ImageIcon } from 'lucide-react'
|
|
2
2
|
import type { BlockConfig } from '../types'
|
|
3
|
+
// ISS-005: grid placeholder items are the block-default-content contract, shared
|
|
4
|
+
// with the prop normalizer (was an inline literal; src:'' is falsy like the
|
|
5
|
+
// original src:undefined → identical Placeholder render).
|
|
6
|
+
import { defaultImageGridItems } from '@/lib/block-default-content'
|
|
3
7
|
|
|
4
8
|
function Placeholder({ className }: { className?: string }) {
|
|
5
9
|
return (
|
|
@@ -41,7 +45,7 @@ export function ImageBlock({ block }: { block: BlockConfig }) {
|
|
|
41
45
|
if (variant === 'grid') {
|
|
42
46
|
const gridImages = images.length > 0
|
|
43
47
|
? images.slice(0, 4)
|
|
44
|
-
:
|
|
48
|
+
: defaultImageGridItems
|
|
45
49
|
|
|
46
50
|
return (
|
|
47
51
|
<div className="px-6 py-12 @lg:px-16 @lg:py-16">
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { BlockConfig } from '../types'
|
|
2
|
+
// ISS-005: fallback logos are the block-default-content contract, shared with
|
|
3
|
+
// the prop normalizer (was an inline literal, value-identical).
|
|
4
|
+
import { defaultLogos } from '@/lib/block-default-content'
|
|
2
5
|
|
|
3
6
|
interface LogoCloudProps {
|
|
4
7
|
title?: string
|
|
5
8
|
logos?: string[]
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
const defaultLogos = ['Vercel', 'Stripe', 'GitHub', 'Figma', 'Notion', 'Linear']
|
|
9
|
-
|
|
10
11
|
function LogoPlaceholder({ name }: { name: string }) {
|
|
11
12
|
return (
|
|
12
13
|
<div className="group cursor-pointer transition-all">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BlockConfig } from '../types'
|
|
2
2
|
import { Check, Star } from 'lucide-react'
|
|
3
|
+
import { defaultPricingTiers } from '@/lib/block-default-content'
|
|
3
4
|
|
|
4
5
|
interface PricingTier {
|
|
5
6
|
name: string
|
|
@@ -17,33 +18,9 @@ interface PricingProps {
|
|
|
17
18
|
tiers?: PricingTier[]
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
price: '$0',
|
|
24
|
-
period: '/month',
|
|
25
|
-
description: 'For personal projects',
|
|
26
|
-
features: ['1 website', '5 blocks', 'Basic export', 'Community support'],
|
|
27
|
-
cta: 'Get Started',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: 'Pro',
|
|
31
|
-
price: '$19',
|
|
32
|
-
period: '/month',
|
|
33
|
-
description: 'For professionals',
|
|
34
|
-
features: ['Unlimited websites', 'All blocks', 'Custom domains', 'Priority support', 'Agent API access', 'Version history'],
|
|
35
|
-
cta: 'Upgrade to Pro',
|
|
36
|
-
featured: true,
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'Team',
|
|
40
|
-
price: '$49',
|
|
41
|
-
period: '/month',
|
|
42
|
-
description: 'For teams and agencies',
|
|
43
|
-
features: ['Everything in Pro', 'Team collaboration', 'Custom components', 'SSO', 'Dedicated support'],
|
|
44
|
-
cta: 'Contact Sales',
|
|
45
|
-
},
|
|
46
|
-
]
|
|
21
|
+
// ISS-005: fallback tiers are the block-metadata contract (defaultPricingTiers),
|
|
22
|
+
// shared with the prop normalizer so shape and render default can never drift.
|
|
23
|
+
const defaultTiers = defaultPricingTiers as PricingTier[]
|
|
47
24
|
|
|
48
25
|
function PricingSimple({ props }: { props: PricingProps }) {
|
|
49
26
|
const tiers = props.tiers || defaultTiers
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlockConfig } from '../types'
|
|
2
|
+
import { defaultStatItems } from '@/lib/block-default-content'
|
|
2
3
|
|
|
3
4
|
interface StatItem {
|
|
4
5
|
value: string
|
|
@@ -10,12 +11,9 @@ interface StatsProps {
|
|
|
10
11
|
items?: StatItem[]
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ value: '50ms', label: 'Avg. response' },
|
|
17
|
-
{ value: '4.9/5', label: 'User rating' },
|
|
18
|
-
]
|
|
14
|
+
// ISS-005: fallback items are the block-default-content contract, shared with
|
|
15
|
+
// the prop normalizer (was an inline literal, value-identical).
|
|
16
|
+
const defaultStats = defaultStatItems as StatItem[]
|
|
19
17
|
|
|
20
18
|
function StatsGrid({ props }: { props: StatsProps }) {
|
|
21
19
|
const items = props.items || defaultStats
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlockConfig } from '../types'
|
|
2
|
+
import { defaultTeamMembers } from '@/lib/block-default-content'
|
|
2
3
|
|
|
3
4
|
interface TeamMember {
|
|
4
5
|
name: string
|
|
@@ -12,12 +13,9 @@ interface TeamProps {
|
|
|
12
13
|
members?: TeamMember[]
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{ name: 'Sam Patel', role: 'Head of Design' },
|
|
19
|
-
{ name: 'Casey Morgan', role: 'Lead Engineer' },
|
|
20
|
-
]
|
|
16
|
+
// ISS-005: fallback members are the block-default-content contract, shared with
|
|
17
|
+
// the prop normalizer (was an inline literal, value-identical).
|
|
18
|
+
const defaultMembers = defaultTeamMembers as TeamMember[]
|
|
21
19
|
|
|
22
20
|
export function TeamBlock({ block }: { block: BlockConfig }) {
|
|
23
21
|
const props = block.props as unknown as TeamProps
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { ChevronLeft, ChevronRight, Star, Quote } from 'lucide-react'
|
|
3
3
|
import type { BlockConfig } from '../types'
|
|
4
|
+
// ISS-005: fallback testimonials are the block-default-content contract, shared
|
|
5
|
+
// with the prop normalizer (was an inline literal; avatar:'' additions are
|
|
6
|
+
// falsy → initials branch renders byte-identically).
|
|
7
|
+
import { defaultTestimonials } from '@/lib/block-default-content'
|
|
4
8
|
|
|
5
9
|
interface Testimonial {
|
|
6
10
|
name: string
|
|
@@ -16,12 +20,6 @@ interface TestimonialsProps {
|
|
|
16
20
|
items?: Testimonial[]
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
const defaultTestimonials: Testimonial[] = [
|
|
20
|
-
{ name: 'Sarah Chen', role: 'CEO at TechCorp', quote: 'OpenPage completely changed how we build landing pages. The JSON config approach is genius.', rating: 5 },
|
|
21
|
-
{ name: 'Marcus Johnson', role: 'Lead Developer', quote: 'Finally, a tool where both designers and AI agents can work together seamlessly.', rating: 5 },
|
|
22
|
-
{ name: 'Emma Wilson', role: 'Product Manager', quote: 'We shipped our marketing site in half the time. The component library is incredible.', rating: 4 },
|
|
23
|
-
]
|
|
24
|
-
|
|
25
23
|
function StarRating({ rating }: { rating: number }) {
|
|
26
24
|
return (
|
|
27
25
|
<div className="flex gap-0.5">
|
|
@@ -87,7 +87,9 @@ function generateResponse(input: string, blocks: { id: string; type: string; var
|
|
|
87
87
|
id: `block-${Date.now()}`,
|
|
88
88
|
type: meta.type,
|
|
89
89
|
variant: meta.variants[0],
|
|
90
|
-
|
|
90
|
+
// ISS-005 review item 3: deep copy — defaultProps now hold shared array-of-object
|
|
91
|
+
// constants; a shallow spread would let this block's edits mutate every future insert.
|
|
92
|
+
props: structuredClone(meta.defaultProps),
|
|
91
93
|
}
|
|
92
94
|
return { action: 'addBlock', block, message: `Adding a ${meta.label} block.` }
|
|
93
95
|
}
|
|
@@ -195,7 +195,9 @@ export function LayersPanel() {
|
|
|
195
195
|
id: `block-${Date.now()}`,
|
|
196
196
|
type,
|
|
197
197
|
variant: meta.variants[0],
|
|
198
|
-
|
|
198
|
+
// ISS-005 review item 3: deep copy — defaultProps now hold shared array-of-object
|
|
199
|
+
// constants; a shallow spread would let this block's edits mutate every future insert.
|
|
200
|
+
props: structuredClone(meta.defaultProps),
|
|
199
201
|
}
|
|
200
202
|
addBlock(block)
|
|
201
203
|
selectBlock(block.id)
|
|
@@ -44,7 +44,9 @@ function ComponentsPanel() {
|
|
|
44
44
|
id: generateBlockId(),
|
|
45
45
|
type,
|
|
46
46
|
variant: meta.variants[0],
|
|
47
|
-
|
|
47
|
+
// ISS-005 review item 3: deep copy — defaultProps now hold shared array-of-object
|
|
48
|
+
// constants; a shallow spread would let this block's edits mutate every future insert.
|
|
49
|
+
props: structuredClone(meta.defaultProps),
|
|
48
50
|
}
|
|
49
51
|
addBlock(block)
|
|
50
52
|
selectBlock(block.id)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISS-005 (class fix) — single source of truth for block default CONTENT.
|
|
3
|
+
*
|
|
4
|
+
* These literals were previously inline fallbacks inside block components
|
|
5
|
+
* (or absent entirely), so the shape-driven prop normalizer had no contract
|
|
6
|
+
* to coerce AI-returned values against. Each constant is value-identical to
|
|
7
|
+
* the fallback it replaced: valid configs render byte-for-byte the same.
|
|
8
|
+
*
|
|
9
|
+
* block-metadata references them from defaultProps; the blocks import them
|
|
10
|
+
* as render fallbacks (still needed for legacy persisted configs).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ISS-005 (review item 1): logocloud logos were inline-only; LogoPlaceholder
|
|
14
|
+
// does name.charAt(0) + renders {name} → object entries crash exactly like
|
|
15
|
+
// navbar links did.
|
|
16
|
+
export const defaultLogos = ['Vercel', 'Stripe', 'GitHub', 'Figma', 'Notion', 'Linear']
|
|
17
|
+
|
|
18
|
+
export const defaultFooterColumns: { title: string; links: string[] }[] = [
|
|
19
|
+
{ title: 'Product', links: ['Features', 'Pricing', 'Changelog', 'Roadmap'] },
|
|
20
|
+
{ title: 'Company', links: ['About', 'Blog', 'Careers', 'Press'] },
|
|
21
|
+
{ title: 'Resources', links: ['Documentation', 'API Reference', 'Guides', 'Community'] },
|
|
22
|
+
{ title: 'Legal', links: ['Privacy', 'Terms', 'Security', 'Cookie Policy'] },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export const defaultPricingTiers: {
|
|
26
|
+
name: string
|
|
27
|
+
price: string
|
|
28
|
+
period?: string
|
|
29
|
+
description?: string
|
|
30
|
+
features: string[]
|
|
31
|
+
cta: string
|
|
32
|
+
featured?: boolean
|
|
33
|
+
}[] = [
|
|
34
|
+
{
|
|
35
|
+
name: 'Starter',
|
|
36
|
+
price: '$0',
|
|
37
|
+
period: '/month',
|
|
38
|
+
description: 'For personal projects',
|
|
39
|
+
features: ['1 website', '5 blocks', 'Basic export', 'Community support'],
|
|
40
|
+
cta: 'Get Started',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Pro',
|
|
44
|
+
price: '$19',
|
|
45
|
+
period: '/month',
|
|
46
|
+
description: 'For professionals',
|
|
47
|
+
features: ['Unlimited websites', 'All blocks', 'Custom domains', 'Priority support', 'Agent API access', 'Version history'],
|
|
48
|
+
cta: 'Upgrade to Pro',
|
|
49
|
+
featured: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Team',
|
|
53
|
+
price: '$49',
|
|
54
|
+
period: '/month',
|
|
55
|
+
description: 'For teams and agencies',
|
|
56
|
+
features: ['Everything in Pro', 'Team collaboration', 'Custom components', 'SSO', 'Dedicated support'],
|
|
57
|
+
cta: 'Contact Sales',
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
export const defaultStatItems: { value: string; label: string }[] = [
|
|
62
|
+
{ value: '10K+', label: 'Sites built' },
|
|
63
|
+
{ value: '99.9%', label: 'Uptime' },
|
|
64
|
+
{ value: '50ms', label: 'Avg. response' },
|
|
65
|
+
{ value: '4.9/5', label: 'User rating' },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
export const defaultFaqItems: { question: string; answer: string }[] = [
|
|
69
|
+
{ question: 'What is OpenPage?', answer: 'OpenPage is a visual website builder that uses structured JSON config as the source of truth. Both humans and AI agents can edit the same config to build beautiful websites.' },
|
|
70
|
+
{ question: 'How does the JSON config work?', answer: 'Every website is represented as a JSON document with blocks, styles, and content. The visual editor reads and writes this JSON, and agents can make surgical edits via the API.' },
|
|
71
|
+
{ question: 'Can I use my own components?', answer: 'Yes! OpenPage supports custom components. You can build your own blocks following our component schema and register them in the block registry.' },
|
|
72
|
+
{ question: 'Is it free to use?', answer: 'OpenPage offers a free tier for personal projects with up to 5 blocks. Pro and Team plans unlock unlimited blocks, custom domains, and priority support.' },
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
export const defaultTeamMembers: { name: string; role: string }[] = [
|
|
76
|
+
{ name: 'Alex Rivera', role: 'CEO & Founder' },
|
|
77
|
+
{ name: 'Jordan Lee', role: 'CTO' },
|
|
78
|
+
{ name: 'Sam Patel', role: 'Head of Design' },
|
|
79
|
+
{ name: 'Casey Morgan', role: 'Lead Engineer' },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
// avatar:'' added vs the original inline literal (falsy → renders the initials
|
|
83
|
+
// fallback exactly as before) so the normalizer has a string shape for it.
|
|
84
|
+
export const defaultTestimonials: {
|
|
85
|
+
name: string
|
|
86
|
+
role: string
|
|
87
|
+
quote: string
|
|
88
|
+
rating?: number
|
|
89
|
+
avatar?: string
|
|
90
|
+
}[] = [
|
|
91
|
+
{ name: 'Sarah Chen', role: 'CEO at TechCorp', quote: 'OpenPage completely changed how we build landing pages. The JSON config approach is genius.', rating: 5, avatar: '' },
|
|
92
|
+
{ name: 'Marcus Johnson', role: 'Lead Developer', quote: 'Finally, a tool where both designers and AI agents can work together seamlessly.', rating: 5, avatar: '' },
|
|
93
|
+
{ name: 'Emma Wilson', role: 'Product Manager', quote: 'We shipped our marketing site in half the time. The component library is incredible.', rating: 4, avatar: '' },
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
// src/caption:'' added vs the original { alt } literals — falsy renders the
|
|
97
|
+
// placeholder icon + skips the caption div, byte-identical output, and gives
|
|
98
|
+
// the normalizer full string shapes for every GalleryImage field.
|
|
99
|
+
export const defaultGalleryImages: { src?: string; alt?: string; caption?: string }[] = [
|
|
100
|
+
{ src: '', alt: 'Image 1', caption: '' }, { src: '', alt: 'Image 2', caption: '' }, { src: '', alt: 'Image 3', caption: '' },
|
|
101
|
+
{ src: '', alt: 'Image 4', caption: '' }, { src: '', alt: 'Image 5', caption: '' }, { src: '', alt: 'Image 6', caption: '' },
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
// Was inline in ImageBlock grid variant as { src: undefined, alt: '1' }.
|
|
105
|
+
// src promoted to '' — renders identically (falsy → Placeholder) but gives
|
|
106
|
+
// the normalizer a string contract for src. Type mirrors ImageBlock's
|
|
107
|
+
// declared { src?, alt? } so props.images || FALLBACK unions stay assignable.
|
|
108
|
+
export const defaultImageGridItems: { src?: string; alt: string }[] = [
|
|
109
|
+
{ src: '', alt: '1' }, { src: '', alt: '2' }, { src: '', alt: '3' }, { src: '', alt: '4' },
|
|
110
|
+
]
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { BlockType } from '@/blocks/types'
|
|
2
|
+
import {
|
|
3
|
+
defaultFooterColumns,
|
|
4
|
+
defaultLogos,
|
|
5
|
+
defaultPricingTiers,
|
|
6
|
+
defaultStatItems,
|
|
7
|
+
defaultFaqItems,
|
|
8
|
+
defaultTeamMembers,
|
|
9
|
+
defaultTestimonials,
|
|
10
|
+
defaultGalleryImages,
|
|
11
|
+
defaultImageGridItems,
|
|
12
|
+
} from '@/lib/block-default-content'
|
|
2
13
|
|
|
3
14
|
export interface BlockMeta {
|
|
4
15
|
type: BlockType
|
|
@@ -9,6 +20,10 @@ export interface BlockMeta {
|
|
|
9
20
|
defaultProps: Record<string, unknown>
|
|
10
21
|
}
|
|
11
22
|
|
|
23
|
+
// ISS-005 (class fix): defaultProps below ARE the prop contracts the generation
|
|
24
|
+
// normalizer (src/lib/prop-normalization.ts) coerces AI output against.
|
|
25
|
+
// Content defaults live in src/lib/block-default-content.ts.
|
|
26
|
+
|
|
12
27
|
export const blockMetadata: BlockMeta[] = [
|
|
13
28
|
{
|
|
14
29
|
type: 'navbar',
|
|
@@ -24,7 +39,9 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
24
39
|
description: 'Full-width hero section with headline and CTAs',
|
|
25
40
|
category: 'Hero',
|
|
26
41
|
variants: ['centered', 'split', 'gradient', 'minimal'],
|
|
27
|
-
|
|
42
|
+
// badge:'' gives the normalizer a string contract for badge (renders only
|
|
43
|
+
// when truthy → byte-identical output vs the key being absent) — ISS-005 review item 1.
|
|
44
|
+
defaultProps: { badge: '', headline: 'Your Headline Here', subheadline: 'A compelling subheadline that explains your value proposition.', primaryCta: 'Get Started', secondaryCta: 'Learn More' },
|
|
28
45
|
},
|
|
29
46
|
{
|
|
30
47
|
type: 'features',
|
|
@@ -40,7 +57,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
40
57
|
description: 'Pricing tiers with feature comparison',
|
|
41
58
|
category: 'Commerce',
|
|
42
59
|
variants: ['simple', 'comparison'],
|
|
43
|
-
defaultProps: { title: 'Pricing', subtitle: 'Choose the plan that fits your needs' },
|
|
60
|
+
defaultProps: { title: 'Pricing', subtitle: 'Choose the plan that fits your needs', tiers: defaultPricingTiers },
|
|
44
61
|
},
|
|
45
62
|
{
|
|
46
63
|
type: 'cta',
|
|
@@ -56,7 +73,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
56
73
|
description: 'Page footer with links and copyright',
|
|
57
74
|
category: 'Navigation',
|
|
58
75
|
variants: ['simple', 'multi-column', 'minimal'],
|
|
59
|
-
defaultProps: { logo: 'Brand', copyright: '2026 Brand. All rights reserved.', links: ['Privacy', 'Terms'] },
|
|
76
|
+
defaultProps: { logo: 'Brand', copyright: '2026 Brand. All rights reserved.', links: ['Privacy', 'Terms'], columns: defaultFooterColumns },
|
|
60
77
|
},
|
|
61
78
|
{
|
|
62
79
|
type: 'testimonials',
|
|
@@ -64,7 +81,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
64
81
|
description: 'Customer testimonials with quotes and ratings',
|
|
65
82
|
category: 'Social Proof',
|
|
66
83
|
variants: ['cards', 'carousel', 'spotlight'],
|
|
67
|
-
defaultProps: { title: 'What our customers say' },
|
|
84
|
+
defaultProps: { title: 'What our customers say', items: defaultTestimonials },
|
|
68
85
|
},
|
|
69
86
|
{
|
|
70
87
|
type: 'stats',
|
|
@@ -72,7 +89,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
72
89
|
description: 'Key metrics and statistics display',
|
|
73
90
|
category: 'Social Proof',
|
|
74
91
|
variants: ['grid', 'bar', 'counter'],
|
|
75
|
-
defaultProps: { title: 'By the numbers' },
|
|
92
|
+
defaultProps: { title: 'By the numbers', items: defaultStatItems },
|
|
76
93
|
},
|
|
77
94
|
{
|
|
78
95
|
type: 'faq',
|
|
@@ -80,7 +97,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
80
97
|
description: 'Frequently asked questions accordion',
|
|
81
98
|
category: 'Content',
|
|
82
99
|
variants: ['accordion'],
|
|
83
|
-
defaultProps: { title: 'Frequently Asked Questions' },
|
|
100
|
+
defaultProps: { title: 'Frequently Asked Questions', items: defaultFaqItems },
|
|
84
101
|
},
|
|
85
102
|
{
|
|
86
103
|
type: 'team',
|
|
@@ -88,7 +105,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
88
105
|
description: 'Team member grid with photos and roles',
|
|
89
106
|
category: 'Content',
|
|
90
107
|
variants: ['grid'],
|
|
91
|
-
defaultProps: { title: 'Meet the Team' },
|
|
108
|
+
defaultProps: { title: 'Meet the Team', members: defaultTeamMembers },
|
|
92
109
|
},
|
|
93
110
|
{
|
|
94
111
|
type: 'contact',
|
|
@@ -112,7 +129,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
112
129
|
description: 'Company logos with hover effects',
|
|
113
130
|
category: 'Social Proof',
|
|
114
131
|
variants: ['default'],
|
|
115
|
-
defaultProps: { title: 'Trusted by leading companies' },
|
|
132
|
+
defaultProps: { title: 'Trusted by leading companies', logos: defaultLogos },
|
|
116
133
|
},
|
|
117
134
|
{
|
|
118
135
|
type: 'content',
|
|
@@ -128,7 +145,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
128
145
|
description: 'Image with text overlay or side-by-side layout',
|
|
129
146
|
category: 'Media',
|
|
130
147
|
variants: ['hero-image', 'side-by-side', 'grid'],
|
|
131
|
-
defaultProps: { title: 'Visual Storytelling', subtitle: 'A picture is worth a thousand words.', imageSide: 'left' },
|
|
148
|
+
defaultProps: { title: 'Visual Storytelling', subtitle: 'A picture is worth a thousand words.', imageSide: 'left', images: defaultImageGridItems },
|
|
132
149
|
},
|
|
133
150
|
{
|
|
134
151
|
type: 'video',
|
|
@@ -144,7 +161,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
144
161
|
description: 'Image gallery in grid or masonry layout',
|
|
145
162
|
category: 'Media',
|
|
146
163
|
variants: ['grid', 'masonry'],
|
|
147
|
-
defaultProps: { title: 'Gallery' },
|
|
164
|
+
defaultProps: { title: 'Gallery', images: defaultGalleryImages },
|
|
148
165
|
},
|
|
149
166
|
{
|
|
150
167
|
type: 'divider',
|
|
@@ -160,7 +177,7 @@ export const blockMetadata: BlockMeta[] = [
|
|
|
160
177
|
description: 'Announcement bar or ribbon',
|
|
161
178
|
category: 'Content',
|
|
162
179
|
variants: ['ribbon', 'bar'],
|
|
163
|
-
defaultProps: { text: 'New: We just launched v2.0!', linkText: 'Learn more' },
|
|
180
|
+
defaultProps: { text: 'New: We just launched v2.0!', linkText: 'Learn more', linkUrl: '#' },
|
|
164
181
|
},
|
|
165
182
|
]
|
|
166
183
|
|
package/src/lib/generate-site.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SiteConfig, BlockConfig, ThemeConfig } from '@/blocks/types'
|
|
2
2
|
import { blockMetadata } from '@/lib/block-metadata'
|
|
3
3
|
import { GENERATION_PROMPT } from '@/lib/generation-prompt'
|
|
4
|
+
import { normalizeBlockProps } from '@/lib/prop-normalization'
|
|
4
5
|
import { getTemplateForPrompt } from '@/lib/templates'
|
|
5
6
|
|
|
6
7
|
const VALID_BLOCK_TYPES = new Set<string>(blockMetadata.map((b) => b.type))
|
|
@@ -35,7 +36,12 @@ export async function generateSiteConfig(
|
|
|
35
36
|
// 2. Injected server fallback (caller-supplied; the engine is route-agnostic)
|
|
36
37
|
if (onServerFallback) {
|
|
37
38
|
try {
|
|
38
|
-
const
|
|
39
|
+
const raw = await onServerFallback(prompt, signal)
|
|
40
|
+
// ISS-005: the host's result must go through the SAME validation as tier 1 —
|
|
41
|
+
// previously it was returned unchecked (object props reached blocks → crash).
|
|
42
|
+
// validateSiteConfig never throws (all inputs type-guarded), so an unexpected
|
|
43
|
+
// throw would still be caught here and degrade to the template fallback.
|
|
44
|
+
const config = validateSiteConfig(raw, prompt)
|
|
39
45
|
return { config, source: 'ai' }
|
|
40
46
|
} catch (err) {
|
|
41
47
|
if (err instanceof Error && err.name === 'AbortError') throw err
|
|
@@ -113,6 +119,9 @@ function validateTheme(raw: Record<string, unknown>): Partial<ThemeConfig> {
|
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
function validateBlock(raw: Record<string, unknown>, index: number): BlockConfig | null {
|
|
122
|
+
// ISS-005 hardening: blocks arrays may contain null/primitives from hostile or
|
|
123
|
+
// malformed AI output — validateSiteConfig must never throw by design.
|
|
124
|
+
if (!raw || typeof raw !== 'object') return null
|
|
116
125
|
const type = raw.type as string
|
|
117
126
|
if (!type || !VALID_BLOCK_TYPES.has(type)) return null
|
|
118
127
|
|
|
@@ -123,7 +132,11 @@ function validateBlock(raw: Record<string, unknown>, index: number): BlockConfig
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
const defaultProps = DEFAULT_PROPS_MAP[type] || {}
|
|
126
|
-
|
|
135
|
+
// ISS-005 root fix: raw prop VALUES are normalized against the defaultProps
|
|
136
|
+
// shape (generic, no per-block guards) before merging over the defaults.
|
|
137
|
+
const props = typeof raw.props === 'object' && raw.props
|
|
138
|
+
? { ...defaultProps, ...normalizeBlockProps(raw.props, defaultProps) }
|
|
139
|
+
: defaultProps
|
|
127
140
|
|
|
128
141
|
return {
|
|
129
142
|
id: typeof raw.id === 'string' && raw.id ? raw.id : `block-${type}-${index}-${Date.now()}`,
|
|
@@ -27,12 +27,12 @@ Each page has its own blocks array. Generate at least 2 pages: Home and one addi
|
|
|
27
27
|
|
|
28
28
|
## Available Block Types
|
|
29
29
|
|
|
30
|
-
1. navbar (variants: default, centered) - Props: { logo, links[], ctaText }
|
|
30
|
+
1. navbar (variants: default, centered) - Props: { logo, links: string[], ctaText }
|
|
31
31
|
2. hero (variants: centered, split, gradient, minimal) - Props: { badge?, headline, subheadline, primaryCta, secondaryCta? }
|
|
32
32
|
3. features (variants: grid, list, alternating) - Props: { label?, title, subtitle?, items: [{ icon?, title, description }] }
|
|
33
|
-
4. pricing (variants: simple, comparison) - Props: { title, subtitle?, tiers?: [{ name, price, period?, description?, features[], cta, featured? }] }
|
|
33
|
+
4. pricing (variants: simple, comparison) - Props: { title, subtitle?, tiers?: [{ name, price, period?, description?, features: string[], cta, featured? }] }
|
|
34
34
|
5. cta (variants: simple, split) - Props: { headline, subheadline?, buttonText }
|
|
35
|
-
6. footer (variants: simple, multi-column, minimal) - Props: { logo, copyright, links[] }
|
|
35
|
+
6. footer (variants: simple, multi-column, minimal) - Props: { logo, copyright, links: string[], columns?: [{ title, links: string[] }] }
|
|
36
36
|
7. testimonials (variants: cards, carousel, spotlight) - Props: { title?, items?: [{ name, role?, quote, rating? }] }
|
|
37
37
|
8. stats (variants: grid, bar, counter) - Props: { title?, items?: [{ value, label }] }
|
|
38
38
|
9. faq (variants: accordion) - Props: { title?, items?: [{ question, answer }] }
|
|
@@ -60,5 +60,6 @@ Fonts: DM Sans, Inter, Space Grotesk, Poppins, Manrope, Outfit, Plus Jakarta San
|
|
|
60
60
|
6. Do NOT use placeholder text like "Lorem ipsum"
|
|
61
61
|
7. Make copy compelling and specific to the described business
|
|
62
62
|
8. Each page needs a unique id (page-home, page-about, etc.), a name, and a path (/, /about, etc.)
|
|
63
|
+
9. Link/label arrays (navbar links, footer links, footer columns[].links, pricing tiers[].features) are plain strings — NEVER objects like {label, href}
|
|
63
64
|
|
|
64
65
|
Return ONLY valid JSON. No markdown, no code fences, no explanation.`
|