必要になったので。案外それっぽいツールないんですよねえ……


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>高機能サイトマップジェネレーター</title>
    <!-- Google Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
    <!-- Font Awesome (for icons) -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />

    <style id="sitemap-styles">
        /* (基本CSSは変更なし) */
        :root {
            --primary-color: #007bff;
            --secondary-color: #6c757d;
            --danger-color: #dc3545;
            --info-color: #17a2b8;
            --bg-color: #f8f9fa;
            --border-color: #dee2e6;
            --text-color: #333;
            --level1-bg: #e0f7fa;
            --level2-bg: #b2ebf2;
            --level3-bg: #80deea;
            --level4-bg: #4dd0e1;
        }
        body {
            font-family: 'Noto Sans JP', sans-serif;
            margin: 0;
            padding: 20px;
            background-color: var(--bg-color);
            color: var(--text-color);
            display: flex;
            gap: 20px;
        }
        .container { display: flex; width: 100%; gap: 20px; }
        .editor-pane, .preview-pane {
            background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: auto;
        }
        .editor-pane { width: 50%; }
        .preview-pane { width: 50%; }
        h1, h2 { margin-top: 0; border-bottom: 2px solid var(--primary-color); padding-bottom: 10px; }

        .control-panel, .output-controls { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid var(--border-color); }
        .btn { padding: 8px 15px; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; display: inline-flex; align-items: center; gap: 8px; transition: background-color 0.2s; }
        .btn:disabled { background-color: #ccc; cursor: not-allowed; }
        .btn i { font-size: 1.1em; }
        .btn-primary { background-color: var(--primary-color); color: white; }
        .btn-primary:hover:not(:disabled) { background-color: #0056b3; }
        .btn-secondary { background-color: var(--secondary-color); color: white; }
        .btn-secondary:hover:not(:disabled) { background-color: #5a6268; }
        .btn-success { background-color: #28a745; color: white; }
        .btn-success:hover:not(:disabled) { background-color: #218838; }
        .btn-danger { background-color: var(--danger-color); color: white; }
        .btn-danger:hover:not(:disabled) { background-color: #c82333; }
        .btn-info { background-color: var(--info-color); color: white; }
        .btn-info:hover:not(:disabled) { background-color: #138496; }
        
        #site-name-input { margin-bottom: 15px; }
        #site-name-input input { width: calc(100% - 10px); padding: 5px; border: 1px solid var(--border-color); border-radius: 4px; }
        .page-input { margin-bottom: 5px; padding-left: 20px; position: relative; }
        .page-item { padding: 8px; background-color: #f9f9f9; border: 1px solid var(--border-color); border-radius: 4px; display: flex; align-items: center; gap: 5px; }
        .page-input[data-level="1"] > .page-item { background-color: var(--level1-bg); }
        .page-input[data-level="2"] > .page-item { background-color: var(--level2-bg); }
        .page-input[data-level="3"] > .page-item { background-color: var(--level3-bg); }
        .page-input[data-level="4"] > .page-item { background-color: var(--level4-bg); }
        .page-item input { border: 1px solid #ccc; padding: 4px; border-radius: 3px; }
        .page-item input.page-name { width: 120px; font-weight: bold; }
        .page-item input.page-url, .page-item input.description { flex-grow: 1; }
        .page-item .handle { cursor: grab; color: #777; }
        .page-item .page-actions button { background: none; border: none; cursor: pointer; font-size: 1.2em; padding: 0 5px; }
        .page-item .page-actions .add-child { color: green; }
        .page-item .page-actions .remove-page { color: red; }
        .children-container { padding-top: 5px; }

        #sitemap-preview { aspect-ratio: 1 / 1.414; padding: 20px; border: 1px solid #ddd; background-color: #ffffff; }
        #sitemap-preview .site-title, .sitemap-body .site-title {
            font-size: 1.8em; font-weight: bold; text-decoration: underline; margin-bottom: 1em; display: block; text-align: center;
        }
        
        .page-details { padding: 5px; border-radius: 4px; }
        .page-line1 { display: flex; align-items: baseline; gap: 10px; }
        .page-url { font-size: 0.9em; color: #555; word-break: break-all; }
        .page-name { font-weight: bold; }
        .page-line2 { margin-top: 4px; padding-left: 15px; }
        .description { font-size: 0.9em; color: #333; }
        .sitemap-list .page-node { margin-bottom: 10px; padding-left: 30px; }
        .sitemap-list .page-node[data-level="1"] .page-details { background-color: var(--level1-bg); }
        .sitemap-list .page-node[data-level="2"] .page-details { background-color: var(--level2-bg); }
        .sitemap-list .page-node[data-level="3"] .page-details { background-color: var(--level3-bg); }
        .sitemap-list .page-node[data-level="4"] .page-details { background-color: var(--level4-bg); }

        .sitemap-tree { padding-left: 20px; }
        .sitemap-tree ul { padding-left: 20px; list-style: none; position: relative; }
        .sitemap-tree ul::before { content: ''; position: absolute; top: 0; left: 0; border-left: 1px solid #ccc; width: 1px; height: 100%; }
        .sitemap-tree li { position: relative; padding: 5px 0; }
        .sitemap-tree li::before { content: ''; position: absolute; top: 18px; left: -20px; border-bottom: 1px solid #ccc; width: 20px; height: 0; }
        .sitemap-tree li:last-child > ul::before { height: 18px; }
        .sitemap-tree .page-details { border: 1px solid var(--border-color); display: inline-block; }
        .sitemap-tree li[data-level="1"] .page-details { background-color: var(--level1-bg); }
        .sitemap-tree li[data-level="2"] .page-details { background-color: var(--level2-bg); }
        .sitemap-tree li[data-level="3"] .page-details { background-color: var(--level3-bg); }
        .sitemap-tree li[data-level="4"] .page-details { background-color: var(--level4-bg); }

        /* ★★★ 修正点:画像キャプチャ時専用のスタイルを追加 ★★★ */
        #sitemap-preview.capture-mode {
            width: 1200px !important;
            height: auto !important;
            aspect-ratio: auto !important;
        }
        #sitemap-preview.capture-mode .page-line1 {
            display: block; /* flexを解除 */
        }
        #sitemap-preview.capture-mode .page-line1 > span {
            display: inline-block; /* inline-blockで横並びを再現 */
            vertical-align: baseline;
            margin-right: 10px; /* gapの代わり */
        }
        /* ★★★ 修正ここまで ★★★ */

        @media print {
            body { display: block; }
            .editor-pane, .preview-pane > h1, .preview-pane .output-controls { display: none; }
            .preview-pane { width: 100%; box-shadow: none; border: none; padding: 0; }
            #sitemap-preview { border: none; aspect-ratio: auto; }
        }
        @media (max-width: 900px) {
            .container { flex-direction: column; }
            .editor-pane, .preview-pane { width: auto; }
        }
    </style>
</head>
<body>

<div class="container">
    <!-- (HTML部分は変更なし) -->
    <div class="editor-pane">
        <h1><i class="fas fa-edit"></i> サイトマップエディタ</h1>
        <div class="control-panel">
            <button id="save-btn" class="btn btn-primary"><i class="fas fa-save"></i> JSON保存</button>
            <button id="load-btn" class="btn btn-secondary"><i class="fas fa-folder-open"></i> JSON読込</button>
            <input type="file" id="file-input" accept=".json" style="display: none;">
        </div>
        <div id="site-name-input">
            <label for="site-name"><strong>サイト名:</strong></label>
            <input type="text" id="site-name" placeholder="サイト名を入力">
        </div>
        <h2><i class="fas fa-sitemap"></i> サイト構成</h2>
        <div id="sitemap-editor-root" class="children-container">
            <div class="page-input" data-level="1" data-id="root">
                <div class="page-item">
                    <span class="handle">☰</span>
                    <input type="text" class="page-name" value="トップページ" placeholder="ページ名">
                    <input type="text" class="page-url" placeholder="URL">
                    <input type="text" class="description" placeholder="説明文">
                    <div class="page-actions"><button class="add-child" title="子ページを追加">+</button></div>
                </div>
                <div class="children-container"></div>
            </div>
        </div>
    </div>
    <div class="preview-pane">
        <h1><i class="fas fa-eye"></i> プレビュー&出力</h1>
        <div class="output-controls">
            <div class="view-options">
                <strong>表示形式:</strong>
                <input type="radio" id="view-list" name="view-type" value="list" checked> <label for="view-list">リスト</label>
                <input type="radio" id="view-tree" name="view-type" value="tree"> <label for="view-tree">ツリー</label>
            </div>
            <button id="generate-btn" class="btn btn-success"><i class="fas fa-cogs"></i> 生成</button>
            <button id="save-png-btn" class="btn btn-primary"><i class="fas fa-file-image"></i> PNG保存</button>
            <button id="save-image-pdf-btn" class="btn btn-primary"><i class="fas fa-image"></i> 画像PDF</button>
            <button id="save-text-pdf-btn" class="btn btn-danger"><i class="fas fa-table"></i> テキストPDF</button>
            <button id="save-html-btn" class="btn btn-info"><i class="fas fa-code"></i> HTML出力</button>
            <button id="print-btn" class="btn btn-secondary"><i class="fas fa-print"></i> 印刷</button>
        </div>
        <div id="sitemap-preview"></div>
    </div>
</div>

<!-- Libraries -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.2/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>


<script>
    $(document).ready(function() {
        // (ライブラリチェックは変更なし)
        const coreLibsLoaded = typeof jQuery !== 'undefined' && typeof Sortable !== 'undefined' && typeof Swal !== 'undefined';
        const canvasLibLoaded = typeof html2canvas !== 'undefined';
        const pdfLibsLoaded = typeof window.jspdf !== 'undefined' && typeof window.jspdf.jsPDF.autoTable === 'function';

        if (!coreLibsLoaded) {
            alert('基本ライブラリの読み込みに失敗しました。ページを再読み込みしてください。');
            return;
        }
        if (!canvasLibLoaded) {
            $('#save-png-btn, #save-image-pdf-btn').prop('disabled', true).attr('title', '画像出力ライブラリの読み込みに失敗しました');
        }
        if (!pdfLibsLoaded) {
            $('#save-image-pdf-btn, #save-text-pdf-btn').prop('disabled', true).attr('title', 'PDF出力ライブラリの読み込みに失敗しました');
        }
        
        function generateFilenamePrefix() { /* ... */ }
        function initializeSortable(element) { /* ... */ }
        function updateLevels() { /* ... */ }
        initializeSortable(document.getElementById('sitemap-editor-root'));
        $(document).on('click', '.add-child', function() { /* ... */ });
        $(document).on('click', '.remove-page', function() { /* ... */ });
        function getInputDataAsObject(element) { /* ... */ }
        $('#save-btn').click(function() { /* ... */ });
        function buildInputFromData(dataArray, container) { /* ... */ }
        $('#load-btn').click(() => $('#file-input').click());
        $('#file-input').change(function(event) { /* ... */ });
        function generateSitemapHTML(data, type) { /* ... */ }
        $('#generate-btn').click(function() { /* ... */ });

        // ★★★ ここからが修正箇所です ★★★

        $('#save-png-btn').click(function() {
            const previewArea = $('#sitemap-preview');
            if (previewArea.html().trim() === '') {
                Swal.fire('エラー', '先にサイトマップを生成してください。', 'error');
                return;
            }

            Swal.fire({ title: 'PNG画像を生成中...', text: 'しばらくお待ちください', allowOutsideClick: false, didOpen: () => { Swal.showLoading(); } });

            // キャプチャ用に一時的にスタイルを適用
            previewArea.addClass('capture-mode');

            html2canvas(previewArea.get(0), { // jQueryオブジェクトからDOM要素を取得
                scale: 2,
                windowWidth: previewArea.get(0).scrollWidth,
                windowHeight: previewArea.get(0).scrollHeight
            }).then(canvas => {
                const link = document.createElement('a');
                link.download = generateFilenamePrefix() + '.png';
                link.href = canvas.toDataURL('image/png');
                link.click();
            }).catch(err => {
                Swal.fire('エラー', '画像生成中にエラーが発生しました。', 'error');
                console.error(err);
            }).finally(() => {
                // 成功しても失敗しても、必ず元のスタイルに戻す
                previewArea.removeClass('capture-mode');
                Swal.close();
            });
        });

        $('#save-image-pdf-btn').click(function() {
            const previewArea = $('#sitemap-preview');
            if (previewArea.html().trim() === '') {
                Swal.fire('エラー', '先にサイトマップを生成してください。', 'error');
                return;
            }

            Swal.fire({ title: '画像PDFを生成中...', text: 'しばらくお待ちください', allowOutsideClick: false, didOpen: () => { Swal.showLoading(); } });
            
            // キャプチャ用に一時的にスタイルを適用
            previewArea.addClass('capture-mode');

            html2canvas(previewArea.get(0), {
                scale: 2,
                windowWidth: previewArea.get(0).scrollWidth,
                windowHeight: previewArea.get(0).scrollHeight
            }).then(canvas => {
                const imgData = canvas.toDataURL('image/png');
                const { jsPDF } = window.jspdf;
                const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
                
                const pdfWidth = pdf.internal.pageSize.getWidth();
                const pdfHeight = pdf.internal.pageSize.getHeight();
                const margin = 10;
                
                const ratio = canvas.width / canvas.height;
                let pdfImgWidth = pdfWidth - margin * 2;
                let pdfImgHeight = pdfImgWidth / ratio;

                if (pdfImgHeight > pdfHeight - margin * 2) {
                    pdfImgHeight = pdfHeight - margin * 2;
                    pdfImgWidth = pdfImgHeight * ratio;
                }

                const x = (pdfWidth - pdfImgWidth) / 2;
                const y = (pdfHeight - pdfImgHeight) / 2;

                pdf.addImage(imgData, 'PNG', x, y, pdfImgWidth, pdfImgHeight);
                pdf.save(generateFilenamePrefix() + '_image.pdf');
            }).catch(err => {
                Swal.fire('エラー', 'PDF生成中にエラーが発生しました。', 'error');
                console.error(err);
            }).finally(() => {
                // 必ず元のスタイルに戻す
                previewArea.removeClass('capture-mode');
                Swal.close();
            });
        });


        // --- 他の保存ボタン (変更なし) ---
        $('#save-text-pdf-btn').click(function() { /* ... */ });
        $('#save-html-btn').click(function() { /* ... */ });
        $('#print-btn').click(function() { /* ... */ });
        
        // (省略した関数の中身を再掲)
        function generateFilenamePrefix() { const siteName = ($('#site-name').val().trim() || 'サイトマップ').replace(/[\\/:*?"<>|]/g, ''); const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); const dateString = `${year}${month}${day}`; return `サイトマップ_${siteName}_${dateString}`; }
        function initializeSortable(element) { $(element).find('.children-container').each(function() { new Sortable(this, { animation: 150, group: 'sitemap', handle: '.handle', onEnd: function (evt) { updateLevels(); } }); }); }
        function updateLevels() { $('#sitemap-editor-root').find('.page-input').each(function() { const level = $(this).parents('.page-input').length + 1; $(this).attr('data-level', level); }); }
        $(document).on('click', '.add-child', function() { const parent = $(this).closest('.page-input'); const newLevel = parseInt(parent.data('level')) + 1; if (newLevel > 4) { Swal.fire('エラー', '階層は4階層までです。', 'error'); return; } const newPageId = 'page-' + Date.now(); const newPageInput = ` <div class="page-input" data-level="${newLevel}" data-id="${newPageId}"> <div class="page-item"> <span class="handle">☰</span> <input type="text" class="page-name" placeholder="ページ名"> <input type="text" class="page-url" placeholder="URL"> <input type="text" class="description" placeholder="説明文"> <div class="page-actions"> <button class="add-child" title="子ページを追加">+</button> <button class="remove-page" title="このページを削除">-</button> </div> </div> <div class="children-container"></div> </div> `; const childrenContainer = parent.find('> .children-container'); childrenContainer.append(newPageInput); initializeSortable(childrenContainer[0]); });
        $(document).on('click', '.remove-page', function() { const pageToRemove = $(this).closest('.page-input'); Swal.fire({ title: 'ページを削除しますか?', text: "この操作は元に戻せません。", icon: 'warning', showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: '#3085d6', confirmButtonText: 'はい、削除します', cancelButtonText: 'キャンセル' }).then((result) => { if (result.isConfirmed) { pageToRemove.remove(); Swal.fire('削除しました', '', 'success'); } }); });
        function getInputDataAsObject(element) { const data = []; $(element).find('> .page-input').each(function() { const page = $(this); const pageData = { id: page.data('id'), name: page.find('> .page-item > .page-name').val(), url: page.find('> .page-item > .page-url').val(), description: page.find('> .page-item > .description').val(), children: getInputDataAsObject(page.find('> .children-container')) }; data.push(pageData); }); return data; }
        $('#save-btn').click(function() { const siteData = { siteName: $('#site-name').val(), structure: getInputDataAsObject($('#sitemap-editor-root')) }; const jsonString = JSON.stringify(siteData, null, 2); const blob = new Blob([jsonString], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = generateFilenamePrefix() + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Swal.fire('成功', 'JSONファイルとして保存しました。', 'success'); });
        function buildInputFromData(dataArray, container) { container.empty(); dataArray.forEach(item => { const newLevel = $(container).closest('.page-input').length + 1; const removeButton = item.id !== 'root' ? `<button class="remove-page" title="このページを削除">-</button>` : ''; const newPageInput = $(` <div class="page-input" data-level="${newLevel}" data-id="${item.id || 'page-' + Date.now()}"> <div class="page-item"> <span class="handle">☰</span> <input type="text" class="page-name" value="${item.name || ''}" placeholder="ページ名"> <input type="text" class="page-url" value="${item.url || ''}" placeholder="URL"> <input type="text" class="description" value="${item.description || ''}" placeholder="説明文"> <div class="page-actions"> <button class="add-child" title="子ページを追加">+</button> ${removeButton} </div> </div> <div class="children-container"></div> </div> `); container.append(newPageInput); if (item.children && item.children.length > 0) { buildInputFromData(item.children, newPageInput.find('.children-container')); } }); }
        $('#file-input').change(function(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const data = JSON.parse(e.target.result); $('#site-name').val(data.siteName || ''); buildInputFromData(data.structure, $('#sitemap-editor-root')); initializeSortable($('#sitemap-editor-root')[0]); updateLevels(); Swal.fire('成功', 'データを読み込みました。', 'success'); } catch (err) { Swal.fire('エラー', 'JSONファイルの形式が正しくありません。', 'error'); } }; reader.readAsText(file); $(this).val(''); });
        function generateSitemapHTML(data, type) { function buildPageBlock(item) { const urlHtml = item.url ? `<span class="page-url">${item.url}</span>` : ''; const descriptionHtml = item.description ? `<div class="page-line2"><span class="description">${item.description}</span></div>` : ''; return ` <div class="page-details"> <div class="page-line1"> ${urlHtml} <span class="page-name">${item.name}</span> </div> ${descriptionHtml} </div> `; } if (type === 'list') { let html = ''; function buildList(items, level) { items.forEach(item => { html += `<div class="page-node" data-level="${level}">${buildPageBlock(item)}</div>`; if (item.children && item.children.length > 0) { buildList(item.children, level + 1); } }); } buildList(data, 1); return `<div class="sitemap-list">${html}</div>`; } if (type === 'tree') { let html = ''; function buildTree(items, level) { html += '<ul>'; items.forEach(item => { html += `<li data-level="${level}">${buildPageBlock(item)}`; if (item.children && item.children.length > 0) { buildTree(item.children, level + 1); } html += '</li>'; }); html += '</ul>'; } buildTree(data, 1); return `<div class="sitemap-tree">${html}</div>`; } }
        $('#generate-btn').click(function() { const previewArea = $('#sitemap-preview'); previewArea.empty(); const siteName = $('#site-name').val(); const siteData = getInputDataAsObject($('#sitemap-editor-root')); const viewType = $('input[name="view-type"]:checked').val(); if(!siteData.length) { Swal.fire('情報がありません', 'サイト構成を入力してください。', 'info'); return; } previewArea.append(`<div class="site-title">${siteName}</div>`); previewArea.append(generateSitemapHTML(siteData, viewType)); });
        $('#save-text-pdf-btn').click(function() { const siteData = getInputDataAsObject($('#sitemap-editor-root')); if (!siteData.length) { Swal.fire('エラー', 'サイト構成が入力されていません。', 'error'); return; } Swal.fire({ title: 'テキストPDFを生成中...', allowOutsideClick: false, didOpen: () => { Swal.showLoading(); } }); const { jsPDF } = window.jspdf; const doc = new jsPDF(); doc.setFont('helvetica', 'normal'); const siteName = $('#site-name').val() || 'サイトマップ'; doc.text(siteName, 14, 15); const tableData = []; function flattenData(items, level) { items.forEach(item => { const indent = " ".repeat((level - 1) * 4); tableData.push([ indent + item.name, item.url, item.description ]); if (item.children && item.children.length > 0) { flattenData(item.children, level + 1); } }); } flattenData(siteData, 1); doc.autoTable({ head: [['ページ名', 'URL', '説明文']], body: tableData, startY: 20, styles: { font: 'helvetica', fontSize: 10 }, headStyles: { fillColor: [41, 128, 185] }, }); doc.save(generateFilenamePrefix() + '_text.pdf'); Swal.close(); });
        $('#save-html-btn').click(function() { const previewArea = $('#sitemap-preview'); if (previewArea.html().trim() === '') { Swal.fire('エラー', '先にサイトマップを生成してください。', 'error'); return; } const sitemapBodyHtml = previewArea.html(); const styles = $('#sitemap-styles').html(); const fullHtml = ` <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>${$('#site-name').val() || 'サイトマップ'}</title> <style> ${styles} body { display: block; padding: 20px; } .sitemap-body { border: 1px solid #ccc; padding: 20px; max-width: 1000px; margin: 0 auto; } </style> </head> <body> <div class="sitemap-body"> ${sitemapBodyHtml} </div> </body> </html> `; const blob = new Blob([fullHtml], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = generateFilenamePrefix() + '.html'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); });
        $('#print-btn').click(function() { const previewArea = document.getElementById('sitemap-preview'); if (previewArea.innerHTML.trim() === '') { Swal.fire('エラー', '先にサイトマップを生成してください。', 'error'); return; } window.print(); });
    });
</script>

</body>
</html>