Vizen Shop

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 pathCost
the page of a rubric listing its materialsnothing 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 pageone block; data comes with the page
the body of a materialits 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, authordrawn by the articleHeader widget of the article template; do not add a heading or a cover image to the bodynone — but a second heading in the body is a second <h1> (§5)
another design for one rubric or one materialbindings: PUT /design/bindings/category/{id} or /article/{id} with theme / body_news / body_articlechrome.md §3.6the 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 todayto 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 root

A 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 /categories answers with is_published: false. Until PUT /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_articles is read-only and counts the rubric's own materials.
  • sidebar_config is 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

FieldWriteNotes
category_idrequireda live news rubric of your shop; anything else answers 404 (the platform does not reveal whether the id exists)
titlerequired, min 1the <h1> drawn by articleHeader
announceoptionalthe short summary: card text in feeds, description in the header, description in the JSON-LD
cover_idoptionaluuid of an uploaded file (/v1/storage, see catalogue.md §6); resolved back as cover {url}
badgeoptional, ≤ 48a short label drawn over the cover in feeds; "" removes it
is_publishedboolanonymous readers and the storefront see published materials only
published_attimestampnot stamped by publishing — set it yourself (§5)
slugoptionaltop-level field; falls back to seo.slug; empty → generated from the title; SLUG_TAKEN (409) if the shop already has it
seoobjectpage_title, meta_title, meta_description, meta_keywords[], og_image_id, noindex, nofollow
gallery_ids≤ 15galleries linked to the material (catalogue.md §7); on update add gallery_ids_replace: true to replace the set
authorobject`{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=true aggregates the rubric and its descendants; it needs category_id (§5).
  • sort is recent (default, published_at DESC NULLS LAST, id DESC) or popular (views DESC); anything else is 400 INVALID_SORT.
  • query (≤ 100 runes) searches the title and the summary: substring, case-insensitive including Cyrillic, ranked by similarity (pg_trgm). Longer → 400.
  • page.limit is 1..100 (default 20); 101 → 400. total counts the whole filter, current_page echoes 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:

BlockSectionWhat it is
__page:toptype:"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 &lt;b&gt;)

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):

Groupgroup_typeWidgetsDraws
«Лента новостей»newsbreadcrumbs · pageHeading · articleGrid {view: "tiles", columns: 3, coverAspect: "3/4", limit: 20}the page of every rubric
«Тело статьи»articlegrid: breadcrumbs and articleHeader in main, articleListing {source: "news", limit: 5} in aside; the material's own blocks flow into mainthe 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. view tiles · masonry · rows; columns 1..6 (+ columnsTablet, columnsMobile); coverAspect 3/4 · 4/3 · 3/2 · 16/9 · 1/1 (default 3/2, rows 4/3); limit 1..48 (20); paging none · pages · more · auto (default auto: 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); categoryId 0 = the page's rubric with descendants; rubricsVisible, searchVisible (the field searches inside the page's rubric and submits ?q=); hiddenParts cover · badge · rubric · date · author · announce · readMore; authorPosition inside · below; datePosition meta · title · author; rowCoverWidth 160..640, rowMaxWidth (rows only); readMoreStyle (a button style); surfaces cardStyle, coverStyle, textStyle, rubricsStyle, searchStyle, badgeStyle; text styles headlineStyle, 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 with aria-current.
  • `articleHeader`props.articleHeader. The head of a material from the page context: title (titleTag, default h1), announce as the description, rubric, dates, author (photo authorPhotoSize 24..96), the cover (coverSource article · custom, coverPosition before · after, coverAspect, coverFit) and optional static labels tags[] {key, text, target?}. Missing author or cover render nothing.
  • `articleListing`props.articles. A short selection: source news (automatic: the page's top rubric or categoryId, limit 1..24, default 3) or manual (section.items, hand-written cards); layout feature · feature-list · compact · numbered.
  • `articleRubrics`props.articleRubrics. A rubric switcher (layout list · 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

WrittenWhat 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_idignored; total is the same with and without it (110 = 110)
is_published: true without published_atpublished, 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}/relatedstored 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 enums200, 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: 9200; 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 materialdrawn in addition to the header widget's cover and <h1> — two headings on the page
tags in the seeded text blockprinted literally (&lt;b&gt;), not rendered
company_id of another shop with an owner tokennot an error: the call switches to visitor mode (published only)

Loud, for contrast: sort outside recent|popular400 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

WhatLimit
titlemin 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.limit1..100, default 20
articleGrid.limit1..48, default 20
articleGrid.columns1..6 (each breakpoint)
articleGrid.rowCoverWidth / rowMaxWidth160..640 / 0..3000 px
articleListing.limit1..24, default 3
articleHeader.authorPhotoSize24..96 px
auto-paging3 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; after PUT {is_published: true} both pages 200; the material page holds data-vz-kind="articleHeader" × 1 and <h1> = the title; the rubric page holds .vz-article-grid with the card.
  • POST /articlescontent_blocks seeded (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:top and text only).
  • GET /articles?company_id=181&category_id=1186 anonymous → total: 1, badge: "New", category_name filled; published_at: null sorted last (page 2, position 11 of 11 in a 111-item feed).
  • GET /articles/by-slug/…?company_id=181 → keys result, content_blocks, related, modal_groups, design; design.page.body_id = the article group.
  • page.limit 100 → 200, 101 / 200 / 1000 → 400; sort=bogus → 400; query of 101 runes → 400 with the validation text; include_subtree without a rubric → same total.
  • PUT /content-blocks/{text} with <b>tags</b> → page prints &lt;b&gt;.
  • 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/validate with articleGrid {limit: 999, view: "bogus", nonsense: 1} and props.limitnonsense and props.limit reported ignored; view was applied before 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 html block plus the zone manifest — no seeded cover blocks, so no data migration ships with the fix.

Исходник: https://api.vizen.shop/docs/news

Работает в демо-режиме