Нужен был инструмент для создания карточек Instagram-каруселей. Без Figma, без Canva — прямо из markdown-заметок.

Что получилось

Один markdown-файл = одна карусель. Секции ### = слайды. Превью в браузере, скачивание PNG одной кнопкой.

---
title: Эго — маска труса
layout: trip2g/instaframe
---

### ТВОЁ [ЭГО] НЕ ДЕЛАЕТ ТЕБЯ СИЛЬНЫМ

Оно прячет твою трусость.

Слово в квадратных скобках [ЭГО] получает рамку — акцент на ключевом слове.

Архитектура

Данные в HTML, логика в JS

Первая версия пыталась передать данные напрямую в JavaScript через template-выражения:

const slides = [
  {{ range s := note.Sections(3) }}
  { title: {{ s.TitleHTML }} },
  {{ end }}
];

Сломалось на кириллице — Unexpected identifier 'ИСКАТЬ'. Кавычки не расставились.

Решение — рендерить данные в скрытый HTML, читать из DOM:

<div class="slides-data" style="display:none">
  {{ range s := note.Sections(3) }}
  <div class="slide-item">
    <div class="slide-title">{{ s.TitleHTML }}</div>
    <div class="slide-text">{{ s.ContentHTML }}</div>
  </div>
  {{ end }}
</div>
const slides = Array.from(document.querySelectorAll('.slide-item'))
  .map(el => ({
    title: el.querySelector('.slide-title').innerHTML,
    text: el.querySelector('.slide-text').innerHTML
  }));

Чисто и надёжно. Template-код не смешивается с JS.

Выделение слов рамкой

Markdown не поддерживает произвольные рамки. Использовали синтаксис ссылок [слово] — он не конфликтует с wikilinks, потому что нет () после скобок.

JS на лету заменяет на <mark>:

const parseTitle = html => html.replace(/\[([^\]]+)\]/g, '<mark>$1</mark>');

CSS делает рамку:

.card-title mark {
  background: none;
  border: 2.5px solid #111;
  padding: 0.05em 0.25em;
  display: inline-block;
}

Проблема с line-height

Рамка вокруг слова налезала на соседние строки. Когда текст переносится, border добавляет высоту, но line-height остаётся прежним.

Решение — увеличить line-height заголовка до 1.6:

.card-title {
  line-height: 1.6;
}

.card-title mark {
  display: inline-block;
  line-height: 1;
}

inline-block на mark изолирует его от влияния на общий поток.

Скачивание PNG

Использовали html2canvas — рендерит DOM в canvas, который можно сохранить как PNG.

const canvas = await html2canvas(card, {
  scale: 2,
  backgroundColor: '#e5e5e5'
});

const link = document.createElement('a');
link.download = 'slide-1.png';
link.href = canvas.toDataURL('image/png');
link.click();

scale: 2 даёт чёткость на ретине. Размер карточки 4:5 — стандарт Instagram.

Кнопка «Все N шт.» скачивает слайды по очереди с задержкой 250ms между файлами — иначе браузер блокирует множественные загрузки.

Типографика

Для заголовков — Cormorant SC. Шрифт с капителями и характером старинных карт. Тот же, что в интерактивной карте.

Для текста — Inter. Читается на любом размере.

Интерфейс

Эволюция за три итерации:

v1 — кнопки навигации отдельно от карточки, нумерованные миниатюры внизу. Выглядело разбросанно.

v2 — стрелки по бокам карточки (как в Instagram), точки вместо номеров. Компактнее.

v3 — кнопки скачивания в хедер. Две кнопки: «1/11 ⬇» (текущий) и «11/11 ⬇» (все). Минимум элементов.

Структура файлов

instaframes/
├── _index.md          — инструкция
├── task0.md           — карусель про эго
└── example.md         — пример

Каждый файл — отдельная карусель. Добавить новую — создать markdown с layout: trip2g/instaframe.

CSS в HTML

Весь CSS инлайн в шаблоне. Не зависит от основных стилей сайта. Можно скопировать шаблон в другой проект — будет работать.

Итого

  • Markdown → карусель — секции ### становятся слайдами
  • Выделение [слово] — рамка на ключевом слове
  • Скачивание PNG — html2canvas, scale 2x
  • Типографика — Cormorant SC + Inter
  • Автономный шаблон — CSS и JS инлайн

Контент в Obsidian, результат в Instagram. Без дизайнера, без Figma.


Попробовать: /instaframes/task0

Готовы попробовать?

Напишите в Telegram — поможем настроить и покажем примеры.

Написать в Telegram