javascript

Рефакторинг моего Obsidian: как я перестроил хранилище после предыдущей статьи

  • пятница, 28 августа 2026 г. в 00:00:04
https://habr.com/ru/articles/1075372/

Предысловие

Какое-то время назад я выкладывал статью на Хабр про свое obsidian хранилище.

Я получил хороший фидбэк и некоторые идеи. Также я понял, что довольно большая часть моего obsidian нуждается в оптимизации и структуризации. В итоге нашел свободный выходной, чтобы немного перестроить хранилище. Я все это сделал и тут расскажу, как теперь выглядит мой obsidian.

Кроме простой структуризации и удаления лишнего, я также провел некоторые махинации над кодом dataviewjs своей библиотеки книг и доделал свою домашнюю страницу, добавив интеграцию со своей основной библиотекой и поработав на css составляющей. В общем-то были еще некоторые изменения, но об этом всем будет далее.

Что же нового?

Общая структура папок

Список основных папок и файлов
Список основных папок и файлов

Ниже будет название папки в виде заголовка и описание для чего нужна папка. Папки с _ в начале - это папки служебного назначения.

_files

Здесь хранятся все файлы, которые используются в заметках (фото, рисунки и т.д.). В настройках в Obsidian можно поставить чтобы все не .md файлы складывались туда.

_kanban-tasks

Здесь хранятся заметки, которые создаются Kanban. Используем Kanban Task Temp. (применяется автоматически). В Kanban можно нажать на задачу и выбрать “Attach Note”, тогда создается новая заметка, где можно более подробно расписать задачу.

_templates

Здесь хранятся шаблоны для разных типов файлов. (Book, Note, Plan, Zettel, Kanban Task шаблоны). Для шаблонов советую полезный плагин Templater.

1 - Plans

Здесь хранятся все планы. Используем Plan Temp.

2 - Notes

Здесь хранятся все полезные заметки, записи и тд, которые не относятся к каким-то конкретным областям знаний. Используем Note Temp.

3 - Zettel Rough

Папка для сырых заметок. Начинаю писать здесь. Если заметка неполная, требует доработки или я ещё не уверен в её конечной ценности — она остается здесь. Используем Zettel Temp

4 - Zettel Grown

Здесь хранятся все заметки для моей главной базы знаний. То есть сюда я записываю идеи или материалы, которые могут послужить в будущем и несут ценную информацию.

Главный вопрос: «Хотел бы я, чтобы эта информация держалась у меня в голове долгое время?». Если «да» — информация ценна, и её нужно записать.

Когда заметка становится самодостаточной и ценной, я перемещаю её из Rough Notes в Grown Notes. После переноса из Zettel Rough меняем #rough на #grown.

5 - Books

Здесь хранятся все прочитанные книги. Сначала создаётся файл с названием книги, вставляется шаблон и заполняется. В этом же файле можно писать заметки по книге. Используем Book Temp.

Остальные файлы

  • Cheatsheet (этот файл): исчерпывающая шпаргалка по системе

  • Homepage: домашняя страница с самыми важными ссылками

  • Task Board: Kanban для отложенных дел и заметок

Основные горячие клавиши

  • Alt+S — вставка шаблона

  • Alt+Q — вставка callout

  • Ctrl+O — открыть quick switcher

  • Ctrl+P — открыть command palette

  • Ctrl+E — сменить current view

  • Ctrl+ЛКМ — открыть ссылку в новой вкладке

  • Ctrl+Shift+F — поиск во всех файлах

  • Ctrl+↑ — переместить строку вверх

  • Ctrl+↓ — переместить строку вниз

  • Alt+A - свернуть все списки и заголовки

  • Alt+D - развернуть все списки и заголовки

  • Ctrl+Del - очистить Canvas в Excalidraw

Пример одного из шаблонов

Tags: #zettel #rough
File Links:
# {{Title}}

Заполнение файла стало значительно проще и быстрее, по сравнению с тем что было раньше: быстро написал дополнительно пару тегов, может быть добавил ссылки на другие файлы в хранилище.

Каталог книг

Каталог выглядит таким образом. На странице можно искать книги по жанрам, статусам и по названию:

Book Catalog
Book Catalog

Ниже полный блок кода каталога. Его можно просто скопировать в файл и он будет смотреть все книги в папке “5 - Books”.

Код каталога книг
const bookPages = dv.pages('"5 - Books"').where(p => p.title);
const container = this.container;

const style = document.createElement("style");
style.textContent = `
    .book-table {
        width: 100%;
        border-collapse: separate;
        border-spacing: 0;
        font-family: inherit;
        margin-top: 0.5rem;
        font-size: 14px;
    }
    .book-table th {
        background: rgba(255, 255, 255, 0.03);
        color: var(--text-muted, #9ca3af);
        padding: 10px 14px;
        border-bottom: 1px solid rgba(255, 255, 255, 0.08);
        font-weight: 600;
        text-align: left;
        font-size: 12px;
        text-transform: uppercase;
        letter-spacing: 0.05em;
    }
    .book-table th:first-child,
    .book-table td:first-child {
        width: 120px; /* Было 100px */
        text-align: center;
    }
    .book-table td {
        padding: 12px 14px;
        border-bottom: 1px solid rgba(255, 255, 255, 0.05);
        color: var(--text-normal, #e5e7eb);
        vertical-align: middle;
    }
    .book-table tr {
        transition: background-color 0.15s ease;
    }
    .book-table tr:hover {
        background: rgba(139, 92, 246, 0.04) !important;
    }
    .filter-container {
        background: rgba(0, 0, 0, 0.15);
        border-radius: 10px;
        padding: 0.8rem 1rem;
        margin-bottom: 1.5rem;
        border: 1px solid rgba(255, 255, 255, 0.08);
        display: flex;
        gap: 0.8rem;
        align-items: center;
        flex-wrap: wrap;
    }
    .filter-select, .filter-search {
        padding: 8px 12px;
        border-radius: 8px;
        border: 1px solid rgba(255, 255, 255, 0.12);
        background: var(--background-secondary, #1e1e24);
        color: var(--text-normal, #e5e7eb);
        font-size: 13px;
        outline: none;
        transition: all 0.2s ease;
        height: 38px;
        box-sizing: border-box;
        font-family: inherit;
    }
    .filter-select:hover, .filter-search:focus {
        border-color: rgba(167, 139, 250, 0.5);
        box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.15);
    }
    .filter-search {
        flex: 1;
        min-width: 220px;
    }
    .book-title-link {
        color: var(--text-normal, #f3f4f6);
        text-decoration: none;
        font-weight: 600;
        font-size: 15px;
        transition: color 0.2s;
        line-height: 1.3;
    }
    .book-title-link:hover {
        color: #a78bfa;
    }
    .book-meta-text {
        font-size: 12px;
        color: var(--text-muted, #9ca3af);
        margin-top: 4px;
    }
    .status-dropdown {
        padding: 4px 8px;
        border-radius: 6px;
        font-size: 12px;
        font-weight: 600;
        border: 1px solid transparent;
        outline: none;
        cursor: pointer;
        transition: all 0.2s;
        width: 120px;
        text-align: center;
        height: 32px;
        box-sizing: border-box;
        font-family: inherit;
    }

    .book-cover-img {
        width: 80px;
        height: 120px;
        border-radius: 6px;
        object-fit: cover;
        box-shadow: 0 3px 8px rgba(0,0,0,0.25);
        border: 1px solid rgba(255, 255, 255, 0.08);
        transition: transform 0.2s, box-shadow 0.2s;
        cursor: pointer;
        display: inline-block;
    }
    .book-cover-img:hover {
        transform: translateY(-2px);
        box-shadow: 0 6px 12px rgba(0,0,0,0.35);
    }

    @media (max-width: 600px) {
        .book-table th, .book-table td {
            padding: 8px 6px;
            font-size: 12px;
        }
        .book-table th:first-child,
        .book-table td:first-child {
            width: 85px; /* Было 70px */
        }
        .book-cover-img {
            width: 60px;
            height: 90px;
        }
        .book-title-link {
            font-size: 13px;
        }
        .book-meta-text {
            font-size: 10px;
        }
        .status-dropdown {
            width: 95px;
            font-size: 11px;
            height: 28px;
            padding: 2px 2px;
        }
        .filter-container {
            padding: 0.6rem;
            flex-direction: column;
            align-items: stretch;
            gap: 0.5rem;
        }
        .filter-select, .filter-search {
            width: 100%;
        }
    }
`;
document.head.appendChild(style);

const filterContainer = document.createElement("div");
filterContainer.className = "filter-container";

const genreSelect = document.createElement("select");
genreSelect.className = "filter-select";
const allOption = document.createElement("option");
allOption.textContent = "Все жанры";
allOption.value = "";
genreSelect.appendChild(allOption);

const allGenres = new Set();
for (let p of bookPages) {
    if (p.genre) {
        const genres = Array.isArray(p.genre) ? p.genre : [p.genre];
        genres.forEach(g => g && allGenres.add(g));
    }
}
for (let genre of Array.from(allGenres).sort()) {
    const opt = document.createElement("option");
    opt.textContent = genre;
    opt.value = genre;
    opt.style.background = "var(--background-secondary, #1e1e24)";
    genreSelect.appendChild(opt);
}
filterContainer.appendChild(genreSelect);

const statusSelect = document.createElement("select");
statusSelect.className = "filter-select";
const statusOptions = [
    { value: "", text: "Все статусы" },
    { value: "reading", text: "Читаю" },
    { value: "completed", text: "Прочитано" },
    { value: "planned", text: "В планах" }
];
statusOptions.forEach(o => {
    const opt = document.createElement("option");
    opt.textContent = o.text;
    opt.value = o.value;
    opt.style.background = "var(--background-secondary, #1e1e24)";
    statusSelect.appendChild(opt);
});
filterContainer.appendChild(statusSelect);

const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.className = "filter-search";
searchInput.placeholder = "🔍 Поиск по названию или автору...";
filterContainer.appendChild(searchInput);

container.appendChild(filterContainer);

function determineBookStatus(page) {
    if (page.status) return page.status;
    const today = new Date().toISOString().split('T')[0];
    if (page.date_ended && page.date_ended <= today) return "completed";
    if (page.date_started) return "reading";
    return "planned";
}

function applyStatusStyle(selectElement, status) {
    if (status === "completed") {
        selectElement.style.background = "rgba(16, 185, 129, 0.12)";
        selectElement.style.color = "#10b981";
        selectElement.style.borderColor = "rgba(16, 185, 129, 0.25)";
    } else if (status === "reading") {
        selectElement.style.background = "rgba(139, 92, 246, 0.12)";
        selectElement.style.color = "#a78bfa";
        selectElement.style.borderColor = "rgba(139, 92, 246, 0.25)";
    } else {
        selectElement.style.background = "rgba(156, 163, 175, 0.12)";
        selectElement.style.color = "#9ca3af";
        selectElement.style.borderColor = "rgba(156, 163, 175, 0.25)";
    }
}

async function updatePageProperty(page, property, value) {
    try {
        const file = app.vault.getAbstractFileByPath(page.file.path);
        if (!file) return false;
        await app.fileManager.processFrontMatter(file, (fm) => {
            if (value === undefined || value === "") { delete fm[property]; } 
            else { fm[property] = value; }
        });
        page[property] = value;
        return true;
    } catch (e) { console.error(e); return false; }
}

async function removePageProperty(page, property) {
    try {
        const file = app.vault.getAbstractFileByPath(page.file.path);
        if (!file) return false;
        await app.fileManager.processFrontMatter(file, (fm) => { delete fm[property]; });
        delete page[property];
        return true;
    } catch (e) { console.error(e); return false; }
}

function renderTable(selectedGenre = "", selectedStatus = "", searchTerm = "") {
    const oldTable = container.querySelector(".book-table");
    if (oldTable) oldTable.remove();

    const table = document.createElement("table");
    table.className = "book-table";

    const header = table.insertRow();
    ["Обложка", "Книга", "Статус"].forEach(t => {
        const th = document.createElement("th");
        th.textContent = t;
        header.appendChild(th);
    });

    for (let p of bookPages) {
        const genreMatch = !selectedGenre || (p.genre && ((Array.isArray(p.genre) && p.genre.includes(selectedGenre)) || p.genre === selectedGenre));
        const actualStatus = determineBookStatus(p);
        const statusMatch = !selectedStatus || actualStatus === selectedStatus;
        const searchMatch = !searchTerm || (p.title && p.title.toLowerCase().includes(searchTerm.toLowerCase())) || (p.author && p.author.toLowerCase().includes(searchTerm.toLowerCase()));

        if (!(genreMatch && statusMatch && searchMatch)) continue;

        const row = table.insertRow();
        row.onclick = (e) => {
            if (!e.target.closest('a') && !e.target.closest('select') && !e.target.closest('input') && !e.target.closest('img')) {
                window.location.href = p.file.path;
            }
        };

        const cellCover = row.insertCell();
        if (p.cover) {
            const img = document.createElement("img");
            img.src = p.cover;
            img.className = "book-cover-img";
            img.onclick = () => window.location.href = p.file.path;
            cellCover.appendChild(img);
        } else {
            const placeholder = document.createElement("div");
            placeholder.className = "book-cover-img";
            placeholder.style.background = "rgba(255, 255, 255, 0.03)";
            placeholder.style.display = "flex";
            placeholder.style.alignItems = "center";
            placeholder.style.justifyContent = "center";
            placeholder.style.fontSize = "24px";
            placeholder.style.border = "1px dashed rgba(255, 255, 255, 0.15)";
            placeholder.innerHTML = "📖";
            placeholder.onclick = () => window.location.href = p.file.path;
            cellCover.appendChild(placeholder);
        }

        const cellInfo = row.insertCell();
        const infoWrapper = document.createElement("div");
        infoWrapper.style.display = "flex";
        infoWrapper.style.flexDirection = "column";

        const link = document.createElement("a");
        link.href = p.file.path;
        link.textContent = p.title || p.file.name;
        link.className = "internal-link book-title-link";

        const metaText = document.createElement("div");
        metaText.className = "book-meta-text";
        const author = p.author || "Автор не указан";
        const genre = Array.isArray(p.genre) ? p.genre.join(", ") : (p.genre || "");
        metaText.textContent = author + (genre ? ` • ${genre}` : "");

        infoWrapper.appendChild(link);
        infoWrapper.appendChild(metaText);
        cellInfo.appendChild(infoWrapper);

        const cellStatus = row.insertCell();
        const sel = document.createElement("select");
        sel.className = "status-dropdown";
        
        const opts = [
            { value: "planned", text: "📚 В планах" },
            { value: "reading", text: "📖 Читаю" },
            { value: "completed", text: "✓ Прочитано" }
        ];
        opts.forEach(o => {
            const opt = document.createElement("option");
            opt.value = o.value;
            opt.textContent = o.text;
            opt.style.background = "var(--background-secondary, #1e1e24)";
            if (o.value === actualStatus) opt.selected = true;
            sel.appendChild(opt);
        });
        applyStatusStyle(sel, actualStatus);
        cellStatus.appendChild(sel);

        sel.onchange = async () => {
            const newStatus = sel.value;
            applyStatusStyle(sel, newStatus);
            if (newStatus !== actualStatus) {
                let success = await updatePageProperty(p, "status", newStatus);
                if (success) {
                    if (actualStatus === "completed" && newStatus !== "completed") {
                        await removePageProperty(p, "date_ended");
                    } else if (newStatus === "completed") {
                        await updatePageProperty(p, "date_ended", new Date().toISOString().split('T')[0]);
                    }
                    renderTable(genreSelect.value, statusSelect.value, searchInput.value);
                }
            }
        };
    }

    container.appendChild(table);
}

genreSelect.onchange = () => renderTable(genreSelect.value, statusSelect.value, searchInput.value);
statusSelect.onchange = () => renderTable(genreSelect.value, statusSelect.value, searchInput.value);
searchInput.oninput = () => renderTable(genreSelect.value, statusSelect.value, searchInput.value);

renderTable();

Шаблон для книг:

В status может быть planned, completed, reading.

В cover можно просто вставить ссылку на картинку из интернета.

---
title: {{Title}}
cover:
author:
genre:
status: planned
---
Tags: #book 
File Links: [[Book Catalog]]
# {{Title}}

Ведение заметок (Kanban плагин)

Task Board
Task Board

Тут все быстро - есть 3 списка:

  • ASAP (As soon as possible) - дела которые нужно сделать сейчас

  • Deferred - отложенные дела

  • Done - сделанные дела

Дела просто перетаскиваю из одного списка в другой

Домашняя страница

Страница выглядит подобным образом:

Homepage
Homepage

Использую css файл для расположения элементов по странице и разделения их на блоки:

CSS файл для Homepage
.image-embed[alt*="fade-banner"] {
    position: relative;
    display: block;
    width: 100%;
    height: 260px;
    overflow: hidden;
    margin-bottom: 25px;
    border-radius: 16px;
}

.image-embed[alt*="fade-banner"] img {
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
    transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

.image-embed[alt*="fade-banner"]:hover img {
    transform: scale(1.03);
}

.image-embed[alt*="fade-banner"]::after,
.banner-gradient {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, transparent 20%, var(--background-primary) 100%);
    pointer-events: none;
}

.dashboard {
    font-family: inherit;
    padding: 10px 25px !important;
    --card-bg: color-mix(in srgb, var(--background-secondary) 88%, black 12%);
    --card-bg-hover: color-mix(in srgb, var(--background-secondary) 80%, black 20%);
}

.dashboard .markdown-preview-section {
    max-width: 100%;
}

.dashboard h1 {
    position: relative;
    z-index: 2;
    text-align: center;
    font-size: 2.6em;
    font-weight: 800;
    letter-spacing: -0.02em;
    margin: -92px 0 34px;
    padding-bottom: 16px;
    color: #ffffff;
    text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55), 0 1px 4px rgba(0, 0, 0, 0.4);
}

.dashboard h1::after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 72px;
    height: 3px;
    border-radius: 3px;
    background: linear-gradient(90deg, transparent, var(--interactive-accent), transparent);
}

.dashboard div>ul {
    list-style: none !important;
    display: flex;
    flex-flow: row wrap;
    column-gap: 40px;
    row-gap: 25px;
    padding-left: 0 !important;
    margin: 0 !important;
}

.dashboard div>ul>li {
    flex: 1 1 260px;
    min-width: 260px;
    background-color: transparent !important;
    border: 1px solid transparent !important;
    border-radius: 12px !important;
    padding: 14px 18px !important;
    box-shadow: none !important;
    transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.dashboard div>ul>li>b,
.dashboard div>ul>li>strong,
.dashboard div>ul>li>h3 {
    display: block;
    font-size: 1.1em;
    font-weight: 650;
    margin-bottom: 12px;
    padding-bottom: 6px;
    color: var(--text-normal);
    border-bottom: 1px solid var(--background-modifier-border);
    transition: color 0.2s ease, border-color 0.2s ease;
}

.dashboard div>ul>li:hover>h3,
.dashboard div>ul>li:hover>b,
.dashboard div>ul>li:hover>strong {
    color: var(--interactive-accent);
    border-color: var(--interactive-accent);
}

.dashboard div>ul>li:has(>h3) {
    background: var(--card-bg) !important;
    border: 1px solid var(--background-modifier-border) !important;
    border-radius: 14px !important;
    padding: 16px 20px !important;
}

.dashboard div>ul>li:has(>h3):hover {
    background: var(--card-bg-hover) !important;
    border-color: var(--interactive-accent) !important;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
    transform: translateY(-2px);
}

.dashboard div>ul>li>ul {
    display: flex !important;
    flex-direction: column;
    gap: 6px;
    padding-left: 0 !important;
    list-style: none !important;
}

.dashboard div>ul>li>ul>li {
    background: transparent !important;
    border: none !important;
    padding: 2px 0 !important;
    min-width: auto !important;
    font-size: 0.95em;
    opacity: 0.85;
    transition: opacity 0.2s ease, transform 0.2s ease;
}

.dashboard div>ul>li>ul>li:hover {
    opacity: 1;
    transform: translateX(4px);
}

.dashboard div>ul>li>ul>li a {
    text-decoration: none !important;
    color: var(--text-muted);
    transition: color 0.2s ease;
}

.dashboard div>ul>li>ul>li a:hover {
    color: var(--interactive-accent);
}

.dashboard div.markdown-preview-section>div>ul>li>.list-bullet,
.dashboard.markdown-rendered.show-indentation-guide li>ul::before,
.dashboard.markdown-rendered.show-indentation-guide li>ol::before {
    display: none !important;
}

div.markdown-preview-section>div>ul.has-list-bullet>li {
    padding-left: 0 !important;
}

.dashboard div>ul>li:has(>h2) {
    position: relative;
    background: var(--card-bg) !important;
    border: 1px solid var(--background-modifier-border) !important;
    border-radius: 18px !important;
    padding: 28px 22px !important;
    min-height: 90px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    box-shadow: none;
}

.dashboard div>ul>li:has(>h2):hover {
    background: var(--card-bg-hover) !important;
    border-color: var(--interactive-accent) !important;
    transform: translateY(-4px);
    box-shadow: 0 14px 30px rgba(0, 0, 0, 0.25);
}

.dashboard div>ul>li>h2 {
    font-size: 1.3em;
    font-weight: 700;
    letter-spacing: -0.01em;
    margin: 0 !important;
    padding: 0 !important;
    border: none !important;
    line-height: 1.3;
}

.dashboard div>ul>li>h2 a {
    text-decoration: none !important;
    color: var(--text-normal);
    transition: color 0.2s ease;
}

.dashboard div>ul>li:has(>h2):hover h2 a {
    color: var(--interactive-accent);
}

.reading-section {
    background: var(--card-bg);
    border: 1px solid var(--background-modifier-border);
    border-radius: 18px;
    padding: 22px 24px 26px;
    margin: 8px 0 30px;
}

.reading-section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
}

.reading-section-title {
    font-size: 1.05em;
    font-weight: 700;
    color: var(--text-normal);
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
}

.reading-section-count {
    font-size: 0.72em;
    font-weight: 600;
    color: var(--text-muted);
    background: var(--background-modifier-border);
    padding: 3px 11px;
    border-radius: 20px;
}

.reading-now-empty {
    color: var(--text-muted);
    font-size: 0.9em;
    padding: 4px 0;
}

.reading-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 22px;
}

.reading-card {
    width: 130px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}

.reading-card-cover {
    width: 120px;
    height: 172px;
    border-radius: 8px;
    overflow: hidden;
    background: var(--background-modifier-border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 34px;
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
    cursor: pointer;
    transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
    margin-bottom: 10px;
}

.reading-card-cover:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 26px rgba(0, 0, 0, 0.32);
}

.reading-card-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.reading-card-badge {
    font-size: 0.62em;
    font-weight: 700;
    color: var(--interactive-accent);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 5px;
}

.reading-card-title {
    font-size: 0.88em;
    font-weight: 650;
    line-height: 1.3;
    color: var(--text-normal) !important;
    text-decoration: none !important;
    transition: color 0.2s ease;
}

.reading-card-title:hover {
    color: var(--interactive-accent) !important;
}

.reading-card-author {
    font-size: 0.76em;
    color: var(--text-muted);
    margin-top: 3px;
}

.reading-grid-divider {
    width: 1px;
    align-self: stretch;
    background: var(--background-modifier-border);
    position: relative;
    margin: 0 4px;
}

.reading-grid-divider span {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(-90deg);
    white-space: nowrap;
    font-size: 0.68em;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-faint);
    background: var(--card-bg);
    padding: 2px 6px;
}

.reading-card-planned {
    opacity: 0.68;
    transform: scale(0.94);
    transition: opacity 0.2s ease, transform 0.2s ease;
}

.reading-card-planned:hover {
    opacity: 1;
    transform: scale(1);
}

.reading-card-badge-planned {
    color: var(--text-muted);
}

Теперь сама markdown заметка. Dataviewjs код отображает список книг которые я читаю в данный момент и планирую прочитать. Markdown используется для добавления элементов на страницу:

Homepage.md
---
cssclasses: dashboard
---

![[wallpaper.jpg|fade-banner]]

# 🏠 My Homepage
- ## [[Task Board]]
- ## [[Cheatsheet]]
- ## [[Book Catalog]]

---

- ### 🏋️ Тренировки
    - [[Тренировки зал full-body]]
    - [[Тренировки лето]]
    - [[Доп. Тренировки (3 в неделю)]]
- ### 🎯 По годовым целям
    - [[Цели на 2026]]
    - [[Хочу как-нибудь заняться]]
    - [[K3Stack. Краткий план]]
- ### 👾 Планы Habitica
    - [[Еженедельный летний план 2026]]
    - [[Еженедельный план 2026]]
- ### 🎓 Поступление
    - [[Подготовка к ЕГЭ]]
    - [[Выбор ВУЗа]]
    - [[Доп баллы в Вузах]]
    - [[Олимпиады Вузов]]
    - [[Целевое обучение]]
- ### 📔 Доп. материалы
    - [[Вопросы DevOps Advanced]]
    - [[Вопросы DevOps Basic]]
    - [[Механизм минимальных затрат]]
    - [[NetDevSecOps.canvas|NetDevSecOps canvas]]

---
```dataviewjs
const bookPages = dv.pages('"5 - Books"').where(p => p.title);

function determineBookStatus(page) {
    if (page.status) return page.status;
    const today = new Date().toISOString().split('T')[0];
    if (page.date_ended && page.date_ended <= today) return "completed";
    if (page.date_started) return "reading";
    return "planned";
}

const reading = bookPages.where(p => determineBookStatus(p) === "reading");
const planned = bookPages.where(p => determineBookStatus(p) === "planned");

const section = document.createElement("div");
section.className = "reading-section";

const header = document.createElement("div");
header.className = "reading-section-header";

const title = document.createElement("div");
title.className = "reading-section-title";
title.textContent = "📬 На прочтение";
header.appendChild(title);

const count = document.createElement("span");
count.className = "reading-section-count";
count.textContent = reading.length + planned.length;
header.appendChild(count);

section.appendChild(header);

function renderCard(p, status) {
    const card = document.createElement("div");
    card.className = "reading-card";
    if (status === "planned") card.classList.add("reading-card-planned");

    const cover = document.createElement("div");
    cover.className = "reading-card-cover";
    cover.onclick = () => window.location.href = p.file.path;
    if (p.cover) {
        const img = document.createElement("img");
        img.src = p.cover;
        cover.appendChild(img);
    } else {
        cover.innerHTML = "📖";
    }
    card.appendChild(cover);

    const badge = document.createElement("div");
    badge.className = "reading-card-badge";
    if (status === "planned") badge.classList.add("reading-card-badge-planned");
    badge.textContent = status === "reading" ? "Читаю" : "В планах";
    card.appendChild(badge);

    const link = document.createElement("a");
    link.href = p.file.path;
    link.className = "internal-link reading-card-title";
    link.textContent = p.title || p.file.name;
    card.appendChild(link);

    const author = document.createElement("div");
    author.className = "reading-card-author";
    author.textContent = p.author || "Автор не указан";
    card.appendChild(author);

    return card;
}

if (reading.length === 0 && planned.length === 0) {
    const empty = document.createElement("div");
    empty.className = "reading-now-empty";
    empty.textContent = "📭 Пока здесь пусто";
    section.appendChild(empty);
} else {
    const grid = document.createElement("div");
    grid.className = "reading-grid";

    for (let p of reading) {
        grid.appendChild(renderCard(p, "reading"));
    }

    if (reading.length > 0 && planned.length > 0) {
        const divider = document.createElement("div");
        divider.className = "reading-grid-divider";
        const dividerLabel = document.createElement("span");
        dividerLabel.textContent = "В планах";
        divider.appendChild(dividerLabel);
        grid.appendChild(divider);
    }

    for (let p of planned) {
        grid.appendChild(renderCard(p, "planned"));
    }

    section.appendChild(grid);
}

this.container.appendChild(section);
```

Плагины

  • Better Word Count - улучшенный подсчет слов на странице

  • Dataview - поисковый движок по заметкам в Obsidian. (или что-то вроде)

  • Git - для синхронизации заметок

  • Homepage - открывает определенную заметку при входе в хранилище

  • Iconize - красивые иконки у заметок

  • Kanban - для создания Kanban досок

  • Templater - продвинутая шаблонизация

Заключение

Хранилище в Obsidian постоянно меняется, доходя до неузнаваемости. Чем дальше - тем в итоге проще оно становится. На самом деле не сильно важен сам факт того, какое хранилище в итоге - главное, чтобы оно решало ваши задачи и было удобно для пользования.

В конце еще уточню, что если вам кажется все это излишествами - это нормально, так как, как мне кажется Obsidian, это все таки что-то, что больше для определенной нестандартной аудитории, поэтому, если что-то показалось полезным - буду рад если возьмете себе, если же нет - то проблем в этом тоже нет.