クリック → 右のトレイで差し替え(ヒーローを押すと世界観・演出も変えられます)。編集は自動保存、開き直すと続きから 💾 自動保存ON
📘 公開のしかた
📚 バージョン棚
保存しました!
\n"; } document.getElementById("copyHtml").addEventListener("click", () => { navigator.clipboard.writeText(exportHtml()).then(() => showToast("HTMLをコピーしました!")); }); document.getElementById("dlHtml").addEventListener("click", () => { const blob = new Blob([exportHtml()], {type:"text/html"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "my-lp.html"; a.click(); URL.revokeObjectURL(a.href); showToast("書き出しました!"); }); /* ================= Vercelで公開(v1.1・画像LPメーカーProの実証済み実装を移植) ================= */ /* トークンはこの端末のsessionStorageのみに保存(タブを閉じると消える)。外部サーバーには送らない(Vercel APIを除く) */ async function strToBase64(str){ const dataUrl = await new Promise(res => { const fr = new FileReader(); fr.onload = () => res(fr.result); fr.readAsDataURL(new Blob([str])); }); return dataUrl.split(",")[1]; } const vSlug = v => v.trim().toLowerCase().replace(/[^a-z0-9-]/g,"-").replace(/^-+|-+$/g,"").slice(0,52); let heroVideoFile = null; /* ヒーロー動画の実ファイル(このタブの中だけ。公開時に assets/hero.mp4 として同梱) */ function fileToBase64(file){ return new Promise((res, rej) => { const fr = new FileReader(); fr.onload = () => res(fr.result.split(",")[1]); fr.onerror = rej; fr.readAsDataURL(file); }); } /* v1.1.3: 大きいファイル(動画など)対応。合計が約9MBを超えるときは、先に /v2/files へ1ファイルずつ送ってから、SHA1で参照する方式に切り替える(本文直埋めは10MB制限のため) */ /* シェア用OG画像(1200×630)。ヒーロー背景画像があれば上端から切り抜き、なければ世界観の色+見出しでカードを作る */ function buildOgBlob(){ return new Promise((resolve, reject) => { const W = 1200, H = 630; const c = document.createElement("canvas"); c.width = W; c.height = H; const x = c.getContext("2d"); const p = pal(); const finish = () => c.toBlob(b => b ? resolve(b) : reject(new Error("og")), "image/jpeg", 0.85); const drawTitleCard = () => { x.fillStyle = p.heroBg || "#222"; x.fillRect(0, 0, W, H); x.fillStyle = p.main || "#fff"; x.fillRect(0, H - 14, W, 14); const fam = state.headFontKind === "mincho" ? "'Shippori Mincho',serif" : state.headFontKind === "rounded" ? "'M PLUS Rounded 1c',sans-serif" : "'Noto Sans JP',sans-serif"; const title = (state.text.title || "").replace(/\n/g, " ").trim() || "LP"; x.fillStyle = p.heroText || "#fff"; x.textBaseline = "middle"; x.textAlign = "center"; let size = 72; x.font = "900 " + size + "px " + fam; while(x.measureText(title).width > W - 160 && size > 34){ size -= 4; x.font = "900 " + size + "px " + fam; } x.fillText(title, W/2, H/2 - 20); const sub = (state.text.sub || "").replace(/\n/g, " ").trim(); if(sub){ x.font = "500 30px 'Noto Sans JP',sans-serif"; x.globalAlpha = .85; x.fillText(sub.length > 40 ? sub.slice(0,40) + "…" : sub, W/2, H/2 + 60); x.globalAlpha = 1; } finish(); }; const drawFromImage = () => { const im = new Image(); im.onload = () => { const sh = Math.min(im.width * H / W, im.height); x.drawImage(im, 0, 0, im.width, sh, 0, 0, W, H); finish(); }; im.onerror = drawTitleCard; im.src = state.heroImg; }; /* v1.6.1: ヒーローが動画のときは、動画の1コマ(1秒地点)をシェア画像にする(旧: 画像が無いと「黒地に見出し」のカードになっていた。ゆめ・購入者の指摘)。 コマが取れなければ、背景画像→見出しカードの順で従来どおり */ if(state.useVideo && heroVideoFile){ const v = document.createElement("video"); v.muted = true; v.playsInline = true; v.preload = "auto"; const src = URL.createObjectURL(heroVideoFile); let done = false; const fallback = () => { if(done) return; done = true; URL.revokeObjectURL(src); state.heroImg ? drawFromImage() : drawTitleCard(); }; const timer = setTimeout(fallback, 8000); v.addEventListener("loadedmetadata", () => { try{ v.currentTime = Math.min(1, Math.max(0, (v.duration || 2) / 3)); }catch(e){ fallback(); } }); v.addEventListener("seeked", () => { if(done) return; try{ const vw = v.videoWidth, vh = v.videoHeight; if(!vw || !vh) throw new Error("no frame"); /* 1200×630に「覆う」形で中央を切り抜き */ const scale = Math.max(W / vw, H / vh), dw = vw * scale, dh = vh * scale; x.drawImage(v, (W - dw) / 2, (H - dh) / 2, dw, dh); done = true; clearTimeout(timer); URL.revokeObjectURL(src); finish(); }catch(e){ clearTimeout(timer); fallback(); } }); v.addEventListener("error", () => { clearTimeout(timer); fallback(); }); v.src = src; } else if(state.heroImg){ drawFromImage(); } else drawTitleCard(); }); } async function sha1Hex(buf){ const h = await crypto.subtle.digest("SHA-1", buf); return Array.from(new Uint8Array(h)).map(b => b.toString(16).padStart(2,"0")).join(""); } async function vercelDeployHtml(name, html, token, onStatus, extraBlobs){ let teamQ = ""; try{ const u = await fetch("https://api.vercel.com/v2/user",{headers:{Authorization:"Bearer "+token}}).then(r=>r.json()); if(u && u.user && u.user.defaultTeamId) teamQ = "?teamId=" + encodeURIComponent(u.user.defaultTeamId); }catch(e){ /* 取れなければ個人スコープで続行 */ } /* 送るファイル一覧(index.html + 動画などの追加ファイル) */ const items = [{file:"index.html", blob:new Blob([html], {type:"text/html"})}].concat(extraBlobs || []); const total = items.reduce((s,x) => s + x.blob.size, 0); let files; if(total * 1.37 < 9 * 1024 * 1024){ onStatus("アップロード中…(" + (total/1024/1024).toFixed(1) + "MB)"); files = []; for(const it of items) files.push({file:it.file, data:await fileToBase64(it.blob), encoding:"base64"}); } else { files = []; for(const it of items){ onStatus(it.file + " を送信中…(" + (it.blob.size/1024/1024).toFixed(1) + "MB)"); const buf = await it.blob.arrayBuffer(); const sha = await sha1Hex(buf); const up = await fetch("https://api.vercel.com/v2/files"+teamQ, { method:"POST", headers:{Authorization:"Bearer "+token, "Content-Type":"application/octet-stream", "x-vercel-digest":sha}, body:buf }); if(!up.ok && up.status !== 409){ let msg = await up.text(); try{ msg = JSON.parse(msg).error && JSON.parse(msg).error.message || msg; }catch(e){} throw new Error("ファイル送信に失敗(" + it.file + "・Vercel " + up.status + "): " + msg); } files.push({file:it.file, sha:sha, size:buf.byteLength}); } onStatus("ファイル送信完了。公開を作成中…"); } const res = await fetch("https://api.vercel.com/v13/deployments"+teamQ,{ method:"POST", headers:{Authorization:"Bearer "+token,"Content-Type":"application/json"}, body:JSON.stringify({name:name, target:"production", files:files, projectSettings:{framework:null}}) }); if(!res.ok){ let msg = await res.text(); try{ msg = JSON.parse(msg).error && JSON.parse(msg).error.message || msg; }catch(e){} if(res.status===401||res.status===403) msg += "\n👉 トークンが無効かも。vercel.com/account/tokens で作り直してください。"; throw new Error("Vercel "+res.status+": "+msg); } const dep = await res.json(); onStatus("公開処理中…(数秒待ってね)"); let ready = dep.readyState === "READY"; for(let i=0;i<30 && !ready;i++){ await new Promise(r=>setTimeout(r,2000)); const st = await fetch("https://api.vercel.com/v13/deployments/"+dep.id+teamQ,{headers:{Authorization:"Bearer "+token}}).then(r=>r.json()); if(st.readyState==="READY") ready = true; else if(st.readyState==="ERROR"||st.readyState==="CANCELED") throw new Error("Vercel側で処理が失敗しました。少し待ってもう一度試してください"); } return "https://"+name+".vercel.app"; } document.getElementById("publishBtn").addEventListener("click", () => { openUtil("🚀 Vercelで公開", body => { const tok = sessionStorage.getItem("kisekae-vercel-token") || ""; body.innerHTML = '
⚠️ 2026-09-14 公開先の変更:Vercelの無料プラン(Hobby)は規約で商用利用が禁止です(お店・商品・サービスのLPは商用にあたります)。無料で公開したい方は、公開ガイド(Cloudflare Pages・ドラッグ&ドロップ・無料で商用OK)の手順を使ってください。下の「Vercelに公開」はVercel有料プランの方向けに残しています。
⚠️ Vercelの無料プラン(Hobby)は、規約で「個人・非商用のみ」です。お店・商品・サービスのLPなど商用で使う場合は、Vercelに公開せず、公開ガイド(Cloudflare・無料で商用OK)の手順で公開してください。ここから公開した内容についての規約上の責任は負えません(ご自身の判断でお使いください)。 → 公開ガイド

作ったLPを、そのままあなたのVercelアカウントで公開します(無料プランでOK)。はじめての人は 画像つき公開ガイド を見ながらどうぞ。

' + '

① Vercelトークン vercel.com/account/tokens で作成。このタブを閉じるまでしか保存されません)

' + '' + '

② 公開する名前 (URLになります。半角英数とハイフン。例: yume-nail-lp)

' + '' + '' + '' + '

・同じ名前でもう一度公開すると、そのURLの中身が更新されます(=修正もこのボタンでOK)
・公開したページは誰でも見られます。先に「🩺 公開前チェック」をどうぞ

'; if(tok) body.querySelector("#vTok").value = tok; body.querySelector("#vGo").addEventListener("click", async () => { const token = body.querySelector("#vTok").value.trim(); const name = vSlug(body.querySelector("#vName").value); const out = body.querySelector("#vOut"); const go = body.querySelector("#vGo"); if(!token) return showToast("①にトークンを入れてください"); if(!name) return showToast("②に公開する名前を入れてください"); if(!state.cta.url){ out.style.display = "block"; out.textContent = "❌ CTAボタンのリンク先(URL)が空です。プレビューのボタンをクリック → 「リンク先URL」に予約フォームやLINEのURLを入れてから公開してください。"; return; } sessionStorage.setItem("kisekae-vercel-token", token); body.querySelector("#vName").value = name; go.disabled = true; go.textContent = "公開中…"; out.style.display = "block"; out.textContent = "HTMLを組み立てています…"; try{ const extra = []; if(state.useVideo){ if(!heroVideoFile) throw new Error("ヒーロー動画が見つかりません。ページを開き直すと動画は消えるので、ヒーローの「動画背景」からもう一度選んでから公開してください"); const mb = heroVideoFile.size/1024/1024; if(mb > 60) throw new Error("動画が大きすぎます(" + mb.toFixed(1) + "MB)。30MB以下に圧縮してから入れ直してください(スマホの動画アプリやCapCutの書き出し画質を下げるだけでOK)"); out.textContent = "動画を準備中…(" + mb.toFixed(1) + "MB" + (mb > 20 ? "・大きめなので時間がかかります" : "") + ")"; extra.push({file:"assets/hero.mp4", blob:heroVideoFile}); } /* v1.1.4: シェア用画像(og.jpg 1200×630)を自動生成して同梱。URLを貼ったとき画像つきカードで出る */ out.textContent = "シェア用の画像を作成中…"; const ogBlob = await buildOgBlob().catch(() => null); /* v1.6.1: 版番号を付けて、LINE/Threads側が古いシェア画像を使い回しにくくする(ページURL自体の記憶は向こうの都合で残ることがある→共有時にURL末尾へ ?2 を付けると新しく読み直される) */ const ogUrl = ogBlob ? "https://" + name + ".vercel.app/og.jpg?v=" + Date.now() : ""; if(ogBlob) extra.push({file:"og.jpg", blob:ogBlob}); const url = await vercelDeployHtml(name, exportHtml(ogUrl), token, s => { out.textContent = s; }, extra); out.innerHTML = '✅ 公開しました!
'+url+'
更新したいときは、同じ名前でもう一度公開してね。まちがえて別のLPで同じ名前を使うと上書きされるので注意!'; out.querySelector("#vCopy").addEventListener("click", ()=>navigator.clipboard.writeText(url).then(()=>showToast("URLをコピーしました"))); body.querySelector("#vName").value = ""; showToast("Vercelに公開しました🎉"); }catch(e){ out.textContent = "❌ " + e.message; } finally{ go.disabled = false; go.textContent = "🚀 この内容で公開する"; } }); }); }); /* ================= その他UI ================= */ document.getElementById("deviceSeg").addEventListener("click", e => { const b = e.target.closest("button"); if(!b) return; document.querySelectorAll("#deviceSeg button").forEach(x => x.classList.remove("on")); b.classList.add("on"); document.getElementById("deviceFrame").classList.toggle("pc", b.dataset.dev === "pc"); }); let toastTimer; function showToast(msg){ const t = document.getElementById("toast"); t.textContent = msg; t.classList.add("on"); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove("on"), 1600); } /* ================= 便利ツール群(チェッカー・きせかえ・設計書とりこみ) ================= */ const utilModal = document.getElementById("utilModal"); function openUtil(title, build){ document.getElementById("utilTitle").textContent = title; const body = document.getElementById("utilBody"); body.innerHTML = ""; build(body); utilModal.classList.add("on"); } document.getElementById("utilClose").addEventListener("click", () => utilModal.classList.remove("on")); utilModal.addEventListener("click", e => { if(e.target === utilModal) utilModal.classList.remove("on"); }); /* --- 公開前チェック --- */ document.getElementById("checkerBtn").addEventListener("click", () => { const t = state.text; const finds = []; const NG = /(絶対|必ず|保証|稼げ|儲か|誰でも簡単|副業|今すぐ)/; const scan = (label, s) => { const m = (s || "").match(NG); if(m) finds.push("⚠️ " + label + "に「" + m[1] + "」— 収入保証・煽りに見える言葉です。事実ベースの表現に変えるのがおすすめ"); }; scan("見出し", t.title); scan("サブコピー", t.sub); scan("バッジ", t.badge); scan("CTAボタン", state.cta.label); scan("注記", t.ctaNote); scan("最後のひと押し", t.closeTitle); t.pains.forEach((x,i) => scan("悩み" + (i+1), x)); t.benefits.forEach((b,i) => { scan("できること" + (i+1), b[1]); scan("できること" + (i+1) + "の説明", b[2]); }); (t.steps || []).forEach((s,i) => { scan("ステップ" + (i+1), s[0]); scan("ステップ" + (i+1) + "の説明", s[1]); }); t.faqs.forEach((q,i) => { scan("FAQ" + (i+1), q[0]); scan("FAQ" + (i+1) + "の答え", q[1]); }); if([...t.title.replace(/\n/g,"")].length > 32) finds.push("⚠️ 見出しが長め(" + [...t.title.replace(/\n/g,"")].length + "文字)。スマホで3行を超えると読まれにくいです"); if([...t.sub].length > 70) finds.push("⚠️ サブコピーが長め。2文以内が目安です"); if([...state.cta.label].length > 14) finds.push("⚠️ ボタンの文字が長め。8〜12文字が押されやすい目安です"); if(state.show.price && !t.price.trim()) finds.push("⚠️ 価格セクションが表示中なのに価格が空です"); if(!String(t.title || "").trim()) finds.push("⚠️ 見出しが空です(公開すると空白で出ます)"); if(!String(t.sub || "").trim()) finds.push("⚠️ サブコピーが空です(不要なら短く1行でも入れるのがおすすめ)"); if(!String(state.cta.label || "").trim()) finds.push("⚠️ ボタンの文字が空です"); if(state.cta2 && ((state.cta2.label || "").trim() ? !(state.cta2.url || "").trim() : !!(state.cta2.url || "").trim())) finds.push("⚠️ サブボタンは「文字」と「URL」の両方が必要です(片方だけだと出ません)"); if(state.cta2 && (state.cta2.label || "").trim()) scan("サブボタン", state.cta2.label); if(state.show.sns && !Object.values(state.sns || {}).some(v => (v || "").trim()) && !(state.qrs || []).some(q => (q[0] || "").trim())) finds.push("⚠️ 「SNS・QR」が表示中なのに、URLが1つも入っていません(OFFにするかURLを入れてください)"); if(!state.cta.url) finds.push("⚠️ CTAボタンのリンク先(URL)が空です。このままだとボタンを押しても何も起きません(ボタンをクリック→リンク先URL)"); else if(!/^(https?:\/\/|tel:|mailto:)/.test(state.cta.url)) finds.push("⚠️ CTAのリンク先が https:// (電話なら tel:0X0-XXXX-XXXX、メールなら mailto:)で始まっていません(「" + state.cta.url.slice(0,30) + "」)"); if(t.pains.some(x => x.includes("新しい悩みを書く")) || t.faqs.some(q => q[0].includes("新しい質問"))) finds.push("⚠️ 書きかけの行(「新しい〜を書く」)が残っています"); if(state.heroTextColor && state.heroImg === "" && isDarkColor(state.heroTextColor) === isDarkColor(pal().heroBg)) finds.push("⚠️ ヒーローの文字色と背景色が近くて読みにくいかも"); if(state.show.voice && t.voices.some(v => v[0].includes("新しい声"))) finds.push("⚠️ お客様の声に書きかけの行があります"); openUtil("🩺 公開前チェック", body => { if(finds.length === 0){ body.innerHTML = "

✅ 目立った問題は見つかりませんでした!

"; } else { body.innerHTML = finds.map(f => "

" + f + "

").join(""); } body.innerHTML += '

※これは簡易チェックです。薬機法・景表法などの法律判断はできません。販売ページとして公開する前に、AIでの検品(yume-checker)もおすすめします。

'; }); }); /* --- きせかえコード --- */ const KISEKAE_KEYS = ["palette","heroLayout","headAnim","heroFx","tapFx","opening","bgFixed","headFontKind"]; document.getElementById("kisekaeBtn").addEventListener("click", () => { const obj = {}; KISEKAE_KEYS.forEach(k => obj[k] = state[k]); obj.cta = {shape: state.cta.shape, hover: state.cta.hover}; const code = "YUME1." + btoa(unescape(encodeURIComponent(JSON.stringify(obj)))); openUtil("🎁 きせかえコード", body => { body.innerHTML = '

いまの世界観・演出の設定だけ(文章・画像は含まない)をコードにしました。配布したり、もらったコードを取り込んだりできます。

' + '" + '' + '

もらったコードを取り込む:

' + '' + ''; body.querySelector("#kkCopy").addEventListener("click", () => { navigator.clipboard.writeText(code).then(() => showToast("コピーしました!")); }); body.querySelector("#kkApply").addEventListener("click", () => { try{ const raw = body.querySelector("#kkIn").value.trim(); const json = JSON.parse(decodeURIComponent(escape(atob(raw.replace(/^YUME1\./, ""))))); KISEKAE_KEYS.forEach(k => { if(json[k] !== undefined) state[k] = json[k]; }); if(state.opening === "curtain") state.opening = "spotlight"; /* v1.5: 旧コードの「幕が開く」 */ if(json.cta){ state.cta.shape = json.cta.shape || state.cta.shape; state.cta.hover = json.cta.hover || state.cta.hover; } migrate(); render(); utilModal.classList.remove("on"); showToast("着せ替えました!"); }catch(e){ showToast("コードが読めませんでした…"); } }); }); }); /* --- 設計書とりこみ(β) --- */ document.getElementById("importBrief").addEventListener("click", () => { openUtil("📥 設計書とりこみ(β)", body => { body.innerHTML = '

LP制作メーカーで作った設計書やHTML化プロンプトを丸ごと貼り付けると、読み取れた項目を流し込みます(【商品・サービス名】などの見出しを探します)。

' + '' + ''; body.querySelector("#briefApply").addEventListener("click", () => { const txt = body.querySelector("#briefIn").value; if(!txt.trim()){ showToast("設計書を貼り付けてください"); return; } const map = {}; const re = /【(.+?)】\s*\n?([\s\S]*?)(?=\n【|━|$)/g; let m; while((m = re.exec(txt))){ map[m[1].trim()] = m[2].trim(); } const bad = /^(記載なし|未定|指定なし|未設定|未入力)/; const g = part => { for(const key in map){ if(key.includes(part) && map[key] && !bad.test(map[key])) return map[key].split("\n")[0].trim(); } return ""; }; let n = 0; const head = g("見出し案1") || g("ファーストビュー見出し") || g("商品・サービス名"); if(head){ state.text.title = head; n++; } const sub = g("サブコピー"); if(sub){ state.text.sub = sub; n++; } const painsRaw = (() => { for(const key in map){ if((key.includes("一番の悩み") || key.includes("読者の現在地") || key.includes("ペイン")) && map[key] && !bad.test(map[key])) return map[key]; } return ""; })(); if(painsRaw){ const items = painsRaw.split(/\n|。/).map(s => s.replace(/^[-・\s]+/, "").trim()).filter(s => s.length > 4).slice(0, 4); if(items.length){ state.text.pains = items; n++; } } const price = g("価格"); if(price){ state.text.price = /^[\d,]+$/.test(price) ? "¥" + price : price; n++; } const ctaTxt = g("CTA文言") || g("主CTA") || g("ボタン文言"); if(ctaTxt){ state.cta.label = ctaTxt; n++; } const bonus = g("特典"); if(bonus){ state.text.ctaNote = bonus; n++; } migrate(); render(); utilModal.classList.remove("on"); showToast(n ? n + "項目を取り込みました!" : "読み取れる項目が見つかりませんでした…"); }); }); }); render(); refreshShelf(); resetHistoryBaseline();