FE_Dev / hooks /use-code-download.ts
GitHub Actions
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
68f7925
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 };
}