サイトマップを自作
<!-- 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">▶</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 = "▼";
toggleIcon.classList.add("expanded");
} else {
targetElement.style.display = "none";
toggleIcon.innerHTML = "▶";
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>
## 関連リンク