以前,每天早上我都被任务和念头淹没:现在该做什么、要回复谁、任务的优先级是什么,几天前我到底在想些什么?现在我只需打开一篇笔记,就能一眼看到我所有的项目、任务、习惯和想法。这篇文章里,我会用最简单的语言,一步一步详细讲解我整套每日笔记系统,让你一个晚上就能亲手搭起来。


为什么要在 Obsidian 里写日记

纸质笔记本很棒。但它有一个缺点:你昨天写下的内容,永远停留在“昨天”。

任务不会自己顺延,习惯不会自动汇成图表,想法也会在翻页之间丢失。

在 Obsidian 里一切都不一样。每日笔记成为你一天的中心:

🔁
任务
自动顺延,并来自各个项目
📅
日历
与 Google Calendar 同步
✅
习惯
按钮和进度图表
💭
想法
自由记录灵感与思考的空间

早上醒来,我立刻就能想起昨天在忙什么。没完成的任务会自动顺延到今天。像习惯、冥想这样简单的日常事项,我只需点一下按钮就能标记,它们还会汇总成清晰的变化趋势。而新的任务,我直接从与目标相连的项目中获取。

这套简单的系统让我保持专注:我做真正需要做的事,而不是拖延。让我们一起来搭建它。

我的完整系统以及 PARA 结构的介绍,见本站之前的文章。


第 1 步:需要安装什么

要在 Obsidian 里记录任务、想法和习惯,我们需要几个插件和一项内置设置。

第三方插件

打开 设置 → 第三方插件 → 浏览,逐个安装:

插件用途
Dataview从笔记中汇总数据(例如按日期汇总想法、生成图表)
Meta Bind直接在正文中把笔记属性变成按钮和输入框
Homepage创建一个启动时自动打开的主页
Templater在创建笔记时运行脚本(用于顺延任务)
Buttons创建一键运行命令的按钮
QuickAdd按钮的“大脑”:运行宏和脚本

安装 Dataview 后,进入它的设置,把所有复选框都勾选上(尤其是 Enable JavaScript Queries 和 Enable Inline JavaScript Queries),否则日历小组件无法工作。

启用内置的”日记”(Daily notes)

Obsidian 开箱即用就能写日记。我们进入 设置 → 核心插件,启用 日记(Daily notes)。然后按自己的习惯进行配置。

三个关键设置:

  1. 笔记标题的格式。 我用的就是日、月、年:

    DD-MM-YYYY
    

    这一点很重要:我所有的代码(小组件、任务结转)都是严格按 DD-MM-YYYY 格式来查找笔记的。如果你想在标题里加上星期几,就加上相应的字母(例如 dddd),但这样代码里的格式也要一起改。哪个字母代表什么,可以方便地在 format.cm(Moment.js 文档)上查到。

  2. 新笔记所在的文件夹。 我的日记放在 2. Areas/Дневники/Ежедневные заметки。我所有内容都按 PARA 结构整理,而写日记属于”生活领域”(Area),所以它的位置就在 Areas 文件夹里。

  1. 模板。 这是每篇新笔记创建时所依据的结构。我的模板在 0. Files/4. Templates/Шаблон ежедневных заметок。“Templates”的意思就是模板。

我的日记笔记结构是这样的:

属性 → 周/日历小组件 → 当天计划 → 任务结转 → 习惯 → 饮食 → 想法。 并不是每次都能全部填完。但这种努力本身,让我变得更自律了。

给不想花很长时间摸索的你

试试我现成的 Obsidian 模板,今天就开始整理你的信息

了解模板

第 2 步:笔记的属性(YAML)

我们打开模板,从上到下过一遍。首先看到的是笔记的属性(也叫 YAML 或 frontmatter)。

它们有什么用?属性是笔记的”官方”数据,其他插件可以很方便地读取。比如,习惯图表的数据正是取自属性,而不是普通的正文。这条规则值得记住:对图表和自动化重要的内容,都存在属性里。

属性写在笔记的最上方,夹在两行三个短横线之间:

---
Спорт: false
Чтение: false
Прогулка: false
Завтрак:
Перекусы:
Обед:
Ужин:
Итого_ккал:
---

这里有两组:

  • 习惯(Спорт、Чтение、Прогулка)——“复选框”类型。false 表示”还没完成”。
  • 饮食(Завтрак、Обед、Ужин、Перекусы、Итого_ккал)——“文本”类型,在这里记录吃了什么,以及一个卡路里计数器。

添加自己的属性很简单:在属性模式下点击 "+ 添加属性",输入名称并选择类型(文本、数字、复选框、日期)。填上几天之后,你就能在图表上看到变化趋势了。


第 3 步:与 Google 日历和任务同步

在模板里,属性的正下方是一大段代码。别害怕——它是 AI 写的,我只是粘贴了进去。这段代码会绘制出一个漂亮的小组件:显示星期几、本周的进度,而在”任务”模式下,它会与你的 Google 日历和 Google Tasks 同步。直接在 Obsidian 里就能看到当天的事件和任务,可以标记完成,也可以添加新的。

要让它正常工作,光有模板里的代码还不够。你需要让 Obsidian 获得访问你 Google 账号的权限。这是通过免费的 Google Apps Script 服务来完成的。我们一步一步来,慢慢地、详细地过一遍。

3.1. 在 scripts.google.com 上准备服务器

1
打开 script.google.com,点击 "新建项目"。
2
删除编辑器里的所有文字,然后粘贴我下面给出的代码。
3
连接 Google Tasks API 服务:在左侧的 "服务" 菜单("+"图标)中找到 Tasks API,点击 "添加"。没有这一步,代码就看不到你的任务。

第 3 步是最容易被漏掉的。代码里用到了 Tasks.Tasklists.list(),它属于"高级服务"。如果你没有添加 Tasks API,脚本就会报错崩溃。

下面就是需要粘贴到编辑器里的代码(它负责读取事件/任务,以及创建/关闭任务):

// ======================================
// GOOGLE APPS SCRIPT — TASKS + CALENDAR
// ======================================
 
// Authorisation check (run once manually)
function testAuth() {
  const taskLists = Tasks.Tasklists.list();
  Logger.log(taskLists);
  const events = CalendarApp.getDefaultCalendar().getEvents(new Date(), new Date());
  Logger.log(events.length + " events today");
}
 
// ====== READING: calendar events + tasks for the day ======
function doGet(e) {
  try {
    const tz = Session.getScriptTimeZone();
 
    // DEBUG mode: open GAS_URL?debug=1 in the browser — it will show ALL tasks
    if (e && e.parameter && e.parameter.debug) {
      const debugLists = (Tasks.Tasklists.list().items) || [];
      const all = [];
      debugLists.forEach(list => {
        const items = (Tasks.Tasks.list(list.id, {
          showCompleted: true, showHidden: true, maxResults: 100
        }).items) || [];
        items.forEach(t => {
          all.push({ list: list.title, listId: list.id, title: t.title, due: t.due || null, status: t.status });
        });
      });
      return ContentService
        .createTextOutput(JSON.stringify({
          success: true, scriptTimeZone: tz,
          listsFound: debugLists.map(l => l.title),
          totalTasks: all.length, all: all
        }))
        .setMimeType(ContentService.MimeType.JSON);
    }
 
    const param = (e && e.parameter && e.parameter.date) ? e.parameter.date : null;
    const base = param
      ? new Date(+param.split("-")[0], +param.split("-")[1] - 1, +param.split("-")[2])
      : new Date();
 
    const start = new Date(base.getFullYear(), base.getMonth(), base.getDate(), 0, 0, 0);
    const end   = new Date(base.getFullYear(), base.getMonth(), base.getDate(), 23, 59, 59);
    const dayStr = Utilities.formatDate(base, tz, "yyyy-MM-dd");
 
    const events = CalendarApp.getDefaultCalendar().getEvents(start, end).map(ev => ({
      title: ev.getTitle(),
      allDay: ev.isAllDayEvent(),
      time: ev.isAllDayEvent() ? null : Utilities.formatDate(ev.getStartTime(), tz, "HH:mm")
    }));
 
    const tasks = [];
    const lists = (Tasks.Tasklists.list().items) || [];
    const today = new Date();
    const todayStr = Utilities.formatDate(today, tz, "yyyy-MM-dd");
    const isToday = dayStr === todayStr;
 
    lists.forEach(list => {
      const items = (Tasks.Tasks.list(list.id, { showCompleted: true, showHidden: true }).items) || [];
      items.forEach(t => {
        if (!t.due) return;
        const taskDay = t.due.substring(0, 10);
        const isExactDay = taskDay === dayStr;
        const isOverdue = isToday && taskDay < dayStr && t.status === "needsAction";
        if (isExactDay || isOverdue) {
          tasks.push({
            id: t.id, listId: list.id, title: t.title,
            status: t.status, completed: t.completed || null, overdue: isOverdue
          });
        }
      });
    });
 
    tasks.sort((a, b) => (a.overdue === b.overdue) ? 0 : (a.overdue ? -1 : 1));
 
    return ContentService
      .createTextOutput(JSON.stringify({ success: true, date: dayStr, events, tasks }))
      .setMimeType(ContentService.MimeType.JSON);
 
  } catch (error) {
    return ContentService
      .createTextOutput(JSON.stringify({ success: false, error: error.toString() }))
      .setMimeType(ContentService.MimeType.JSON);
  }
}
 
// ====== WRITING: create OR close a task ======
function doPost(e) {
  try {
    const data = JSON.parse(e.postData.contents);
 
    if (data.action === "complete") {
      const listId = data.listId || Tasks.Tasklists.list().items[0].id;
      Tasks.Tasks.patch({ status: "completed" }, listId, data.taskId);
      return ContentService
        .createTextOutput(JSON.stringify({ success: true, message: 'Task done!' }))
        .setMimeType(ContentService.MimeType.JSON);
    }
 
    const title = data.title;
    const dueDate = data.dueDate;
    const taskLists = Tasks.Tasklists.list();
    const defaultTaskList = taskLists.items[0].id;
 
    const task = Tasks.Tasks.insert({ title: title, due: dueDate, status: 'needsAction' }, defaultTaskList);
 
    return ContentService
      .createTextOutput(JSON.stringify({ success: true, taskId: task.id, message: 'Task added!' }))
      .setMimeType(ContentService.MimeType.JSON);
 
  } catch (error) {
    return ContentService
      .createTextOutput(JSON.stringify({ success: false, error: error.toString() }))
      .setMimeType(ContentService.MimeType.JSON);
  }
}

3.2. 首次授权

在你亲自允许之前,Google 不会让这段代码访问你的日历。我们先手动操作一次:

1
在顶部的函数下拉菜单中选择 testAuth,然后点击 "运行"(▶)。
2
会弹出 "需要授权" 窗口 → 点击 "查看权限",然后选择你的 Google 账号。
3
接着会出现一个吓人的页面:"Google 尚未验证此应用"。这是正常的——这个应用就是你自己的。点击底部的 "高级" → "转至……(不安全)"。
4
点击 "允许"。搞定——权限已授予。在日志中你会看到自己的任务列表。

每个人的个人脚本都会出现"Google 尚未验证此应用"这个页面。你并没有把数据传给任何外部的人——这段代码只在你自己的账号内运行。

3.3. 部署以及该复制哪个链接

现在我们把这段代码变成一个真正的网址,供 Obsidian 访问:

1
在右上角点击 "部署" → "新建部署"。
2
点击选择类型的齿轮图标,然后选择 "网页应用"。
3
填写:
• 执行身份:"我"(你的账号)
• 谁可以访问:"任何人"
4
点击 "部署"。复制 "网页应用网址"——就是以 /exec 结尾的那个链接。

一定要复制 以 /exec 结尾的链接。带有 /dev 的链接在 Obsidian 中无法使用。

https://script.google.com/macros/s/AKfycbx…………/exec

3.4. 把小组件代码粘贴到模板中

现在我们把小组件代码粘贴到每日笔记模板里。这是一个 ```dataviewjs 代码块,它会绘制一张显示周数和日历的卡片。最重要的一点——把第一行引号里的链接替换成你在 3.3 步骤中得到的链接:

const GAS_URL = "PASTE_YOUR_LINK_ENDING_IN_exec_HERE";

小组件代码的开头大致是这样的(你只需要替换带有 GAS_URL 的那一行,其余部分不要动):

```dataviewjs
const GAS_URL = "https://script.google.com/macros/s/AKfyc…………/exec";

const obsidian = require("obsidian");

const fileName = dv.current().file.name;
const m = fileName.match(/^(\d{2})-(\d{2})-(\d{4})$/);

if (!m) {
  dv.paragraph("⚠️ The note title must be in the DD-MM-YYYY format");
} else {
  // … then comes the long widget code: week, progress,
  //   the "Tasks" button, loading events and tasks from the calendar …
}
```

Done! Now in the note you see the tasks for today, overdue tasks are highlighted in red, tasks can be marked as done and new ones added. It’s especially convenient on the phone: opened the widget — and quickly jotted down your tasks.

Checking that it works. Open your link in the browser, adding ?debug=1 at the end. If you see JSON with a list of your tasks — the server is working correctly.

For those who don't want to figure it out for ages

Try my ready-made Obsidian template and start systematising your information today

Learn about the template

Step 4. Plans for the day and buttons

The next section of the template is plans for the day. Here I have button labels from the Buttons plugin:

`button-spaced-repetition` `button-sport` `button-morning-routine` `button-work-on-project` `button-litso`

Each such label is one button that I press during the day:

ButtonWhat it launches
🃏 Spaced repetitionRecall the material I’m learning
💪 WorkoutsToday’s exercises (change automatically by muscle group)
🙏 MorningMy morning ritual — meditation and concentration
📋 TaskOpen the projects and take a task from there (the main button!)
😌 FaceA personal skincare routine

These functions were added gradually. At first the note had only the morning ritual. Then I added spaced repetition, workouts and the rest — as needed, so as not to overload the brain. I advise you too to start with one or two buttons.


Step 5. How the buttons (Buttons) work — in simple words

The most frequent misunderstanding of beginners: “why is `button-sport` written in the note, but a beautiful button appears?” Let’s figure it out.

The logic is divided into two parts: where the button is described and where it’s shown.

1. The "MOC - Buttons" note
Here lie the full descriptions of all the buttons (code + the ^button-… label). This is the "warehouse" of buttons.
→
2. The daily note
Here there's only the short label `button-sport` — and the plugin substitutes the button from the "warehouse."

Why so complicated? So as not to copy the long button code into every note. Described it once — insert it with a short label as many times as you like.

What a button description looks like

All my buttons are stored in one note MOC - Buttons (or just “Buttons”). Here’s an example of one button’s description:

```button
name 💪 Workouts
type command
action QuickAdd: Open workouts
color black
class btn-inline
```
^button-sport

Let’s go through it line by line:

nameWhat's written on the button (can be with emoji).
type commandThe "command" type — the button launches an Obsidian command.
actionExactly which command to launch (here — a choice from QuickAdd).
color / classThe appearance. btn-inline — a compact inline button.
^button-sport⭐ The main thing: the label (block id). This is the button's "address."

What ^button-... and the label in the text are

The line ^button-sport right under the code block is a block id (a block anchor) in Obsidian. It gives the button a unique address.

After this, in any note it’s enough to write the label as inline code (in single backticks):

`button-sport`

The Buttons plugin sees this label, finds the needed description by the address ^button-sport and draws a working button in its place. You can put several labels in a row in one line — you’ll get a row of buttons.

The rule is simple:

^button-name (with the “caret”) — this is where the button is described. `button-name` (in quotes) — this is where the button is shown. The name after button- must match.

How to create your own button from scratch

1
In the "MOC - Buttons" note, copy any ready ```button … ``` block.
2
Change the name (the label) and the action (which command to launch).
3
Below, set a unique label, for example ^button-water.
4
In the needed note write `button-water` — and the button will appear.

Step 6. The main “Task” button: connecting the diary and projects

The ”📋 Task” button is one of the main ones. Its label is button-work-on-project, and it launches the project-to-daily script via QuickAdd.

First, about where the tasks come from. All my projects are in the MOC - Projects note (the 1. Projects folder by the PARA structure). This is a kanban board on the Kanban plugin: just task cards that are dragged from status to status.

💡 Ideas
→
🔄 In progress
→
✅ Done

The “Task” button does two smart things:

  1. Pulls tasks from the projects into the diary. A window with your kanban board opens, you tick the needed cards and click “Add” — they appear in the “My plans for the day” section.
  2. Moves tasks across the board on completion. When you mark a task as done (- [x]) right in the diary, it automatically moves to the next column of the kanban (e.g. from “In progress” to “Done”) and is removed from the note.

There's also a "turbo mode": if you turn on the "🏁 To the last" toggle (or add the 🏁 icon to the task), on completion it'll go straight to the very last column of the board.

What to replace in the script for yourself

The script is in my 0. Files/4. Templates/Scripts/project-to-daily_01.js. If you take it for yourself, at the very top there are two lines that need to be configured for your system:

const MOC_PATH = "1. Projects/MOC - Projects.md"; // ← the path to YOUR kanban board
const LAST_COLUMN_MARKER = "🏁";                   // ← the "straight to the last column" icon
  • MOC_PATH —— 填写你主看板(项目看板)的路径,也就是它的名称以及所在的文件夹。
  • LAST_COLUMN_MARKER —— “极速模式”的标记图标。你可以保留 🏁,也可以换成自己喜欢的任何图标。

还有一点很重要:脚本会根据标题 # Мои планы на день(我的今日计划)来查找插入任务的位置。你的每日笔记中必须有这个标题——否则任务会被添加到笔记末尾。

给不想花很长时间摸索的你

试试我现成的 Obsidian 模板,今天就开始整理你的信息吧

了解模板

第 7 步:自动结转未完成的任务

除了按钮,模板里还有一段代码——它会把前一天未完成的任务结转到今天。它基于 Templater 插件运行:新建笔记时,这段代码执行一次,然后变成一份普通的任务列表。

逻辑很简单,也非常方便:

📄 昨天的笔记
找到当天之前的最后一篇笔记
→
🔍 查找 "- [ ]"
收集所有未关闭的任务
→
📥 今天
把它们插入新笔记

下面就是模板里的这段代码(原样粘贴即可,无需修改——如果你愿意,只需改一下日记文件夹的路径):

<%*
const moment = tp.obsidian.moment;
const fn = tp.file.title;
const date = moment(fn, 'DD-MM-YYYY');
 
// On Sundays we add a backup button
if (date.isValid() && date.day() === 0) {
  tR += '- [ ] `button-local-backup`\n';
}
 
const pathToDailyNotes = "2. Areas/Дневники/Ежедневные заметки"; // ← your path
const currentDate = moment(tp.file.title, 'DD-MM-YYYY');
const today = moment().startOf('day');
const referenceDate = currentDate.isAfter(today) ? today.clone().add(1, 'day') : currentDate;
 
function findLatestDailyNoteBeforeDate(beforeDate) {
  const folder = app.vault.getAbstractFileByPath(pathToDailyNotes);
  if (!folder || !folder.children) return null;
  const dailyFiles = folder.children
    .filter(file => {
      if (file.extension !== "md") return false;
      if (!file.name.match(/^\d{2}-\d{2}-\d{4}\.md$/)) return false;
      const fileDate = moment(file.name.replace('.md', ''), 'DD-MM-YYYY');
      return fileDate.isBefore(beforeDate) && fileDate.isSameOrBefore(today);
    })
    .sort((a, b) => {
      const dateA = moment(a.name.replace('.md', ''), 'DD-MM-YYYY');
      const dateB = moment(b.name.replace('.md', ''), 'DD-MM-YYYY');
      return dateB.valueOf() - dateA.valueOf();
    });
  return dailyFiles.length > 0 ? dailyFiles[0] : null;
}
 
if (!currentDate.isValid()) {
  tR += `❌ Couldn't determine the date from the file name. Format: DD-MM-YYYY.`;
} else {
  const yesterday = referenceDate.clone().subtract(1, 'day').format('DD-MM-YYYY');
  let targetFile = app.vault.getAbstractFileByPath(`${pathToDailyNotes}/${yesterday}.md`);
  let targetDate = yesterday;
  if (!targetFile) {
    targetFile = findLatestDailyNoteBeforeDate(referenceDate);
    if (targetFile) targetDate = targetFile.name.replace('.md', '');
  }
  if (!targetFile) {
    tR += `❌ No daily notes before ${referenceDate.format('DD-MM-YYYY')}.`;
  } else {
    const fileContent = await app.vault.read(targetFile);
    const tasks = fileContent.split("\n").filter(line => line.trim().startsWith("- [ ]"));
    if (tasks.length === 0) {
      tR += `✅ No uncompleted tasks for ${targetDate}.`;
    } else {
      tR += `## 🔁 Uncompleted tasks from ${targetDate} (the last note)\n\n`;
      tR += tasks.join("\n");
    }
  }
}
%>

如果你的日记中断了好几天,脚本依然会找到当天之前的 最后一篇笔记,并从中提取任务——不会丢失任何内容。只有当你的文件夹不同时,才需要修改 pathToDailyNotes。


第 8 步:习惯和饮食按钮(Meta Bind)

模板接下来是习惯部分。它基于 Meta Bind 插件运行,这个插件让你可以直接在正文中编辑笔记的属性——不用再滚动到顶部去打开属性面板。

`INPUT[toggle:Спорт]` 🏃‍♂️ Sport
`INPUT[toggle:Чтение]` 📖 Reading
`INPUT[toggle:Прогулка]` 🚶‍♂️ Walk

这里的 INPUT[toggle:Спорт] 是一个与 Спорт 属性关联的开关。你在笔记正文中拨动开关,属性里的复选框就会自动勾选。而图表正是从属性中获取数据的,所以你的习惯会立刻进入统计。

饮食的设置方式相同,只是用的是文本框:

🥚 **Breakfast:** `INPUT[text:Завтрак]`
🍕 **Lunch:** `INPUT[text:Обед]`
🥧 **Dinner:** `INPUT[text:Ужин]`
🥨 **Snack:** `INPUT[text:Перекусы]`
 **Total:** `INPUT[text:Итого_ккал]`

`button-calories`

最后是 button-calories 按钮——”🌮 计算卡路里”。这是我个人的按钮,它会借助 AI,根据我在饮食字段里填写的内容估算卡路里。

不可能每天都填得完美无缺——这很正常。但哪怕每周只填几天,习惯图表和卡路里图表也能呈现出清晰的变化趋势。


第 9 步:想法——最重要的部分

最重要的是——模板最底部的**“想法”**部分。这里没有代码,也没有模板,是一块完全空白、用来思考的地方。

我白天会在这里记下:

  • 未来的任务和灵感;
  • 让我担心的问题;
  • 思考和观察。

如果我现在解决不了某个任务,我不会逼自己。我会在"想法"里写下我 为什么做不了。通常会发现,大脑其实卡在了另一件外部的事情上——而意识到这一点本身,就能消除那种呆滞感。

这些想法之后也继续为我所用:

  1. 在主页上。 明天,我会在主页(Homepage)的一个专门区块里看到昨天的想法。
  2. 在 MOC - Дневники 笔记中。 那里有一段代码,会收集所有日记笔记中的想法,并按月份逐日展示。新的任务和原子笔记就从中诞生。
  3. 在手机上。 我设置了一个小组件,可以把想法直接写入今天笔记的对应部分。在街上突然有灵感时,它必不可少。

代码:按月汇总所有日记笔记中的想法

我把这个代码块粘贴到一篇单独的笔记 MOC - Дневники 中(它位于我的 2. Areas/Дневники)。它会遍历当月所有的日记笔记,从每篇中提取**“想法”**部分(以及属性 Мысль_дня 和 Инсайт_дня,如果存在的话),并以列表形式输出。”← 上一月 / 下一月 →“按钮用来翻看各个月份,”🔽 最新优先”用来切换排序。

需要替换成你自己的内容

代码中有一行是日记文件夹的路径——请换成你自己的路径:

dv.pages('"2. Areas/Дневники/Ежедневные заметки"')

此外,该代码块会在笔记中查找标题 ## Мысли(想法)和属性 Дата(日期)。如果你的部分名称不同(例如 # Мысли),依然可以正常工作(它会匹配 1 到 3 个 #)。Дата 属性必须位于日记笔记的属性中,并且是日期格式。

请完整复制整个代码块——从带有 dataviewjs 的那一行,一直到结尾的反引号:

```dataviewjs
let currentDate = new Date();
let sortOrder = "desc";

const container = dv.el("div", "");

// Control panel
const controls = dv.el("div", "");
controls.style.cssText = `display: flex; align-items: center; gap: 8px; margin: 12px 0 20px 0; flex-wrap: wrap;`;

// Style for the buttons
function styleBtn(btn) {
  Object.assign(btn.style, {
    display: "inline-flex", "align-items": "center", "justify-content": "center",
    gap: "6px", padding: "8px 16px", "border-radius": "8px",
    border: "1px solid var(--background-modifier-border)",
    background: "var(--background-secondary)", color: "var(--text-normal)",
    cursor: "pointer", "font-size": "13px", "font-family": "inherit", "line-height": "1",
    transition: "background 0.15s, transform 0.1s, box-shadow 0.15s"
  });
  btn.onmouseenter = () => { btn.style.background = "var(--background-modifier-hover)"; btn.style.transform = "translateY(-1px)"; };
  btn.onmouseleave = () => { btn.style.background = "var(--background-secondary)"; btn.style.transform = "translateY(0)"; };
}

const spacer = dv.el("div", "");
spacer.style.cssText = "flex: 1;";

const prevBtn = dv.el("button", "⬅️ Previous");
const nextBtn = dv.el("button", "Next ➡️");
const sortBtn = dv.el("button", "🔽 Newest first");
styleBtn(prevBtn); styleBtn(nextBtn); styleBtn(sortBtn);

controls.appendChild(prevBtn);
controls.appendChild(nextBtn);
controls.appendChild(spacer);
controls.appendChild(sortBtn);
container.appendChild(controls);

const contentDiv = dv.el("div", "");
container.appendChild(contentDiv);

async function renderMonth(date) {
  contentDiv.innerHTML = "";
  const start = new Date(date.getFullYear(), date.getMonth(), 1);
  const end = new Date(date.getFullYear(), date.getMonth() + 1, 0);

  const title = dv.el("h2", `${start.toLocaleDateString("en", { month: "long", year: "numeric" })}`);
  title.style.cssText = "margin: 0 0 12px 0; font-size: 1.1em;";
  contentDiv.appendChild(title);

  // ↓↓↓ REPLACE the path with your own daily-notes folder ↓↓↓
  const dailyNotes = dv.pages('"2. Areas/Дневники/Ежедневные заметки"')
    .where(p => {
      if (!p.Дата) return false;
      const d = new Date(p.Дата);
      return d >= start && d <= end;
    })
    .sort(p => p.Дата, sortOrder);

  for (const note of dailyNotes) {
    const content = await dv.io.load(note.file.path);
    const match = content.match(/#{1,3}\s*Мысли\s*\n([\s\S]*?)(?=\n#{1,3}\s|$)/i);
    const thoughtOfDay = note["Мысль_дня"];
    const insightOfDay = note["Инсайт_дня"];

    if (!match && !thoughtOfDay && !insightOfDay) continue;

    const block = dv.el("div", "");
    block.style.cssText = "border-bottom: 1px solid var(--background-modifier-border); padding: 8px 0;";

    block.appendChild(dv.el("h4", note.file.name));
    if (thoughtOfDay) block.appendChild(dv.el("p", `💭 Thought of the day: ${thoughtOfDay}`));
    if (insightOfDay) block.appendChild(dv.el("p", `💡 Insight: ${insightOfDay}`));
    if (match) block.appendChild(dv.el("p", `📝 Thoughts:\n${match[1].trim()}`));

    contentDiv.appendChild(block);
  }
}

prevBtn.onclick = () => { currentDate.setMonth(currentDate.getMonth() - 1); renderMonth(currentDate); };
nextBtn.onclick = () => { currentDate.setMonth(currentDate.getMonth() + 1); renderMonth(currentDate); };
sortBtn.onclick = () => {
  if (sortOrder === "desc") { sortOrder = "asc"; sortBtn.textContent = "🔼 Oldest first"; }
  else { sortOrder = "desc"; sortBtn.textContent = "🔽 Newest first"; }
  renderMonth(currentDate);
};

await renderMonth(currentDate);
```
📷 Screenshot: the MOC - Дневники note with a list of thoughts by month and toggle buttons
(insert an image here)

The home page: habits and thoughts

Every morning Obsidian opens for me not on an empty spot, but on a home page (the MOC - HOME note, the Homepage plugin). On it I immediately see the two most important things: the habits chart for the current month and yesterday’s thoughts. So the day starts with a short “overview of myself.”

This block consists of two tabs — “Habits” and “Thoughts”. The “Habits” tab builds a completion chart for the month (it takes the data from the daily notes’ properties). The “Thoughts” tab shows the “Thoughts” section from yesterday’s note.

What to replace for yourself

At the start of the code there’s one line with the path to the daily-notes folder — specify your path:

const HABITS_FOLDER = "2. Areas/Дневники/Ежедневные заметки";

And in the habits array, list your habits — the names must exactly match the property names in the template (Спорт, Чтение, Прогулка, etc.).

For the habits chart you need the Obsidian Charts plugin (it provides the window.renderChart function). Install it the same way as the rest: Browse → "Charts" → Install and enable.

Copy the block whole — from the line with the word dataviewjs and to the closing backticks:

```dataviewjs
// ⚙️ SETTINGS — replace the paths and the habit list for yourself
const { MarkdownRenderer } = require('obsidian');
const HABITS_FOLDER = "2. Areas/Дневники/Ежедневные заметки"; // ← your daily-notes folder
const habits = [
  { name: "Sport",    key: "Спорт" },
  { name: "Reading",  key: "Чтение" },
  { name: "Walk",     key: "Прогулка" }
];

// 📅 Current month
const now       = new Date();
const year      = now.getFullYear();
const month     = now.getMonth() + 1;
const pad       = n => String(n).padStart(2, "0");
const monthStr  = pad(month);
const totalDays = new Date(year, month, 0).getDate();
const MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const monthName = MONTH_NAMES[month - 1];

// 📅 Yesterday's date (for the "Thoughts" section)
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
const yFileName = `${pad(yesterday.getDate())}-${pad(yesterday.getMonth() + 1)}-${yesterday.getFullYear()}`;
const yFilePath = `${HABITS_FOLDER}/${yFileName}.md`;

// 🛠️ Helpers
const asBool = v => v === true || v === "true" || v === 1;
function gradientColor(pct) {
  let r, g, b = 0;
  if (pct < 50) { r = 255; g = Math.round(255 * pct / 50); }
  else          { g = 255; r = Math.round(255 - 255 * (pct - 50) / 50); }
  return `rgb(${r},${g},${b})`;
}

// 🚀 Render
let viewMode = "habits";

async function render() {
  this.container.innerHTML = "";

  const tabs = this.container.createEl("div");
  tabs.style.cssText = "display:flex; gap:8px; margin-bottom:14px; flex-wrap:wrap;";
  const makeTab = (label, mode) => {
    const active = viewMode === mode;
    const btn = tabs.createEl("button", { text: label });
    btn.style.cssText = "padding:6px 16px; border-radius:6px; cursor:pointer; border:1px solid var(--interactive-accent); " +
      (active ? "background:var(--interactive-accent); color:var(--text-on-accent); font-weight:600;"
              : "background:transparent; color:var(--text-normal);");
    btn.addEventListener("click", () => { if (viewMode !== mode) { viewMode = mode; render.call(this); } });
  };
  makeTab("Habits",   "habits");
  makeTab("Thoughts", "thoughts");

  const box = this.container.createEl("div");

  // ── Habits ──────────────────────────────────────────────
  if (viewMode === "habits") {
    let pages = dv.pages(`"${HABITS_FOLDER}"`).where(p => {
      if (p.date) { const d = dv.date(p.date); return d && d.year === year && d.month === month; }
      const n = p.file.name;
      return n.includes(`-${monthStr}-${year}`) || n.includes(`${year}-${monthStr}-`);
    });
    if (pages.length === 0) { box.createEl("p", { text: `No data for ${monthStr}.${year}.` }); return; }

    const counts   = habits.map(h => pages.filter(p => asBool(p[h.key])).length);
    const percents = counts.map(c => Math.round((c / totalDays) * 100));
    window.renderChart({
      type: "bar",
      data: {
        labels: habits.map(h => h.name),
        datasets: [{
          label: `Completion for ${monthStr}.${year} (of ${totalDays} days)`,
          data: percents,
          backgroundColor: percents.map(p => gradientColor(p)),
          borderRadius: 4,
        }]
      },
      options: {
        indexAxis: "y",
        scales: {
          x: { min: 0, max: 100, ticks: { callback: v => v + "%" }, title: { display: true, text: "Percent" } },
          y: { title: { display: true, text: "Habits" } }
        },
        plugins: {
          legend: { display: false },
          tooltip: { callbacks: { label: ctx => `${ctx.raw}% (${counts[ctx.dataIndex]} of ${totalDays} days)` } },
          title: { display: true, text: `Habits — ${monthName} ${year}` }
        }
      }
    }, box);

  // ── Thoughts (from yesterday's note) ─────────────────────
  } else if (viewMode === "thoughts") {
    const tfile = app.vault.getAbstractFileByPath(yFilePath);
    box.createEl("div", { attr: { style: "font-weight:600; margin-bottom:10px; color:var(--text-muted);" } })
       .setText(`Thoughts — yesterday (${yFileName})`);
    if (!tfile) { box.createEl("p", { text: `Note not found: ${yFileName}`, attr: { style: "color:var(--text-muted);" } }); return; }

    const content = await app.vault.read(tfile);
    const match = content.match(/^#{1,6}\s*Мысли\s*$/m);
    if (!match) {
      box.createEl("p", { text: "The 'Thoughts' section wasn't found in the note.", attr: { style: "color:var(--text-muted);" } });
    } else {
      const startIdx    = content.indexOf(match[0]) + match[0].length;
      const afterText   = content.slice(startIdx);
      const nextHeading = afterText.match(/\n#{1,6}\s/);
      const thoughtsRaw = nextHeading ? afterText.slice(0, nextHeading.index).trim() : afterText.trim();
      if (thoughtsRaw) {
        const card = box.createEl("div", { attr: { style: "background:var(--background-secondary); border-left:3px solid var(--interactive-accent); border-radius:6px; padding:12px 14px; line-height:1.75; max-height:320px; overflow-y:auto;" } });
        await MarkdownRenderer.render(app, thoughtsRaw, card, yFilePath, this.component);
      } else {
        box.createEl("p", { text: "The 'Thoughts' section is empty.", attr: { style: "color:var(--text-muted);" } });
      }
    }
    const linkWrap = box.createEl("div", { attr: { style: "margin-top:10px;" } });
    linkWrap.createEl("a", {
      text: `Open ${yFileName} — Thoughts`, cls: "internal-link",
      attr: { href: `${yFileName}#Мысли`, "data-href": `${yFileName}#Мысли`, style: "font-size:0.85em; color:var(--interactive-accent); text-decoration:none;" }
    });
  }
}
render.call(this);
```
📷 截图:带有"习惯"和"想法"标签页的主页
(在此插入图片)

给不想花很长时间摸索的你

试试我现成的 Obsidian 模板,今天就开始整理你的信息吧

了解模板

纸质日记也在系统里

我并没有完全放弃纸张——有时手写的感觉很不错。我的系统里有一个专门的按钮——”📸 照片转文字”(标签 button-image-2-text,脚本 image-to-text)。

逻辑很简单:我给纸质笔记本的一页拍照 → 按钮借助 AI 识别手写文字 → 我可以把它复制到日记笔记里,或者用它新建一篇笔记。

📓 一页纸
→
📸 照片转文字(AI)
→
📝 笔记中的文字

我还喜欢手绘示意图——任何这样的手绘都可以轻松插入笔记(通过 Excalidraw 插件,或者同样的绘图按钮)。所以在 Obsidian 里写日记功能上没有任何限制:纸张、手绘、语音、照片——一切都汇入同一个地方。


小结

这就是整套系统。看起来规模不小,其实都是用简单的积木搭起来的:

1.插件:Dataview、Meta Bind、Homepage、Templater、Buttons、QuickAdd。
2.内置的“日记”核心插件 + 放在 Areas 文件夹里的模板(PARA)。
3.用属性(YAML)记录习惯和饮食。
4.通过 scripts.google.com 实现的 Google 日历小组件。
5.用按钮和脚本把日记与项目连接起来。
6.自动顺延任务、习惯、饮食以及“想法”部分。

最重要的建议: 不要一次性把所有东西都搭起来。 先启用日记功能,再加一个晨间仪式按钮。等它成为习惯后,再加上任务顺延,然后是日历,最后是项目。系统应该是给大脑减负,而不是增加负担。

在接下来的内容里,我会展示我是怎么给自己做了一个读书插件(我会划出片段,并用它们创建笔记),以及我如何在阅读过程中直接用 AI 来弄清楚不理解的信息。

给不想花大量时间摸索的你

试试我现成的 Obsidian 模板,今天就开始整理你的信息吧

了解模板

© 2026 Elton Labs. All rights reserved.