GitHub likeな2カラムブログテーマです。
プレビュー -> https://saica94.hatenablog.com/
テーマを完璧に動作させる為に、以下の変更をご自身で適用してください。
1. 設定 -> 詳細設定 -> <head>要素にメタデータを追加
<!--Web Font 読み込み--> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=BIZ+UDPGothic&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans+JP&family=M+PLUS+1+Code:wght@100..700&display=swap" rel="stylesheet">
2. デザイン -> カスタマイズ -> フッタ
<!-- シンタックスハイライト --> <script type="module"> import { codeToHtml } from "https://esm.sh/shiki@3.0.0" /** * data-lang を言語名とラベルに分割する * 例: "tsx:testfile.tsx" → ["tsx", "testfile.tsx"] * "javascript" → ["javascript", null] */ function splitLangAndLabel(dataLang) { if (!dataLang) return [dataLang, null] const index = dataLang.indexOf(":") if (index === -1) return [dataLang, null] return [dataLang.substring(0, index), dataLang.substring(index + 1)] } const pres = document.querySelectorAll("pre.code") pres.forEach((pre) => { const [lang, label] = splitLangAndLabel(pre.dataset.lang) const rawCode = pre.textContent.trim() // 言語が取得できない場合はスキップ if (!lang) return ; (async () => { let codeHtml try { codeHtml = await codeToHtml(rawCode, { theme: "github-dark", lang: lang, transformers: [ { name: 'add-meta', pre(node) { this.addClassToHast(node, 'shiki') // ラベルがあれば表示用にセット、なければ言語名を表示 node.properties['data-lang'] = label || lang }, line(node, line) { this.addClassToHast(node, 'line') } } ] }) } catch (e) { // サポート外の言語の場合、プレーンテキストとしてフォールバック console.warn(`Shiki: lang "${lang}" not supported, falling back to plaintext`, e) codeHtml = await codeToHtml(rawCode, { theme: "github-dark", lang: "text", transformers: [ { name: 'add-meta', pre(node) { this.addClassToHast(node, 'shiki') node.properties['data-lang'] = label || lang }, line(node, line) { this.addClassToHast(node, 'line') } } ] }) } // 1. HTML文字列をDOM要素に変換 const tempDiv = document.createElement("div") tempDiv.innerHTML = codeHtml const shikiPre = tempDiv.querySelector("pre") // 2. コピーボタンを作成 const btn = document.createElement("button") btn.className = "copy-btn" btn.textContent = "Copy" // 3. クリックイベントを設定 btn.onclick = () => { navigator.clipboard.writeText(rawCode).then(() => { const originalText = btn.textContent btn.textContent = "Copied!" btn.classList.add("copied") setTimeout(() => { btn.textContent = originalText btn.classList.remove("copied") }, 2000) }).catch(err => { console.error('Copy failed', err) btn.textContent = "Error" }) } // 4. 生成されたpreの中にボタンを追加 shikiPre.appendChild(btn) // 5. 元のpreを置き換え pre.replaceWith(shikiPre) })() }) </script> <!-- シンタックスハイライト --> <!-- 見出しアンカーリンク & 目次リンク修正 --> <script> document.addEventListener("DOMContentLoaded", () => { const entryContents = document.querySelectorAll(".entry-content") entryContents.forEach((entry) => { const headings = entry.querySelectorAll("h1, h2, h3, h4, h5, h6") const usedIds = new Set() // Step 1: すべての見出しにidを付与 headings.forEach((heading) => { const text = heading.textContent.trim() if (!heading.id) { let id = text .replace(/\s+/g, "-") .replace(/[<>"'&]/g, "") // 重複回避 let uniqueId = id let counter = 1 while (usedIds.has(uniqueId)) { uniqueId = `${id}-${counter}` counter++ } usedIds.add(uniqueId) heading.id = uniqueId } else { usedIds.add(heading.id) } }) // Step 2: 目次の <li> にリンクを追加 // はてなブログの [:contents] は <li> にプレーンテキストのみ出力するため、 // JSでリンクを生成して挿入する const tocLists = entry.querySelectorAll(".table-of-contents") tocLists.forEach((toc) => { const items = toc.querySelectorAll("li") items.forEach((li) => { // 子要素の <ul> は除外してテキスト部分だけ取得 // (はてなのキーワードリンク <a class="keyword"> のテキストも含める) const childUl = li.querySelector(":scope > ul") // li直下のテキストを取得(子ulのテキストは含めない) let itemText = "" li.childNodes.forEach((node) => { if (node === childUl) return if (node.nodeType === Node.TEXT_NODE) { itemText += node.textContent } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== "UL") { itemText += node.textContent } }) itemText = itemText.trim() if (!itemText) return // テキストに一致する見出しを探す const match = Array.from(headings).find((h) => { const headingText = h.textContent.replace(/#$/, "").trim() return headingText === itemText }) if (!match) return // li直下のテキストノード・要素を <a> でラップ const link = document.createElement("a") link.href = `#${match.id}` link.style.color = "inherit" link.style.textDecoration = "none" // li直下のノード(ul以外)をリンクの中に移動 const nodesToWrap = [] li.childNodes.forEach((node) => { if (node !== childUl) { nodesToWrap.push(node) } }) nodesToWrap.forEach((node) => link.appendChild(node)) // リンクをliの先頭に挿入(ulの前に) if (childUl) { li.insertBefore(link, childUl) } else { li.appendChild(link) } }) }) }) }) </script> <!-- テキスト置換(アイコン化) --> <script> document.addEventListener("DOMContentLoaded", () => { const replacements = [ { pattern: "[!point]", emoji: "📌" }, { pattern: "[!caution]", emoji: "⚠️" }, { pattern: "[!tips]", emoji: "💡" }, { pattern: "[!memo]", emoji: "📝" }, { pattern: "[!important]", emoji: "❗" } ] // テキストノードを再帰的に走査して置換する function walkTextNodes(node) { if (node.nodeType === Node.TEXT_NODE) { let text = node.textContent let changed = false replacements.forEach(({ pattern, emoji }) => { if (text.includes(pattern)) { text = text.replaceAll(pattern, emoji) changed = true } }) if (changed) node.textContent = text return } // コードブロック内はスキップ if (node.tagName === "PRE" || node.tagName === "CODE") return node.childNodes.forEach((child) => walkTextNodes(child)) } document.querySelectorAll(".entry-content").forEach(walkTextNodes) }) </script> <!-- 追従するトップへ戻るボタンHTML --> <a href="#" id="page-top"><i class="blogicon-chevron-up"></i></a> <!-- 追従するトップへ戻るボタンHTML -->
本文中に以下を記載することにより自動的に絵文字へ変換されます。
[!point] -> 📌
[!caution] -> ⚠️
[!tips] -> 💡
[!memo] -> 📝
[!important] -> ❗
コードのシンタックスハイライトを適用する際、以下のように記載するとファイル名もヘッダーに記載出来ます。
lang:file_name.extension
例) rust:test_file.rs


