【準備編】Obsidian上で小説執筆を完結させるための環境構築【創作系Obsidian Advent Calendar2025】

2025-11-29Diaryobsidian,小説

この記事は、創作系Obsidian Advent Calendar 2025 – Adventarの6日目の記事です。
今回私は6日目と7日目を担当し、前後編でお届けします。
本日が【準備編】(13000字)、明日が【実践編】(32000字)です。よろしくお願いします。

Obsidianについての説明は割愛します。創作×Obsidianということで、どのような創作を私が行っているかについても簡単に触れなければならないかと思いますので、軽く自己紹介をします。

自己紹介(小説を書いている)

藤井佯(ふじい・よう)と申します。「鳥の神話を伝えます」をコンセプトに小説を書いています。SF/FT/奇想×純文学といった作風で書いています。キャラクターや世界観の設定資料をつくりこむことはあまりないかもしれません。
最近、自主制作の短編集を出しました。
全13編、324ページの文庫本です。【実践編】では、実際にこの短編集の表題作「見習い鳥卜」をどのようにObsidian上で執筆していったかを記録したいと思っています。


見習い鳥卜 藤井佯短編集 – 鳥の神話 – BOOTH


Obsidianを使い始めたのは今年の9月末からでして、まだまだ始めたばかりという感じです。しかし、Obsidianは自分にとってこれ以上ないツールだと確信しています。そして、深くのめりこみ、小説の構想から執筆までをObsidian上で完結させて使いこなすまでになりました。
現在、私はObsidianを下記の用途で使っています。Obsidianを小説執筆だけに使っているわけではなく、総合的に活用している形になります。

・デイリーノート、ウィークリーノート、マンスリーノート、四半期ノート、年単位のノートなど毎日記入していくもの
・小説や詩の執筆
・蔵書管理
・視聴した映画や、展覧会などの活動にまつわるメモ
・タロットリーディングの記録
・ブログ(Wordpress)の執筆(Obsidian上で記事を書いてプラグインでWordpressにアップロードしています)
・自身の思考の整理ログ

本来であれば、これらのノートが有機的に絡まり合って小説ができあがる様子までお見せできたら良いのですが、大変な分量になりそうですので、今回は小説執筆にまつわる私のObsidianの使い方を中心にお話できればなと思っています。

準備の準備 – ルールを決めよう!

Obsidianを活用するにあたって最初に参考にしたのが、かの有名な「オブつな」こと『Obsidianでつなげる情報管理術【完成版】』です。


Obsidianでつなげる情報管理術【完成版】 | Pouhon | 一般・入門書 | Kindleストア | Amazon


正直、この1冊だけ読めばObsidianの活用の方法はばっちりだと思います。この本のなかで口を酸っぱくして言われるのが「Obsidianは自由度が高い分、管理方法を誤ると煩雑になりがち。シンプルなルールを決めておこう!(大意)」ということです。
この本を参考に色々とコミュニティプラグインを導入したり、フォルダ階層をつくっていきました。
私の、創作にまつわるフォルダは「00_Works」とナンバリングされています。
現状、このような階層構造になっています。

①00_Works/鳥の神話について.md(ピン留め)
②00_Works/鳥の神話を伝えます_学習ログ.md(ピン留め)
③00_Works/鳥の神話を伝えます、という私のプロジェクトについて.md(ピン留め)
④00_Works/575/各プロジェクトフォルダ/575形式の作品.md
⑤00_Works/syousetu/執筆ダッシュボード.md(ピン留め)
⑥00_Works/syousetu/各小説のプロジェクトのフォルダ/プロジェクトごとに必要なファイルや原稿.md

①〜③は、私の執筆コンセプトである「鳥の神話を伝えます」という思考についてまとめた、いわば自分用のマニフェストです。このノートを読めばいつでも自分の原点に立ち返れるというわけです。
④は、575形式の短詩を書くこともある(スマホから書けるようにショートカットを作成しています)ので、それを溜めるための場所です。
⑤〜⑥が今回のお話で中心となる箇所かと思います。

執筆ダッシュボードまわり

スクリーンショット 2025-11-29 17.37.15.png

まずこのように、全体を俯瞰できる「執筆ダッシュボード」を用意して、プロジェクトごとに原稿を管理するという運用を考えました。そして、特に変更することなくそのようにして今も使っています。

スクリーンショット 2025-11-29 17.07.32.png

スクリーンショット 2025-11-29 17.08.04.png

スクリーンショット 2025-11-29 17.08.24.png

執筆ダッシュボードはこのような構成になっています。最初に「進行中のプロジェクト」が見えるようにしておき、各プロジェクトの「00_概要.md」へのリンクを作成しています。

スクリーンショット 2025-11-29 17.46.14.png

dataviewで下記のようにして表示しています(余談ですが、私は非エンジニアで、こうした簡単なコードでさえもすべてClaudeに教えてもらっています。今後登場するコードはすべてClaudeが作成したものです)。

TABLE WITHOUT ID
  link(file.link, title) as "プロジェクト",
  deadline as "締切",
  award as "応募先",
  target_words as "目標字数",
  current_words as "現在字数",
  choice(target_words > 0, round((current_words / target_words) * 100, 1) + "%", "-") as "進捗",
  choice(date(deadline) - date(today) < dur(7 days), "🔴", choice(date(deadline) - date(today) < dur(14 days), "🟡", "🟢")) as "状態",
  choice(date(deadline) >= date(today), (date(deadline) - date(today)).days + "日", "⚠️ 期限切れ") as "残り日数"
FROM "00_Works/syousetu"
WHERE type = "project" AND status != "未着手" AND status != "完成"
SORT deadline ASC

また、次に簡単なガントチャートを表示させています。これが意外と便利です。
スクリーンショット 2025-11-29 17.46.19.png

dataviewjsで以下のようなコードを貼り付けています。

// タイムラインを持つプロジェクトを取得(構想中・執筆中・推敲中のみ)
try {
  const projects = dv.pages('"00_Works/syousetu"')
    .where(p => {
      // type, status, timeline がすべて存在することを確認
      if (!p.type || !p.status || !p.timeline) return false;
      if (!Array.isArray(p.timeline) || p.timeline.length === 0) return false;
      
      // ステータスが構想中・執筆中・推敲中のいずれか
      const validStatuses = ["構想中", "執筆中", "推敲中"];
      return p.type === "project" && validStatuses.includes(p.status);
    })
    .sort(p => p.deadline, 'asc');

  if (projects.length === 0) {
    dv.paragraph("⚠️ タイムライン設定されたプロジェクトがありません");
  } else {
    // Mermaidガントチャート用のデータを生成
    let mermaidChart = `gantt
    title 執筆プロジェクトスケジュール
    dateFormat YYYY-MM-DD
    axisFormat %m/%d
`;

    for (let project of projects) {
      // プロジェクト名をセクションとして追加
      const projectTitle = project.title ? String(project.title).replace(/:/g, '').replace(/\n/g, ' ') : 'Untitled';
      mermaidChart += `\n    section ${projectTitle}\n`;
      
      // 各フェーズを追加
      if (project.timeline && Array.isArray(project.timeline)) {
        for (let i = 0; i < project.timeline.length; i++) {
          const phase = project.timeline[i];
          
          // 必須プロパティのチェック
          if (!phase || !phase.phase || !phase.start || !phase.end) continue;
          
          const phaseName = String(phase.phase).replace(/:/g, '').replace(/\n/g, ' ');
          
          // 日付をYYYY-MM-DD形式に変換
          let start, end;
          try {
            // DataviewのDateオブジェクトをYYYY-MM-DD形式に変換
            const startDate = dv.date(phase.start);
            const endDate = dv.date(phase.end);
            start = startDate.toFormat('yyyy-MM-dd');
            end = endDate.toFormat('yyyy-MM-dd');
          } catch (e) {
            console.log(`日付変換エラー: ${phase.start} - ${phase.end}`);
            continue;
          }
          
          // ID生成(依存関係用)
          const phaseId = `p${projects.indexOf(project)}_${i}`;
          
          // ステータスに応じてタグを付ける
          let tag = '';
          try {
            const today = dv.date('today');
            const startDate = dv.date(phase.start);
            const endDate = dv.date(phase.end);
            
            if (today >= startDate && today <= endDate) {
              tag = 'active, ';  // 進行中
            } else if (today > endDate) {
              tag = 'done, ';  // 完了
            }
          } catch (e) {
            // 日付解析エラーはスキップ
          }
          
          // フェーズごとに異なるクラスを設定(色分け用)
          let cssClass = '';
          if (phaseName === '構想') {
            cssClass = 'crit, ';
          } else if (phaseName === '提出') {
            // 提出はマイルストーンとして表示
            mermaidChart += `    ${phaseName} :milestone, ${tag}${phaseId}, ${end}, 0d\n`;
            continue;
          }
          
          mermaidChart += `    ${phaseName} :${cssClass}${tag}${phaseId}, ${start}, ${end}\n`;
        }
        
        // プロジェクトの締切をマイルストーンとして追加
        try {
          const deadlineDate = dv.date(project.deadline);
          const deadline = deadlineDate.toFormat('yyyy-MM-dd');
          const milestoneId = `m${projects.indexOf(project)}`;
          mermaidChart += `    締切 :milestone, crit, ${milestoneId}, ${deadline}, 0d\n`;
        } catch (e) {
          // 締切の日付変換エラーはスキップ
        }
      }
    }
    
    // Mermaidチャートを描画
    dv.paragraph('```mermaid\n' + mermaidChart + '```');
  }
} catch (error) {
  dv.paragraph("⚠️ エラーが発生しました: " + error.message);
  console.error(error);
}

あとは、実行中のプロジェクトの締切のカウントダウン。

スクリーンショット 2025-11-29 17.46.25.png

これもdataviewです。

LIST WITHOUT ID 
  "**" + title + "** (" + award + ") まで残り **" + (date(deadline) - date(today)).days + "日** — 締切: " + deadline
FROM "00_Works/syousetu"
WHERE type = "project" AND status != "完成" AND date(deadline) >= date(today)
SORT deadline ASC
LIMIT 5

このようにして、各プロジェクトを一覧できるダッシュボードを用意しておくことで、複数のプロジェクトが走っていても自分の現状や今すべきことを把握しやすいです。

各プロジェクトの管理方法

スクリーンショット 2025-11-29 17.50.07.png

各プロジェクトについては、メタデータを入力することで執筆ダッシュボードにガントチャートを表示させたりステータスを表示したりできるようにしています。

これは、Templaterでテンプレートを用意しています。テンプレートを起動すると「プロジェクト名」やら「締切」やらを細かく聞かれ、それらをもとに自動でプロジェクトのフォルダと「00_概要.md」が作成されるようになっています。

テンプレートのコードは下記のようになっていますが、私のフォルダ階層に最適化されているため、もし応用したい方がいらっしゃればご自身のフォルダ階層に合わせてカスタマイズしてみてください。

<%*
// プロジェクト情報を対話的に入力
const projectName = await tp.system.prompt("プロジェクト名を入力してください");
if (!projectName) {
    new Notice("プロジェクト名が入力されませんでした");
    return;
}

const award = await tp.system.prompt("応募先/賞の名前を入力してください");
const targetWords = await tp.system.prompt("目標字数を入力してください(数字のみ)");
const deadlineStr = await tp.system.prompt("締切を入力してください(YYYY-MM-DD形式)", tp.date.now("YYYY-MM-DD"));

// タイムライン設定の確認
const setupTimeline = await tp.system.suggester(
    ["はい - 詳細なタイムラインを設定する", "いいえ - 後で設定する"],
    [true, false],
    false,
    "詳細なタイムライン(構想・執筆・推敲等)を今設定しますか?"
);

let timelineSection = "";
let timelineYaml = "";
if (setupTimeline) {
    // 構想期間
    const planStart = await tp.system.prompt("構想開始日(YYYY-MM-DD)", tp.date.now("YYYY-MM-DD"));
    const planEnd = await tp.system.prompt("構想終了日(YYYY-MM-DD)", tp.date.now("YYYY-MM-DD", 7));
    
    // 執筆期間
    const writeStart = await tp.system.prompt("執筆開始日(YYYY-MM-DD)", planEnd);
    const writeEnd = await tp.system.prompt("執筆終了日(YYYY-MM-DD)", tp.date.now("YYYY-MM-DD", 21));
    
    // 推敲期間
    const editStart = await tp.system.prompt("推敲開始日(YYYY-MM-DD)", writeEnd);
    const editEnd = await tp.system.prompt("推敲終了日(YYYY-MM-DD)", tp.date.now("YYYY-MM-DD", 28));
    
    // 提出日
    const submitDate = await tp.system.prompt("提出日(YYYY-MM-DD)", deadlineStr);
    
    timelineYaml = `timeline:
  - phase: 構想
    start: ${planStart}
    end: ${planEnd}
  - phase: 執筆
    start: ${writeStart}
    end: ${writeEnd}
  - phase: 推敲
    start: ${editStart}
    end: ${editEnd}
  - phase: 提出
    start: ${submitDate}
    end: ${submitDate}`;

    timelineSection = `

## 📅 タイムライン

\`\`\`dataview
TABLE WITHOUT ID
  t.phase as "フェーズ",
  t.start as "開始日",
  t.end as "終了日",
  choice(date(today) >= date(t.start) AND date(today) <= date(t.end), "🔵 進行中", choice(date(today) > date(t.end), "✅ 完了", "⏳ 予定")) as "状態"
FROM "00_Works/syousetu"
WHERE file = this.file
FLATTEN timeline as t
WHERE t.phase != null
\`\`\``;
}

// 年を取得
const year = tp.date.now("YYYY");

// フォルダパスを生成
const folderPath = `00_Works/syousetu/${year}_${projectName}`;

// 現在の日時を取得
const now = tp.date.now("YYYY-MM-DDTHH:mm");

// 本文を先に生成
const bodyContent = `
# ${projectName}

## 📋 プロジェクト情報

**応募先:** ${award}
**締切:** \`= this.deadline\`
**目標字数:** \`= this.target_words\`字
**現在字数:** \`= this.current_words\`字
**進捗率:** \`= round((this.current_words / this.target_words) * 100, 1)\`%
**残り:** \`= this.target_words - this.current_words\`字

**締切まで:** \`= dur(date(this.deadline) - date(today)).days\`
${timelineSection}

## 💡 企画概要

あらすじやテーマ、企画意図など

## 📝 進捗日報

### ${tp.date.now("YYYY-MM-DD")}
- 執筆字数:字
- 累計:字
- メモ:プロジェクト開始

---

## 🔗 関連ノート

- `;

// フロントマターを構築
tR += `---
title: "${projectName}"
type: project
deadline: ${deadlineStr}
award: "${award}"
target_words: ${targetWords}
current_words: 0
status: 未着手
priority: 中
${timelineYaml}
created: ${now}
updated: ${now}
---
${bodyContent}`;

// ファイルを移動
await tp.file.move(`${folderPath}/00_概要`);
%>

基本的には、このプロジェクトのフォルダの中に、ノートをどんどん追加していって、執筆していくことになります。具体的な運用方法は【実践編】にて書こうと思っていますが、基本的には「構想中・執筆中・推敲中・完成」の4つのステータスを用意し、「00_概要.md」のメタデータでステータスを管理しつつ、執筆を進めていくことになります。

Obsidianで小説を書くためのプラグインやスニペット

Obsidianで日本語小説を書く設定 | こたつろぐ


Obsidianで、日本語で小説を書く際にやっておくと便利な設定は、上記ブログを参考にしています。
特に字下げ変問題については、日本語の小説には行頭下げがあるので確実に対応しておきたいところです。私はこのようなスニペットを入れておき、メタデータでcssclassesが「novel」であるときのみ、そのスニペットが有効になるように設定しています。

/* novel クラスでの折りたたみ機能を完全無効化 */
.novel .cm-foldGutter {
    display: none !important;
}

.novel .cm-gutters {
    display: none !important;
}

/* CodeMirror 6の折りたたみインジケーター */
.novel .cm-fold-indicator {
    display: none !important;
}

/* 折りたたみプレースホルダー */
.novel .cm-fold-placeholder {
    display: none !important;
}

/* より広範囲に適用 */
.novel .CodeMirror-foldgutter,
.novel .cm-gutterElement.cm-foldGutter {
    display: none !important;
}

上記ブログには下記のスニペットが紹介されています。

/* remove text indent in source-view */
.markdown-source-view:not(.is-live-preview) .cm-line{
	text-indent: 0 !important;
	padding-inline-start: 0 !important;
}
/* force plain-text-like view in preview-view p */
.markdown-preview-view p{
	white-space:pre-line;
}
.cm-line{
	text-indent: 0 !important;
}

これらによって、行頭に空白を入れたときに次の行が格納されてしまう(トグルになってしまう)問題を防ぐことができますし、エンターキーを押すと自動で1マス空白を入れてくれます。

↓スニペットを入れた状態。

スクリーンショット 2025-11-29 18.02.26.png

↓スニペットが無効になっている状態
スクリーンショット 2025-11-29 18.04.00.png

さらに、残念ながらObsidianでは縦書きのエディタの実現は難しいのですが(多分)、プレビューモードであれば縦書き表示が可能です。下記のコードは、この記事の転載です。


Obsidianで小説・シナリオを書く | 白樺開発日記

.cm-line,
.markdown-reading-view p {
    text-indent: 0 !important;
    padding-inline-start: 1rem !important;
    white-space: pre-line;
}

.novel * {
    font-family: 'Noto Serif JP' !important;
    /* ここのフォントファミリーはお好きなのを。結合用濁点を使えるフォントじゃないと縦書きが若干きもちわるくなります */
}

div[data-mode="preview"] .novel .markdown-preview-section {
    writing-mode: vertical-rl;
    word-break: break-all;
    overflow: auto;
    --line-width: auto;
    text-orientation: upright;
    line-height: 1.6rem;
    max-height: 100%;
    height: 42rem;
    padding-bottom: 0 !important;
    min-height: 0 !important;
    margin: 0;
    padding-left: 50%;
}

div[data-mode="preview"] div.markdown-rendered.novel.markdown-preview-view {
    display: flex;
    max-height: 1000px;
    align-items: center;
    width: 100%;
}

div[data-mode="preview"] .markdown-preview-view.is-readable-line-width .markdown-preview-sizer {
    max-width: 100%;
}

div[data-mode="preview"] .novel .markdown-preview-section div {
    display: inline-block;
    height: 100%;
    margin: 0;
}

こんな感じになります。自分の好きなフォントで表示することもできますよ(というか、フォントによってはダッシュなどが縦書きに対応できなくなってしまうので私はNoto Serif JPを使用しています)。

スクリーンショット 2025-11-29 18.08.28.png

スクリーンショット 2025-11-29 18.10.02 2.png

そう、実はルビを振れるプラグインまであるのです。

Obsidian用の日本語小説用ルビプラグインをリリースしました | こたつろぐ

コミュニティプラグインで公開されているので、簡単にインストールできます。青空文庫のルビ記法でルビを振ることができます。こんな感じ。

スクリーンショット 2025-11-29 18.24.40.png

私もプラグインをつくってみた

まだ審査待ちでして、コミュニティプラグインからの直接インストールはできません。この記事の公開に間に合えばよかったのですが……。Githubにはコードがあるので、使ってみたい方はここからダウンロードしてください。無事に審査が終わり、コミュニティプラグインが公開されました!(2026.01.21)
私がつくったのは、400字詰原稿用紙枚数換算機能のついた文字カウントのプラグインです。

obsidian://show-plugin?id=japanese-manuscript-counter

↑ここからDLできます。

スクリーンショット 2025-11-29 18.16.46.png

公募に応募するとき(特に純文学系の公募)って、枚数制限が未だに400字詰原稿用紙の換算枚数で規定されていることがありますよね(100〜400枚とか)。いつも有志がweb上に公開してくれている文字カウントサービスを使っていたのですが、Obsidian上でわかったほうがいいじゃん、と思い、つくりました。
シンプルにそのノートの文字数を数えてくれます。選択すると、その箇所の文字数と原稿用紙に換算したときに何枚になるかを教えてくれます。

スクリーンショット 2025-11-29 18.19.39.png

また、これはデバッグ用につけた機能だったのですが、どのようにカウントされているのか詳細を確認することもできます。便利なのでこのまま残しています。

こんな感じで、有志が用意してくれた日本語での小説執筆用のプラグインやスニペット、そして自分でつくってみた文字カウント機能、でかなり快適にObsidian上で執筆をすることができるようになりました。

準備完了!執筆だ

さて、これでいよいよプロジェクトを動かす下準備が整いました。【実践編】では、実際にObsidian上でどのように小説を書いていったかを私の短編集の書き下ろし「見習い鳥卜」の執筆工程を例にとって書いてみたいと思います。
おつかれさまでした!

つづき↓