From 31eaa9343a97eb4aa30c6ace30721283799ea001 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Sat, 15 Nov 2025 13:57:42 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(export-options):=20Google=20Sheets=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EB=AA=A8=EB=93=9C=20=EC=98=B5?= =?UTF-8?q?=EC=85=98(UI+extras)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/BuilderClientPage.tsx | 22 +++++++++++++++++++--- lib/exporter/extras.ts | 7 +++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/components/BuilderClientPage.tsx b/components/BuilderClientPage.tsx index 66a27d3..a0b1e5d 100644 --- a/components/BuilderClientPage.tsx +++ b/components/BuilderClientPage.tsx @@ -8,6 +8,7 @@ import { AssetManager } from '@/lib/assets/assetManager' import { buildZip } from '@/lib/exporter/zip' import { buildExtrasForPage } from '@/lib/exporter/extras' import { buildAssetsMetadata } from '@/lib/exporter/assets.meta' +import { buildAssetsIndex } from '@/lib/exporter/assets.index' import type { Section } from '@/lib/state/store' import { createFormStore, type FormState } from '@/lib/state/formStore' import FormBuilderPanel from '@/components/FormBuilderPanel' @@ -56,6 +57,7 @@ export default function BuilderClientPage() { const [manifestDisplay, setManifestDisplay] = useState('') const [manifestThemeColor, setManifestThemeColor] = useState('') const [robotsDisallow, setRobotsDisallow] = useState('') + const [sheetsMode, setSheetsMode] = useState<'auto' | 'include' | 'exclude'>('auto') const [viewport, setViewport] = useState<'desktop' | 'mobile'>(() => { if (typeof window !== 'undefined') { try { @@ -238,14 +240,14 @@ export default function BuilderClientPage() { const disallowLines = lines.map((l) => (l.startsWith('Disallow:') ? l : `Disallow: ${l}`)) return `User-agent: *\n${disallowLines.join('\n')}` })() - const baseExtras = buildExtrasForPage(valid, { robotsText: robotsTextOverride, manifest: manifestOverride }) + const baseExtras = buildExtrasForPage(valid, { robotsText: robotsTextOverride, manifest: manifestOverride, sheetsMode }) const extras: Record = { ...baseExtras, - ...(am ? { 'assets.json': buildAssetsMetadata(am) } : {}), + ...(am ? { 'assets.json': buildAssetsMetadata(am), 'assets.index.json': buildAssetsIndex(am) } : {}), } const zipBytes = await buildZip({ html: exported.html, css: exported.css, js: exported.js, assets, extras }) download('landing.zip', zipBytes) - }, [pageData, am, manifestName, manifestShortName, manifestStartUrl, manifestDisplay, manifestThemeColor, robotsDisallow]) + }, [pageData, am, manifestName, manifestShortName, manifestStartUrl, manifestDisplay, manifestThemeColor, robotsDisallow, sheetsMode]) return (
@@ -381,6 +383,20 @@ export default function BuilderClientPage() { placeholder="/private\n/tmp" /> +
} diff --git a/lib/exporter/extras.ts b/lib/exporter/extras.ts index 84cd4dd..cfc2b1c 100644 --- a/lib/exporter/extras.ts +++ b/lib/exporter/extras.ts @@ -9,6 +9,7 @@ export type ExtrasOverrides = { display: string theme_color: string }> + sheetsMode?: 'auto' | 'include' | 'exclude' } function toBytes(s: string): Uint8Array { @@ -42,10 +43,12 @@ export function buildExtrasForPage(page: Page, overrides?: ExtrasOverrides): Rec const manifestJson = JSON.stringify(manifestMerged, null, 2) out['site.webmanifest'] = toBytes(manifestJson) - // Google Sheets 템플릿: actionUrl 미지정 시 포함 + // Google Sheets 템플릿: 기본 auto. overrides.sheetsMode로 강제 포함/제외 가능 + const mode = overrides?.sheetsMode ?? 'auto' const action = page.form?.actionUrl?.trim() const hasAction = !!(action && action.length > 0) - if (!hasAction) { + const shouldInclude = mode === 'include' || (mode === 'auto' && !hasAction) + if (shouldInclude) { const code = `/** * Google Apps Script Web App for form submissions * - Deploy as Web App and set URL as form action if needed. -- 2.52.0 From 2d6afaf665fef1718eb39d5d6f0ac9f7eb6f5d58 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Sat, 15 Nov 2025 14:22:41 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(font):=20Export=20Options=EC=97=90=20?= =?UTF-8?q?=ED=8F=B0=ED=8A=B8=20preconnect=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20exporter=20=EC=98=B5=EC=85=98?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 58 ++++++++++++++++++++++++++++++++ components/BuilderClientPage.tsx | 25 +++++++++++--- lib/exporter/html.ts | 12 +++---- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e215bc4..2efff98 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,61 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + +## Export Bundle (Landing ZIP) + +The Builder can export a production-ready ZIP that contains: + +- index.html, styles.css, app.js +- assets/ hashed assets (optionally grouped by type) +- site.webmanifest, robots.txt +- Optional: sheets/ (Google Apps Script template) +- Metadata files: assets.json, assets.index.json + +### Export Options UI + +- Manifest + - Name, Short Name, Start URL, Display (dropdown), Theme Color (HEX only) +- Robots + - Disallow lines (textarea). Each line becomes a `Disallow:` rule. +- Google Sheets Template Mode + - Auto: include when `form.actionUrl` is empty + - Include: always include + - Exclude: never include + +### site.webmanifest and robots.txt + +- `site.webmanifest` is derived from page data; Export Options can override fields. +- `robots.txt` defaults to a permissive header and adds `Disallow` lines from UI. + +### Asset paths and grouping + +- AssetManager writes files to `assets/` with stable hash-based filenames. +- Path strategy can be configured: + - `flat` (default): `assets/.` + - `grouped`: `assets/images/...`, `assets/fonts/...`, `assets/other/...` + +### assets.json + +Structured metadata for all assets. Example fields per entry: + +- originalName, mimeType, size +- hash8, ext, zipPath +- integrity: `sha256-` (placeholder, base64 of bytes) +- group: `images|fonts|other` +- referencedAt: array of `local:` when referenced by the page + +### assets.index.json + +Simple index to quickly find assets by group: + +- images: string[] of zip paths +- fonts: string[] of zip paths +- other: string[] of zip paths + +### Google Sheets template (optional) + +- When included, ZIP contains: + - `sheets/Code.gs`: minimal Apps Script handler (Web App) + - `sheets/README.txt`: setup instructions +- Inclusion is controlled by `sheetsMode` (Auto/Include/Exclude) and `form.actionUrl` in Auto mode. diff --git a/components/BuilderClientPage.tsx b/components/BuilderClientPage.tsx index a0b1e5d..d28bdff 100644 --- a/components/BuilderClientPage.tsx +++ b/components/BuilderClientPage.tsx @@ -58,6 +58,8 @@ export default function BuilderClientPage() { const [manifestThemeColor, setManifestThemeColor] = useState('') const [robotsDisallow, setRobotsDisallow] = useState('') const [sheetsMode, setSheetsMode] = useState<'auto' | 'include' | 'exclude'>('auto') + // Font optimization: allow toggling Google Fonts preconnect links in exported HTML + const [fontPreconnect, setFontPreconnect] = useState(true) const [viewport, setViewport] = useState<'desktop' | 'mobile'>(() => { if (typeof window !== 'undefined') { try { @@ -178,7 +180,8 @@ export default function BuilderClientPage() { const w = window as unknown as { __exportPreview?: () => { html: string; css: string; js: string } } w.__exportPreview = () => { try { - return exportPage(pageSchema.parse(pageData), { assetManager: am ?? undefined }) + // Export preview with current optimization options + return exportPage(pageSchema.parse(pageData), { assetManager: am ?? undefined, fontPreconnect }) } catch { const fixed: Page = { ...pageData, @@ -188,12 +191,12 @@ export default function BuilderClientPage() { spamProtection: pageData.form?.spamProtection ?? { honeypotFieldName: '_hp', minSubmitSeconds: 2 }, }, } - return exportPage(pageSchema.parse(fixed), { assetManager: am ?? undefined }) + return exportPage(pageSchema.parse(fixed), { assetManager: am ?? undefined, fontPreconnect }) } } } } catch {} - }, [pageData, am]) + }, [pageData, am, fontPreconnect]) const doExport = useCallback(async () => { let valid = pageData as Page @@ -210,7 +213,8 @@ export default function BuilderClientPage() { } as Page valid = pageSchema.parse(fixed) } - const exported = exportPage(valid, { assetManager: am ?? undefined }) + // Respect font optimization toggle when exporting + const exported = exportPage(valid, { assetManager: am ?? undefined, fontPreconnect }) try { if (typeof window !== 'undefined') { const w = window as unknown as { __captureExport?: boolean; __lastExport?: { html: string; css: string; js: string } } @@ -247,7 +251,7 @@ export default function BuilderClientPage() { } const zipBytes = await buildZip({ html: exported.html, css: exported.css, js: exported.js, assets, extras }) download('landing.zip', zipBytes) - }, [pageData, am, manifestName, manifestShortName, manifestStartUrl, manifestDisplay, manifestThemeColor, robotsDisallow, sheetsMode]) + }, [pageData, am, manifestName, manifestShortName, manifestStartUrl, manifestDisplay, manifestThemeColor, robotsDisallow, sheetsMode, fontPreconnect]) return (
@@ -312,6 +316,17 @@ export default function BuilderClientPage() {

Export Options

+ {/* Font optimization toggle for Google Fonts preconnect */} +