サイトマップを自作

<!-- markdown-mode-on --> ## **概要** JetThemeのデフォルトのサイトマップにもっさり感があり、好みではないので自作してみた。もちろん、自分の能力では無理なので、<svg width="64" height="24" class="blog-logo" aria-label="i-gemini"><use href="#i-gemini"/></svg> に作ってもらった。 最初は「**BloggerのJSONフィードの取得件数制限(最大150件)**」にぶち当たり、全記事タイトルの取得ができなかった。しかし、[BlogToc](https://clusteramaryllis.github.io/blogtoc/)で使われている段階的な記事取得(記事の取得を50件ずつ繰り返す)手法を使って全記事を取得できるようになった。 <a name="more"></a> ## 仕様 * JetTheme のデフォルトサイトマップと同等の挙動 * カテゴリ(ラベル)ごとに展開可能なブロック(初期非表示、クリックで展開) * カテゴリとハッシュタグを両方すべて網羅 * Bloggerの全ラベルを表示(使用頻度・投稿数問わず) * 各ラベルに属する記事タイトルリストを表示(タイトルと公開日を1行) * ラベルが未設定の投稿も「未分類」として表示 * カテゴリとハッシュタグが両方未設定だと、直接に記事タイトルリストを表示 * すべての処理をJavaScriptで動的に行う * data-label を手書きしない汎用的な構造 * Bloggerの仕様による制限(取得件数制限・ラベル一覧APIの欠如)を回避するロジック:[BlogToc](https://clusteramaryllis.github.io/blogtoc/) * JetTheme のレイアウトやスタイルに馴染むデザイン ## コード 管理画面」→「ページ」→「+新しいページ」から、HTML編集画面に次のコードを貼り付けます。 <details> <summary>DIY Site Map</summary> ```html <div id="sitemap-container"> <p>サイトマップを読み込み中...</p> </div> <style type="text/css"> /* カスタム変数定義(JetThemeのスタイルに合わせる) */ :root { --jt-border-light: #ddd; /* 適切な色に調整またはJetThemeの変数があればそれを使用 */ --jt-primary-color: #007bff; /* JetThemeのプライマリ色に合わせて調整 */ --jt-heading-color: #333; /* JetThemeの見出し色に合わせて調整 */ --jt-text-color: #555; /* JetThemeのテキスト色に合わせて調整 */ --jt-link-color: #007bff; /* JetThemeのリンク色に合わせて調整 */ --jt-meta-color: #888; /* JetThemeのメタ情報色に合わせて調整 */ --jt-bg-color: #ffffff; /* JetThemeの背景色に合わせて調整 */ } /* サイトマップ全体 */ .sitemap-wrapper { margin-top: 20px; background-color: var(--jt-bg-color); padding: 15px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } /* カテゴリタイトル */ .sitemap-category-title { cursor: pointer; padding: 10px 0; border-bottom: 1px solid var(--jt-border-light); margin-bottom: 5px; display: flex; justify-content: flex-start; /* アイコンを左に寄せる */ align-items: center; font-size: 1em; font-weight: normal; color: var(--jt-heading-color); transition: color 0.2s ease-in-out; /* ここからH3見出しタグの装飾を削除するための変更 */ background: none !important; /* 背景色・画像を削除 */ border: none !important; /* 全てのボーダーを削除 */ box-shadow: none !important; /* 影を削除 */ text-decoration: none !important; /* 下線などを削除 */ outline: none !important; /* フォーカス時のアウトラインを削除 */ line-height: 1.3 !important; /* 行の高さを詰める (h3の垂直方向の空間に影響) */ min-height: 1.2em !important; /* テキストが見える最低限の高さを確保 (font-sizeと同じ値) */ padding-top: 0.2em !important; /* 上パディングを0に */ padding-bottom: 0 !important; /* 下パディングを0に */ margin-top: 0 !important; /* 上マージンを0に */ margin-bottom: 0 !important; /* 下マージンを0に */ } .sitemap-category-title:hover { color: var(--jt-primary-color); } /* 展開/折りたたみアイコン */ .sitemap-toggle-icon { font-size: 0.8em; /* 80%に縮小 */ margin-right: 8px; /* アイコンとテキストの間隔 */ color: var(--jt-text-color); /* デフォルトの色 */ transition: color 0.2s ease-in-out, transform 0.2s ease-in-out; } .sitemap-category-title:hover .sitemap-toggle-icon { color: var(--jt-primary-color); /* ホバー時にテキスト色と合わせる */ } .sitemap-category-title.expanded .sitemap-toggle-icon { transform: rotate(90deg); /* 展開時にアイコンを回転 */ } /* 記事リスト */ .sitemap-post-list { list-style: none; padding: 0; margin-top: 6px; /* 行間を狭める */ margin-bottom: 15px; } /* カテゴリがない場合のリストのための新しいスタイル */ .sitemap-all-posts-list { list-style: none; padding: 0; margin-top: 0; /* カテゴリタイトルがないためマージンを調整 */ margin-bottom: 15px; } .sitemap-post-list li, .sitemap-all-posts-list li { /* 両方のリストに適用 */ margin-bottom: 2px; /* 行間を狭める */ padding-left: 15px; position: relative; display: flex; /* Flexboxを適用 */ justify-content: space-between; /* 記事タイトルと日付を両端に配置 */ align-items: baseline; /* ベースラインを揃える */ } .sitemap-post-list li a, .sitemap-all-posts-list li a { /* 両方のリストに適用 */ text-decoration: none; color: var(--jt-link-color); transition: color 0.2s ease-in-out; flex-grow: 1; /* リンクが利用可能なスペースを占める */ margin-right: 10px; /* 日付との間にスペース */ font-size: 0.9em; /* 例: 少し小さくする */ } .sitemap-post-list li a:hover, .sitemap-all-posts-list li a:hover { /* 両方のリストに適用 */ color: var(--jt-primary-color); } /* 投稿日 */ .sitemap-post-date { font-size: 0.8em; color: var(--jt-meta-color); white-space: nowrap; /* 日付が改行されないように */ } </style> <script type="text/javascript"> //<![CDATA[ var sitemapData = { posts: [], labels: new Set(), totalFetched: 0, totalPosts: 0, callbackCounter: 0, // コールバックの完了を追跡するためのカウンター }; function handlePosts(json) { var entries = json.feed.entry; if (entries) { sitemapData.totalFetched += entries.length; if (sitemapData.totalPosts === 0) { sitemapData.totalPosts = parseInt( json.feed.openSearch$totalResults.$t, 10 ); } entries.forEach(function (entry) { var post = { title: entry.title.$t, published: new Date(entry.published.$t), url: (function () { for (var k = 0; k < entry.link.length; k++) { if ( entry.link[k].rel === "alternate" && entry.link[k].type === "text/html" ) { return entry.link[k].href; } } return "#"; // URLが見つからない場合のフォールバック })(), // ラベルが存在しない場合、空の配列を設定し、「未分類」は追加しない labels: entry.category ? entry.category.map(function (cat) { return cat.term; }) : [] }; sitemapData.posts.push(post); // ラベルが存在する場合のみSetに追加 post.labels.forEach(function (label) { if (label) sitemapData.labels.add(label); }); }); } // すべての記事が取得されたかチェック if (sitemapData.totalFetched < sitemapData.totalPosts) { var nextStartIndex = sitemapData.totalFetched + 1; var script = document.createElement("script"); script.src = "/feeds/posts/summary?start-index=" + nextStartIndex + "&max-results=150&alt=json-in-script&callback=handlePosts"; document.body.appendChild(script); } else { // 全ての記事が取得完了したらサイトマップを描画 renderSitemap(); } } function renderSitemap() { var sitemapContainer = document.getElementById("sitemap-container"); if (!sitemapContainer) { console.error( 'サイトマップコンテナが見つかりません。IDが "sitemap-container" の要素を作成してください。' ); return; } var sitemapHtml = '<div class="sitemap-wrapper">'; // ラベルが存在するかどうかをチェック if (sitemapData.labels.size > 0) { // ラベルが存在する場合(カテゴリありの場合) var postsByLabel = {}; sitemapData.labels.forEach(function (label) { postsByLabel[label] = []; }); sitemapData.posts.forEach(function (post) { // 記事にラベルが割り当てられている場合のみ追加 if (post.labels && post.labels.length > 0) { post.labels.forEach(function (label) { if (postsByLabel[label]) { postsByLabel[label].push(post); } }); } }); // 各ラベルの記事を公開日でソート(新しい順) for (var label in postsByLabel) { if (postsByLabel.hasOwnProperty(label)) { postsByLabel[label].sort(function (a, b) { return b.published - a.published; }); } } // ラベルのソート順を定義: #ハッシュタグ -> 通常のラベル var sortedLabels = Array.from(sitemapData.labels).sort(function (a, b) { var aIsHash = a.startsWith("#"); var bIsHash = b.startsWith("#"); if (aIsHash && !bIsHash) return -1; if (!aIsHash && bIsHash) return 1; return a.localeCompare(b); }); sortedLabels.forEach(function (label) { // ラベルに紐づく記事がない場合は表示しない (念のため) if (postsByLabel[label].length === 0) { return; } var labelId = "label-" + label.replace(/[^a-zA-Z0-9]/g, "-"); var articleCount = postsByLabel[label].length; sitemapHtml += '<div class="sitemap-category">' + '<h3 class="sitemap-category-title" data-toggle="' + labelId + '">' + '<span class="sitemap-toggle-icon">&#x25b6;</span>' + label + " (" + articleCount + ")" + "</h3>" + '<ul id="' + labelId + '" class="sitemap-post-list" style="display: none;">'; postsByLabel[label].forEach(function (post) { var dateString = post.published.toLocaleDateString("ja-JP", { year: "numeric", month: "2-digit", day: "2-digit", }); sitemapHtml += "<li><a href=\"" + post.url + '">' + post.title + '</a> <span class="sitemap-post-date">' + dateString + "</span></li>"; }); sitemapHtml += "</ul></div>"; }); } else { // ラベルが存在しない場合(カテゴリなしの場合) // すべての記事を公開日(新しい順)でソート sitemapData.posts.sort(function (a, b) { return b.published - a.published; }); sitemapHtml += '<ul class="sitemap-all-posts-list">'; // 新しいクラス名を使用 sitemapData.posts.forEach(function (post) { var dateString = post.published.toLocaleDateString("ja-JP", { year: "numeric", month: "2-digit", day: "2-digit", }); sitemapHtml += "<li><a href=\"" + post.url + '">' + post.title + '</a> <span class="sitemap-post-date">' + dateString + "</span></li>"; }); sitemapHtml += "</ul>"; } sitemapHtml += "</div>"; sitemapContainer.innerHTML = sitemapHtml; // クリックイベントリスナーを設定 (カテゴリがある場合のみ) if (sitemapData.labels.size > 0) { sitemapContainer .querySelectorAll(".sitemap-category-title") .forEach(function (title) { title.addEventListener("click", function () { var targetId = this.dataset.toggle; var targetElement = document.getElementById(targetId); var toggleIcon = this.querySelector(".sitemap-toggle-icon"); if (targetElement) { if (targetElement.style.display === "none") { targetElement.style.display = "block"; toggleIcon.innerHTML = "&#x25bc;"; toggleIcon.classList.add("expanded"); } else { targetElement.style.display = "none"; toggleIcon.innerHTML = "&#x25b6;"; toggleIcon.classList.remove("expanded"); } } }); }); } } // サイトマップの取得を開始 // 初期呼び出しはDOMContentLoaded後に行う document.addEventListener("DOMContentLoaded", function () { var initialScript = document.createElement("script"); initialScript.src = "/feeds/posts/summary?start-index=1&max-results=150&alt=json-in-script&callback=handlePosts"; document.body.appendChild(initialScript); }); //]]> </script> ``` </details> ## 関連リンク
Next Post Previous Post