Spaces:
Sleeping
Sleeping
| import JSZip from 'jszip'; | |
| import { useState } from 'react'; | |
| interface UseCodeDownloadOptions { | |
| proposalName: string; | |
| } | |
| export function useCodeDownload({ proposalName }: UseCodeDownloadOptions) { | |
| const [isDownloading, setIsDownloading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const downloadCode = async (fvHtml: string, cnHtml: string, fvCss: string, cnCss: string, defaultCss: string, mainCopy?: string) => { | |
| setIsDownloading(true); | |
| setError(null); | |
| // no-image.webpが含まれているかチェック | |
| const noImageCount = (cnHtml.match(/\/images\/no-image\.webp/g) || []).length; | |
| console.log('[Download] no-image.webp occurrences in CN HTML:', noImageCount); | |
| try { | |
| const zip = new JSZip(); | |
| // FVとCNのHTMLから画像を分離して管理 | |
| const fvImages: { [key: string]: string } = {}; | |
| const cnImages: { [key: string]: string } = {}; | |
| let fvImageCounter = 1; | |
| let cnImageCounter = 1; | |
| // デバッグ: 元のHTMLの画像形式を確認 | |
| const fvImgTagMatches = fvHtml.match(/<img[^>]+src=["'][^"']+["'][^>]*>/gi); | |
| console.log('[Download] Found img tags in FV HTML:', fvImgTagMatches?.length || 0); | |
| if (fvImgTagMatches && fvImgTagMatches.length > 0) { | |
| console.log('[Download] Sample FV img tag:', fvImgTagMatches[0].substring(0, 200)); | |
| } | |
| const cnImgTagMatches = cnHtml.match(/<img[^>]+src=["'][^"']+["'][^>]*>/gi); | |
| console.log('[Download] Found img tags in CN HTML:', cnImgTagMatches?.length || 0); | |
| if (cnImgTagMatches && cnImgTagMatches.length > 0) { | |
| console.log('[Download] Sample CN img tag:', cnImgTagMatches[0].substring(0, 200)); | |
| // data:image/jpeg;base64の形式かチェック | |
| const dataUrlMatches = cnHtml.match(/src=["']data:image\/jpeg;base64,[^"']+["']/gi); | |
| console.log('[Download] Found data:image/jpeg;base64 patterns:', dataUrlMatches?.length || 0); | |
| if (dataUrlMatches && dataUrlMatches.length > 0) { | |
| console.log('[Download] Sample data URL:', dataUrlMatches[0].substring(0, 100) + '...'); | |
| } | |
| } | |
| // FV画像の抽出と置換 - より柔軟なパターンマッチング | |
| let processedFvHtml = fvHtml; | |
| // Base64画像の処理(スペースや改行を許容) | |
| processedFvHtml = processedFvHtml.replace(/src\s*=\s*["']data:image\/([^;]+);base64,\s*([^"']+)["']/gi, (match, format, base64Data) => { | |
| // Base64データから空白文字を除去 | |
| const cleanBase64 = base64Data.replace(/\s+/g, ''); | |
| const imageName = `fv_img_${fvImageCounter.toString().padStart(3, '0')}.${format}`; | |
| fvImages[imageName] = cleanBase64; | |
| console.log(`[Download] Extracted FV image ${imageName}, size: ${cleanBase64.length}`); | |
| fvImageCounter++; | |
| return `src="common/images/fv/${imageName}"`; | |
| }); | |
| // コンテンツ画像の抽出と置換 | |
| let processedCnHtml = cnHtml; | |
| // Base64画像の処理(スペースや改行を許容) | |
| processedCnHtml = processedCnHtml.replace(/src\s*=\s*["']data:image\/([^;]+);base64,\s*([^"']+)["']/gi, (match, format, base64Data) => { | |
| // Base64データから空白文字を除去 | |
| const cleanBase64 = base64Data.replace(/\s+/g, ''); | |
| const imageName = `cn_img_${cnImageCounter.toString().padStart(3, '0')}.${format}`; | |
| cnImages[imageName] = cleanBase64; | |
| console.log(`[Download] Extracted CN image ${imageName}, size: ${cleanBase64.length}`); | |
| cnImageCounter++; | |
| return `src="common/images/content/${imageName}"`; | |
| }); | |
| // URLで参照されている画像を抽出して処理 | |
| const fvImageUrls = new Set<string>(); | |
| const cnImageUrls = new Set<string>(); | |
| // FV画像URLの抽出(元のHTMLと処理後のHTML両方から抽出) | |
| // 元のHTMLからURL画像を抽出(Base64に変換される前のURL画像を取得) | |
| const originalFvImgUrlMatches = fvHtml.matchAll(/<img[^>]+src=["']([^"']+)["']/gi); | |
| for (const match of originalFvImgUrlMatches) { | |
| const url = match[1]; | |
| console.log('[Download] Found FV img src in original HTML:', url); | |
| // HTTPのURLまたは相対パス(/で始まる)の画像を処理 | |
| if ((url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//') || url.startsWith('/')) && !url.includes('data:image')) { | |
| fvImageUrls.add(url); | |
| console.log('[Download] Added to fvImageUrls from original:', url); | |
| } | |
| } | |
| // 処理後のHTMLからもURL画像を抽出(Base64処理されなかったURL画像を取得) | |
| const processedFvImgUrlMatches = processedFvHtml.matchAll(/<img[^>]+src=["']([^"']+)["']/gi); | |
| for (const match of processedFvImgUrlMatches) { | |
| const url = match[1]; | |
| console.log('[Download] Found FV img src in processed HTML:', url); | |
| // HTTPのURLまたは相対パス(/で始まる)の画像を処理(common/images/fvは除外) | |
| if ( | |
| (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//') || url.startsWith('/')) && | |
| !url.includes('data:image') && | |
| !url.includes('common/images/fv') | |
| ) { | |
| fvImageUrls.add(url); | |
| console.log('[Download] Added to fvImageUrls from processed:', url); | |
| } | |
| } | |
| // コンテンツ画像URLの抽出 | |
| const cnImgUrlMatches = processedCnHtml.matchAll(/<img[^>]+src=["']([^"']+)["']/gi); | |
| console.log('[Download] Extracting CN image URLs from processedCnHtml'); | |
| for (const match of cnImgUrlMatches) { | |
| const url = match[1]; | |
| console.log('[Download] Found img src in CN HTML:', url); | |
| // cn-image-error.webpは処理をスキップ(既にプレースホルダー) | |
| if (url.includes('/images/cn-image-error.webp')) { | |
| console.log('[Download] Skipping cn-image-error.webp'); | |
| continue; | |
| } | |
| // HTTPのURLまたは相対パス(/で始まる)の画像を処理 | |
| if ((url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//') || url.startsWith('/')) && !url.includes('data:image')) { | |
| cnImageUrls.add(url); | |
| console.log('[Download] Added to cnImageUrls:', url); | |
| } | |
| } | |
| console.log('[Download] Found FV image URLs:', fvImageUrls.size); | |
| console.log('[Download] Found CN image URLs:', cnImageUrls.size); | |
| // プレースホルダー画像のパスを相対パスに置換(最後の処理としても実行) | |
| const replaceNoImagePaths = () => { | |
| // cn-image-error.webp(AI生成エラー時)のパス置換 | |
| const hasErrorImageInCn = processedCnHtml.includes('/images/cn-image-error.webp'); | |
| if (hasErrorImageInCn) { | |
| console.log('[Download] cn-image-error.webp placeholder detected in content HTML'); | |
| processedCnHtml = processedCnHtml.replace(/src="\/images\/cn-image-error\.webp"/gi, 'src="common/images/cn-image-error.webp"'); | |
| console.log('[Download] Replaced cn-image-error.webp paths'); | |
| } | |
| // no-image.webp(画像クリア機能使用時)のパス置換 | |
| const hasNoImageInCn = processedCnHtml.includes('/images/no-image.webp'); | |
| if (hasNoImageInCn) { | |
| console.log('[Download] no-image.webp placeholder detected in content HTML'); | |
| processedCnHtml = processedCnHtml.replace(/src="\/images\/no-image\.webp"/gi, 'src="common/images/no-image.webp"'); | |
| console.log('[Download] Replaced no-image.webp paths'); | |
| } | |
| }; | |
| // 初回のパス置換 | |
| replaceNoImagePaths(); | |
| // FV画像URLからダウンロード | |
| let urlFvImageCounter = fvImageCounter; | |
| for (const imageUrl of fvImageUrls) { | |
| try { | |
| // 相対パスの場合は現在のオリジンを使用 | |
| let fullUrl = imageUrl; | |
| if (imageUrl.startsWith('//')) { | |
| fullUrl = `https:${imageUrl}`; | |
| } else if (imageUrl.startsWith('/')) { | |
| // 相対パスの場合は現在のオリジンを追加 | |
| fullUrl = `${window.location.origin}${imageUrl}`; | |
| } | |
| console.log(`[Download] Fetching FV image from: ${fullUrl}`); | |
| const response = await fetch(fullUrl); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| const buffer = Buffer.from(arrayBuffer); | |
| const base64String = buffer.toString('base64'); | |
| // 拡張子を取得 | |
| const urlParts = imageUrl.split('?')[0].split('.'); | |
| const extension = urlParts[urlParts.length - 1] || 'jpg'; | |
| const imageName = `fv_img_${urlFvImageCounter.toString().padStart(3, '0')}.${extension}`; | |
| fvImages[imageName] = base64String; | |
| // HTMLの画像パスを置換(正規表現をエスケープ) | |
| const escapedUrl = imageUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| processedFvHtml = processedFvHtml.replace(new RegExp(`src=["']${escapedUrl}["']`, 'gi'), `src="common/images/fv/${imageName}"`); | |
| console.log(`[Download] Downloaded FV image: ${imageName} (${base64String.length} bytes)`); | |
| urlFvImageCounter++; | |
| } else { | |
| console.error(`[Download] Failed to fetch FV image (${response.status}): ${fullUrl}`); | |
| } | |
| } catch (error) { | |
| console.error(`[Download] Error downloading FV image from ${imageUrl}:`, error); | |
| } | |
| } | |
| // コンテンツ画像URLからダウンロード(URLが存在する場合のみ) | |
| if (cnImageUrls.size > 0) { | |
| console.log('[Download] Processing CN image URLs...'); | |
| let urlCnImageCounter = cnImageCounter; | |
| for (const imageUrl of cnImageUrls) { | |
| try { | |
| // 相対パスの場合は現在のオリジンを使用 | |
| let fullUrl = imageUrl; | |
| if (imageUrl.startsWith('//')) { | |
| fullUrl = `https:${imageUrl}`; | |
| } else if (imageUrl.startsWith('/')) { | |
| // 相対パスの場合は現在のオリジンを追加 | |
| fullUrl = `${window.location.origin}${imageUrl}`; | |
| } | |
| console.log(`[Download] Fetching CN image from: ${fullUrl}`); | |
| const response = await fetch(fullUrl); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| const buffer = Buffer.from(arrayBuffer); | |
| const base64String = buffer.toString('base64'); | |
| // 拡張子を取得 | |
| const urlParts = imageUrl.split('?')[0].split('.'); | |
| const extension = urlParts[urlParts.length - 1] || 'jpg'; | |
| const imageName = `cn_img_${urlCnImageCounter.toString().padStart(3, '0')}.${extension}`; | |
| cnImages[imageName] = base64String; | |
| // HTMLの画像パスを置換(正規表現をエスケープ) | |
| const escapedUrl = imageUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| processedCnHtml = processedCnHtml.replace(new RegExp(`src=["']${escapedUrl}["']`, 'gi'), `src="common/images/content/${imageName}"`); | |
| console.log(`[Download] Downloaded CN image: ${imageName} (${base64String.length} bytes)`); | |
| urlCnImageCounter++; | |
| } else { | |
| console.error(`[Download] Failed to fetch CN image (${response.status}): ${fullUrl}`); | |
| } | |
| } catch (error) { | |
| console.error(`[Download] Error downloading CN image from ${imageUrl}:`, error); | |
| } | |
| } | |
| } else { | |
| console.log('[Download] No CN image URLs to download (all using cn-image-error.webp)'); | |
| } | |
| // FV CSS の処理(背景画像なども対応) | |
| let processedFvCss = fvCss; | |
| let fvBgImageCounter = 1; | |
| // CSSのurl()内のBase64画像を処理(クォート有無両対応、スペース許容) | |
| processedFvCss = processedFvCss.replace(/url\s*\(\s*["']?data:image\/([^;]+);base64,\s*([^"')]+)["']?\s*\)/gi, (match, format, base64Data) => { | |
| // Base64データから空白文字と括弧を除去 | |
| const cleanBase64 = base64Data.replace(/[\s)]+/g, ''); | |
| const imageName = `fv_bg_${fvBgImageCounter.toString().padStart(3, '0')}.${format}`; | |
| fvImages[imageName] = cleanBase64; | |
| console.log(`[Download] Extracted FV background image ${imageName}, size: ${cleanBase64.length}`); | |
| fvBgImageCounter++; | |
| return `url("common/images/fv/${imageName}")`; | |
| }); | |
| // CN CSS の処理(背景画像なども対応) | |
| let processedCnCss = cnCss; | |
| let cnBgImageCounter = 1; | |
| // CSSのurl()内のBase64画像を処理(クォート有無両対応、スペース許容) | |
| processedCnCss = processedCnCss.replace(/url\s*\(\s*["']?data:image\/([^;]+);base64,\s*([^"')]+)["']?\s*\)/gi, (match, format, base64Data) => { | |
| // Base64データから空白文字と括弧を除去 | |
| const cleanBase64 = base64Data.replace(/[\s)]+/g, ''); | |
| const imageName = `cn_bg_${cnBgImageCounter.toString().padStart(3, '0')}.${format}`; | |
| cnImages[imageName] = cleanBase64; | |
| console.log(`[Download] Extracted CN background image ${imageName}, size: ${cleanBase64.length}`); | |
| cnBgImageCounter++; | |
| return `url("common/images/content/${imageName}")`; | |
| }); | |
| // CSS内のURL画像も処理する関数 | |
| const processCssUrls = async (css: string, images: { [key: string]: string }, prefix: string): Promise<string> => { | |
| let processedCss = css; | |
| const urlMatches = [...css.matchAll(/url\s*\(\s*["']?([^"')]+)["']?\s*\)/gi)]; | |
| let bgCounter = prefix === 'fv' ? fvBgImageCounter : cnBgImageCounter; | |
| for (const match of urlMatches) { | |
| const url = match[1]; | |
| if ((url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) && !url.includes('data:image')) { | |
| try { | |
| const fullUrl = url.startsWith('//') ? `https:${url}` : url; | |
| console.log(`[Download] Fetching CSS background image from: ${fullUrl}`); | |
| const response = await fetch(fullUrl); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| const buffer = Buffer.from(arrayBuffer); | |
| const base64String = buffer.toString('base64'); | |
| // 拡張子を取得 | |
| const urlParts = url.split('?')[0].split('.'); | |
| const extension = urlParts[urlParts.length - 1] || 'jpg'; | |
| const imageName = `${prefix}_bg_${bgCounter.toString().padStart(3, '0')}.${extension}`; | |
| images[imageName] = base64String; | |
| // CSSの画像パスを置換(正規表現をエスケープ) | |
| const escapedUrl = url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| processedCss = processedCss.replace( | |
| new RegExp(`url\\s*\\(\\s*["']?${escapedUrl}["']?\\s*\\)`, 'gi'), | |
| `url("common/images/${prefix === 'fv' ? 'fv' : 'content'}/${imageName}")`, | |
| ); | |
| console.log(`[Download] Downloaded CSS background image: ${imageName}`); | |
| bgCounter++; | |
| } else { | |
| console.error(`[Download] Failed to fetch CSS image (${response.status}): ${fullUrl}`); | |
| } | |
| } catch (error) { | |
| console.error(`[Download] Error downloading CSS image from ${url}:`, error); | |
| } | |
| } | |
| } | |
| return processedCss; | |
| }; | |
| // FVとCN CSSの背景画像URLを処理 | |
| processedFvCss = await processCssUrls(processedFvCss, fvImages, 'fv'); | |
| processedCnCss = await processCssUrls(processedCnCss, cnImages, 'cn'); | |
| // 最終的にcn-image-error.webpのパスを再度確認・置換(URLダウンロード処理後) | |
| replaceNoImagePaths(); | |
| console.log('[Download] Total FV images extracted:', Object.keys(fvImages).length); | |
| console.log('[Download] Total CN images extracted:', Object.keys(cnImages).length); | |
| console.log('[Download] Final CN HTML first 1000 chars:', processedCnHtml.substring(0, 1000)); | |
| // ガイドライン準拠のHTML構造を作成 | |
| const htmlContent = `<!DOCTYPE html> | |
| <html lang="ja"> | |
| <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/website#"> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="keywords" content=""> | |
| <title>${proposalName} - Preview</title> | |
| <meta name="description" content="${proposalName}のプレビューページです。"> | |
| <link rel="canonical" href="https://example.com/"> | |
| <!-- OGP --> | |
| <meta property="og:type" content="website"> | |
| <meta property="og:title" content="${proposalName} - Preview"> | |
| <meta property="og:url" content="https://example.com/"> | |
| <meta property="og:image" content="https://example.com/common/images/img_ogp.jpg"> | |
| <meta property="og:description" content="${proposalName}のプレビューページです。"> | |
| <meta property="og:site_name" content="${proposalName}"> | |
| <!-- Twitter --> | |
| <meta name="twitter:card" content="summary_large_image"> | |
| <!-- /OGP --> | |
| <!-- Favicon --> | |
| <link rel="icon" href="/common/images/favicon.ico"> | |
| <link rel="apple-touch-icon" href="/common/images/apple-touch-icon.png"> | |
| <link rel="icon" type="image/png" href="/common/images/android-192x192.png"> | |
| <!-- CSS --> | |
| <link rel="stylesheet" href="/common/styles/main.css"> | |
| </head> | |
| <body> | |
| ${mainCopy ? `<h1 class="sr-only">${mainCopy}</h1>` : ''} | |
| <div class="container"> | |
| <!-- FV Section --> | |
| <section id="fv-section" aria-label="FV"> | |
| ${processedFvHtml} | |
| </section> | |
| <!-- Content Section --> | |
| <section id="content-section" aria-label="Content"> | |
| ${processedCnHtml} | |
| </section> | |
| </div> | |
| </body> | |
| </html>`; | |
| // すべてのCSSを1つのmain.cssにまとめる | |
| const mainCssContent = `@charset "UTF-8"; | |
| /* ======================================== | |
| Reset CSS | |
| ======================================== */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| html, body { | |
| font-family: YuGothic, 'Yu Gothic Medium', 'Yu Gothic', -apple-system, BlinkMacSystemFont, Roboto, Meiryo, sans-serif; | |
| line-height: 1.6; | |
| -webkit-text-size-adjust: 100%; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| background-color: #f9f9f9; | |
| } | |
| img { | |
| max-width: 100%; | |
| height: auto; | |
| vertical-align: bottom; | |
| display: block; | |
| } | |
| a { | |
| text-decoration: none; | |
| color: inherit; | |
| } | |
| ul, ol { | |
| list-style: none; | |
| } | |
| table { | |
| border-collapse: collapse; | |
| border-spacing: 0; | |
| } | |
| button { | |
| background-color: transparent; | |
| border: none; | |
| cursor: pointer; | |
| padding: 0; | |
| } | |
| /* ======================================== | |
| Container Styles (スマホサイズ固定) | |
| ======================================== */ | |
| .container { | |
| width: 412px; | |
| margin: 0 auto; | |
| padding: 0; | |
| background-color: #ffffff; | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); | |
| min-height: 100vh; | |
| } | |
| /* スマホやタブレットでの表示 */ | |
| @media (max-width: 412px) { | |
| .container { | |
| width: 100%; | |
| box-shadow: none; | |
| } | |
| } | |
| /* ======================================== | |
| Common Styles | |
| ======================================== */ | |
| /* デフォルトスタイルは削除(上記で定義済み) */ | |
| /* Screen reader only */ | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| border: 0; | |
| } | |
| /* ======================================== | |
| FV Section Styles | |
| ======================================== */ | |
| #fv-section { | |
| width: 100%; | |
| } | |
| /* FV固有のスタイル */ | |
| ${processedFvCss} | |
| /* ======================================== | |
| Content Section Styles | |
| ======================================== */ | |
| #content-section { | |
| width: 100%; | |
| } | |
| /* コンテンツ固有のスタイル */ | |
| ${processedCnCss} | |
| /* ======================================== | |
| Responsive Design | |
| ======================================== */ | |
| @media (max-width: 412px) { | |
| body { | |
| font-size: 14px; | |
| } | |
| #fv-section, | |
| #content-section { | |
| padding: 10px; | |
| } | |
| /* SP向けの調整 */ | |
| img { | |
| width: 100%; | |
| } | |
| }`; | |
| // ガイドライン準拠のディレクトリ構成でzipに追加 | |
| // ルートHTML | |
| zip.file('index.html', htmlContent); | |
| // CSS ディレクトリ | |
| const stylesFolder = zip.folder('common/styles'); | |
| if (stylesFolder) { | |
| stylesFolder.file('main.css', mainCssContent); | |
| } | |
| // 画像ディレクトリ | |
| const imagesFolder = zip.folder('common/images'); | |
| if (imagesFolder) { | |
| // FV画像フォルダ | |
| const fvImagesFolder = imagesFolder.folder('fv'); | |
| if (fvImagesFolder) { | |
| Object.entries(fvImages).forEach(([imageName, base64Data]) => { | |
| console.log(`[Download] Adding FV image to ZIP: ${imageName}`); | |
| fvImagesFolder.file(imageName, base64Data, { base64: true }); | |
| }); | |
| } | |
| // コンテンツ画像フォルダ | |
| const contentImagesFolder = imagesFolder.folder('content'); | |
| if (contentImagesFolder) { | |
| Object.entries(cnImages).forEach(([imageName, base64Data]) => { | |
| console.log(`[Download] Adding CN image to ZIP: ${imageName}`); | |
| contentImagesFolder.file(imageName, base64Data, { base64: true }); | |
| }); | |
| } | |
| // cn-image-error.webpファイルを追加(エラー時用) | |
| try { | |
| const response = await fetch('/images/cn-image-error.webp'); | |
| const blob = await response.blob(); | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| imagesFolder.file('cn-image-error.webp', arrayBuffer); | |
| console.log('[Download] Added cn-image-error.webp to ZIP'); | |
| } catch (e) { | |
| console.warn('[Download] Failed to load cn-image-error.webp, using fallback'); | |
| // フォールバック: 簡単なダミー画像 | |
| const dummyImage = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; | |
| imagesFolder.file('cn-image-error.webp', dummyImage, { base64: true }); | |
| } | |
| // no-image.webpファイルを追加(画像クリア機能用) | |
| try { | |
| const response = await fetch('/images/no-image.webp'); | |
| const blob = await response.blob(); | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| imagesFolder.file('no-image.webp', arrayBuffer); | |
| console.log('[Download] Added no-image.webp to ZIP'); | |
| } catch (e) { | |
| console.warn('[Download] Failed to load no-image.webp, using fallback'); | |
| // フォールバック: 簡単なダミー画像 | |
| const dummyImage = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; | |
| imagesFolder.file('no-image.webp', dummyImage, { base64: true }); | |
| } | |
| // ダミーのファビコンファイルを追加(実際の案件では適切なファビコンを用意) | |
| const dummyFavicon = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; | |
| imagesFolder.file('favicon.ico', dummyFavicon, { base64: true }); | |
| imagesFolder.file('apple-touch-icon.png', dummyFavicon, { base64: true }); | |
| imagesFolder.file('android-192x192.png', dummyFavicon, { base64: true }); | |
| imagesFolder.file('img_ogp.jpg', dummyFavicon, { base64: true }); | |
| } | |
| // README.md を追加(制作ガイドライン情報) | |
| const readmeContent = `# ${proposalName} - LP制作ファイル | |
| ## ファイル構成 | |
| 本ファイルは電通デジタル HTML制作ガイドライン ver1.2 に準拠しています。 | |
| \`\`\` | |
| / | |
| ├─ index.html # メインHTMLファイル | |
| ├─ common/ # サイト共通ファイル | |
| │ ├─ images/ # 画像ファイル | |
| │ │ ├─ fv/ # FV画像 | |
| │ │ │ ├─ fv_img_001.jpg | |
| │ │ │ ├─ fv_bg_001.jpg (背景画像) | |
| │ │ │ └─ ... | |
| │ │ ├─ content/ # コンテンツ画像 | |
| │ │ │ ├─ cn_img_001.jpg | |
| │ │ │ ├─ cn_bg_001.jpg (背景画像) | |
| │ │ │ └─ ... | |
| │ │ ├─ cn-image-error.webp # プレースホルダー画像 | |
| │ │ ├─ favicon.ico # ファビコン | |
| │ │ ├─ apple-touch-icon.png | |
| │ │ ├─ android-192x192.png | |
| │ │ └─ img_ogp.jpg # OGP画像 | |
| │ └─ styles/ # CSSファイル | |
| │ └─ main.css # 統合CSS(リセット + FV + コンテンツ + 共通) | |
| └─ README.md # このファイル | |
| \`\`\` | |
| ## CSS構成 | |
| ### main.css | |
| 1つのCSSファイルにすべてのスタイルを統合: | |
| - **Reset CSS**: ブラウザ間の差異を統一 | |
| - **Common Styles**: 全体共通のスタイル | |
| - **FV Section Styles**: FVセクション専用スタイル | |
| - **Content Section Styles**: コンテンツセクション専用スタイル | |
| ## 画像管理 | |
| ### FV画像 (/common/images/fv/) | |
| - FVセクションで使用される画像 | |
| - ファイル名: fv_img_xxx.jpg(img要素の画像) | |
| - ファイル名: fv_bg_xxx.jpg(CSS背景画像) | |
| ### コンテンツ画像 (/common/images/content/) | |
| - コンテンツセクションで使用される画像 | |
| - ファイル名: cn_img_xxx.jpg(img要素の画像) | |
| - ファイル名: cn_bg_xxx.jpg(CSS背景画像) | |
| ### プレースホルダー画像 | |
| - **cn-image-error.webp**: 画像クリア機能使用時のプレースホルダー | |
| ## 推奨環境 | |
| ### PC | |
| - Windows 最新版/Microsoft Edge 最新版、Chrome 最新版 | |
| - Mac OS X 最新版/Safari 最新版 | |
| ### SP | |
| - iOS 最新版/Safari 最新版 | |
| - Android 最新版/Chrome 最新版 | |
| ## 表示仕様 | |
| - **幅412px固定表示**:PCでもスマホサイズ(幅412px)で表示 | |
| - **左右中央配置**:画面が広い場合でも左右中央に配置 | |
| - **背景色**:コンテナ外は薄いグレー(#f9f9f9)、コンテナ内は白(#ffffff) | |
| - **ボックスシャドウ**:コンテナに適用してスマホ画面を演出 | |
| ## ブレイクポイント | |
| - 413px以上:幅412px固定で中央配置 | |
| - 412px以下:画面幅100%表示 | |
| ## 注意事項 | |
| - ファビコンファイルは仮のものです。実際の案件では適切なファビコンに差し替えてください。 | |
| - canonical URLや OGP画像URLは example.com になっています。実際のドメインに変更してください。 | |
| - 画像はFVとコンテンツで分離管理されています。用途に応じて適切なフォルダから参照してください。 | |
| - CSSは1つのファイルに統合されていますが、セクションごとにコメントで区切られています。 | |
| `; | |
| zip.file('README.md', readmeContent); | |
| // zipファイル生成 | |
| console.log('[Download] Generating ZIP file...'); | |
| const zipBlob = await zip.generateAsync({ type: 'blob' }); | |
| const url = URL.createObjectURL(zipBlob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `${proposalName}-guideline-compliant.zip`; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| // メモリリークを防ぐためURLを解放 | |
| URL.revokeObjectURL(url); | |
| console.log('[Download] Download completed successfully'); | |
| } catch (e: unknown) { | |
| const message = e instanceof Error ? e.message : 'ダウンロードに失敗しました'; | |
| console.error('[Download] Error:', message); | |
| setError(message); | |
| } finally { | |
| setIsDownloading(false); | |
| } | |
| }; | |
| return { downloadCode, isDownloading, error }; | |
| } | |