FE_Dev / server /lib /templates /ContentsHtmlGenerator.ts
GitHub Actions
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
68f7925
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
// 大区分の型定義
import type { ContentSection, SectionType } from '@/schema/proposal';
type RequestData = {
companyInfo: {
name: string;
title?: string;
};
heroSection: {
コピー: {
メインコピー: string;
サブコピー1?: string;
サブコピー2?: string;
サブコピー3?: string;
};
CTA: {
ボタンテキスト: string;
マイクロコピー?: string;
};
ビジュアル?: {
内容: string;
作成指示?: string;
};
権威付け?: {
内容: string;
};
};
contentSections?: ContentSection[];
};
// aria-label マッピング
const ariaLabelMapping: Record<SectionType, string> = {
'問題提起/共感': '問題提起/共感',
シミュレーション: 'シミュレーション',
'商品/サービスの特徴': '商品/サービスの特徴',
'解決策/ベネフィット提示': '解決策/ベネフィット提示',
'SPオファー/限定特典': 'SPオファー/限定特典',
'料金/プラン': '料金/プラン',
'商品/サービスの詳細情報': '商品/サービスの詳細情報',
'成功事例/ユーザーボイス紹介': '成功事例/ユーザーボイス紹介',
'ご利用の流れ/ステップ': 'ご利用の流れ/ステップ',
メディア掲載情報: 'メディア掲載情報',
競合比較: '競合比較',
'スタッフ・メンバー紹介': 'スタッフ・メンバー紹介',
権威訴求: '権威訴求',
店舗情報: '店舗情報',
活用シーン: '活用シーン',
'FAQ/よくある質問': 'FAQ/よくある質問',
'News/更新情報': 'News/更新情報',
};
// セクションタイプマッピング
const sectionMapping: Record<SectionType, string> = {
'問題提起/共感': 'problem-solution',
シミュレーション: 'simulation',
'商品/サービスの特徴': 'features',
'解決策/ベネフィット提示': 'benefits',
'SPオファー/限定特典': 'special-offer',
'料金/プラン': 'pricing',
'商品/サービスの詳細情報': 'product-details',
'成功事例/ユーザーボイス紹介': 'testimonials',
'ご利用の流れ/ステップ': 'usage-flow',
メディア掲載情報: 'media-coverage',
競合比較: 'comparison',
'スタッフ・メンバー紹介': 'staff',
権威訴求: 'authority',
店舗情報: 'store-info',
活用シーン: 'use-cases',
'FAQ/よくある質問': 'faq',
'News/更新情報': 'news-updates',
};
// HTMLテンプレート生成クラス
export class ContentsHtmlGenerator {
private baseTemplate: string;
private imageIdCounter: number;
constructor() {
this.baseTemplate = this.loadTemplateFromFile();
this.imageIdCounter = 0;
}
/**
* ユニークな画像IDを生成
*/
private generateUniqueImageId(sectionType: string, index: number): string {
this.imageIdCounter++;
const uniqueId = `${sectionType}-img-${index}-${this.imageIdCounter}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
return uniqueId;
}
// メインのHTML生成メソッド
generateHTML(data: RequestData): string {
let html = this.baseTemplate;
// 会社情報の置換
html = html.replace(/{{COMPANY_NAME}}/g, data.companyInfo.name);
html = html.replace(/{{COMPANY_TITLE}}/g, data.companyInfo.title || data.heroSection.コピー.メインコピー);
// ヒーローセクションの置換
html = html.replace(/{{HERO_MAIN_COPY}}/g, this.formatHeroTitle(data.heroSection.コピー.メインコピー));
html = html.replace(/{{HERO_FEATURES}}/g, this.generateHeroFeatures(data.heroSection.コピー));
html = html.replace(/{{CTA_BUTTON_TEXT}}/g, data.heroSection.CTA.ボタンテキスト);
html = html.replace(/{{CTA_MICRO_COPY}}/g, data.heroSection.CTA.マイクロコピー || '');
// ヒーロー画像の置換は削除(CNデータには通常ヒーロー画像はなく、コンテンツ画像のIDが上書きされる問題を防ぐため)
// コンテンツセクションの生成
const contentSections = this.generateContentSections(data.contentSections || []);
html = html.replace(/{{CONTENT_SECTIONS}}/g, contentSections);
// 権威付けセクションの生成
const authoritySection = this.generateAuthoritySection(data.heroSection.権威付け);
html = html.replace(/{{AUTHORITY_SECTION}}/g, authoritySection);
return html;
}
/**
* ヒーロータイトルのフォーマット
*/
private formatHeroTitle(title: string): string {
return title || '';
}
/**
* ヒーローフィーチャーの生成
*/
private generateHeroFeatures(copyData: { メインコピー: string; サブコピー1?: string; サブコピー2?: string; サブコピー3?: string }): string {
const features = [];
if (copyData.サブコピー1) features.push(copyData.サブコピー1);
if (copyData.サブコピー2) features.push(copyData.サブコピー2);
if (copyData.サブコピー3) features.push(copyData.サブコピー3);
if (features.length === 0) {
return '';
}
return features.map((feature) => `<li>${feature}</li>`).join('');
}
/**
* コンテンツセクションとフッターのみを生成(ヘッダーとヒーローセクションを除外)
*/
generateContentSectionsOnly(data: { contentSections?: any[]; companyName?: string }): string {
// コンテンツセクションの生成
const contentSections = this.generateContentSections(data.contentSections || []);
// フッターを追加(コンテンツ単体で表示される場合のため)
const footer = this.generateFooterSection(data.companyName);
const html = contentSections + footer;
return html;
}
// コンテンツセクションの生成
private generateContentSections(sections: ContentSection[]): string {
const results = sections.map((section) => {
const sectionType = this.mapSectionType(section.大区分);
const html = this.generateSectionByType(sectionType, section);
return html;
});
// すべてのセクションを返す(空のチェックを削除)
return results.join('');
}
// 大区分からセクションタイプへのマッピング
private mapSectionType(type: SectionType): string {
return sectionMapping[type] || 'generic';
}
// セクションタイプ別のHTML生成
private generateSectionByType(type: string, section: ContentSection): string {
switch (type) {
case 'features':
return this.generateFeaturesSection(section);
case 'benefits':
return this.generateBenefitsSection(section);
case 'authority':
return this.generateAuthorityContentSection(section);
case 'problem-solution':
return this.generateProblemSolutionSection(section);
case 'usage-flow':
return this.generateUsageFlowSection(section);
case 'product-details':
return this.generateProductDetailsSection(section);
case 'simulation':
return this.generateSimulationSection(section);
case 'special-offer':
return this.generateSpecialOfferSection(section);
case 'pricing':
return this.generatePricingSection(section);
case 'testimonials':
return this.generateTestimonialsSection(section);
case 'media-coverage':
return this.generateMediaCoverageSection(section);
case 'comparison':
return this.generateComparisonSection(section);
case 'staff':
return this.generateStaffSection(section);
case 'store-info':
return this.generateStoreInfoSection(section);
case 'use-cases':
return this.generateUseCasesSection(section);
case 'faq':
return this.generateFaqSection(section);
case 'news-updates':
return this.generateNewsUpdatesSection(section);
default:
return this.generateGenericSection(section);
}
}
// 特徴セクションの生成
private generateFeaturesSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// ユニークIDを生成(タイムスタンプ + インデックス + ランダム文字列)
const imageId = this.generateUniqueImageId('feature', index);
// 変数を即座にテンプレートに埋め込む(クロージャの問題を回避)
const imageAlt = section.大区分 || '画像生成中...';
const imageTitle = section.大区分 || '';
const itemNumber = String(index + 1).padStart(2, '0');
const itemHeading = item.見出し?.value || '';
const itemContent = item.内容?.value || '';
// 4:3のプレースホルダーを生成
const placeholderHtml = `<img
id="${imageId}"
class="innovation__image"
src="/loading-placeholder.svg"
alt="${imageAlt}"
title="${imageTitle}"
width="800"
height="600"
data-image-status="loading"
data-image-role="illustration"
data-section-type="features"
style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; object-fit: cover; aspect-ratio: 4 / 3;"
loading="lazy" />`;
return `
<div class="innovation__item">
<div class="innovation__number">${itemNumber}</div>
<div class="innovation__content">
${placeholderHtml}
<h3 class="innovation__subtitle">${itemHeading}</h3>
<p class="innovation__text">${itemContent}</p>
</div>
</div>
`;
})
.join('') || '';
return `
<section class="innovation section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="innovation__container">
<h2 class="innovation__title">${section.中区分}</h2>
${items}
</div>
</section>
<!-- /.innovation -->
`;
}
// ベネフィットセクションの生成
private generateBenefitsSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map(
(item, index) => `
<div class="solution-benefits-section__item">
<div class="solution-benefits-section__header">
<div class="solution-benefits-section__icon">✓</div>
${item.見出し?.value ? `<h3 class="solution-benefits-section__item-title">${item.見出し.value}</h3>` : ''}
</div>
<div class="solution-benefits-section__text">
${item.内容?.value || ''}
${item.注釈?.value ? `<span class="solution-benefits-section__note">${item.注釈.value}</span>` : ''}
</div>
</div>
`,
)
.join('') || '';
return `
<section class="solution-benefits-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="solution-benefits-section__container">
<h2 class="solution-benefits-section__title">${section.中区分}</h2>
<div class="solution-benefits-section__items">
${items}
</div>
</div>
</section>
<!-- /.solution-benefits-section -->
`;
}
// 権威付けセクションの生成
private generateAuthorityContentSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map(
(item, index) => `
<div class="award-item">
<div class="award-wreath">
<div class="award-title">${item.見出し?.value || ''}</div>
</div>
<p class="award-description">${item.内容?.value || ''}</p>
</div>
`,
)
.join('') || '';
return `
<section class="awards-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="awards-container">
<h2 class="awards-main-title">${section.中区分}</h2>
<div class="awards-content">
${items}
</div>
</div>
</section>
`;
}
// 問題解決セクションの生成
private generateProblemSolutionSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
const imageId = this.generateUniqueImageId('problem', index);
// 変数を即座にキャプチャ
const imageAlt = item.見出し?.value || '画像生成中...';
const imageTitle = item.見出し?.value?.substring(0, 200) + '...' || '';
const itemHeading = item.見出し?.value || '';
const itemContent = item.内容?.value || '';
const itemNote = item.注釈?.value || '';
return `
<div class="strategy-card">
<img
id="${imageId}"
class="strategy-image"
src="/loading-placeholder.svg"
alt="${imageAlt}"
title="${imageTitle}"
width="800"
height="600"
data-image-status="loading"
data-image-role="illustration"
data-section-type="problem"
style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; object-fit: cover; aspect-ratio: 4 / 3;"
loading="lazy" />
<div class="strategy-content">
<h3 class="strategy-subtitle">${itemHeading}</h3>
<p class="strategy-text">${itemContent}</p>
${itemNote ? `<p class="strategy-note">${itemNote}</p>` : ''}
</div>
</div>
`;
})
.join('') || '';
return `
<section class="global-strategy section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="global-strategy-container">
<h2 class="global-strategy-title">${section.中区分}</h2>
${items}
</div>
</section>
`;
}
// 利用フローセクションの生成
private generateUsageFlowSection(section: ContentSection): string {
const steps =
(section.小区分json ?? [])
.map((item, index: number) => {
const imageId = this.generateUniqueImageId('flow', index);
// 動的フィールドをチェック
const itemAny = item as any;
const hasStep = itemAny.ステップ?.value;
const hasIntro = itemAny.導入?.value;
const title = hasStep ? itemAny.ステップ.value : item.見出し?.value || '';
return `
<div class="flow-step">
<div class="flow-number">${String(index + 1).padStart(2, '0')}</div>
<img
id="${imageId}"
class="flow-image"
src="/loading-placeholder.svg"
alt="${title || '画像生成中...'}"
title="${title?.substring(0, 200) + '...' || ''}"
width="800"
height="600"
data-image-status="loading"
data-image-role="illustration"
data-section-type="flow"
style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; object-fit: cover; aspect-ratio: 4 / 3;"
loading="lazy" />
<h3 class="flow-subtitle">${title}</h3>
${hasIntro ? `<div class="flow-intro">${itemAny.導入.value}</div>` : ''}
<p class="flow-text">${item.内容?.value || ''}</p>
${item.注釈?.value ? `<p class="flow-note">${item.注釈?.value}</p>` : ''}
</div>
`;
})
.join('') || '';
return `
<section class="usage-flow-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="usage-flow-container">
<h2 class="usage-flow-title">${section.中区分}</h2>
<div class="usage-flow-content">
${steps}
</div>
</div>
</section>
`;
}
// 商品詳細セクションの生成
private generateProductDetailsSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
const imageId = this.generateUniqueImageId('product', index);
// 変数を即座にキャプチャ
const imageAlt = item.見出し?.value || '画像生成中...';
const imageTitle = item.見出し?.value?.substring(0, 200) + '...' || '';
const productSubtitle = item.見出し?.value || '';
const productText = item.内容?.value || '';
return `
<div class="product-detail-item">
<img
id="${imageId}"
class="product-detail-image"
src="/loading-placeholder.svg"
alt="${imageAlt}"
title="${imageTitle}"
width="800"
height="600"
data-image-status="loading"
data-image-role="illustration"
data-section-type="service"
style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; object-fit: cover; aspect-ratio: 4 / 3;"
loading="lazy" />
<h3 class="product-detail-subtitle">${productSubtitle}</h3>
<p class="product-detail-text">${productText}</p>
</div>
`;
})
.join('') || '';
return `
<section class="product-details-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="product-details-container">
<h2 class="product-details-title">${section.中区分}</h2>
<div class="product-details-content">
${items}
</div>
</div>
</section>
`;
}
// シミュレーションセクションの生成
private generateSimulationSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasQA = itemAny.質疑?.value;
const hasButton = itemAny.ボタン?.value;
const hasIntro = itemAny.導入?.value;
const title = item.見出し?.value || '';
const content = hasQA ? itemAny.質疑.value : item.内容?.value || '';
const intro = hasIntro ? itemAny.導入.value : '';
const buttonText = hasButton ? itemAny.ボタン.value : '';
return `
<div class="simulation-card">
<h3 class="simulation-title">${title}</h3>
${intro ? `<div class="simulation-intro">${intro}</div>` : ''}
<div class="simulation-content">${content}</div>
${buttonText ? `<button class="simulation-button">${buttonText}</button>` : ''}
${item.注釈?.value ? `<p class="simulation-notes">${item.注釈?.value}</p>` : ''}
</div>
`;
})
.join('') || '';
return `
<section class="simulation-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="simulation-container">
<h2 class="simulation-title">${section.中区分}</h2>
${items}
</div>
</section>
`;
}
// 特別オファーセクションの生成
private generateSpecialOfferSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasOfferContent = itemAny['オファー/特典内容']?.value;
const hasButton = itemAny.ボタン?.value;
const hasIntro = itemAny.導入?.value;
const title = item.見出し?.value || '';
const content = hasOfferContent ? itemAny['オファー/特典内容'].value : item.内容?.value || '';
const intro = hasIntro ? itemAny.導入.value : '';
const buttonText = hasButton ? itemAny.ボタン.value : '';
return `
<div class="special-offer-card">
<h3 class="special-offer-title">${title}</h3>
${intro ? `<div class="special-offer-intro">${intro}</div>` : ''}
<p class="special-offer-content">${content}</p>
${buttonText ? `<button class="special-offer-button">${buttonText}</button>` : ''}
${item.注釈?.value ? `<p class="special-offer-notes">${item.注釈?.value}</p>` : ''}
</div>
`;
})
.join('') || '';
return `
<section class="special-offer-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="special-offer-container">
<h2 class="special-offer-title">${section.中区分}</h2>
${items}
</div>
</section>
`;
}
// 料金セクションの生成
private generatePricingSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック(互換性のため型アサーションを使用)
const itemAny = item as any;
const hasPrice = itemAny.料金?.value;
const hasPlan = itemAny.プラン名?.value;
// 動的フィールドがある場合は特別な処理
if (hasPrice || hasPlan) {
return `
<div class="pricing-card">
${
hasPlan
? `<h3 class="pricing-card-title">${itemAny.プラン名.value}</h3>`
: `<h3 class="pricing-card-title">${item.見出し?.value || ''}</h3>`
}
${hasPrice ? `<div class="pricing-card-price">${itemAny.料金.value}</div>` : ''}
<p class="pricing-card-description">${item.内容?.value || ''}</p>
${item.注釈?.value ? `<p class="pricing-card-description">${item.注釈.value}</p>` : ''}
</div>
`;
}
// 標準フィールドのみの場合
return `
<div class="pricing-card">
<h3 class="pricing-card-title">${item.見出し?.value || ''}</h3>
<p class="pricing-card-description">${item.内容?.value || ''}</p>
${item.注釈?.value ? `<p class="pricing-card-description">${item.注釈?.value}</p>` : ''}
</div>
`;
})
.join('') || '';
return `
<section class="pricing-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="pricing-container">
<h2 class="pricing-title">${section.中区分}</h2>
<div class="pricing-cards">
${items}
</div>
</div>
</section>
`;
}
// お客様の声セクションの生成
private generateTestimonialsSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map(
(item, index) => `
<div class="review-card">
<div class="review-header">
<div class="review-avatar">👤</div>
<div class="review-headline">${item.見出し?.value || ''}</div>
</div>
<p class="review-text">${item.内容?.value || ''}</p>
${item.ユーザー情報?.value ? `<div class="review-attribution">${item.ユーザー情報?.value}</div>` : ''}
${item.注釈?.value ? `<p class="review-note">${item.注釈?.value}</p>` : ''}
</div>
`,
)
.join('') || '';
return `
<section class="success-stories-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="success-stories-container">
<h2 class="success-stories-title">${section.中区分}</h2>
<div class="success-stories-content">
${items}
</div>
</div>
</section>
`;
}
// メディア掲載セクションの生成
private generateMediaCoverageSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasTitle = itemAny.タイトル?.value;
const title = hasTitle ? itemAny.タイトル.value : item.見出し?.value || '';
return `
<div class="media-item">
<h3 class="media-subtitle">${title}</h3>
<p class="media-text">${item.内容?.value || ''}</p>
${item.注釈?.value ? `<p class="media-note">${item.注釈?.value}</p>` : ''}
</div>
`;
})
.join('') || '';
return `
<section class="media-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="media-container">
<h2 class="media-title">${section.中区分}</h2>
<div class="media-content">
${items}
</div>
</div>
</section>
`;
}
// 競合比較セクションの生成
private generateComparisonSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasComparisonItem = itemAny.比較項目?.value;
const title = hasComparisonItem ? itemAny.比較項目.value : item.見出し?.value || '';
return `
<div class="comparison-item">
<div class="comparison-icon">🔍</div>
<div class="comparison-item-content">
<h3 class="comparison-subtitle">${title}</h3>
<p class="comparison-text">${item.内容?.value || ''}</p>
${item.注釈?.value ? `<p class="comparison-note">${item.注釈?.value}</p>` : ''}
</div>
</div>
`;
})
.join('') || '';
return `
<section class="comparison-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="comparison-container">
<div class="comparison-content">
<h2 class="comparison-title">${section.中区分}</h2>
${items}
</div>
</div>
</section>
`;
}
// スタッフ紹介セクションの生成
private generateStaffSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasName = itemAny.名前?.value;
const hasIntro = itemAny.導入?.value;
const hasDescription = itemAny.紹介文?.value;
const name = hasName ? itemAny.名前.value : item.ユーザー情報?.value || '';
const role = item.見出し?.value || '';
const intro = hasIntro ? itemAny.導入.value : '';
const bio = hasDescription ? itemAny.紹介文.value : item.内容?.value || '';
return `
<div class="staff-member">
<div class="staff-avatar">👤</div>
<div class="staff-role">${role}</div>
<div class="staff-name">${name}</div>
${intro ? `<div class="staff-intro">${intro}</div>` : ''}
<p class="staff-bio">${bio}</p>
${item.注釈?.value ? `<p class="staff-note">${item.注釈?.value}</p>` : ''}
</div>
`;
})
.join('') || '';
return `
<section class="staff-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="staff-container">
<h2 class="staff-title">${section.中区分}</h2>
<div class="staff-content">
${items}
</div>
</div>
</section>
`;
}
// 店舗情報セクションの生成
private generateStoreInfoSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasStore = itemAny.店舗?.value;
const hasIntro = itemAny.導入?.value;
const hasDetails = itemAny.詳細情報?.value;
const storeName = hasStore ? itemAny.店舗.value : item.見出し?.value || '';
const description = item.内容?.value || '';
const intro = hasIntro ? itemAny.導入.value : '';
const details = hasDetails ? itemAny.詳細情報.value : '';
// 地図画像用のユニークIDを生成
const mapImageId = this.generateUniqueImageId('map', index);
// 変数を即座にキャプチャ
const mapAlt = `地図: ${storeName}`;
const mapTitle = `地図: ${storeName}`;
const noteValue = item.注釈?.value || '';
return `
<div class="store-item">
<img
id="${mapImageId}"
class="store-map-image"
src="/loading-placeholder.svg"
alt="${mapAlt}"
title="${mapTitle}"
width="800"
height="600"
data-image-status="loading"
data-image-role="map"
data-section-type="store"
style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; object-fit: cover; aspect-ratio: 4 / 3; width: 100%; max-width: 400px;"
loading="lazy" />
<h3 class="store-name">${storeName}</h3>
${intro ? `<p class="store-intro">${intro}</p>` : ''}
<p class="store-description">${description}</p>
${
details || noteValue
? `
<div class="store-info">
<div class="store-info-item">
<div class="store-info-label">詳細情報</div>
<div class="store-info-value">${details || noteValue}</div>
</div>
</div>`
: ''
}
</div>
`;
})
.join('') || '';
return `
<section class="store-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="store-container">
<h2 class="store-title">${section.中区分}</h2>
<div class="store-content">
${items}
</div>
</div>
</section>
`;
}
// 活用シーンセクションの生成
private generateUseCasesSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map(
(item, index) => `
<div class="use-case-item">
<div class="use-case-header">${item.見出し?.value || ''}</div>
<div class="use-case-content">
<p class="use-case-text">${item.内容?.value || ''}</p>
${
item.注釈?.value
? `
<div class="use-case-notes">
${item.注釈?.value}
</div>`
: ''
}
</div>
</div>
`,
)
.join('') || '';
return `
<section class="use-cases-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="use-cases-container">
<h2 class="use-cases-title">${section.中区分}</h2>
<div class="use-cases-content">
${items}
</div>
</div>
</section>
`;
}
// FAQ/よくある質問セクションの生成
private generateFaqSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasQA = itemAny.質疑?.value;
if (hasQA) {
// 質疑フィールドがある場合(Q:とA:を分離して処理)
const qaValue = itemAny.質疑.value;
const qaLines = qaValue.split('<BR>').map((line: string) => line.trim());
let question = '';
let answer = '';
// Q:とA:を分離
qaLines.forEach((line: string) => {
if (line.startsWith('Q:')) {
question = line.substring(2).trim();
} else if (line.startsWith('A:')) {
answer = line.substring(2).trim();
}
});
// 注釈がある場合は追加
const annotation = itemAny.注釈?.value ? `<p class="faq-annotation">${itemAny.注釈.value}</p>` : '';
return `
<div class="faq-item">
<div class="faq-question">
<span class="faq-q-mark">Q</span>
<span class="faq-q-text">${question || 'よくあるご質問'}</span>
</div>
<div class="faq-answer">
<div class="faq-answer-content">
<span class="faq-a-mark">A</span>
<div class="faq-a-text">
${answer || ''}
${annotation}
</div>
</div>
</div>
</div>
`;
}
// 標準フィールドのみの場合(見出し・内容)
const questionText = item.見出し?.value || '';
const answerText = item.内容?.value || '';
const annotation = item.注釈?.value ? `<p class="faq-annotation">${item.注釈.value}</p>` : '';
return `
<div class="faq-item">
<div class="faq-question">
<span class="faq-q-mark">Q</span>
<span class="faq-q-text">${questionText}</span>
</div>
<div class="faq-answer">
<div class="faq-answer-content">
<span class="faq-a-mark">A</span>
<div class="faq-a-text">
${answerText}
${annotation}
</div>
</div>
</div>
</div>
`;
})
.join('') || '';
// FAQセクション用のスタイル(回答を常に表示)
return `
<section class="faq-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<style>
.faq-section {
padding: 60px 20px;
background: #f8f9fa;
}
.faq-container {
max-width: 1200px;
margin: 0 auto;
}
.faq-title {
text-align: center;
font-size: 2.5rem;
color: #333;
margin-bottom: 3rem;
}
.faq-item {
background: white;
border-radius: 8px;
margin-bottom: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
transition: all 0.3s ease;
}
.faq-item:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.faq-question {
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
background: white;
border-bottom: 2px solid #f0f0f0;
}
.faq-q-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #5a6c7d;
color: white;
border-radius: 50%;
font-weight: bold;
flex-shrink: 0;
}
.faq-q-text {
flex: 1;
font-size: 1.1rem;
color: #333;
font-weight: 500;
}
.faq-answer {
background: #fafafa;
display: block;
}
.faq-answer-content {
padding: 1.5rem;
display: flex;
gap: 1rem;
}
.faq-a-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #5a6c7d;
color: white;
border-radius: 50%;
font-weight: bold;
flex-shrink: 0;
}
.faq-a-text {
flex: 1;
font-size: 1rem;
color: #555;
line-height: 1.6;
}
.faq-annotation {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e0e0e0;
font-size: 0.9rem;
color: #777;
font-style: italic;
}
</style>
<div class="faq-container">
<h2 class="faq-title">${section.中区分}</h2>
<div class="faq-content">
${items}
</div>
</div>
</section>
`;
}
// News/更新情報セクションの生成
private generateNewsUpdatesSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map((item, index) => {
// 動的フィールドをチェック
const itemAny = item as any;
const hasTitle = itemAny.タイトル?.value;
const title = hasTitle ? itemAny.タイトル.value : item.見出し?.value || '';
return `
<div class="news-item">
<div class="news-header">
<div class="news-date">2024.12.${String(15 - index).padStart(2, '0')}</div>
<div class="news-category">プレスリリース</div>
</div>
<h3 class="news-title-text">${title}</h3>
<p class="news-summary">${item.内容?.value || ''}</p>
${item.注釈?.value ? `<p class="news-note">${item.注釈?.value}</p>` : ''}
</div>
`;
})
.join('') || '';
return `
<section class="news-section section-base" id="${this.mapSectionType(section.大区分)}" aria-label="${ariaLabelMapping[section.大区分]}">
<div class="news-container">
<h2 class="news-title">${section.中区分}</h2>
<div class="news-content">
${items}
</div>
<div class="news-more">
<a href="#" class="news-more-button">すべてのニュースを見る</a>
</div>
</div>
</section>
`;
}
// 汎用セクションの生成
private generateGenericSection(section: ContentSection): string {
const items =
(section.小区分json ?? [])
.map(
(item, index) => `
<div class="content-item">
<h3>${item.見出し?.value || ''}</h3>
<p>${item.内容?.value || ''}</p>
${item.注釈?.value ? `<small>${item.注釈?.value}</small>` : ''}
</div>
`,
)
.join('') || '';
return `
<section class="content-section section-base" data-section-type="その他">
<div style="max-width: 1200px; margin: 0 auto; padding: 0 20px;">
<h2>${section.中区分}</h2>
${items}
</div>
</section>
`;
}
// 権威付けセクションの生成(ヒーローセクション用)
private generateAuthoritySection(authority?: { 内容: string }): string {
if (!authority) return '';
return `
<section class="special-offer-section section-base">
<div class="special-offer-card">
<div class="special-offer-intro">${authority.内容}</div>
</div>
</section>
`;
}
// フッターセクションの生成
private generateFooterSection(companyName?: string): string {
return `
<footer class="footer">
<div class="back-to-top">
<button onclick="scrollToTop()" aria-label="ページトップへ戻る">
<span>&#9650;</span>
</button>
</div>
<div class="footer-content">
<div class="social-icons">
<a href="#" aria-label="Facebook"
><div class="social-icon">f</div></a
>
<a href="#" aria-label="YouTube"
><div class="social-icon">▶</div></a
>
<a href="#" aria-label="LinkedIn"
><div class="social-icon">in</div></a
>
</div>
<div class="footer-nav">
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>サービス <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>ナレッジ <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>イベント&セミナー <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>採用 <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>ニュース <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>サステナビリティ <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>会社情報 <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>お問い合わせ <span class="arrow">&#10095;</span></a
>
</div>
</div>
<div class="footer-links">
<a href="#" class="footer-link">サイトマップ</a>
<a href="#" class="footer-link">個人情報保護方針</a>
<a href="#" class="footer-link">特定個人情報基本方針</a>
<a href="#" class="footer-link">ご利用にあたって</a>
</div>
<div class="copyright">
Copyright© All Rights Reserved.
</div>
</div>
</footer>
`;
}
// template-generator.htmlファイルからテンプレートを読み込み
private loadTemplateFromFile(): string {
try {
const templatePath = join(process.cwd(), 'server', 'lib', 'templates', 'template-generator.html');
return readFileSync(templatePath, 'utf-8');
} catch (error) {
console.error('template-generator.htmlの読み込みに失敗しました:', error);
// フォールバック用の簡単なテンプレート
return this.getFallbackTemplate();
}
}
// フォールバック用テンプレート
private getFallbackTemplate(): string {
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{COMPANY_TITLE}}</title>
</head>
<body>
<header class="header">
<div class="logo">{{COMPANY_NAME}}</div>
</header>
<section class="hero-section">
<div class="hero-container">
<h1 class="hero-title">{{HERO_MAIN_COPY}}</h1>
<div class="features">
<div class="features-container">
{{HERO_FEATURES}}
</div>
</div>
<div class="cta-section">
<p class="cta-subtitle">{{CTA_MICRO_COPY}}</p>
<a href="#" class="cta-button">{{CTA_BUTTON_TEXT}}</a>
</div>
</div>
</section>
{{CONTENT_SECTIONS}}
{{AUTHORITY_SECTION}}
<footer class="footer">
<div class="back-to-top">
<button onclick="scrollToTop()" aria-label="ページトップへ戻る">
<span>&#9650;</span>
</button>
</div>
<div class="footer-content">
<div class="social-icons">
<a href="#" aria-label="Facebook"
><div class="social-icon">f</div></a
>
<a href="#" aria-label="YouTube"
><div class="social-icon">▶</div></a
>
<a href="#" aria-label="LinkedIn"
><div class="social-icon">in</div></a
>
</div>
<div class="footer-nav">
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>サービス <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>ナレッジ <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>イベント&セミナー <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>採用 <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>ニュース <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>サステナビリティ <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>会社情報 <span class="arrow">&#10095;</span></a
>
</div>
<div class="footer-nav-item">
<a href="#" class="footer-nav-link"
>お問い合わせ <span class="arrow">&#10095;</span></a
>
</div>
</div>
<div class="footer-links">
<a href="#" class="footer-link">サイトマップ</a>
<a href="#" class="footer-link">個人情報保護方針</a>
<a href="#" class="footer-link">特定個人情報基本方針</a>
<a href="#" class="footer-link">ご利用にあたって</a>
</div>
<div class="copyright">
Copyright© {{COMPANY_NAME}}. All Rights Reserved.
</div>
</div>
</footer>
</body>
</html>`;
}
}