Zensical 再現手順¶
会社の Claude に、このページの「指示文」をそのまま渡す。 解釈や改善はさせず、書いてあるファイルだけ作らせる。
単一リポジトリ向けである。 docs-hub のような複数リポジトリ集約は含めない。
Zensical 本体に図の拡大はない。
虫眼鏡は docs/stylesheets/mermaid.css と docs/javascripts/mermaid-zoom.js で足している。
横幅は docs/stylesheets/layout.css で広げている。
指示文にこれらを含めている。
フォルダ構成の意図¶
かつては 構成(overview/)と 手順(runbooks/)の2系統だけだった。
これだと「何を言えばいいか」「データはどこから入るか」「長い SQL やアーカイブはどこか」が、どちらにも収まりにくかった。
ページを足すたびに入口が肥大し、読者は自分の問いに合う場所を探しづらくなる。
いまは問いごとに系統を分ける。
| 系統 | ディレクトリ | 読者の問い |
|---|---|---|
| 使い方 | guide/ |
何をすればよいか。用語や画面の意味は何か |
| アーキテクチャ | architecture/ |
どう動くか。何が正本か |
| 取込 | intake/ |
データやイベントはどこから入るか(入口があるプロジェクトだけ) |
| 運用 | ops/ |
いま何の作業をするか。コマンドは何か |
| 参照 | reference/ |
長い定義、SQL、外部 URL、アーカイブはどこか |
分け方の基準は「情報の種類」ではなく「読者が今持っている問い」である。 仕組みの説明を運用ページに書かない。 手順をアーキテクチャページに書かない。 両方必要なときは、それぞれ短く書いて相互リンクする。
intake/ と reference/ は必須ではない。
入口が無いプロジェクト(例: pve-infra)では intake/ を置かない。
長い参照物が無いときは reference/ を薄くするか、外部リンク1枚だけにする。
ナビは zensical.toml にページ一覧を書かず、フォルダから自動生成する。
系統の名前とフォルダ名がそのまま左ナビの親になる。
集約サイト(docs-hub)でも、finance-manager / pve-infra / home-kitchen / docs-hub が同じ問いの地図を共有できる。
検索タグも系統名とリポジトリ名だけにする。 内容トピック(Discord、Cloudflare など)をタグにすると、フィルターが増えて逆に探しにくくなるためである。
指示文¶
以下をコピーして渡す。
あなたは Zensical の設定だけを再現する。推測で機能を足さない。
書いていない feature / extension / テンプレート上書きは作らない。
nav は zensical.toml に書かない(フォルダから自動生成させる)。
navigation.sections は使わない(タブと併用するとセクションの > が消える)。
variant は書かない(デフォルトの modern を使う)。
# フォルダ
docs/
index.md
javascripts/
search-filters-open.js
mermaid-zoom.js
stylesheets/
layout.css
mermaid.css
guide/
index.md
(使い方のページ)
architecture/
index.md
(仕組みのページ)
intake/ # データ入口があるプロジェクトだけ
index.md
ops/
index.md
(手順のページ)
reference/ # 長い参照・アーカイブ
index.md
zensical.toml はリポジトリルート。
フォルダ分けの意図(守ること):
- 読者の問いで分ける。情報の種類では分けない。
- guide = 何をすればよいか / 用語の意味。architecture = どう動くか / 何が正本か。
- intake = データの入口(無いプロジェクトでは作らない)。ops = 作業手順とコマンド。
- reference = 長い定義・SQL・外部 URL・アーカイブ。入口の説明はここへ逃がさない。
- 仕組みを ops に書かない。手順を architecture に書かない。両方要るときは短く書いて相互リンク。
- かつての overview(構成)/ runbooks(手順)の2系統には戻さない。問いが足りなかったためである。
- nav はフォルダから自動生成されるので、フォルダ名がそのままナビの親になる。
# アイコン
各 Markdown の frontmatter に icon を書く。
- docs/index.md → lucide/library
- docs/guide/index.md → lucide/book-open
- docs/architecture/index.md → lucide/boxes
- docs/ops/index.md → lucide/wrench
- docs/reference/index.md → lucide/library
- 個別ページ → 内容が分かる lucide アイコン(index 用アイコンの連打はしない)
frontmatter 例:
---
title: 使い方
description: (1行)
tags:
- 使い方
- <リポジトリ名>
icon: lucide/book-open
---
ops の tags は `- 運用`。architecture は `- アーキテクチャ`。
すべてのページにリポジトリ名タグを付ける(例: `- finance-manager`)。
検索フィルターは系統とリポジトリ名だけにする。内容トピックは付けない。
# zensical.toml
次をそのまま使う。山括弧だけ置き換える。
```toml
[project]
site_name = "<サイト名>"
site_url = "<公開URL>"
site_description = "<1行説明>"
docs_dir = "docs"
site_dir = "site"
extra_css = [
"stylesheets/layout.css",
"stylesheets/mermaid.css",
]
extra_javascript = [
"javascripts/search-filters-open.js",
"javascripts/mermaid-zoom.js",
]
repo_url = "https://github.com/<org>/<repo>"
repo_name = "<org>/<repo>"
edit_uri = "edit/main/docs/"
[project.theme]
features = [
"navigation.indexes",
"navigation.tabs",
"navigation.tabs.sticky",
"navigation.path",
"navigation.top",
"navigation.instant",
"navigation.instant.prefetch",
"navigation.instant.progress",
"navigation.tracking",
"toc.follow",
"search.highlight",
"search.suggest",
"content.code.copy",
"content.code.select",
"content.action.edit",
]
[project.theme.icon]
repo = "fontawesome/brands/github"
edit = "material/file-edit-outline"
[[project.theme.palette]]
media = "(prefers-color-scheme)"
primary = "blue"
accent = "blue"
toggle.icon = "lucide/sun-moon"
toggle.name = "ライトモードに切り替え"
[[project.theme.palette]]
media = "(prefers-color-scheme: light)"
scheme = "default"
primary = "blue"
accent = "blue"
toggle.icon = "lucide/sun"
toggle.name = "ダークモードに切り替え"
[[project.theme.palette]]
media = "(prefers-color-scheme: dark)"
scheme = "slate"
primary = "blue"
accent = "blue"
toggle.icon = "lucide/moon"
toggle.name = "システム設定に合わせる"
[project.markdown_extensions.admonition]
[project.markdown_extensions.pymdownx.details]
[project.markdown_extensions.pymdownx.inlinehilite]
[project.markdown_extensions.pymdownx.superfences]
custom_fences = [
{ name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" },
]
[project.markdown_extensions.pymdownx.highlight]
anchor_linenums = true
line_spans = "__span"
pygments_lang_class = true
[project.markdown_extensions.toc]
permalink = true
permalink_title = "この見出しへのリンク"
```
# docs/javascripts/search-filters-open.js
次のファイルをそのまま置く。検索を開いたときタグフィルターを最初から出す。
```javascript
/* 検索を開いたとき、タグフィルターを最初から表示する。 */
(function () {
var openedThisSession = false
var timers = []
function filtersVisible() {
return Array.prototype.some.call(document.querySelectorAll("h3"), function (el) {
return el.textContent.trim() === "Filters" && el.offsetParent !== null
})
}
function filterButton() {
var root = document.querySelector("[data-md-component=search]")
if (!root) return null
var buttons = Array.prototype.filter.call(root.querySelectorAll("button"), function (btn) {
return !btn.classList.contains("md-search__button")
})
return buttons.length ? buttons[buttons.length - 1] : null
}
function ensureOpen() {
if (filtersVisible()) return
var btn = filterButton()
if (btn) btn.click()
}
function startSession() {
if (openedThisSession) return
openedThisSession = true
;[0, 50, 200, 400].forEach(function (ms) {
timers.push(setTimeout(ensureOpen, ms))
})
}
function endSession() {
openedThisSession = false
timers.forEach(clearTimeout)
timers = []
}
function bind() {
var toggle = document.getElementById("__search")
if (!toggle || toggle.dataset.filtersDefault === "1") return
toggle.dataset.filtersDefault = "1"
toggle.addEventListener("change", function () {
if (toggle.checked) startSession()
else endSession()
})
}
if (window.document$) {
document$.subscribe(bind)
} else {
document.addEventListener("DOMContentLoaded", bind)
}
})()
```
# docs/stylesheets/layout.css
次のファイルをそのまま置く。コンテンツの横幅を広げる。
```css
/* 既定は約 61rem。本文と表、図が窮屈なので広げる。 */
.md-grid {
max-width: 90rem;
}
```
# docs/stylesheets/mermaid.css
次のファイルをそのまま置く。Mermaid 図の右上に、コードコピーと同じ系統の虫眼鏡を出す。
Zensical 本体に図の拡大はない。SVG は閉じた Shadow DOM に入る。
```css
/* コードブロックのコピーボタンと同じ位置と色にする。 */
.md-typeset .mermaid-frame {
position: relative;
margin: 1em 0;
}
.md-typeset .mermaid-frame > .mermaid {
margin: 0;
overflow: auto;
max-height: min(80vh, 48rem);
}
.md-typeset .mermaid-zoom-btn {
position: absolute;
top: 0.4rem;
right: 0.4rem;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
padding: 0;
border: 0;
border-radius: 0.1rem;
background: color-mix(in srgb, var(--md-default-bg-color) 82%, transparent);
color: var(--md-default-fg-color--lightest);
cursor: zoom-in;
}
.md-typeset .mermaid-frame:hover .mermaid-zoom-btn,
.md-typeset .mermaid-zoom-btn:focus-visible {
color: var(--md-default-fg-color--light);
}
.md-typeset .mermaid-zoom-btn:hover {
color: var(--md-accent-fg-color);
}
.md-typeset .mermaid-zoom-btn svg {
width: 1.05rem;
height: 1.05rem;
fill: currentColor;
}
html.mermaid-lightbox-open {
overflow: hidden;
}
.mermaid-lightbox {
position: fixed;
inset: 0;
z-index: 400;
background: color-mix(in srgb, var(--md-default-bg-color) 94%, transparent);
}
.mermaid-lightbox__close {
position: absolute;
top: 0.6rem;
right: 0.6rem;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
padding: 0;
border: 0;
border-radius: 0.1rem;
background: transparent;
color: var(--md-default-fg-color--light);
cursor: pointer;
}
.mermaid-lightbox__close:hover {
color: var(--md-accent-fg-color);
}
.mermaid-lightbox__close svg {
width: 1.1rem;
height: 1.1rem;
fill: currentColor;
}
.mermaid-lightbox__stage {
position: absolute;
inset: 0;
overflow: hidden;
cursor: grab;
}
.mermaid-lightbox__stage.is-dragging {
cursor: grabbing;
}
.mermaid-lightbox__stage .mermaid {
display: inline-block;
}
```
# docs/javascripts/mermaid-zoom.js
次のファイルをそのまま置く。描画後のホストを拡大ビューへ移す。
ラベルは出さない。title は「拡大」と「閉じる」だけにする。
```javascript
/* Mermaid は閉じた Shadow DOM のため、描画後のホストを拡大ビューへ移す。 */
(function () {
var MAGNIFY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79L20.5 19 19 20.5l-4.79-4.79v-.79l-.27-.27A6.5 6.5 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5z"/></svg>'
var CLOSE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>'
var overlay = null
var home = null
var timers = []
var observer = null
var dragMove = null
var dragUp = null
function clearTimers() {
timers.forEach(clearTimeout)
timers = []
}
function isHost(el) {
return el && el.nodeType === 1 && el.classList.contains("mermaid") && el.tagName === "DIV"
}
function restoreHost() {
if (!home) return
home.host.style.transform = ""
home.host.style.transformOrigin = ""
home.parent.insertBefore(home.host, home.next)
home = null
}
function unbindDrag() {
if (dragMove) window.removeEventListener("mousemove", dragMove)
if (dragUp) window.removeEventListener("mouseup", dragUp)
dragMove = null
dragUp = null
}
function closeLightbox() {
restoreHost()
if (!overlay) return
overlay.remove()
overlay = null
document.documentElement.classList.remove("mermaid-lightbox-open")
document.removeEventListener("keydown", onKey)
unbindDrag()
}
function onKey(event) {
if (event.key === "Escape") closeLightbox()
}
function iconButton(svg, className, label) {
var btn = document.createElement("button")
btn.type = "button"
btn.className = className
btn.title = label
btn.setAttribute("aria-label", label)
btn.innerHTML = svg
return btn
}
function openLightbox(host) {
closeLightbox()
home = {
host: host,
parent: host.parentNode,
next: host.nextSibling
}
overlay = document.createElement("div")
overlay.className = "mermaid-lightbox"
overlay.setAttribute("role", "dialog")
overlay.setAttribute("aria-modal", "true")
overlay.setAttribute("aria-label", "図の拡大表示")
var closeBtn = iconButton(CLOSE, "md-icon mermaid-lightbox__close", "閉じる")
closeBtn.addEventListener("click", closeLightbox)
var stage = document.createElement("div")
stage.className = "mermaid-lightbox__stage"
stage.appendChild(host)
overlay.appendChild(closeBtn)
overlay.appendChild(stage)
document.body.appendChild(overlay)
document.documentElement.classList.add("mermaid-lightbox-open")
document.addEventListener("keydown", onKey)
closeBtn.focus()
var scale = 1.4
var x = 24
var y = 24
var dragging = false
var lastX = 0
var lastY = 0
function apply() {
host.style.transform = "translate(" + x + "px, " + y + "px) scale(" + scale + ")"
host.style.transformOrigin = "0 0"
}
apply()
stage.addEventListener("wheel", function (event) {
event.preventDefault()
var next = scale * (event.deltaY < 0 ? 1.12 : 0.89)
scale = Math.min(8, Math.max(0.25, next))
apply()
}, { passive: false })
stage.addEventListener("mousedown", function (event) {
if (event.button !== 0) return
dragging = true
stage.classList.add("is-dragging")
lastX = event.clientX
lastY = event.clientY
})
dragMove = function (event) {
if (!dragging) return
x += event.clientX - lastX
y += event.clientY - lastY
lastX = event.clientX
lastY = event.clientY
apply()
}
dragUp = function () {
dragging = false
stage.classList.remove("is-dragging")
}
window.addEventListener("mousemove", dragMove)
window.addEventListener("mouseup", dragUp)
}
function enhance() {
document.querySelectorAll("div.mermaid").forEach(function (el) {
if (!isHost(el)) return
if (el.closest(".mermaid-lightbox")) return
if (el.dataset.zoomBound === "1") return
el.dataset.zoomBound = "1"
var frame = document.createElement("div")
frame.className = "mermaid-frame"
el.parentNode.insertBefore(frame, el)
frame.appendChild(el)
var btn = iconButton(MAGNIFY, "md-icon mermaid-zoom-btn", "拡大")
btn.addEventListener("click", function () {
openLightbox(el)
})
frame.appendChild(btn)
})
}
function watch() {
if (observer) observer.disconnect()
var root = document.querySelector("article") || document.querySelector(".md-content") || document.body
observer = new MutationObserver(enhance)
observer.observe(root, { childList: true, subtree: true })
}
function bind() {
closeLightbox()
clearTimers()
enhance()
watch()
;[50, 150, 400, 800, 1600, 3200].forEach(function (ms) {
timers.push(setTimeout(enhance, ms))
})
}
function patchMermaidConfig() {
if (typeof window.mermaid === "undefined" || window.mermaid instanceof Element) return false
if (window.mermaid.__docsHubPatched) return true
if (typeof window.mermaid.initialize !== "function") return false
var orig = window.mermaid.initialize.bind(window.mermaid)
window.mermaid.initialize = function (config) {
config = config || {}
orig(Object.assign({}, config, {
er: Object.assign({ useMaxWidth: false }, config.er),
flowchart: Object.assign({ useMaxWidth: false }, config.flowchart)
}))
}
window.mermaid.__docsHubPatched = true
return true
}
var patchTries = 0
var patchTimer = setInterval(function () {
patchTries += 1
if (patchMermaidConfig() || patchTries > 200) clearInterval(patchTimer)
}, 25)
if (window.document$) {
document$.subscribe(bind)
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bind)
} else {
bind()
}
})()
```
# ナビの見え方(完成条件)
- ヘッダー直下にタブ。トップレベル(Home と guide / architecture / ops など)がタブになる
- 左ナビは各系統(使い方・アーキテクチャ・運用など)が親。横に > があり、子ページが格納される
- 親はデフォルトで閉じている。現在ページへの経路だけ開く
- 右上に GitHub リポジトリ名と、ライト/ダーク切替がある
- 本文右上に編集ボタンがある(repo_url + edit_uri から組み立て。overrides は作らない)
- 見出しホバーで ¶ パーマリンク
- コードブロックホバーでコピー
- コードブロックホバーで行選択ボタンが出る。押したあと行をクリック/ドラッグして範囲を選べる
- 検索は入力中に候補が出る
- 検索を開くとタグフィルター(Filters)が最初から開いている
- ページ遷移はフルリロードしない
- スクロールで URL に #見出し が付く
- 右目次は現在の見出しについてくる
- コンテンツの横幅は既定より広い(`.md-grid` の max-width が 90rem)
- 言語指定 mermaid のフェンスは図になる。コード添付ではない
- 図の右上に虫眼鏡だけがある。文言のボタンは出さない。title は「拡大」
- 虫眼鏡を押すと拡大する。ホイールで拡大縮小、ドラッグで移動できる
- Esc または右上の × で閉じる。× もアイコンだけである
# 禁止
- navigation.sections
- custom_dir / overrides(単一リポジトリでは不要)
- features の追加・削除・並べ替え以外の改変
- zensical.toml への nav 追加