import type { Page, FormField } from '@/lib/schema/page' import type { AssetManager } from '@/lib/assets/assetManager' export type Exported = { html: string; css: string; js: string } export type ExportOptions = { assetManager?: AssetManager } function renderFaq(section: { props: { items: Array<{ q: string; a: string }> } }) { const items: Array<{ q: string; a: string }> = Array.isArray(section.props.items) ? section.props.items : [] return ` FAQ ${items .map( (it) => ` ${escapeHtml(it.q)} ${escapeHtml(it.a)} ` ) .join('')} ` } function renderFeatures(section: { props: { items: string[] } }) { const items: string[] = Array.isArray(section.props?.items) ? section.props.items : [] return ` Features ${items.map((it) => `${escapeHtml(String(it))}`).join('')} ` } function renderTestimonials(section: { props: { items: Array<{ author: string; quote: string }> } }) { const items: Array<{ author: string; quote: string }> = Array.isArray(section.props?.items) ? section.props.items : [] return ` Testimonials ${items .map( (it) => ` “${escapeHtml(it.quote || '')}” — ${escapeHtml(it.author || '')} ` ) .join('')} ` } function renderCta(section: { props: { text: string; href?: string } }) { const { text, href } = section.props return ` CTA ${escapeHtml(text)} ` } const escapeHtml = (s: string) => s .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') function renderHero(section: { props: { heading: string; subheading?: string; imageUrl: string } }, opts?: ExportOptions) { const { heading, subheading } = section.props const am = opts?.assetManager const imageUrlRaw: string = section.props.imageUrl const imageUrl = am ? am.rewriteUrl(imageUrlRaw) : imageUrlRaw return ` ${escapeHtml(heading)} ${subheading ? `${escapeHtml(subheading)}` : ''} ` } function renderField(field: FormField) { const pat = 'pattern' in field && field.pattern ? ` pattern="${escapeHtml(field.pattern)}"` : '' const max = 'maxLength' in field && field.maxLength ? ` maxlength="${field.maxLength}"` : '' const common = `name="${escapeHtml(field.name)}" aria-label="${escapeHtml(field.label)}" ${field.required ? 'required' : ''}${pat}${max}` switch (field.type) { case 'text': case 'email': case 'tel': case 'date': return `${escapeHtml(field.label)}` case 'textarea': return `${escapeHtml(field.label)}` case 'checkbox': return `${escapeHtml(field.label)}` case 'radio': { const { options } = field as Extract return ` ${escapeHtml(field.label)} ${options .map( (opt: string) => `${escapeHtml(opt)}` ) .join('')} ` } case 'select': { const { options } = field as Extract return ` ${escapeHtml(field.label)} ${options .map((opt: string) => `${escapeHtml(opt)}`) .join('')} ` } default: return '' } } function renderForm(page: Page) { const { form } = page const hasAction = !!(form.actionUrl && form.actionUrl.trim().length > 0) const hp = form.spamProtection.honeypotFieldName const dataFallback = hasAction ? '' : ' data-sheets-fallback="true"' return ` ${form.fields.map(renderField).join('\n')} Submit ` } function buildHead(page: Page) { const ogImage = page.seo.ogImage const analyticsParts: string[] = [] const ga = page.analytics?.ga4MeasurementId if (ga && ga.trim()) { analyticsParts.push(` `) } const pixel = page.analytics?.metaPixelId if (pixel && pixel.trim()) { analyticsParts.push(` `) } const custom = page.analytics?.customHeadHtml || '' return ` ${escapeHtml(page.seo.title)} ${ogImage ? `` : ''} ${analyticsParts.join('\n')} ${custom} ` } function buildCss(page: Page) { return ` :root{ --primary:${page.theme.primaryColor}; } body{ font-family: ${page.theme.fontFamily}, system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, "Apple Color Emoji","Segoe UI Emoji"; margin:0; color:#0f172a } a.skip-link{ position:absolute; left:-9999px; top:auto; width:1px; height:1px; overflow:hidden } a.skip-link:focus{ position:absolute; left:1rem; top:1rem; width:auto; height:auto; padding:.5rem 1rem; background:#111827; color:#fff; border-radius:.375rem; z-index:1000 } .container{ max-width: 960px; margin: 0 auto; padding: 2rem } .hero .text{ margin-bottom: 1rem } img{ max-width: 100%; height: auto } label{ display:block; margin: .5rem 0 } button{ background: var(--primary); color:#fff; border:none; padding:.75rem 1rem; border-radius:.5rem } button:focus, a:focus, input:focus, select:focus, textarea:focus{ outline: 3px solid #2563eb; outline-offset:2px } .faq-list{ list-style:none; padding:0; margin:0 } .faq-item{ margin: .75rem 0 } .faq-q{ font-size: 1rem; font-weight:600; } .faq-a{ color:#334155 } .btn-cta{ background: var(--primary); color:#fff; padding:.75rem 1rem; border-radius:.5rem; display:inline-block; text-decoration:none } .features{ background:#f8fafc } .features-list{ list-style:none; padding:0; margin:0; display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: .75rem } .feature-item{ background:#fff; border:1px solid #e2e8f0; border-radius:.5rem; padding:.75rem } .testimonials{ background:#ffffff } .testimonial{ border-left:4px solid var(--primary); padding:.5rem 1rem; margin: .75rem 0; background:#fefefe } .testimonial blockquote{ margin:0; font-style:italic; color:#1f2937 } .testimonial figcaption{ color:#475569; margin-top:.25rem } @media (max-width: 640px){ .container{ padding: 1rem } h1{ font-size: 1.5rem } } @media (prefers-reduced-motion: reduce){ *{ transition: none !important; animation: none !important } } ` } function buildJs() { // timestamp 채우기(스팸 방지용 제출 지연 체크용 데이터) return ` (function(){ var ts = document.querySelector('input[name="_ts"]'); if(ts){ ts.value = String(Date.now()); } })(); ` } export function exportPage(page: Page, opts?: ExportOptions): Exported { const sections = page.sections .map((s: Page['sections'][number]) => { if (s.type === 'hero') return renderHero(s, opts) if (s.type === 'faq') return renderFaq(s) if (s.type === 'cta') return renderCta(s) if (s.type === 'features') return renderFeatures(s) if (s.type === 'testimonials') return renderTestimonials(s) return '' }) .join('\n') const formHtml = renderForm(page) const html = ` ${buildHead(page)} Skip to content ${sections} ${formHtml} ` return { html, css: buildCss(page), js: buildJs() } }
${escapeHtml(it.a)}
“${escapeHtml(it.quote || '')}”
${escapeHtml(subheading)}