サイト解析・統合メーカー

福原弘基が作成しました
福原弘基が作成しました
プロ仕様の単一HTMLツール — CORS経由で解析・統合・コピーを一発で

入力とオプション

複雑なサイトは時間がかかる場合があります。https:// を含めて入力してください。

解析・統合オプション

大きな画像はサイズ・時間的に重くなります。適切に調整してください。

解析ログと結果

URL
プロキシ
状態待機中
準備完了。
注: CSP/X-Frame-Optionsなど元サイトの制約は、コンテンツ取得には影響しません(fetch+プロキシ使用)。同一動作の完全再現はサイト依存です。
`); }else if(sc.abs){ try{ const r = await fetchResource(sc.abs, proxyIndex, opts.timeoutMs); let js = (r.type==='text'? r.content : ''); if(opts.stripTracking){ js = stripTrackingJS(js); } headParts.push(`\n/* ${escapeHtml(sc.abs)} */\n${js}\n`); log(`JS inline: ${sc.abs}`); }catch(e){ log(`JS failed: ${sc.abs} ${e.message}`, 'warn'); const src = opts.rewriteLinks ? PROXIES[proxyIndex].build(sc.abs) : sc.abs; headParts.push(``); } } } }else{ scripts.forEach(sc=>{ if(sc.inline){ let js = sc.text||''; if(opts.stripTracking){ js = stripTrackingJS(js); } headParts.push(`\n${js}\n`); }else if(sc.abs){ const src = opts.rewriteLinks ? PROXIES[proxyIndex].build(sc.abs) : sc.abs; headParts.push(``); } }); } // 本文構築: 元HTMLの を取得 const bodyEl = state.dom?.doc?.body || analyzeHtml(state.htmlText, baseUrl).doc.body; let bodyHtml = bodyEl ? bodyEl.innerHTML : (state.htmlText || ''); bodyHtml = rewriteBodyResources(bodyHtml, baseUrl, proxyIndex, imgMap, opts); // ヘッダに「福原弘基が作成しました」明記 bodyParts.push(`
福原弘基が作成しました このページは統合された単一HTML版です
`); bodyParts.push(bodyHtml); const built = ` ${escapeHtml(title||'統合版')} ${headParts.join('\n')} ${bodyParts.join('\n')} `; return built; } function rewriteBodyResources(html, baseUrl, proxyIndex, imgMap, opts){ // href/src/srcset などをプロキシ化 or dataURL化 // シンプルな属性置換(HTML構文の揺れに注意) const proxify = u => PROXIES[proxyIndex].build(toAbs(baseUrl, u)); let out = html; // 画像 src → dataURL または proxify out = out.replace(/(]*\bsrc\s*=\s*["'])([^"']+)(["'][^>]*>)/gi, (m,p1,url,p3)=>{ const abs = toAbs(baseUrl, url); const data = imgMap.get(abs); if(data) return `${p1}${data}${p3}`; if(opts.rewriteLinks) return `${p1}${proxify(url)}${p3}`; return m; }); // srcset out = out.replace(/(\bsrcset\s*=\s*["'])([^"']+)(["'])/gi, (m,p1,list,p3)=>{ if(!opts.rewriteLinks && imgMap.size===0) return m; const items = list.split(',').map(s=>{ const [u, w] = s.trim().split(/\s+/); const abs = toAbs(baseUrl, u); const data = imgMap.get(abs); const nu = data ? data : (opts.rewriteLinks ? proxify(u) : u); return [nu, w].filter(Boolean).join(' '); }).join(', '); return `${p1}${items}${p3}`; }); // リンク href if(opts.rewriteLinks){ out = out.replace(/(]*\bhref\s*=\s*["'])([^"']+)(["'][^>]*>)/gi, (m,p1,url,p3)=>{ const abs = toAbs(baseUrl, url); return `${p1}${proxify(abs)}${p3}`; }); } // 危険なインラインイベント属性除去 if(opts.sanitizeInline){ out = out.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, ''); out = out.replace(/\sjavascript\s*:/gi, '#'); } return out; } function stripTrackingJS(js){ // 極めて単純なヒューリスティック: 代表的なトラッキング/ビーコンを取り除く(過剰除去の可能性あり) const patterns = [ /gtag\(.*?\);?/gs, /ga\(.*?\);?/gs, /googletagmanager\.com\/gtm\.js/gi, /analytics\.js/gi, /fbevents\.js/gi, /pixel\.js/gi, /hotjar\.com/gi, /mixpanel/gi, /segment\.com/gi, /amplitude/gi, /yandex\.ru\/metrika/gi, /beacon/gi ]; let out = js; patterns.forEach(re=>{ out = out.replace(re, '/* stripped */'); }); return out; } function escapeHtml(s){ return String(s).replace(/[&<>"']/g, c=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } // ========= ボタン動作 ========= $('#probe').addEventListener('click', async ()=>{ probeTags.innerHTML = ''; const target = $('#targetUrl').value.trim(); const idx = parseInt(proxySelect.value,10); if(!target) return; try{ stateStatus.textContent = 'テスト中'; stateStatus.className = 'status-warn'; const r = await fetchViaProxy(target, idx, parseInt($('#timeoutMs').value,10)||15000); const pill = document.createElement('div'); pill.className = 'pill'; pill.innerHTML = `${escapeHtml(PROXIES[idx].name)} HTTP ${r.status} ${escapeHtml((r.text||'').slice(0,80).replace(/\n/g,' '))}...`; probeTags.appendChild(pill); stateStatus.textContent = r.ok?'成功':'失敗'; stateStatus.className = r.ok?'status-ok':'status-bad'; }catch(e){ stateStatus.textContent = '失敗'; stateStatus.className = 'status-bad'; log(`プロキシテスト失敗: ${e.message}`, 'error'); } }); $('#probeAll').addEventListener('click', async ()=>{ probeTags.innerHTML = ''; const target = $('#targetUrl').value.trim(); if(!target) return; stateStatus.textContent = '一括テスト中'; stateStatus.className = 'status-warn'; for(let i=0;i${escapeHtml(PROXIES[i].name)} HTTP ${r.status}`; probeTags.appendChild(pill); }catch(e){ const pill = document.createElement('div'); pill.className = 'pill'; pill.innerHTML = `${escapeHtml(PROXIES[i].name)} ERROR`; probeTags.appendChild(pill); } } stateStatus.textContent = '完了'; stateStatus.className = 'status-ok'; }); $('#analyzeBtn').addEventListener('click', async ()=>{ logBox.textContent = ''; singleHtmlBox.value = ''; const target = $('#targetUrl').value.trim(); const idx = parseInt(proxySelect.value,10); state.url = target; state.proxyIndex = idx; updateStateHeader(); const timeoutMs = parseInt($('#timeoutMs').value,10)||15000; const autoFallback = $('#autoFallback').checked; try{ stateStatus.textContent = '解析中'; stateStatus.className = 'status-warn'; const r = autoFallback ? await tryFetchWithFallback(target, idx, timeoutMs) : await fetchViaProxy(target, idx, timeoutMs); state.proxyIndex = r.proxyIdx ?? idx; state.htmlText = r.text; state.dom = analyzeHtml(r.text, target); const s = { title: state.dom.title, counts: { stylesheets: state.dom.stylesheets.length, styleTags: state.dom.styleTags.length, scripts: state.dom.scripts.length, images: state.dom.images.length, links: state.dom.links.length, metas: state.dom.metas.length } }; state.summary = s; summaryBox.value = JSON.stringify(s, null, 2); stateStatus.textContent = '解析完了'; stateStatus.className = 'status-ok'; log(`解析完了: title="${s.title}" CSS[ext=${s.counts.stylesheets}, inline=${s.counts.styleTags}] JS[${s.counts.scripts}] IMG[${s.counts.images}] LINK[${s.counts.links}] META[${s.counts.metas}]`); }catch(e){ stateStatus.textContent = '失敗'; stateStatus.className = 'status-bad'; log(`解析失敗: ${e.message}`, 'error'); } }); $('#buildBtn').addEventListener('click', async ()=>{ if(!state.htmlText){ log('先に「サイトを解析」を実行してください','warn'); return; } stateStatus.textContent = '統合中'; stateStatus.className = 'status-warn'; try{ const opts = { inlineCss: $('#inlineCss').checked, inlineJs: $('#inlineJs').checked, inlineImages: $('#inlineImages').checked, rewriteLinks: $('#rewriteLinks').checked, stripTracking: $('#stripTracking').checked, sanitizeInline: $('#sanitizeInline').checked, addCsp: $('#addCsp').checked, imgMax: $('#imgMax').value, timeoutMs: parseInt($('#timeoutMs').value,10)||15000 }; const built = await buildSingleFile(state, opts); singleHtmlBox.value = built; stateStatus.textContent = '統合完了'; stateStatus.className = 'status-ok'; log('統合HTMLの生成が完了しました。'); }catch(e){ stateStatus.textContent = '失敗'; stateStatus.className = 'status-bad'; log(`統合失敗: ${e.message}`, 'error'); } }); $('#downloadBtn').addEventListener('click', ()=>{ if(!singleHtmlBox.value){ log('統合HTMLが未生成です','warn'); return; } const blob = new Blob([singleHtmlBox.value], {type:'text/html;charset=utf-8'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); const safeTitle = (state.summary?.title || 'single-file').replace(/[^\w\u3040-\u30ff\u4e00-\u9faf-]+/g,'_').slice(0,70); a.download = `${safeTitle || 'single-file'}.html`; document.body.appendChild(a); a.click(); a.remove(); log('ダウンロードを開始しました'); }); $('#copyHtml').addEventListener('click', async ()=>{ if(!singleHtmlBox.value){ log('コピー対象がありません','warn'); return; } try{ await navigator.clipboard.writeText(singleHtmlBox.value); log('統合HTMLをクリップボードへコピーしました'); }catch(e){ log('クリップボードへの書き込みに失敗しました','warn'); } }); $('#minifyHtml').addEventListener('click', ()=>{ if(!singleHtmlBox.value){ log('統合HTMLが未生成です','warn'); return; } // ざっくりとした軽量化(改行/インデント削減) — 動作に影響する場合あり singleHtmlBox.value = singleHtmlBox.value .replace(/>\s+<') .replace(/\n{2,}/g,'\n') .replace(/[ \t]{2,}/g,' '); log('軽量化しました(内容によっては不具合の可能性があります)','warn'); }); $('#resetBtn').addEventListener('click', ()=>{ logBox.textContent = '準備完了。'; summaryBox.value = ''; singleHtmlBox.value = ''; stateUrl.textContent = '—'; stateProxy.textContent = '—'; stateStatus.textContent = '待機中'; stateStatus.className = 'status-warn'; probeTags.innerHTML = ''; log('状態をリセットしました'); }); // ========= 初期状態 ========= updateStateHeader(); log('ツールが読み込まれました。対象URLを入力し、プロキシを選択して解析してください。');