はじめに

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 PublishQuartz + GItHub/CF PagesVercel + Hugo
手軽さ
コスト月 $8〜無料無料
Obsidian 互換性
デザイン自由度
独自ドメイン

Quartz + GitHub + Cloudflare Pages

今回は GitHub Pages は使わず、Cloudflare Pages を利用することにした。別で持っているドメインも Cloudflare で購入しているため、すでにアカウントがあったことや、設定も色々したことがあるためこちらを選択した。

Valut からコピーしたいディレクトリのみを指定して Quartz のディレクトリへコピー、GitHub へ push するシェルスクリプトを作成した。push した時点で cloudflare が deploy を自動で開始してくれる。

なのでシェルスクリプトを実行するだけでデプロイまでが自動で行われる。

20260103_01.png

環境構築

1. 初期セットアップ

GitHub より quartz を clone してくる

git clone https://github.com/jackyzha0/quartz.git quartz
cd quartz
npm install
npx quartz create

ここで quartz が実行できるかを確認

npx quartz build --serve

localhost:8080 へ接続して問題なく表示されれば OK。

2. リポジトリ作成、デプロイ設定 (GitHub、Cloudflare Pages)

  1. GitHub :リポジトリを Private で作成
  2. Cloudflare Pages:コンピューティングと AI Workers & Pages を選択
    1. “+” から Pages を選択 20260103_02.png
  3. 既存リポジトリを選択し、先ほど作成したリポジトリを選択
  4. ビルド設定
    • Build command: npx quartz build
    • Build output directory: public
  5. 独自ドメインは設定後、同じページの「カスタムドメイン」より設定すれば 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].git

4. デプロイ用シェルスクリプトの作成

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",
    },
  }),
}

ReaderModeGraphView をコメントアウト。 folderClickBehaviorcollapse へ変更。

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 毎の境界はいろを微妙に変えるなどして見やすくしたいところ。

Screenshot 2026-01-03 at 17.55.37 1.png