Стили
Два способа оформить сайт: именованные токены дизайна с дефолтами темы и один свободный стилевой файл, который перебивает всё. Где лежит каждый, в каком порядке их печатает тема и что на самом деле действует на страницах документации.
Под /site живут две разные вещи, и одна другую не заменяет.
Токены дизайна — закрытый набор именованных значений: цвета, радиус скругления, типографика восьми уровней, ширина колонки, отступы. У каждого токена есть дефолт, и берётся он из самой темы, а не из списка внутри сервиса. Меняете только то, что нужно.
Произвольный CSS — один свободный стилевой файл на сайт: пишете что угодно и перебиваете что угодно.
Порядок каскада
Порядок задан намеренно: произвольный CSS всегда побеждает токены, и никогда наоборот. Тема печатает в <head>:
Приоритет wp_head | Что | Откуда |
|---|---|---|
| 1 | дизайн и типографика, <style id="onpress-sidepanel-typography"> | опции onpress-sidepanel-design и onpress-sidepanel-typography |
| 2 | остальные токены, <style id="onpress-design-tokens"> | опция onpress_design_tokens |
| 999 | произвольный CSS сайта | опция onpress_custom_css |
Токены печатаются под селектором html:root: он специфичнее :root из _variables.css темы, поэтому токены перебивают дефолты независимо от порядка загрузки стилей. Произвольный CSS идёт последним и перебивает и то, и другое.
Ни токены, ни произвольный CSS во фреймы не запекаются: они приходят на страницу CSS-ом на каждом запросе, так что перепечка после изменения не нужна. С кэшем страниц другая история, о ней ниже.
Где лежат значения
Параллельного ничего не изобретали: тема и раньше печатала дизайн и типографику из двух опций, и ручка пишет в них же.
| Опция | Что хранит | Кто пишет |
|---|---|---|
onpress-sidepanel-design | theme, primaryColor, secondaryColor, bgColor, borderRadiusDefault | интерфейс боковой панели, v1 /design, эта ручка |
onpress-sidepanel-typography | стили по уровням: key, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, color | те же трое |
onpress_design_tokens | плоская карта «CSS-переменная — значение», готовая к печати | только эта ручка |
Третья опция нужна потому, что первую расширить нельзя: v1 PUT /design пересобирает её ровно из пяти ключей, и всё добавленное туда молча пропало бы при следующем сохранении из боковой панели. Ширины, отступы, дополнительные цвета и акцент документации живут в собственной строке, которой владеет v2.
Дефолты читаются из темы: сервис разбирает assets/css/parts/_variables.css темы сайта и берёт первое объявление каждой переменной — десктопный блок, потому что медиазапросы ниже по файлу переопределяют те же имена под телефон. defaults_source в ответе называет файл либо говорит builtin, если файл прочитать не удалось. Тема — опция сайта stylesheet; параметр theme читает дефолты другой темы.
Что где действует
Прочитайте это раньше, чем пообещаете, что токен что-то изменит.
На обычных страницах темы работают все токены: их как CSS-переменные потребляют стили темы.
На страницах документации типографические токены не действуют. Шаблон документации гасит систему typo-* ради референсного вида: его стили принудительно ставят font-family, font-size, font-weight, line-height, letter-spacing и color в inherit внутри тела документа, так что --h1-fz и его родня перебиваются при любом значении. Токены раскладки там тоже ничего не делают: стили документации не используют CSS-переменных и задают ширины сами.
Что на страницах документации работает — это акцент. Референсный бандл обращается к --primary, --primary-light и --primary-dark примерно в 370 местах и нигде их не определяет. color.primary заполняет все три (светлый и тёмный получаются подмешиванием 25% белого и 25% чёрного), и документация подхватывает фирменный цвет. color.accent, color.accent_light и color.accent_dark задают их явно.
Акцент хранится каналами, а не цветом:
22 110 63, а не#166e3f.
Бандл читает эти переменные только как rgb(var(--primary)). Hex превратится в rgb(#166e3f), это невалидно, браузер откатится на чёрный, и акцент погаснет сразу везде. Ручка принимает на входе hex, rgb() или готовую тройку, переводит перед сохранением и возвращает тройку. Повторно переводить на стороне темы не нужно, и писать эти три переменные в hex в произвольном CSS тоже не нужно.
Акцент следует за color.primary, пока он выведен из него: сменили основной цвет — три переменные пересчитались. Задали любую из них руками — следование прекращается. Сброс color.accent либо color.primary, пока акцент ещё выведен, убирает все три разом: они пишутся группой, а наполовину убранный акцент оставил бы прежние светлый и тёмный варианты без хозяина.
GET /site/design
Все токены с текущим значением, дефолтом и происхождением значения.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/site/design?blog_id=233"
{
"success": true,
"blog_id": 233,
"theme": "onpress-v2",
"defaults_source": "/var/www/…/wp-content/themes/onpress-v2/assets/css/parts/_variables.css",
"stores": {
"design": "onpress-sidepanel-design",
"typography": "onpress-sidepanel-typography",
"tokens": "onpress_design_tokens"
},
"total": 66,
"changed": [],
"css": "",
"tokens": {
"color.primary": {
"value": "#000000",
"default": "#000000",
"source": "default",
"css_var": "--primary-color",
"type": "color",
"description": "Primary color"
},
"layout.container_width": {
"value": "calc(910px + var(--padding, 2.5rem) * 2)",
"default": "calc(910px + var(--padding, 2.5rem) * 2)",
"source": "default",
"css_var": "--container-max-width",
"type": "length",
"description": "Content column width"
},
"color.accent": {
"value": "0 0 0",
"default": "0 0 0",
"source": "default",
"css_var": "--primary",
"type": "rgb-channels",
"description": "Accent of the documentation template: the --primary variable of the reference styles. Stored as a TRIPLE OF CHANNELS (\"22 110 63\"), because the bundle calls it as rgb(var(--primary)); hex is accepted on input as well. Defaults to the primary color"
},
"h1.size": { "value": 48, "default": 48, "source": "default", "css_var": "--h1-fz", "type": "px", "description": "h1: size" },
"…": "…"
}
}
source равен default, пока стоит значение темы, и site, как только у сайта появилось своё. changed перечисляет переопределённые токены, css — ровно то, что тема печатает из третьей опции.
Список токенов
| Токен | Тип | CSS-переменная |
|---|---|---|
color.primary | цвет | --primary-color; заодно заполняет акцент документации |
color.secondary | цвет | --secondary-color |
color.background | цвет | --bg-color |
color.background_alt | цвет | --bg-second-color |
color.text, color.text_muted | цвет | --font-main-color, --font-second-color |
color.link, color.line | цвет | --link-color, --line-color |
color.accent, color.accent_light, color.accent_dark | каналы rgb | --primary, --primary-light, --primary-dark |
radius.default | px | --border-radius-default |
skin | lined или pure | нет, это скин темы |
layout.container_width | длина | --container-max-width |
layout.padding, layout.padding_mobile | длина | --padding, --padding-container-mobile |
layout.header_height, layout.border_width | длина | --header-height, --border-width |
<уровень>.font | строка | --<уровень>-ff |
<уровень>.size | px, число: 48 — это 48px | --<уровень>-fz |
<уровень>.weight | целое от 1 до 1000 | --<уровень>-fw |
<уровень>.line_height | отношение от 0,5 до 4 | --<уровень>-lh |
<уровень>.letter_spacing | em, от −1 до 1 | --<уровень>-ls |
<уровень>.color | цвет | --<уровень>-color |
Уровней восемь: h1…h6, subtitle и paragraph, всего 66 токенов. Интерлиньяж и трекинг тема хранит процентами, а ручка принимает и отдаёт их в единицах CSS и переводит в обе стороны: 1.2, а не 120%, и -0.05, а не -5%.
PATCH /site/design
Меняет отдельные значения; всё нетронутое остаётся на дефолтах темы.
| Поле | Тип | Что делает |
|---|---|---|
tokens | объект | {"color.primary": "#166e3f", "h1.size": 44} |
dry_run | bool | проверить и доложить, ничего не записывая; отчёт приходит в том же поле changed, что и у боевого вызова |
theme | строка | тема, чьи дефолты используются; по умолчанию stylesheet сайта |
Цвет — это #rrggbb, rgb() или rgba() либо имя цвета; длина — с единицей или calc(); у размеров, насыщенности, интерлиньяжа и трекинга есть диапазоны. Негодное значение — 400 bad_token_value, неизвестное имя — 400 unknown_token, и в обоих случаях не пишется ничего.
curl -s -X PATCH -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/site/design" -d '{
"blog_id": 235,
"dry_run": true,
"tokens": { "color.primary": "#166e3f", "h1.size": 44 }
}'
{
"success": true,
"dry_run": true,
"applied": false,
"blog_id": 235,
"changed": [
{ "token": "color.primary", "value": "#166e3f", "css_var": "--primary-color", "store": "onpress-sidepanel-design" },
{ "token": "h1.size", "value": 44, "css_var": "--h1-fz", "store": "onpress-sidepanel-typography" },
{ "token": "color.accent", "value": "22 110 63", "css_var": "--primary", "store": "onpress_design_tokens", "derived": true }
],
"css": "html:root{--primary:22 110 63;--primary-light:80 146 111;--primary-dark:17 83 47;}"
}
store говорит, в какую из трёх опций уходит значение; derived: true помечает то, что ручка досчитала сама, — здесь акцент из основного цвета. Боевой вызов возвращает то же тело с "applied": true и без dry_run.
{
"success": false,
"error": "bad_token_value",
"message": "\"h1.size\": a number is expected, got \"big\"",
"token": "h1.size"
}
{
"success": false,
"error": "unknown_token",
"message": "there is no token \"color.primaryy\"; look at GET /site/design — it lists every name with its default",
"token": "color.primaryy"
}
Одно свойство закрепляет весь уровень типографики. Тема хранит типографику уровнями целиком, поэтому один h1.size записывает остальные свойства h1 текущими дефолтами, и весь уровень начинает отчитываться как site. Сброс свойства освобождает его, а уровень, у которого не осталось ничего своего, удаляется целиком.
Запись токенов не сбрасывает кэш страниц. Страницы, уже лежащие в кэше, держат старые переменные, пока не устареют; сбросьте их через POST /site/cache/flush с {"supercache": true}, см. Сайты и настройки.
DELETE /site/design
Возвращает значения к дефолтам темы. С {"tokens": ["color.primary", "h1.size"]} освобождаются только названные токены; ответ — reset со списком и новый css. С пустым телом удаляются все три опции, ответ — "reset": "all" с removed_options, и сайт полностью возвращается к теме.
curl -s -X DELETE -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/site/design" -d '{"blog_id": 233, "tokens": ["color.accent"]}'
Как тема печатает токены
Первые две опции тема печатает на wp_head с приоритетом 1. Третья — один небольшой блок с приоритетом 2, раньше произвольного CSS:
$tokens = json_decode((string) get_option('onpress_design_tokens', '{}'), true);
if (is_array($tokens) && $tokens) {
$css = '';
foreach ($tokens as $var => $value) {
$css .= $var . ':' . $value . ';';
}
echo '<style id="onpress-design-tokens">html:root{' . $css . '}</style>' . "\n";
}
Карта хранится готовой к печати: имена — уже имена CSS, значения — уже значения CSS, на стороне темы ничего не переводится.
Произвольный CSS
Стилевой файл сайта — опция этого одного сайта сети, а не сетевая (стенд делит базу с продом, и сетевая опция ушла бы на все сайты разом). Две строки: onpress_custom_css с голым CSS и onpress_custom_css_meta с записью о последнем сохранении — когда, кем, сколько байт, sha256, что вырезано. Тема печатает его обычным get_option() внутри <style> последним элементом <head>.
GET /site/css
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/site/css?blog_id=233"
{
"success": true,
"blog_id": 233,
"home_url": "https://example.onpress.pro",
"option": "onpress_custom_css",
"exists": true,
"bytes": 109790,
"sha256": "35e2fde4b168baa136a2eb12a9bb8b7428a745e2c5d7fb51e55de775bd564e28",
"autoload": "no",
"meta": {
"updated_at_gmt": "2026-09-13 12:05:43",
"updated_by": 1,
"bytes": 109790,
"sha256": "35e2fde4b168baa136a2eb12a9bb8b7428a745e2c5d7fb51e55de775bd564e28",
"source": "node",
"stripped": []
},
"css": "…"
}
Стили этого сайта весят около 107 КБ, это выше порога автозагрузки, отсюда "autoload": "no".
PUT /site/css
Заменяет CSS сайта целиком. POST на тот же путь делает то же самое — для клиентов, которым неудобен PUT.
| Поле | Тип | По умолчанию | Что делает |
|---|---|---|---|
css | строка | — | весь стилевой файл, обязательно; пустая строка допустима и означает «ничего» |
dry_run | bool | false | только проверить: ответ показывает очищенный CSS и вырезанное, база не тронута |
flush_cache | bool | true | сбросить кэш страниц сайта после записи |
blog_id / domain | int / строка | — | сайт сети |
Замена целиком — это буквально замена. Ручки «добавить правило» нет: прочитайте текущий CSS, измените, отправьте обратно целиком. Иначе затрёте чужую работу, а на сайте, где параллельно работают несколько агентов, это происходит мгновенно.
Что вырезается и почему
Контракт узкий и назван прямо: сохранённая строка безопасна внутри <style>…</style>. В этом элементе браузер не разбирает ни теги, ни сущности, и прекращает его только </style. Поэтому сервис вырезает то, что может вырваться из стилевого контекста или исполниться, а не всё, что выглядит тревожно:
| Правило | Что вырезается |
|---|---|
style_close_tag | </style> в любом регистре и с любыми пробелами — единственный выход из элемента |
script_tag | теги <script> и </script>; текст между ними остаётся |
js_url | адреса javascript: и vbscript: |
ie_expression | expression( из старого IE |
control_chars | управляющие символы |
@import не вырезается — это валидный CSS, им грузят шрифты, — но получает предупреждение: лишний запрос до первой отрисовки, а внешний адрес может заблокировать политика безопасности. Незакрытый комментарий закрывается автоматически в самом конце файла, иначе он съел бы все правила после себя; правила после его начала так и остаются закомментированными, и предупреждение об этом говорит. Молча ничего не пропадает: stripped перечисляет каждое правило с числом срабатываний и образцами, warnings — словесные замечания.
curl -s -X PUT -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/site/css" -d '{
"blog_id": 233,
"dry_run": true,
"css": ".x{color:red}</style><script>alert(1)</script>\n.y{background:url(javascript:alert(1))}\n@import url(https://fonts.googleapis.com/css2?family=Inter);"
}'
{
"success": true,
"blog_id": 233,
"option": "onpress_custom_css",
"bytes": 111,
"max_bytes": 262144,
"sha256": "130bb118cda8ad37c36c3424318797ffd9b4b3183ca3c2b30d3287ec729c69d0",
"autoload": "yes",
"stripped": [
{ "rule": "style_close_tag", "count": 1, "reason": "закрывающий тег </style> прекращает элемент стилей и пускает остаток как разметку", "samples": ["</style>"] },
{ "rule": "script_tag", "count": 2, "reason": "тег <script> в стилях не нужен; внутри <style> он мёртв, но в любом другом контексте — нет", "samples": ["<script>", "</script>"] },
{ "rule": "js_url", "count": 1, "reason": "javascript:/vbscript: в url() — исполняемый адрес, а не стиль", "samples": ["javascript:"] }
],
"warnings": [
"@import встречается 1 раз(а): такие файлы грузятся отдельным запросом и тормозят первую отрисовку, а внешний адрес может быть заблокирован политикой безопасности"
],
"dry_run": true,
"applied": false,
"css": ".x{color:red}alert(1)\n.y{background:url(alert(1))}\n@import url(https://fonts.googleapis.com/css2?family=Inter);"
}
Боевая запись возвращает "applied": true, updated_at_gmt и cache вместо очищенного css.
Пределы
Предел — 256 КБ (CUSTOM_CSS_MAX_BYTES), больше — 413 css_too_large с фактическим размером в bytes и пределом в max_bytes. Стили до 64 КБ (CUSTOM_CSS_AUTOLOAD_BYTES) пишутся с autoload = yes и приезжают вместе с остальными автозагружаемыми опциями; большие — с autoload = no, потому что автозагружаемая опция такого размера раздувает кэш опций на каждом запросе. get_option() всё равно их вернёт, одним лишним запросом, и для темы ничего не меняется.
Кэш страниц
После записи и после очистки сервис удаляет каталог WP Super Cache этого сайта — иначе кэш продолжал бы отдавать страницы со старым встроенным стилем. Итог приходит в поле cache со статусом flushed, empty, skipped или failed. Проваленный сброс не отменяет сохранённый CSS, он просто назван в ответе. skipped значит, что сервис не знает, где лежит wp-content (WP_CONTENT_DIR в его .env). Отключить сброс — "flush_cache": false.
DELETE /site/css
Удаляет обе опции, и сайт возвращается к стилям темы. cleared говорит, было ли что удалять; cache — итог сброса кэша.
curl -s -X DELETE -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/site/css" -d '{"blog_id": 233}'
Коды ошибок
| Код | HTTP | Когда |
|---|---|---|
unknown_token | 400 | такого токена нет; полный список — в GET /site/design |
bad_token_value | 400 | значение не подходит под тип или диапазон токена |
nothing_to_update | 400 | PATCH /site/design без объекта tokens |
css_required | 400 | PUT /site/css без строки css; очистить — DELETE |
css_too_large | 413 | CSS больше 256 КБ |
Своих шрифтов через API не добавить: реестр доступных семейств зашит в PHP темы. Переменные темы, не попавшие в список токенов, правятся файлом темы либо перебиваются произвольным CSS — обычно это и есть правильный ответ.