Tilda — управление сайтом через API
Внутренний (не публичный) API Tilda для проектов, потоков (Feeds), постов, страниц и блоков. Собран реверс-инжинирингом
sendRequest/td__ajax.
⚠️ Это внутренний API. Tilda его не документирует, не поддерживает и не обязана сохранять. Он может измениться без предупреждения в любой день — тогда часть рецептов ниже перестанет работать, и это не поломка навыка. Публичный API Tilda (https://help.tilda.cc/api) умеет только чтение и на порядок беднее; всё, что здесь написано, существует потому, что через публичный этого не сделать. Для боевых процессов держи ручной запасной путь через UI.
Что тебе понадобится
| Что | Зачем | Где взять |
|---|---|---|
| Свой аккаунт Tilda | всё остальное | tilda.cc — на платном тарифе; Feeds есть не на всех |
TILDA_EMAIL / TILDA_PASSWORD в окружении |
логин для сессионных вызовов | свои же учётные данные |
TILDA_PUBLIC_KEY / TILDA_UPLOAD_KEY |
загрузка файлов на Tilda CDN | Настройки проекта → API |
| Playwright | половина API работает только из залогиненной вкладки | pip install playwright && playwright install chromium |
requests |
HTTP-часть клиента scripts/tilda_feeds.py |
pip install requests |
Ключи кладутся в переменные окружения или в свой ~/.claude/.credentials.master.env
(образец — ~/.claude/templates/.credentials.master.env.example). В репозиторий — никогда.
Когда использовать
- Создание/редактирование/удаление постов в потоках (МЕРОПРИЯТИЯ, БЛОГ, МАТЕРИАЛЫ)
- Массовое обновление карточек (обложки, описания, даты, теги, ссылки) — за минуты вместо часов в UI
- Изменение настроек блоков на страницах (например
input1Feed-блока — лимит постов) - CSV-импорт постов
- Загрузка обложек на Tilda CDN
- Публикация страниц
- Авторизация и поддержание сессии для всего вышеперечисленного
⚠️ Главные принципы (выученное на ошибках)
- Используй прямой
fetch('/submit/')через urlencoded, а неsendRequest—sendRequestчерез MCP Playwright виснет на 30+ сек, теряет контекст. posts_Addпринимает ТОЛЬКО{title, feeduid, partuid}— возвращает новыйuid. Остальные поля заполняются отдельнымposts_Edit.- Имя API —
posts_GetList, неposts_List.posts_Delete, неposts_Remove. posts_Active— toggle, не setter. Хочешь установитьactive=y→ проверь текущее состояние и вызывай только если выключено.- Список постов возвращается как OBJECT keyed by uid, не массив.
Object.entries(resp.data.posts). - Body —
application/x-www-form-urlencoded, не FormData. FormData возвращает «Неизвестная ошибка при отправке запроса». - CSV-импорт: запрещены
;в текстовых полях — Tilda CSV parser ломается. Заменяй,или удаляй. - Tilda CDN не принимает PDF — используй Yandex Disk для документов.
- Inline CSS с
;вdescr— ломает CSV import. Делай без inline-стилей или замени;→,после строки. - PostUid — короткий ID типа
l2sv7pzxj1, генерируется сервером. imageИmediadataОБА должны быть установлены — иначе обложка не отрендерится в карточке.- Лимит постов на странице — поле
input1Feed-блока (тип 897). Если на сайте видно 3 поста из 42 активных — этоinput1=3. posts_EditБЕЗ поляtitleобнуляет заголовок! Возвращаетerror: "Post title is empty"либо тихо ставит пустоту. ВСЕГДА передавайtitleв каждыйposts_Edit, даже если только обложку меняешь. Поведение: Tilda трактует отсутствие поля как пустую строку, не как «не трогать». Это касается ВСЕХ полей —posts_Editэто не PATCH с partial update, это PUT всего поста.posts_EditСБРАСЫВАЕТpartsиactiveесли их не передать. Перед каждым Edit делайposts_Get, забирайp.partsи явно передавайparts: p.parts,active: 'y'. Иначе пост пропадёт из правильной категории МЕДИА/БЛОГ и станет неактивным. См. troubleshooting «posts_Edit обнулил parts/active».- API возвращает
descr/titleHTML-encoded на transport уровне («→«). Это не баг, это transport encoding. Render на сайте показывает«корректно. Двойное«в API response — вот это баг: значит фактически stored«literal, страница показывает«text. Decoder черезtextarea.innerHTML → .valueитеративно. См. troubleshooting «иероглифы в descr». - FormData работает для
/submit/— это противоречит старой записи в скилле. В апреле 2026 проверено:new FormData()принимается дляposts_Get,posts_Edit,posts_Active,posts_GetList. URLSearchParams тоже работает. Что НЕ работает:feeds_Publish— возвращает «Неизвестная ошибка», но изменения и так автопубликуются еслиactive=y. - Поле
textдля тела поста — это JSON-массив блоков:text=JSON.stringify([{ty:'html', co:'<HTML>'}]). На API возврате всё HTML-encoded для transport. Нельзя оставлятьtextпустым — иначе reading time на странице будет «1 минута», даже если карточка большая. Tilda поддерживает несколькоtyблоков (см. ниже секцию «Block types»). Правильный выбор: используйtext(неhtml) — Tilda тогда применяет нативные стилиt-redactor__text,t-redactor__h2,t-redactor__quote. Сhtmlстили не работают, текст рендерится без форматирования. - Поле
mediatypeобязательно для image-постов:mediatype=image. Без него обложка может не отрендериться корректно даже при заполненныхimage+mediadata. feeds.tilda.ru/submit/отвечает RELOGIN из Pythonrequests.Sessionс куками от Playwright — даже если PHPSESSID для feeds.tilda.ru есть. Решение: делай fetch ИЗНУТРИ страницыfeeds.tilda.ru/posts/?feeduid=...черезpage.evaluate()сcredentials:'include'. Same-origin запрос работает, Python session — нет. См.references/auth.mdсекцию «Handshake-цепочка».- Чтобы попасть на
feeds.tilda.ru/posts/?feeduid=...после логина, нужна handshake-цепочка (порядок важен):tilda.cc/login/→tilda.cc/projects/projectinfo/?projectid=X→tilda.cc/projects/manage/?projectid=X→tilda.cc/page/?pageid=Y&projectid=X(любой реальный pageid проекта) →feeds.tilda.ru/feeds/?projectid=X→feeds.tilda.ru/posts/?feeduid=Z&projectid=X. Без шага через page editor feeds.tilda.ru редиректит обратно наtilda.ru/projects/. Подтверждено 2026-04-25 на webinar covers v6. - Substring matching webinar→post иногда промахивается на small differences (тире vs длинное тире, пробелы, кавычки,
(не)vs(не )). Подготовь hardcodedOVERRIDES = {slug: postuid, ...}map для известных пар, чтобы не зависеть от fuzzy matching. Особенно важно перед массовым PUT — иначе обновишь не тот пост. - CSS
content: var(--xxx)в HTML-шаблонах для рендера обложек — удобный паттерн для batch-генерации SVG/PNG из одного template. Для каждого slug подставляй CSS-переменные через regex replace в:root. Безопаснее чем string interpolation в HTML (не нужен XSS escape), но требуетreplace('"', '\\"')для кавычек внутри значений. Пример вwebinar_assets/render_covers_v6.py. - 🚨
posts_GetНЕ возвращает полеactive— толькоposts_GetListотдаёт его. Если ты копируешь поля изposts_Getвposts_Editpayload, полеactiveбудет отсутствовать → Tilda молча сбросит в""→ пост невидим на сайте. Всегда дополнительно вызывайposts_GetListчтобы получитьactiveиparts, ИЛИ используй готовый helpersafe_in_page_edit(page, feeduid, postuid, **updates)изscripts/tilda_feeds.py. Этот баг повторно случился 2026-04-25 на 46 постах МЕРОПРИЯТИЯ — потому что предыдущий троублешут запомнил «передавай active», но не объяснил чтоposts_Getего НЕ возвращает. После применения обложек ВСЕГДА проверяй чтоactivecount не уменьшился:sum(p.get('active')=='y' for p in posts_GetList().data.posts.values()). - 🚨
posts_GetListНЕ возвращает поляimage/mediadata/descr/text— толькоposts_Getвозвращает реальные значения. Это значит проверка типаp.get('image')черезposts_GetListдаёт false negative — выглядит будто image не установлен, хотя он есть. Чтобы проверить реальное состояние post после edit — вызывайposts_Getдля конкретного uid. Симметрично #23:posts_Getбезactive/parts,posts_GetListбезimage/descr/text/mediadata. Полная картина = оба эндпоинта вместе. Verified 2026-04-26. - T123 Custom HTML блоки (
data-record-type="131") — отдельный мир. Рендерятся как<div id="rec<recordid>">с произвольным HTML/CSS/JS внутри. Используются когда нужен кастомный дизайн которого нет в стандартных Tilda-блоках. Поля настроек:code(HTML тело),body_html(доп. блок),css(стили),js(скрипты). Сохраняются черезcomm=saverecordна/page/submit/сrecordid=<rec>,pageid=<pageid>,<field>=<value>.onlythisfieldне обязательное — можно слать сразу несколько полей, главноеcomm=saverecord. - 🚨 ACE editor в T123 НЕ автосинхронизируется с
<textarea name="code">— если правишь HTML черезeditor.setValue(...), перед save надо вручную обновить textarea:document.querySelector('textarea[name="code"]').value = editor.getValue(). Иначеsaverecordотправит старое содержимое. Verified 2026-04-29 на example.com /workshop SEO fix. - Большие HTML (>2KB) ломают
evaluate_scriptесли передавать одной строкой. Решение — chunked upload через base64 + atob + TextDecoder UTF-8: разбей HTML на куски по ~2KB, помести в массив строк, на странице сделайatob(chunks.join(''))→new TextDecoder('utf-8').decode(bytes)→editor.setValue(decoded). Альтернатива — залить HTML на свой сервер иfetch()оттуда (CORS должен быть*). Проверено на 24 КБ карусели. - Программное создание T123 блока:
window.tp__addRecord(131, '<after_recid>', null)— создаёт новый блок типа 131 после указанного recid. Возвращает Promise с новым recordid. Полезно когда нужно добавить блок ниже существующего без UI. <script type="application/ld+json">напрямую вcodeTilda T123 валидируется DOMParser-ом и может корраптить closing tag. Обход: вместо raw<script>вставляй JS который черезdocument.createElement('script')+JSON.stringify(schemas)создаёт элемент рантаймом. Tilda DOMParser не трогает строковые JSON-литералы внутри JS-кода. Verified 2026-04-29 на example.com (4 JSON-LD schemas).- 🚨 Pattern: T123 shim + payload SVG на CDN — для гигантских страниц (100KB+ HTML). Вместо запихивания всего в
code(где есть лимит и DOMParser-валидация) используй: (а) HTML wrap as base64 inside<svg><text id="payload">...</text></svg>→ upload на Tilda CDN; (б) в T123codeвставь shim ~3.7KB который фетчит payload SVG → atob → инжектит в<div id="th-mount">. Bonus: bootstrap-вариант — T123 загружает<script src="https://your-server.example.com/runner.js">, runner.js (на твоём сервере) знает payload URL, и обновления контента не требуют re-publish в Tilda. Проверено на лендинге 116 КБ и блоге 134 КБ. - 🚨 Tilda CDN отвергает mp4/mp3 для некоторых проектов (проверено на проекте, созданном через tilda.ru). Workaround: храни видео/аудио на собственном сервере (
/var/www/your-site/media/,/media/audio/) и линкуй абсолютным URL. Аудио обёрнутое в mp4-контейнер с fake video тоже отвергается. - Tilda CDN upload требует
Origin: https://tilda.ru+Referer: https://tilda.ru/для проектов созданных через tilda.ru (не tilda.cc). Без этих хедеров — 403/JS error. Старый skill говорил «Origin: https://feeds.tilda.ru» — это работает для основного проекта (старый проект), но НЕ для новых проектов. Если 403 — попробуй оба варианта. - 🟢
saverecordдля T123 РАБОТАЕТ — корректный набор полей (проверено на боевом проекте):comm=saverecord+pageid+projectid+recordid+code=<full_html>. БЕЗonlythisfield— этот параметр работает для стандартных Tilda-блоков (типаinput1Feed-блока 897), но для T123 ломает запись. Server отвечает plain textOK(2 байта), не JSON. Verify через повторный GET/page/edit/с тем же recordid → проверьdata.record.code. После save →/page/publish/(отдаёт JSON с link/wslink). Пример рабочего payload:
await fetch('/page/submit/', {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
body: new URLSearchParams({
comm:'saverecord', pageid, projectid, recordid,
code: newHtml, // FULL HTML, без onlythisfield
}).toString(),
}); // → "OK"
ВАЖНО про чтение текущего code: API /page/edit/ (POST с pageid+projectid+recordid) возвращает JSON {record: {code, tplid, ...}, tpl}. Поле record.code HTML-encoded (как <style>) — декодируй через textarea.innerHTML → .value перед обработкой. Никогда не отправляй назад HTML-encoded версию — Tilda её удвоит.
34. Custom domain через A-record на Tilda IP 176.57.66.20. HTTPS provisioning через Let's Encrypt занимает 24h после смены DNS. Без HTTPS — 30 c).** saverecord полного документа + publish + verify в одном https://your-domain.ru отдаёт чужой сертификат (соседний клиент Tilda на том же IP). Проверка: curl -I https://your-domain.ru должен вернуть Server: TildaSN.
35. <projectsubdomain>.tilda.ws — публичный preview. Например one-warm-saury.tilda.ws — Tilda выдаёт случайное трёхсловное имя. Если изменения видны на preview но не на custom domain — это CDN cache (5-15 мин). Если НЕ видны даже на preview — saverecord не отработал, см. п.33.
36. 🚨 SEO / og / title страницы редактируются ТОЛЬКО через Page Settings, НЕ через saverecord. Открой форму из списка страниц проекта: td__showform__EditPageSettings('<pageid>') (вызывать со вкладки tilda.ru/projects/?projectid=X, НЕ из page-editor) → появляется попап с inputs по name: title, descr, meta_title, meta_descr, meta_keywords, fb_title, fb_descr, fb_img (og:image, URL-строка), link_canonical, nosearch, meta_nofollow. Меняешь .value нужных → клик input.js-ps-popup-submit (текст «Сохранить изменения») → comm=savepagesettings уходит сам. Потом /page/publish/ чтобы применилось. Title в <title> живёт ЗДЕСЬ, не в T123-коде. Verified 2026-06-11 (techpred школа: title/fb_descr/meta_descr).
37. 🚨 Чекбоксы Page Settings читать через .checked, НЕ .value. У <input type=checkbox name=nosearch> атрибут value всегда "on" независимо от состояния — поэтому дамп формы по .value даёт ЛОЖНУЮ тревогу «noindex включён». Реальное состояние = el.checked. nosearch=on (checked) = страница закрыта от поиска, meta_nofollow=on = nofollow. Снять = if(el.checked) el.click(). Verified 2026-06-11 (ложная noindex-паника на школе).
38. 🚨 og/twitter/canonical НИКОГДА не клади в project-head — только в Page Settings конкретной страницы. Project-head (tilda.ru/projects/editheadcode/?projectid=X → textarea[name="headcode"]) — ОБЩИЙ <head>-код для ВСЕХ страниц проекта, и он идёт в <head> раньше page-level тегов. Скрейперы (Telegram, FB) берут ПЕРВЫЙ встреченный og-тег → если в project-head лежит og:title/og:url/twitter:title одной из страниц, ВСЕ остальные страницы шарят её превью. Хуже того, <link rel=canonical> в project-head делает все страницы «каноникой» одной — прямой SEO-вред. Правило: в project-head только verification-меты, og:locale, общие схемы; всё страничное (og:title/description/image, twitter:*, canonical) — в Page Settings. Правка project-head: меняешь textarea + синхронишь ACE (ace.edit(document.querySelector('.ace_editor')).setValue(v,-1)) → клик .js-btn-save → republish страниц. Verified 2026-06-11 (превью школы показывало конференцию).
39. 🚨 После смены og — сбрось кэш Telegram-превью через @WebpageBot. Telegram кэширует превью ссылки и сам не обновляет даже после правки og. Отправь URL боту @WebpageBot (руками или Telethon) → ответ «Link previews was updated successfully». Слать обе формы (https и http), если домен пока на http. Без этого новый баннер/заголовок в превью не появится. Verified многократно (aimipt, школа).
40. 🚨 Заливка большого/полного-документа T123 через S3, а не localhost. Для HTML 50KB+ (целая страница в одном T123): залей файл в любое S3-совместимое хранилище (boto3, свои креды в переменных окружения, ACL=public-read, ContentType text/html) → в браузере (вкладка tilda.ru, залогинен) fetch(s3_url) → saverecord. НЕ через http://127.0.0.1 — Chrome PNA (Private Network Access) блокирует fetch с localhost из https-страницы НАМЕРТВО, даже с заголовком Access-Control-Allow-Private-Network. S3-хранилище в нужной юрисдикции даёт правильный CORS и MIME из коробки. (Свой сервер — тоже вариант, но S3 надёжнее.) Проверено.
41. 🚨 Кросс-доменный POST к tilda.ru работает ТОЛЬКО со вкладки на домене tilda.ru. fetch('https://tilda.ru/page/submit/', {credentials:'include'}) со вкладки techpred.online или любого чужого домена → Failed to fetch (CORS + куки не уходят). Перед saverecord/publish/page-settings навигируй вкладку на tilda.ru/projects/?projectid=X. Verified 2026-06-11.
42. **🚨 Длинные fetch-цепочки в одном evaluate таймаутят протокол DevTools (evaluate_script → Runtime.callFunctionOn timed out, хотя запрос на сервере прошёл. Паттерн: положи результат в window.__x = 'PENDING', запусти fetch без await (.then(t => window.__x = t)), верни сразу 'STARTED'; следующим коротким evaluate опрашивай window.__x в цикле. (Родственно fire-and-forget window.BG, но именно из-за protocol-timeout, а не длины цикла.)
43. Tilda отдаёт 403 на curl с query-string (?cb=...) — бот-защита. Live-страницу после правок проверяй ТОЛЬКО из браузера (chrome-devtools navigate + читать сырой HTML через fetch с той же вкладки), не через curl. Без query-string curl обычно проходит, но кэш не сбивается.
44. .t123 a link-bleed: дефолтный оранжевый цвет ссылок Tilda бьёт твою вёрстку. Внутри T123 Tilda применяет свой .t-text a {color: #ff8562} к ссылкам без явного цвета (ghost-кнопки на тёмном фоне особенно). Фикс: .t123 a{color:inherit !important} .t123 a span{color:inherit !important}, а цветным ссылкам (footer-dim, tg-blue) дай бОльшую специфичность .t123 .footer__col a{color:... !important}.
Endpoint и формат
POST https://feeds.tilda.ru/submit/ # Posts/Feeds API
POST https://tilda.cc/page/edit/ # Get block settings
POST https://tilda.cc/page/submit/ # Save block field
POST https://tilda.cc/page/publish/ # Publish page
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: <сессия после логина на tilda.cc/login/>
Тело: action=<имя>&<field>=<value>&... (URL-encoded).
Ответ Feeds API: {data: {...}|[], error: "" | "сообщение"}.
Быстрый старт (Python)
from skills.tilda.scripts.tilda_feeds import TildaFeedsClient, TildaCDN, TildaPageEditor
# Получи cookies через ручной логин или Playwright export
COOKIES = {'tildauid': '...', 'PHPSESSID': '...'}
client = TildaFeedsClient(COOKIES)
posts = client.list_posts('100000000001') # data.posts — Object keyed by uid
# Создать + заполнить + активировать
new = client.add_post('100000000001', 'Заголовок поста')
uid = new['data']['uid']
client.edit_post(uid, title='...', descr='...', date='2025-04-26 12:00',
url='https://...', image='https://static.tildacdn.com/...webp',
mediadata='https://static.tildacdn.com/...webp', tags='AI, Воркшоп')
client.ensure_active(uid, '100000000001') # idempotent (НЕ toggle)
# Загрузить обложку на CDN (без авторизации)
cdn = TildaCDN() # ключи берутся из окружения: TILDA_PUBLIC_KEY / TILDA_UPLOAD_KEY
url = cdn.upload('cover.webp')
# Изменить настройки блока на странице (например, лимит Feed-блока)
page = TildaPageEditor(COOKIES)
page.save_field(pageid='70000004', recordid='1000000001', field='input1', value='50')
page.publish('70000004', '12345678') # → опубликует страницу
Быстрый старт (JavaScript / Playwright)
// На открытой https://feeds.tilda.ru/posts/?feeduid=100000000001
const t = new TildaFeed('100000000001');
const posts = await t.list({items: 500}); // Object keyed by uid
const uid = await t.add('Заголовок');
await t.edit(uid, {descr:'...', date:'2025-04-26 12:00', image:'...', mediadata:'...'});
await t.ensureActive(uid);
// Bulk обновление обложек по нормализованному title
await t.bulkSetCovers({
'воркшоп иитрансформация в компании': 'https://static.tildacdn.com/...webp',
// ...
});
Структура
| Файл | Содержит |
|---|---|
references/feeds-api.md |
Все API actions: posts_Get/GetList/Add/Edit/Active/Delete/Restore/Pin/Reorder/Duplicate, feeds_GetList/Get, parts_GetList |
references/text-blocks.md |
Block types для text поля (text/heading/quote/br/html), готовые рецепты форматирования (TOC, embeds, цветные коробки, стат-карточки, footer-паттерн), типичные ошибки |
references/pages-api.md |
Pages API: чтение/правка настроек блоков (input1 для лимита Feed-блока 897), saverecord, publish |
references/cdn-upload.md |
Tilda CDN: endpoint, ключи, формат файлов, Python пример, ограничения |
references/csv-import.md |
CSV формат: разделители, escape, известные колонки, гочи |
references/auth.md |
Логин, cookies, домены tilda.cc/tilda.ru, капча, Python session |
references/troubleshooting.md |
Типичные проблемы и решения (лимит постов, кривой импорт, MCP виснет, неактивные после Active) |
scripts/tilda_feeds.py |
Python клиент: TildaFeedsClient, TildaCDN, TildaPageEditor — все CRUD + bulk helpers |
scripts/browser_helper.js |
JS классы для DevTools/Playwright: TildaFeed, TildaPage, captureSubmits + fire-and-forget pattern |
Свои ID: где взять
Все числовые идентификаторы ниже — твои, они видны в URL и в ответах API.
Ни один из них не «секретный», но выписать их один раз стоит: без projectid и feeduid
не работает даже handshake-цепочка авторизации.
| ID | Где увидеть |
|---|---|
projectid |
URL списка страниц: tilda.ru/projects/?projectid=<...> |
pageid |
открой страницу в редакторе: tilda.cc/page/?pageid=<...>&projectid=<...> |
feeduid |
feeds.tilda.ru/feeds/?projectid=<...> → открыть поток, feeds.tilda.ru/posts/?feeduid=<...> |
recordid блока |
в DOM редактора: [data-record-type="897"] → el.id без префикса record |
publickey / uploadkey |
Настройки проекта → API |
| preview-хост | <projectsubdomain>.tilda.ws — виден в настройках домена проекта |
Программно список потоков и страниц: feeds_GetList с projectid и
https://tilda.ru/projects/?projectid=<...>.
Заведи себе табличку и держи её ВНЕ пака (например ~/tilda-ids.md), чтобы она не
уехала вместе с конфигом, если ты им поделишься:
projectid: 12345678
domain: example.com
hostname: myproject-dev.tilda.ws (preview)
feeduids:
100000000001 — БЛОГ
100000000002 — МЕРОПРИЯТИЯ
pageids:
70000001 — / (главная)
70000004 — /events (recordid 1000000001 — Feed-блок 897, поле input1)
CDN upload:
publickey: TILDA_PUBLIC_KEY (env)
uploadkey: TILDA_UPLOAD_KEY (env)
endpoint: https://upload.tildacdn.com/api/upload/
Workflow для типовой задачи «обновить N постов»
- Открыть
https://feeds.tilda.ru/posts/?feeduid=<feeduid>в Playwright (сессия должна быть залогинена) - В одном
browser_evaluateблоке:fetch /submit/ posts_GetList→ найти uid целевых постов- Для каждого:
fetch /submit/ posts_Editс полями - Если нужны новые посты:
posts_Add→posts_Editдля дозаполнения posts_Activeдля тех у когоactive=''
- Финальный
posts_GetListдля верификации
Не делай N отдельных browser_evaluate вызовов — каждый вход через MCP стоит ~3-5 сек overhead. Лучше один большой evaluate с циклом внутри.
Для длинных циклов (50+ постов) — fire-and-forget с window.BG:
window.BG = {step:'init', results:[]};
(async () => { /* долгий цикл */ window.BG.step='done'; })();
return {kicked:true}; // вернётся мгновенно
// Потом отдельно: return window.BG;
Workflow «изменить лимит постов на странице»
// 1. Открой редактор страницы
// https://tilda.cc/page/?pageid=<pageid>&projectid=<projectid>
// 2. Найди recordid Feed-блока (тип 897)
const blocks = [...document.querySelectorAll('[data-record-type="897"]')]
.map(el => el.id.replace(/^record/, ''));
// 3. Сохрани новое значение input1 (количество постов)
await fetch('/page/submit/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
body: new URLSearchParams({
comm:'saverecord', pageid:'70000004', recordid: blocks[0],
onlythisfield:'input1', input1:'50',
}).toString(),
credentials:'include',
});
// 4. Опубликуй страницу
await fetch('/page/publish/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
body: new URLSearchParams({pageid:'70000004', projectid:'12345678'}).toString(),
credentials:'include',
});
// Изменения видны на проде через 1-5 минут (CDN кэш)
Antipatterns (никогда так не делай)
- ❌
sendRequest('posts_List', ...)черезbrowser_evaluate— виснет, неправильное имя - ❌
FormDataдля/submit/— сервер не парсит - ❌
posts_Addс полями данных — игнорирует всё кроме title/feeduid/partuid - ❌ Inline CSS с
;в descr — ломает CSV import при следующем импорте - ❌ Загрузка PDF на Tilda CDN — отклоняется
- ❌ Множественные мелкие
evaluateвместо одного большого блока - ❌ Игнорирование
errorв ответе — Tilda пишет туда читаемый русский текст - ❌
posts_Activeбез проверки текущего состояния — это TOGGLE - ❌ Установка только
imageбезmediadata— обложка не отрисуется в карточке - ❌ Изменение
input1без последующего publish — изменения не попадут на прод
Разборы реальных случаев
1. Создание 4 постов в Feed МЕРОПРИЯТИЯ (2026-04-25)
Симптом: posts_Add через sendRequest создал 3 пустых "Основы методологии Agile" + ошибка на 4-м.
Причина: posts_Add не принимает данные. + sendRequest колбэки виснут в MCP.
Решение: repurpose 3 broken через posts_Edit (заполнение полями) + 1 новый через add+edit. Через прямой fetch /submit/ urlencoded. Все 4 за 2 секунды.
Файл: webinar_assets/tilda_meropriyatiya_4new.json
2. На /events видно 3 поста из 42 активных (2026-04-25)
Симптом: в Feed 42 активных поста, на сайте только 3 верхних.
Причина: input1=3 в Feed-блоке 897 на странице /events.
Решение: один POST /page/submit/ saverecord input1=50 + один POST /page/publish/. 5 секунд.
3. CSV-импорт сломал 16 постов (предыдущая сессия)
Симптом: posts с пустыми title и кашей в date/mediadata после CSV import.
Причина: inline CSS с ; (text-align:center;margin:30px) в Description — Tilda CSV parser сплитнул по ;.
Решение: восстановление 16 постов через posts_Edit каждого uid вручную. Урок: всегда replace ; → , перед CSV.
4. БЛОГ — 127 постов с двойным экранированием entities (2026-04-25)
Симптом: На карточках БЛОГа на сайте видны literal &amp;mdash;, &amp;quot; вместо —, ". На API возврате descr содержит &amp;mdash;.
Причина: Цикл posts_Get → posts_Edit без декодирования накапливал уровни HTML-encoding. Каждый раунд: & → & при сохранении.
Решение: итеративный decode через textarea.innerHTML → .value до стабильности (≤5 проходов), потом resave с actual chars. Tilda на сохранение не encode actual «»—, а на API-возврат снова encode для transport — это норма.
const decode = (s) => { const ta = document.createElement('textarea'); ta.innerHTML = s; return ta.value; };
const decodeAll = (s) => {
let prev = s, curr = decode(s);
let g = 0;
while (prev !== curr && g++ < 5) { prev = curr; curr = decode(curr); }
return curr;
};
Truncation gotcha: Tilda обрезает descr на ~250 chars. Если обрезка пришлась на середину entity, остаётся фрагмент &am…. После decodeAll получаем &am… — невалидный entity, на странице видится как text. Дополнительный regex для хвоста:
const stripBrokenTail = (s) => s.replace(/\s*&[^;\s]*…?\s*$/, '…');
127/127 постов починены за один скан + 2 broken-tail прохода. Финальный скан: 0 с &amp;.
5. МЕДИА — даты карточек не совпадают с постерами (2026-04-25)
Симптом: На постере прямого эфира видна дата «9 февраля, 20:00 МСК», в Tilda карточка показывает 22.02.2024.
Причина: Поле date ставилось от балды/импорта, не из источника.
Решение: проверка дат по оригинальным источникам, batch posts_Edit с правильными date (формат YYYY-MM-DD HH:MM):
- Ведомости/Коммерсантъ/РБК статьи →
curl + grep '"datePublished":"[^"]*"'из source HTML - VK видео →
fetch('/al_video.php?act=show&al=1&module=videofeed&video=-{group_id}_{video_id}', {credentials:'include'})на vkvideo.ru, ищи"date":<unix_ts>в response - RuTube →
https://rutube.ru/api/video/{vid}/?format=json→created_ts - Постеры прямых эфиров — текст на изображении («9 февраля, 20:00 МСК») — самый надёжный источник для in-house контента
6. МЕДИА body posts_Edit обнулил parts/active (2026-04-25)
Симптом: posts_Edit с новым text прошёл (error:''), но карточка пропала из категории «Упоминания в СМИ» и стала неактивной.
Причина: не передал parts и active=y → Tilda обнулил оба.
Решение: перед каждым posts_Edit делать posts_Get, забирать p.parts, явно передавать. Потом отдельный posts_Active с active=y для надёжности (на случай если Tilda всё равно сбросил).
const p = (await sub('posts_Get', {postuid, feeduid, projectid})).data;
await sub('posts_Edit', {
postuid, feeduid, projectid,
title: p.title, descr: p.descr,
text: JSON.stringify([{ty:'html', co: newHtml}]),
mediatype: p.mediatype || 'image',
image: p.image, thumb: p.thumb || p.image, mediadata: p.mediadata || p.image,
parts: p.parts || '146561154411', // КРИТИЧНО — иначе категория сбросится
active: 'y',
date: p.date,
// прочие опциональные: imagealt, postalias, authorname, directlink
});
await sub('posts_Active', {postuid, feeduid, projectid, active:'y'}); // double-insure
7. Большое HTML-тело (5-7K chars) для МЕДИА-дайджеста (2026-04-25)
Кейс: надо вставить полноценную выжимку статьи как text поста. Inline в browser_evaluate — function body раздувается, могут быть проблемы с экранированием.
Решение: залить body.html на свой сервер (любой хост, отдающий статику с CORS), fetch из admin-сессии:
const html = await (await fetch('https://your-server.example.com/articles/body.html')).text();
await sub('posts_Edit', {postuid, ..., text: JSON.stringify([{ty:'html', co: html}])});
Nginx alias /portal/ уже отдаёт static с Access-Control-Allow-Origin: * — fetch из feeds.tilda.ru проходит. Это удобнее чем вкладывать длинные строки в evaluate-функцию.
8a. Миграция Next.js-сайта на Tilda через T123 shim + SVG payload
Кейс: перенос landing 17 секций (~116KB HTML) и blog SPA с 201 статьёй (134KB HTML) на Tilda без переписывания в нативные блоки.
Архитектура:
- Сборка финального HTML на стороне (Next.js export → одна single-file страница)
- Wrap в SVG:
<svg xmlns="http://www.w3.org/2000/svg"><text id="payload" style="display:none">BASE64_ENCODED_HTML</text></svg> - Upload SVG на Tilda CDN → получаешь
tild<hex>/file.svgURL (149KB landing, 146KB blog) - В T123 блоке
code— шим ~3.7KB который:fetch(PAYLOAD_URL)→ достаёт base64 из<text id="payload">atob()→TextDecoder('utf-8').decode()document.body.innerHTML = decoded(или injects в<div id="th-mount">)- Вытаскивает
<script>теги отдельно черезnew Function()— innerHTML не выполняет inline scripts
Bonus pattern (bootstrap): вместо хардкода __PAYLOAD_URL__ в T123 шиме — пусть T123 загружает <script src="https://your-server.example.com/tilda/blog-runner.js">, а blog-runner.js (на твоём nginx с no-cache header) знает payload URL. Тогда обновления = scp нового JS на свой сервер, без re-publish в Tilda.
T123 IDs того проекта (пример структуры):
- Landing: pageid
138379196, T123 recordid2220451661 - Blog: pageid
138390926(slug=blog), T123 recordid2220572931
Проблема (Phase I, 2026-05-01): saverecord для этих T123 тихо не пишет в БД (см. принцип #33). Live остался на старом payload, требуется ручной save через ACE editor.
Headers для CDN upload (иначе, чем для проектов с tilda.cc):
# Для проекта, созданного через tilda.ru (а не tilda.cc):
publickey = 'uiyejbdskjfiowe32'
# uploadkey — JWT, может expire, бери свежий из Tildaupload_UPLOADKEY на странице редактора
headers = {'Origin': 'https://tilda.ru', 'Referer': 'https://tilda.ru/'}
8b. feeds_Publish не работает (2026-04-25)
Симптом: posts_Edit + posts_Active прошли, но при попытке feeds_Publish возвращается {error:"Неизвестная ошибка при отправке запроса"}.
Причина: неизвестна, action либо не существует, либо требует другой endpoint.
Решение: не звать feeds_Publish вообще. Изменения автопубликуются в feed-блоках на сайте сразу как active=y. Verify через curl https://<твой-домен>/blog/tpost/{postuid}-... через 5-10 сек после Edit.
Альтернатива: Tilda публичный API (read-only)
Для бэкапа страниц или экспорта структуры:
GET https://api.tildacc.com/v1/getprojectslist/?publickey=&secretkey=
GET https://api.tildacc.com/v1/getpagefullexport/?publickey=&secretkey=&pageid=
Документация: https://help-ru.tilda.cc/api
Не позволяет редактировать — только читать. Для редактирования всегда используй внутренний API через сессию tilda.cc.