はじめに
Obsidian に蓄積した情報を世界に発信するための環境を整える。今回は Quartz を活用して静的ページを生成し、これを GitHub に Push、それをキーに Cloudfare pages にて公開を行った。ついでに独自ドメインも設定する。
なぜ Quartz+GitHub+Cloudflare Page なのか
候補としてはいくつか挙げられる。
- Obsidian Publish
- Quartz + Github Pages or Cloudflare Pages
- Vercel or Netlify + Hugo
Obsidian Publish
Obsidian 公式が提供しているサービスであり、Obsidian に蓄積した情報を発信するには最もシームレスである。セットアップ〜公開までの手軽さや内部リンク等の動作を考えるとよく聞こえるのだが、利用自体に $8 かかる。
Quartz + GitHub Pages or Cloudflare Pages
Vault の一部を GithHub に push して、これを GitHub Pages または Cloudflare Pages で公開する方法。Obsidian の独自機能も割と網羅しており互換性も良いらしい。Git への push やトラブル発生時の解決は割と面倒。
Vercel or Netlify + Hugo
従来のサイトジェネレータを使いつつ、Obsidian っぽいテーマを適用する方法。Hugo を使うのでビルドがめちゃくちゃ早い代わりに Obsidian の独自機能にどこまで対応しているかが未知。
まとめ
まとめるとこんな感じ。今回はお金をかけず (独自ドメイン除く) に公開したいと思い、Quartz + GitHub/CF Pages の構成にすることにした。
| 特徴 | Obsidian Publish | Quartz + GItHub/CF Pages | Vercel + Hugo |
|---|---|---|---|
| 手軽さ | ◎ | ○ | △ |
| コスト | 月 $8〜 | 無料 | 無料 |
| Obsidian 互換性 | ◎ | ◎ | △ |
| デザイン自由度 | △ | ◎ | ◎ |
| 独自ドメイン | ○ | ○ | ○ |
Quartz + GitHub + Cloudflare Pages
今回は GitHub Pages は使わず、Cloudflare Pages を利用することにした。別で持っているドメインも Cloudflare で購入しているため、すでにアカウントがあったことや、設定も色々したことがあるためこちらを選択した。
Valut からコピーしたいディレクトリのみを指定して Quartz のディレクトリへコピー、GitHub へ push するシェルスクリプトを作成した。push した時点で cloudflare が deploy を自動で開始してくれる。
なのでシェルスクリプトを実行するだけでデプロイまでが自動で行われる。

環境構築
1. 初期セットアップ
GitHub より quartz を clone してくる
git clone https://github.com/jackyzha0/quartz.git quartz
cd quartz
npm install
npx quartz createここで quartz が実行できるかを確認
npx quartz build --servelocalhost:8080 へ接続して問題なく表示されれば OK。
2. リポジトリ作成、デプロイ設定 (GitHub、Cloudflare Pages)
- GitHub :リポジトリを Private で作成
- Cloudflare Pages:コンピューティングと AI → Workers & Pages を選択
- “+” から Pages を選択

- “+” から Pages を選択
- 既存リポジトリを選択し、先ほど作成したリポジトリを選択
- ビルド設定
- Build command:
npx quartz build - Build output directory:
public
- Build command:
- 独自ドメインは設定後、同じページの「カスタムドメイン」より設定すれば OK
3. ローカルリポジトリの初期設定
書いてて思ったけど fork したらいいだけだった。。。
.git を削除して自身のリポジトリ情報にする。
cd [Clone してきたリポジトリ]
rm -rf .git
git init
git branch -M main
git remote add origin [email protected]:[User ID]/[Repository Name].git4. デプロイ用シェルスクリプトの作成
Valut からコピーを行う。画像を全部アップするとプライベートなものもあがるので、画像として定義されているもののみをコピーして push するようになっている。
もし利用する場合、rm -rf 使っているので利用には注意が必要。
#!/bin/bash
# ==========================================
# 設定エリア
# ==========================================
# 1. Obsidianの「公開したい記事フォルダ」
BLOG_DIR="デプロイしたいValutのフォルダを指定"
# 2. Obsidianの「画像フォルダ」
ATTACH_DIR="Valut内で画像を保存しているフォルダを指定"
# 3. Quartzのディレクトリ
QUARTZ_DIR="リポジトリのパス"
CONTENT_DIR="$QUARTZ_DIR/content"
# 作業用の一時フォルダパス
TEMP_DIR="$QUARTZ_DIR/temp_build"
# ==========================================
# 処理開始
# ==========================================
echo "🚀 デプロイ処理を開始します..."
# 1. 安全確認
if [ ! -d "$BLOG_DIR" ]; then
echo "❌ エラー: 公開元フォルダが見つかりません: $BLOG_DIR"
exit 1
fi
if [ ! -d "$ATTACH_DIR" ]; then
echo "⚠️ 注意: 画像フォルダが見つかりません: $ATTACH_DIR (画像なしで進行します)"
fi
# 2. 準備: 作業用一時フォルダを作成
rm -rf "$TEMP_DIR"
mkdir -p "$TEMP_DIR"
# 一時フォルダ内に画像用フォルダも作っておく
mkdir -p "$TEMP_DIR/99_attach"
# 3. 記事のコピー: 50_Blog の中身を一時フォルダのルートへコピー
echo "📄 記事を準備中..."
cp -r "$BLOG_DIR/" "$TEMP_DIR/"
# 4. 画像の抽出とコピー: 記事内で使われている画像リンク (![[...]]) を検索
echo "🔍 使用されている画像を検索中..."
# 記事内から ![[ファイル名]] を抽出 -> 整形 -> 重複排除
IMAGE_LIST=$(grep -r -h -o -E '!\[\[[^]]+\]\]' "$TEMP_DIR" | sed 's/^!\[\[//; s/\]\]$//; s/|.*//' | sort | uniq)
echo "🖼️ 画像を収集中 (Source: 99_attach)..."
COUNT=0
# ファイル名にスペースがある場合に対応するためIFSを変更
IFS=$'\n'
for img in $IMAGE_LIST; do
# 99_attach にその画像があるかチェック
if [ -f "$ATTACH_DIR/$img" ]; then
cp "$ATTACH_DIR/$img" "$TEMP_DIR/99_attach/"
((COUNT++))
fi
done
unset IFS
echo " -> $COUNT 枚の画像を抽出しました。"
# 5. Quartzのcontentフォルダをクリーンアップ
echo "🧹 古いコンテンツを削除中..."
rm -rf "$CONTENT_DIR"/*
# 6. obsidian-export の実行 (一時フォルダに対して実行)
echo "📤 Quartz形式へ変換中..."
obsidian-export "$TEMP_DIR" "$CONTENT_DIR"
if [ $? -ne 0 ]; then
echo "❌ obsidian-export に失敗しました。"
# 失敗時は一時フォルダを消して終了
rm -rf "$TEMP_DIR"
exit 1
fi
# 7. 一時フォルダのお片付け
rm -rf "$TEMP_DIR"
# 8. Quartz Sync (Commit & Push)
echo "🌍 GitHubへ同期中..."
cd "$QUARTZ_DIR"
npx quartz sync --no-pull
echo "✅ デプロイ完了!数分後にCloudflare Pagesが更新されます。"5. Obsidian からのシェルスクリプトの実行
Shell Commands などを導入し、 cmd+p などから実行できるようにする。
source ~/.zshrc && /絶対パス/deploy.shその他 細かな調整
quartz.config.ts
今回変更した内容は以下
- title
- locale
- baseUrl
quartz.layout.ts
Footer の表示内容を変更
// components shared across all pages
export const sharedPageComponents: SharedLayout = {
head: Component.Head(),
header: [],
afterBody: [],
footer: Component.Footer({
links: {
"X (Twitter)": "https://x.com/tnmu_",
Bluesky: "https://bsky.app/profile/tnmu.bsky.social",
},
}),
}ReaderMode と GraphView をコメントアウト。
folderClickBehavior を collapse へ変更。
export const defaultContentPageLayout: PageLayout = {
beforeBody: [
Component.ConditionalRender({
component: Component.Breadcrumbs(),
condition: (page) => page.fileData.slug !== "index",
}),
Component.ArticleTitle(),
Component.ContentMeta({ showReadingTime: false }),
Component.TagList(),
],
left: [
Component.PageTitle(),
Component.MobileOnly(Component.Spacer()),
Component.Flex({
components: [
{
Component: Component.Search(),
grow: true,
},
{ Component: Component.Darkmode() },
//{ Component: Component.ReaderMode() },
],
}),
// Component.Explorer(),
Component.Explorer({
title: "Menu",
folderClickBehavior: "collapse",
folderDefaultState: "open",
}),
],
right: [
// Component.Graph(),
Component.DesktopOnly(Component.TableOfContents()),
Component.Backlinks(),
],
}quartz/components/styles/List.scss
以下を追加
/* Index のみを日付部分を非表示 */
body[data-slug="index"] .content-meta {
display: none !important;
}
/* Table of Contents のヘッダー(ボタン部分)を非表示にする */
.toc > button {
display: none !important;
}
/* 中身のリストを強制的に表示状態にする */
.toc > #toc-content {
display: block !important;
max-height: none !important; /* 高さ制限も解除 */
visibility: visible !important;
}quartz/components/styles/explorer.scss
以下を追加
button.desktop-explorer {
display: none;
}quartz/components/Footer.tsx
Footer の部分をアイコンにしたかったので以下に変更
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/footer.scss"
import { version } from "../../package.json"
import { i18n } from "../i18n"
interface Options {
links: Record<string, string>
}
// ==========================================
// 1. アイコンの定義(ここにSVGデータを追加します)
// ==========================================
const Icons: Record<string, any> = {
// X (Twitter) のアイコン
"X (Twitter)": (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
),
// Bluesky のアイコン(公式または類似のSVG)
Bluesky: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.58 7.424-4.784 7.424-4.784.207.326.42.65.639.976.22-.326.433-.65.639-.976 0 0 2.41 10.363 7.424 4.784 4.557-5.073 1.082-6.498-2.83-7.078-.139-.016-.277-.034-.415-.056.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.479 0-.688-.139-1.86-.902-2.203-.659-.299-1.664-.621-4.3 1.24C16.046 4.748 13.087 8.686 12 10.8z" />
</svg>
),
// GitHub のアイコン
GitHub: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
),
}
export default ((opts?: Options) => {
const Footer: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
const year = new Date().getFullYear()
const links = opts?.links ?? []
return (
<footer class={`${displayClass ?? ""}`}>
<p>© {year} TNMU</p>
<ul>
{/* ==========================================
2. アイコン表示ロジック
========================================== */}
{Object.entries(links).map(([text, link]) => {
// マッピング表にアイコンがあればそれを使い、なければテキストのまま表示
const content = Icons[text] || text
return (
<li>
{/* title属性で、マウスホバー時にテキストを表示します */}
<a href={link} title={text} target="_blank" rel="noopener noreferrer">
{content}
</a>
</li>
)
})}
</ul>
</footer>
)
}
Footer.css = style
return Footer
}) satisfies QuartzComponentConstructor完了
デフォルトだと煩わしいところも多いなと思ったが、意外と簡単に変えられた pane 毎の境界はいろを微妙に変えるなどして見やすくしたいところ。
