Editorial content: rubrics, materials and their feeds
/docs/newscurrentEN· проверено 2026-09-24
Резюме по-русски. Область редакционного контента магазина: рубрики (категории сtype: "news"), датированные материалы внутри них (/articles) и виджеты, которые их показывают. Слово «материал» здесь намеренно общее — платформе всё равно, называет продавец это новостями, блогом, кейсами или справочником: это страница с заголовком, датой, обложкой и телом из блоков. Страница рубрики и страница материала собираются СТРОГО из виджетов двух шаблонов, которые сеются каждому магазину («Лента новостей», «Тело статьи»); код витрины ничего не дорисовывает. Материал создаётся только в рубрику и приходит с одним пустым текстовым блоком — областью для содержимого; любые другие блоки добавляются как у любой страницы. Три ловушки, из-за которых написан документ: рубрика создаётся НЕОПУБЛИКОВАННОЙ (витрина отвечает 404), публикация материала НЕ проставляет дату (безpublished_atон уезжает в конец ленты), а обложку и заголовок рисует виджет шаблона — заголовок в теле даёт второй H1. Старая боковая колонка (sidebar_config) снесена 2026-09-24: поле принимается и ничего не делает.
Status: current · Verified: 2026-09-24, live calls on the stand (shop 181, owner token + anonymous) and curl of the storefront pages; data shapes read on a local copy of the production database · Owner: news-standard line Serves: GET /docs/news Related: catalogue.md §8.1 (the author of a material), chrome.md §3.6 (a layout per rubric or per material), widgets.md (how a widget block is written), own-markup.md (your own HTML inside a material)
1. What you can do here
Build the editorial part of a shop: a tree of rubrics, dated materials inside them, and feeds of those materials — on the rubric page itself and on any other page (home, a product page, a landing). The platform draws the rubric page and the material page from two templates it seeds into every shop; you configure their widgets, add blocks to a material's body, and bind other designs to rubrics or single materials the same way as for products.
A material is a page with a title, a short summary, a cover, an author, dates, a badge, galleries, SEO fields and a body made of ordinary content blocks. What the seller calls it is not encoded anywhere: the same objects serve a company blog, a news column, case studies or a help section.
2. Decide first (forks)
| If you want… | Take this path | Cost |
|---|---|---|
| the page of a rubric listing its materials | nothing to build — the shop's news template (breadcrumbs · heading · articleGrid) draws it; configure the feed widget (§3.5) | props only; the look is bounded by the widget |
| a feed of materials on another page (home, a product page) | articleListing (props.articles, compact, 1..24 items) for a short selection, or articleGrid (props.articleGrid) for the full feed with rubric switcher, search and paging — as a block of that page | one block; data comes with the page |
| the body of a material | its own content blocks: the seeded text block for plain text, an html block for your own markup (own-markup.md), any widget (widgets.md) | the same blocks as any page; attach them with PUT /articles/{id}/content-blocks (§3.3) |
| the head of a material — cover, title, summary, rubric, dates, author | drawn by the articleHeader widget of the article template; do not add a heading or a cover image to the body | none — but a second heading in the body is a second <h1> (§5) |
| another design for one rubric or one material | bindings: PUT /design/bindings/category/{id} or /article/{id} with theme / body_news / body_article — chrome.md §3.6 | the same ladder as products; article is a design resource |
| materials linked to one another ("read also") | PUT /articles/{id}/related stores the list and GET /articles/by-slug/{slug} returns it as related[] — no storefront widget draws it today | to show it, print related[] from your own markup on the material page |
3. Objects and where they live
3.1. Rubric = a category with type: "news"
POST /categories
{ "item": { "name": "Guides", "type": "news", "parent_id": 0 } } // parent_id: another rubric's id, or 0 for the rootA rubric is an ordinary category (catalogue.md) whose type is news: it holds materials instead of products, and sub-rubrics are news categories under it. Everything else — seo.slug, seo.page_title, description, is_show_menu, galleries, design bindings — works as for any category.
- Created unpublished.
POST /categoriesanswers withis_published: false. UntilPUT /categories/{id} {"item": {"is_published": true}}the storefront answers 404 for the rubric and 308 for its materials (the address collapses to the root-level slug). Measured 2026-09-24. count_articlesis read-only and counts the rubric's own materials.sidebar_configis deprecated: accepted, validated and never read (§5).
Storefront address: the tree path of the category, /<parent-slug>/<rubric-slug>; the feed reads ?page=N and the search field submits ?q=<text>.
3.2. Material = /articles
| Field | Write | Notes | ||
|---|---|---|---|---|
category_id | required | a live news rubric of your shop; anything else answers 404 (the platform does not reveal whether the id exists) | ||
title | required, min 1 | the <h1> drawn by articleHeader | ||
announce | optional | the short summary: card text in feeds, description in the header, description in the JSON-LD | ||
cover_id | optional | uuid of an uploaded file (/v1/storage, see catalogue.md §6); resolved back as cover {url} | ||
badge | optional, ≤ 48 | a short label drawn over the cover in feeds; "" removes it | ||
is_published | bool | anonymous readers and the storefront see published materials only | ||
published_at | timestamp | not stamped by publishing — set it yourself (§5) | ||
slug | optional | top-level field; falls back to seo.slug; empty → generated from the title; SLUG_TAKEN (409) if the shop already has it | ||
seo | object | page_title, meta_title, meta_description, meta_keywords[], og_image_id, noindex, nofollow | ||
gallery_ids | ≤ 15 | galleries linked to the material (catalogue.md §7); on update add gallery_ids_replace: true to replace the set | ||
author | object | `{mode: "none"\ | "member"\ | "manual", user_id, name ≤ 255, description ≤ 500, photo_id} — catalogue.md` §8.1 |
Read-only in the answer: views (public reads of a published material on the production contour increment it; sort=popular orders by it), category_name, created_by / updated_by (owner only), dev_dirty.
GET /articles?company_id=181&category_id=1173&include_subtree=true&sort=recent&query=sofa&page.number=1&page.limit=20
GET /articles/by-slug/{slug}?company_id=181 # public: result + content_blocks + related + modal_groups + design
GET /articles/{id} # owner token: result + content_blocks + related; no design
POST /articles { "item": { … } }
PUT /articles/{id} { "item": { … } } # partial: only the fields you send change
DELETE /articles/{id} # soft, 30-day window; PUT {"item":{"is_deleted":false}} restores- `company_id` is required for an anonymous call — without it the answer is
403 COMPANY_ID_PROBLEM. With an owner token it defaults to the token's shop; naming another shop switches the call to visitor mode (published only). include_subtree=trueaggregates the rubric and its descendants; it needscategory_id(§5).sortisrecent(default,published_at DESC NULLS LAST, id DESC) orpopular(views DESC); anything else is400 INVALID_SORT.query(≤ 100 runes) searches the title and the summary: substring, case-insensitive including Cyrillic, ranked by similarity (pg_trgm). Longer → 400.page.limitis 1..100 (default 20); 101 → 400.totalcounts the whole filter,current_pageechoes the page.
Storefront address of a material: /<rubric tree path>/<slug>. The flat /article/<slug> and the numeric /<id> forms answer 308 to that path.
3.3. The body: the material's own blocks
A new material comes with two blocks already attached:
| Block | Section | What it is |
|---|---|---|
__page:top | type:"text", payload {v:2, kind:"zone", refs:[…]} | the technical manifest of the page's top zone: refs is the order in which the own blocks are drawn. Keep it, keep it first, keep refs in sync with what you attach |
Текст статьи | type:"text", payload {v:2, origin:"builder", text:""} | the seeded area for the material's text: plain text, line breaks kept, tags printed literally (<b> shows as <b>) |
Until 2026-09-24 a third block, a cover section with the title, was seeded too; together with articleHeader it produced two <h1> on every new material, so it is no longer created. Materials that still carry one show it as a dark banner with the title above the header widget — detach it (below).
// 1. write the text
PUT /content-blocks/2533
{ "id": 2533, "item": { "sections": [
{ "type": "text", "payload": { "v": 2, "origin": "builder", "text": "First paragraph.\nSecond paragraph." } } ] } }
// 2. add any other block (own markup, a widget) — widgets.md §5, own-markup.md
POST /content-blocks { "item": { "name": "Gallery of the project", "sections": [ … ] } } // → id 2600
// 3. attach the set (replace-set, ≤ 50 items, blocks must be yours) and update the manifest
PUT /articles/132/content-blocks
{ "items": [ { "block_id": 2534, "sort_order": 1 }, { "block_id": 2533, "sort_order": 2 }, { "block_id": 2600, "sort_order": 3 } ] }
PUT /content-blocks/2534
{ "id": 2534, "item": { "sections": [ { "type": "text", "payload": { "v": 2, "kind": "zone", "refs": [2533, 2600] } } ] } }sections is a replace-set (send the whole list), items is a replace-set too. A block that is not yours answers 404 BLOCK_NOT_FOUND; the same block twice answers 400 DUPLICATE_BLOCK_IN_SET. A block you detach is not deleted — DELETE /content-blocks/{id} soft-deletes it when you no longer need it.
3.4. The two templates: the pages are widgets, nothing else
Every shop is seeded with two body templates (groups, chrome.md §3.1):
| Group | group_type | Widgets | Draws |
|---|---|---|---|
| «Лента новостей» | news | breadcrumbs · pageHeading · articleGrid {view: "tiles", columns: 3, coverAspect: "3/4", limit: 20} | the page of every rubric |
| «Тело статьи» | article | grid: breadcrumbs and articleHeader in main, articleListing {source: "news", limit: 5} in aside; the material's own blocks flow into main | the page of every material |
The storefront draws nothing beyond these: no breadcrumbs, heading, cover or side column from code. To change the look of all rubrics, edit the widgets of the news group (GET /content-blocks → the block with group_type: "news" → its props.widgets are the ids → PUT the widget block). To give one rubric or one material another body, bind body_news / body_article on a layout or theme on the resource — chrome.md §3.6. JSON-LD (NewsArticle, BreadcrumbList, ItemList of the feed page) is printed by the page from the same data and needs no widget.
3.5. The widgets
The generated reference is GET /docs/widgets?kind=<kind>; the write path of each widget is nested (widgets.md §5).
- `articleGrid` —
props.articleGrid. The feed of a rubric.viewtiles · masonry · rows;columns1..6 (+columnsTablet,columnsMobile);coverAspect3/4 · 4/3 · 3/2 · 16/9 · 1/1 (default 3/2, rows 4/3);limit1..48 (20);pagingnone · pages · more · auto (defaultauto: the next pages load by themselves near the end, three in a row, then a button; the page links stay in the markup for crawlers);categoryId0 = the page's rubric with descendants;rubricsVisible,searchVisible(the field searches inside the page's rubric and submits?q=);hiddenPartscover · badge · rubric · date · author · announce · readMore;authorPositioninside · below;datePositionmeta · title · author;rowCoverWidth160..640,rowMaxWidth(rows only);readMoreStyle(a button style); surfacescardStyle,coverStyle,textStyle,rubricsStyle,searchStyle,badgeStyle; text stylesheadlineStyle,rubricStyle,datesStyle,announceStyle,authorStyle. The badge is drawn over the cover: a material without a cover shows no badge. Markup markers:.vz-article-grid[data-view=…],.vz-article-grid__item, the rubric switcher marks the current rubric witharia-current. - `articleHeader` —
props.articleHeader. The head of a material from the page context: title (titleTag, defaulth1),announceas the description, rubric, dates, author (photoauthorPhotoSize24..96), the cover (coverSourcearticle · custom,coverPositionbefore · after,coverAspect,coverFit) and optional static labelstags[] {key, text, target?}. Missing author or cover render nothing. - `articleListing` —
props.articles. A short selection:sourcenews (automatic: the page's top rubric orcategoryId,limit1..24, default 3) or manual (section.items, hand-written cards);layoutfeature · feature-list · compact · numbered. - `articleRubrics` —
props.articleRubrics. A rubric switcher (layoutlist · chips) for pages that need it outside the feed. - `breadcrumbs`, `pageHeading` — no props; fed by the page.
4. Recipes
4.1. A rubric, a first material, published and visible
API=https://api.vizen.shop; H="Authorization: Bearer $PAT"; J="Content-Type: application/json"
curl -s -X POST $API/categories -H "$H" -H "$J" -d '{"item":{"name":"Guides","type":"news","parent_id":0}}'
# → result.id = 1186, result.is_published = false
curl -s -X PUT $API/categories/1186 -H "$H" -H "$J" -d '{"item":{"is_published":true}}'
curl -s -X POST $API/articles -H "$H" -H "$J" -d '{"item":{"category_id":1186,"title":"How the set is assembled","announce":"Six steps, two tools.","is_published":true,"published_at":"2026-09-24T09:00:00Z","badge":"New"}}'
# → result.id = 132, result.slug = "how-the-set-is-assembled"Verify: GET /articles?company_id=<shop>&category_id=1186 without a token answers total: 1; GET /articles/by-slug/how-the-set-is-assembled?company_id=<shop> answers with design.page.body_id (the article template) and content_blocks (2 blocks); curl <storefront>/<rubric-path>/how-the-set-is-assembled answers 200 with one data-vz-kind="articleHeader" and one <h1> holding the title; curl <storefront>/<rubric-path> shows the card inside .vz-article-grid.
4.2. Text and other blocks in the body
Read GET /articles/{id} → content_blocks; find the block named Текст статьи and PUT /content-blocks/{id} its section with your text (§3.3). Verify by re-reading the block and by curl of the page: the text is printed inside data-vz-block="{id}". For formatted content create an html block instead (own-markup.md), attach it with PUT /articles/{id}/content-blocks and add its id to the manifest's refs.
4.3. Another look for every rubric feed
curl -s "$API/content-blocks?page.limit=100" -H "$H" | jq '.result[] | select(.group_type=="news") | {id, name, widgets: .sections[0].payload.props.widgets}'
# → the third id is the feed widget; read it, change one leaf, send the whole sections list back
curl -s -X PUT $API/content-blocks/<feedId> -H "$H" -H "$J" -d '{"id":<feedId>,"item":{"sections":[{"type":"text","payload":{"v":2,"kind":"articleGrid","props":{"articleGrid":{"view":"rows","limit":20,"paging":"more"}}}}]}}'Verify: re-read the block (props.articleGrid.view == "rows"), then curl the rubric page and find data-view="rows" on .vz-article-grid and the "show more" button after the cards. Run POST /docs/validate with the same sections first: a value outside an enum comes back as unknown with the allowed list.
4.4. Search and paging from outside
The feed page answers the same addresses a visitor uses: ?q=sofa searches inside the rubric and its descendants, ?page=2 is the second page (in pages mode as links; in more/auto the links are still in the markup for crawlers). The API behind both is §3.2: query, page.number, page.limit.
5. Silently ignored
| Written | What happens |
|---|---|
sidebar_config on a rubric (PUT /categories/{id}) | 200, validated as a JSON array of {type} (≤ 20), never read: the side column of a material is the aside of the article template. Measured 2026-09-24: PUT → 200, page unchanged |
include_subtree=true without category_id | ignored; total is the same with and without it (110 = 110) |
is_published: true without published_at | published, but the date stays empty: no date on the card or in the header, and the material sorts last in sort=recent (NULLS LAST; measured: position 111 of 111). Send published_at |
PUT /articles/{id}/related | stored and returned as related[]; no widget on the storefront draws it — the page does not mention the linked titles (grep: 0) |
props.articleGrid.view, paging, coverAspect, datePosition, authorPosition outside their enums | 200, the default is drawn. /docs/validate names the allowed values since 2026-09-24 (before that date it reported such a value as applied) |
props.articleGrid.limit: 999, columns: 9 | 200; clamped to 48 and 6 |
a feed prop at the root of props (props.limit) | 200, ignored — the widget reads props.articleGrid.limit; /docs/validate reports it |
| a cover image or a heading in the body of a material | drawn in addition to the header widget's cover and <h1> — two headings on the page |
| tags in the seeded text block | printed literally (<b>), not rendered |
company_id of another shop with an owner token | not an error: the call switches to visitor mode (published only) |
Loud, for contrast: sort outside recent|popular → 400 INVALID_SORT; page.limit > 100 → 400; query > 100 runes → 400; a category_id that is not a news rubric of your shop → 404; the same block twice → 400 DUPLICATE_BLOCK_IN_SET; a material related to itself → 400 SELF_RELATED.
6. Limits
| What | Limit |
|---|---|
title | min 1 character |
badge | ≤ 48 characters; "" removes it |
author.name / author.description | ≤ 255 / ≤ 500 |
gallery_ids | ≤ 15 per material |
related items | ≤ 50 per material |
own blocks attached (content-blocks items) | ≤ 50 per material |
query | ≤ 100 runes |
page.limit | 1..100, default 20 |
articleGrid.limit | 1..48, default 20 |
articleGrid.columns | 1..6 (each breakpoint) |
articleGrid.rowCoverWidth / rowMaxWidth | 160..640 / 0..3000 px |
articleListing.limit | 1..24, default 3 |
articleHeader.authorPhotoSize | 24..96 px |
| auto-paging | 3 pages in a row, then a button |
| soft-delete window (material, block) | 30 days |
7. How this was verified
2026-09-24, stand (core-api from commit 7f8e4220 + this line, storefront dfd4d6f2 + this line), shop 181, owner token test@test.com and anonymous curl:
POST /categories {type: "news"}→is_published: false; storefront 404 for the rubric, 308 →/<slug>for its material; afterPUT {is_published: true}both pages 200; the material page holdsdata-vz-kind="articleHeader"× 1 and<h1>= the title; the rubric page holds.vz-article-gridwith the card.POST /articles→content_blocksseeded (before the fix:__page:top, cover, text — the page showed<h1>× 2; an older material with a cover image showed the same; after the fix:__page:topand text only).GET /articles?company_id=181&category_id=1186anonymous →total: 1,badge: "New",category_namefilled;published_at: nullsorted last (page 2, position 11 of 11 in a 111-item feed).GET /articles/by-slug/…?company_id=181→ keysresult, content_blocks, related, modal_groups, design;design.page.body_id= thearticlegroup.page.limit100 → 200, 101 / 200 / 1000 → 400;sort=bogus→ 400;queryof 101 runes → 400 with the validation text;include_subtreewithout a rubric → sametotal.PUT /content-blocks/{text}with<b>tags</b>→ page prints<b>.PUT /articles/132/related [117]→related[]returned; storefront page does not contain the linked title.PUT /categories/1186 {sidebar_config}→ 200, page unchanged.POST /docs/validatewitharticleGrid {limit: 999, view: "bogus", nonsense: 1}andprops.limit→nonsenseandprops.limitreportedignored;viewwasappliedbefore the enum lists were added to the widget the same day.- Production data shape: a local copy of the production database (2026-09-24) holds 29 materials of one shop, all built from an
htmlblock plus the zone manifest — no seeded cover blocks, so no data migration ships with the fix.
Исходник: https://api.vizen.shop/docs/news