FIELD KINDS (user-authorable): text, editor, number, boolean, array, object, enum, json, datetime, image, gallery, file, icon, reference, link, form, element.
- text: short/simple text (plain or HTML) — titles, names, summaries, captions. Add {editor:"html"} for a small rich fragment, {editor:"raw"} to pin plain, {max_length:N} for a cap; unset = the content editor picks. NOT for the main body of an article/page — that is kind
editor. VALUE SHAPE: a text value is ALWAYS a plain string — the text itself, or the HTML fragment itself on a field declared editor:"html"; NEVER a {type:"html"|"raw", value} wrapper (that was the pre-2026-10-22 shape; the write tools flatten one you send). ONE LINE IS BARE: a single line of rich text is stored without awrapper ("Studená polévka") — templates drop such fields into headings and inline slots (
<h2>{{{ title }}}</h2>), and a<p>there would nest a block inside a heading; write…
(or, - /
- ,
) only when the value has two or more blocks ("
First.
Second.
"). The console's editor stores the same way, so a value you write round-trips unchanged. The schema'seditoris the only place the mode lives. A string carrying an HTML tag renders as markup (under {{ }} and {{{ }}} alike) and opens as rich text in click-to-edit, so a plain-text field must NOT contain tags — put rich text on an editor:"html" field. Templates bind editor:"html" fields with triple braces (double works too) and every other text field with double braces. An editor:"html" fragment uses the same tag vocabulary as aneditortext block (below) — other tags and attributes are stripped at render. - editor: block editor (standalone kind) — the PREFERRED kind for every large content body (an article's
body, a page's main content, any long-form mixed text): proposeeditor, nottext, for such fields. The schema STAYS field-based —editorreplaces only the body field, never the structured fields around it (an article is still title:text, cover:image, body:editor, author:reference — don't fold titles/images/references into one editor field). VALUE in Data documents is the envelope {type:"blocks", value:{version:1, blocks:[…]}}; elsewhere the same {version, blocks} object as a JSON string. Each block is {id, type, …} with type one of: text ({html} — ONE rich fragment of/
/ - number, boolean, icon (inline SVG/base64 — the ONE kind whose plain-string value is markup; templates bind it with triple braces), json (free-form key→value).
- datetime: {date:true} and/or {time:true}.
- image / gallery: ALWAYS configure sizing — never propose a bare image field. ONE image field per picture: a smaller rendition of the same picture (thumb, preview, card, mobile) is NEVER a second image field — it is a
sizesvariant on the one field (athumbfield beside acoverfield is WRONG; onecoverwith a thumb variant is right — the editor uploads once, every rendition is generated). {max:{width,height}} bounds the uploaded original, which is the image the rendered site serves: size it to the field's largest render in the template — the dimensions the template actually displays it at (or slightly above); guides: ≈1920×1080 full-bleed hero/cover, ≈1200×900 in-content/card photo, ≈512×512 logo/avatar; 2048×2048 only when the use is genuinely unknown. ALSO declare {sizes:[{name,max_width,max_height}]} variants for every OTHER real usage of the picture — typically a thumb for listing cards/previews ({name:"thumb",max_width:400,max_height:300}) and a responsive/mobile rendition ({name:"mobile",max_width:768}); variants are generated AT upload time, so a schema missing them leaves already-uploaded images without any. Crop vs fit is derived from the dimensions: BOTH set → exact crop to that size (cover); ONE set → fit (bound that side, keep aspect). Optional {format:"original"|"webp"|"jpg"|"png"} — prefer "webp" for photographic content. Stored image VALUES carry the generated renditions denormalized ({path, ref, variants:{name:{path,width,height}}}) — write the value exactly as the image tools return it; templates address renditions as {{field.thumb}}/{{field.srcset}}. An optional {variant:""} on the value pins which rendition {{field}} itself renders — normally a human picks it in the editor; set it yourself only when the render width is obvious (e.g. a card grid → the thumb variant). PER-PICTURE TEXT (alt, caption, credit) IS metadata, NEVER A COMPANION FIELD: declare {metadata:[{key:"alt",kind:"text",label:"Alt text"},{key:"caption",kind:"text",label:"Caption"}]} on the image/gallery field itself — the admin then edits that text beside each uploaded picture, and the value carries it per usage ({path, ref, variants, metadata:{alt,caption}}), so the SAME photograph can be captioned differently in two placements. Templates bind it off the image value ({{cover.alt}}, {{cover.caption}}, {{cover.metadata}} for the whole map); those names are reserved and never resolve to the image URL, and a size variant may not use one. Analttext field NEXT TO an image field is the anti-pattern (it makes the editor fill two rows for one picture, and nothing keeps them together when the picture is replaced). A picture that is the SAME in every language but captioned per language is{general:true}on the image/gallery field withlocalized:trueon the metadata entries that must stay per language ({general:true, metadata:[{key:"alt",kind:"text",label:"Alt text",localized:true},{key:"credit",kind:"text",label:"Credit"}]}): the file (andcredit) is mirrored into every locale doc,altis edited on each language tab;localizedmeans nothing on a non-general field, where everything is per language already. - gallery: a SET of pictures the editor manages as one —
kind:"gallery"with the same {max, format, sizes, metadata} config asimage. It takes a MULTI-FILE drop/pick (many photographs in one go, reordered in place, metadata per image); its VALUE is an array of image values, so templates iterate it:{{#each gallery}}<img src="{{this.thumb}}" srcset="{{this.srcset}}" alt="{{this.alt}}">{{/each}}with{{this.caption}}beside it. NEVER model a photo set askind:"array"of objects{image, alt, caption}— that is the same data with a worse editor (one upload per row, no multi-select, no reorder of the pictures themselves) and it is the single most common mis-modelling of a gallery. An array of objects is right only when each entry carries fields that are NOT about the picture (a slide with its own heading and link). - file: ONE uploaded file of ANY type — a PDF, a document, an archive, a price list, a manual: the download / attachment counterpart of
image. Use it wherever a visitor downloads or opens something that is not a picture (a "documents" list is an array of objects each holding {key:"file",kind:"file"} plus the text around it); NEVER model a download as alinkholding a bucket URL or as text fields for the name / type / size — the editor cannot replace such a file, and the info goes stale. VALUE: the same node as an image, {path, ref, metadata?}, plus the file's denormalizedname(original filename),size(bytes) andtype(MIME) — write exactly whatuploadStoreFilereturns ({path: storagePath, ref, name, size, type}). Nosizes/max/format(there are no renditions); optional {accept:["application/pdf",".docx",".zip"]} (HTML accept tokens) narrows what the editor's upload control offers; per-usage text ABOUT the file (a description) ismetadataexactly like an image's ({metadata:[{key:"title",kind:"text",label:"Title"}]}) — but the LABEL of a download the visitor clicks ("Download PDF") is atextsibling in ONE object with the file,{label:text, file:file}(the GROUPING RULE below). Templates:{{doc.file}}is the URL,{{doc.file.name}}/{{doc.file.size}}(bytes) /{{doc.file.size_text}}(localized "15.3 MB") /{{doc.file.type}}/{{doc.file.ext}}("pdf") the info,{{doc.file.title}}the metadata — see HANDLEBARS BINDING. - enum: reference an existing enum schema with {enum_ref:"
"} (preferred), or inline {options:[{value,label}]}. - object: a group — inline {fields:[…]} or {template_ref:"
"} to reuse another template. Everything a visitor clicks as one thing (a button, a download, a linked picture, an icon link) is ALWAYS one object — the address plus what stands for it — see the GROUPING RULE below. - array: {item_type:"
"}; for arrays of objects also give {item:{key:"",kind:"object",fields:[…]}}. - reference: a pointer into the store's DATA collections — use it instead of copying another document's content into this one. Config: {reference_type:"document"|"list"|"collection"} (absent = document) and {collection_ref:"
"} to lock the picker to one collection (blank = the content editor picks). SET collection_refWHENEVER the target is one collection — almost always: an article's author →authors, related recipes →recipes, a service's branch →branches, a list of team members →team(forlistandcollectiontoo); the picker then opens on that collection's documents and the editor picks one directly, instead of choosing the collection first in every document. Leave it unset only for a field that really points into several collections (a "related content" teaser that may pick an article OR a recipe), and say so in the field'sdescription. The VALUE is an envelope, never the target's data: document ⇒ {type:"document", collection, document, ref}; list (an ordered multi-pick from ONE collection) ⇒ {type:"list", collection, documents:["id",…], ref}; collection (a whole collection the editor narrows) ⇒ {type:"collection", collection, query?, limit?, order_by?, ref} —queryfilters ({field: value} equality shorthand, or {field: {operator…}} like the listing endpoint),limitcaps the count,order_bysorts ({field:"asc"|"desc"}); all three are honored by the renderer, so sort/narrow in the envelope instead of rendering everything and hiding with JS. Aqueryon one field combined withorder_by(or a range) on ANOTHER runs on a QUERY KEY of the collection — built in for the common shapes (status, the firstenumfield or adatetimefield, sorted by date or newest), declared withregisterCollectionkeysonly beyond them (readRule("data-collections") QUERY KEYS); a sibling value that carriesqueryruns on the same key[<query field>, <order field>].order_bymay only name a field the schema declares and EVERY document populates — the sort key doubles as a filter (documents missing it are silently excluded; a field no document has returns an EMPTY list with no error). The stored value is ALWAYS the envelope — never write the resolved documents into the field (that freezes the listing and breaks paging). For a "load more"/paged listing, readRule("load-more") FIRST — the same envelope drives the later pages.refis the locale-templated data path (/data/{locale}/{collection}[/{id}]) — consumers substitute the active locale for the literal {locale} token. A document reference may also carry {render:true}: the renderer then resolves the target through ITS OWN schema template and adds anhtmlkey to the resolved value, so a page can drop the whole component in with {{{.html}}} while {{ .someField}} still resolves — that is how a contact form or CTA band lives once and appears on many pages. Top-level fields ONLY: never use reference as an array item_type(reference_type:"list" already covers "many documents"). PREV/NEXT (SIBLING) VALUES: for "previous/next article" links adocumentreference field takes a DYNAMIC value instead of a pick — ALWAYS author the canonical form {type:"sibling", direction:"next", order_by:{: "asc"|"desc"}, collection:" ", ref:"/data/{locale}/ "} — resolved at render time to the ONE document next to the document that CARRIES the value, where the order_bydirection alone selects the neighbour: {publish_date:"asc"} = the FOLLOWING document (e.g. anextfield), {publish_date:"desc"} = the PRECEDING one (aprevfield). The cursor is exact and tie-safe (equal sort values still walk correctly). Rules:collectionMUST be the carrying doc's own collection (same-collection by construction — a mismatch resolves to nothing); the order field must exist on EVERY document of the sequence (it doubles as a filter, theorder_bycaveat above); at the END of the sequence the value resolves to NOTHING, so templates always guard ({{#if next}}…{{/if}}); optionalquerynarrows the sequence (same vocabulary as a collection reference, e.g. {published:true});render:trueworks exactly like a document reference's. "Previous in the manual drag order" cannot be expressed (only order-field sequences reverse). You may READ a value with direction:"prev" (a legacy spelling: it inverts the stored order, so prev+asc ≡ next+desc — identical query); treat it as equivalent, do not "fix" it unprompted, and never WRITE it — the admin picker and this rule both use the direction:"next" form. NEVER build a prev/next chain out of hand-pointed staticdocumentpicks — it fails SILENTLY (a deleted/renamed target renders nothing, a new document is linked from nowhere); a static pick is only right when the human explicitly curates the sequence. - link: a link TO a page — never a copy of it. Use it for every href/URL field (buttons, CTAs, menu items, footer links, card links) — it may live inside array items and objects, unlike reference. The VALUE is ONE of two forms: (a) a PLAIN STRING used verbatim — anchors ("#kontakt"), tel:/mailto:, external https:// URLs, uploaded file addresses; (b) the ENVELOPE {type:"link", collection:"
", document:" ", hash?:"section", ref:"/data/{locale}/ / "} pointing at a store document — the renderer resolves it at render time to that document's PUBLIC URL in the page's locale (the homepage → "/", else its seo.slug under its scope + the collection route, else /ref/{collection}/{document}; a missing target resolves to NOTHING, so guard with {{#if}}); omit documentto link a ROUTED collection's own listing ("/{segment}"). RULE: NEVER hand-type an internal path ("/o-nas", "/recepty/bruschetta") when the target is a store document — write the envelope (find targets with listStoreCollection(path, mask:["seo","title"])); a typed path breaks silently on the next slug rename and cannot follow the locale. Config: optional {collection_ref:""} locks the picker to one collection, or {collection_default:" "} only PRESELECTS it (an empty link opens as a Page pick on that collection; the editor may still choose another collection or a URL — use it when one collection is the usual target, e.g. blog articles); declare {general:true} ONLY on a standalone root link with no label whose address is the same in every language (a hidden canonical / source URL) — never inside a label + link object (see GROUPING RULE). GROUPING: a link never stands alone as a field — it lives in ONE object with its label / image / icon (the GROUPING RULE below). BINDING: href="{{cta.href}}" (double braces — a plain value) guarded by {{#if cta.href}}; {{cta.href.title}} is the target's title in the page's locale (auto-labelled buttons/menus). A mismatched object stored on a textfield shows EMPTY in the admin editor — flip the field's kind tolinkfirst (writeSchema / the element'sfields), then write the envelope. - form: a public form visitors submit (contact, request, …). Give {form:"
", fields:[…]} — ONE ordered fields list mixing static content fields (headline, consent text) with submittable inputs: {key, kind:"variable", input:"input"|"text"|"toggle"|"image"|"file"|"files"} (uploads rejected in v1). The schema entry of a variable carries ONLY key+input; its label, description/placeholder and validation constraints (required, pattern, pattern_message, max_length, max_size, accept, max_files) live in the DOCUMENT VALUE under the variable's own key as {type:"", label?, description?, required?, pattern?, …} — self-contained per-locale data both the rendered site and the server validate from (error messages localize with the doc). Submissions go to /public/form once the admin ACTIVATES the form (store/{sid}/forms/{slug}); the submitting page passes source: "{collection}/{docId}"so the server validates against that document's contract (the render server does this automatically from the page path). - element: a placement of a prebuilt element from the store's element library (store/{sid}/elements — reusable blocks bundling fields + a Handlebars html fragment + css/js). Config: {element_ref:"
"} locks the picker to one element; absent = the editor picks any. The VALUE is pure data — {type:"element", element:" ", data:{…}} where datais shaped by the element's ownfields; NEVER put the element's html/css/js in a document. ONLY the envelope keys (type, element, embedded, data) belong at the value level: per-placement content goes UNDERdata, never beside it (beside it the element renders with empty data), and any other stray key is junk the renderer ignores — repair one with deleteStoreKeys. A value MISSINGtype:"element"or theelementid is not a placement at all — it is stored as a plain object and renders NOTHING, silently; always write the full envelope. Live (default): the code resolves from the library at render time, so element edits propagate to every placement; pages bind the rendered placement as {{{.html }}} (the fragment's css/js are hoisted into the page shell automatically). Embedded ({…, embedded:true} on the value): the editor copied the element's code into the page's TEMPLATE as a frozen block ("{{#with }}…{{/with}}" in html, "/* element: /…/ /element: */" in css/js) — the renderer skips the live fragment for it. Preserve those marker blocks when editing a template (move them freely, don't strip the markers). KIT-SOURCED ELEMENTS: an element carrying origin.templatecame from a platform kit import that no longer exists — it is an ordinary store-owned element; edit it directly, nothing overwrites it. MODULE ELEMENTS: an element shipped by an installed store module (origin.module) KEEPS a direct edit — it becomes customized and module updates SKIP it from then on — and any element that uses a module must carrymoduleand is never embedded; follow the MODULE ELEMENTS rule (module-elements) before editing or building one. - Mark a field {general:true} when its value is shared across ALL locales (logos, prices, flags); leave it off for translatable per-locale text. HOW a general value is kept in sync: every locale doc stores its own full copy (
data/{locale}/{collection}/{id}all carry the general keys) and the ADMIN EDITOR mirrors the General tab into every locale doc on save — nothing propagates server-side, andupdateStoreDocumentwrites ONE locale doc. So when you write a general field programmatically, write it to EVERY store locale (one call per locale), or the other languages keep the old value until an editor saves the document; renderers always read the serve locale's own doc. - GROUPING RULE — everything a visitor clicks as ONE thing is ONE
objectfield: the ADDRESS (linkfor a page / URL / anchor,filefor a download) together with WHAT STANDS FOR IT on the page (a label text, an image, an icon) and its own options (a style, a badge) — NEVER sibling fields spread over the document (cta_label+cta_href,button_text+button_url,pdf+pdf_label,logo+logo_url). Reason: the console's Studio opens the clicked label's object as ONE form, so an editor who retypes the text sees the address beside it; a flat pair hides the URL / file from click-to-edit. THE CASES: a button / CTA / menu item / footer link → {key:"cta", kind:"object", label:"Call to action", fields:[{key:"label",kind:"text"},{key:"href",kind:"link"}]}; a download ("Download PDF", a price list, a manual, a brochure) → {key:"download", kind:"object", fields:[{key:"label",kind:"text"},{key:"file",kind:"file"}]} — the label is the object'stext(click-editable), the file'sname/ext/size_textcome off the file value, and the file'smetadatais for text ABOUT the file (a description), never the clickable label; a linked picture (a banner, a logo, a partner logo, a teaser) → {image:image, href:link} (+ alabel/captiontext when one is shown); an icon link (a social profile, a contact row, an app-store badge) → {icon:icon, label:text, href:link}; a CONTACT ROW (an e-mail, a phone, a web address — the most common case) → {label:text, href:link} (+icon), the href a PLAIN-STRING link value ("tel:+420777123456","mailto:info@example.cz","https://…") and the human form inlabel("+420 777 123 456") — NEVER anemail/phonetextfield the template turns intohref="mailto:{{email}}", and never a display text beside the dialable one (tel_display+tel_label); a LIST of any of these (a menu, a documents list, a logo wall, a social bar) → anarraywhoseitemis that object. NOT this rule: a card that teases a store document is areference(it brings the target's data and route); an image'salt/captionstay itsmetadata. LOCALE: the object is PER-LOCALE — nogeneralon it or on its sub-fields (the locale split is decided per ROOT field, so a nestedgeneralhas no effect): the label is translated, and on a multi-language site the address may differ by language too — a localized external page, a per-language PDF, another target document; a store page that is the same in every language is the same envelope written to every locale doc (one write per locale), resolved to each language's slug; a file shared by every language is the same file node written to every locale doc (the editor picks the existing upload from Files, no re-upload).general:truebelongs only on a standalone rootlink/filewith no label whose address never changes by language (a hidden canonical / source URL). - Cross-kind props {hidden} + {default}: {hidden:true} hides the field from content editors in the admin document form (and element-placement forms) — admin managers can reveal it via the document menu; the value still renders on the public site. {default:
} is a predefined value seeded into every NEW document the admin creates and every NEW element placement; the value shape follows the field's kind (string, number, enum value, reference ENVELOPE, …). THE canonical pairing is a machine-managed reference: a next/prev link field is a documentreference carrying default:{type:"sibling", direction:"next", order_by:{:"asc"|"desc"}, collection:" ", ref:"/data/{locale}/ "} PLUS hidden:true — every new document links itself into the sequence with zero editor involvement (order_by asc = the following doc, desc = the preceding one; all the sibling rules above apply). A curated "featured" reference can likewise carry a static document pick or a collection envelope as its default. TWO CAVEATS: (1) proposeSchema/writeSchema/updateSchemaFields REPLACE the fields list wholesale — when you resend a list, ECHO every existing field's hidden/default props (omitting them silently deletes them); (2) defaults are applied by the ADMIN editor at creation time only — when YOU create documents directly (updateStoreDocument), stamp the schema's declared defaults into the new document yourself; there is no server-side application and no render-time fallback.
BUILT-IN CONTAINERS (booleans on proposeSchema): seo (title/description/image per document, plus custom meta tags meta: [{key, value}] — og:type, twitter:card, …), jsonld (schema.org structured data), status (none/public/private/hidden picker; a NEW document starts as public unless statusDefault names another value — e.g. statusDefault:"hidden" for a collection whose documents are drafted first and published by hand). Turn them on when the collection is public web content (pages, articles, products) and the user wants SEO / publishing control — and on EVERY schema whose documents are pages, the PER-DOCUMENT ones included (general/pages/{doc}, the pages of a routed collection): a page whose schema has no seo cannot get a title or a description of its own (it shows the site's), and checkSite reports it (seo.noContainer). jsonld belongs on content with a schema.org type (articles, products, recipes, FAQs, services, branches, events). base:"product" inherits the locked commerce fields (price, stock, …) — use it only for the products collection, and readRule("commerce-products") first: a product is a base commerce document plus per-locale content, not a plain data collection.
ENUMS ON THE SITE: an enum field stores the entry's value (else its key), and the renderer prints exactly that — the entry's name is the console's label and is NOT localized. So the convention is: keys as the stored values (leave value unset), and the visitor-facing label through a translations group named after the enum, one key per entry per locale: {{t "blog_category" category}} (the hash form looks the key up in the group) — a multi-value tag set is an array with item_type:"enum". Do not reach for a data collection just to give a picklist labels (readRule("data-collections") TAXONOMY).
SEO META TAGS — seo.meta is a LIST of custom <meta> tags [{key, value}] (a list, not a map, so a key may repeat: og:image, article:tag). Keys start with a letter, then letters / digits / _ . : -; values are plain strings. An entry with an invalid key or an empty value is dropped silently at render — nothing refuses it on write, so check your keys. TWO LEVELS: a document's seo.meta (that page) and setup/web seo.meta (every page). The renderer picks the attribute — Open Graph namespaces (og:, article:, product:, profile:, book:, music:, video:, fb:, business:, place:) become property=, everything else (twitter:*, robots, author, theme-color) name=; never store the attribute. WHICH TAG WINS per key, most specific first: the document's meta > the document's seo title / description / image (they GENERATE og:title, description + og:description, og:image) > the site's meta > the site's seo fields. So: never copy seo.title / seo.description into og:title / og:description meta (they are generated already) — add those keys only for DIFFERENT social copy (no "| Brand" suffix); put site-wide constants (og:site_name, twitter:card, fb:app_id) on setup/web, page-specific ones (og:type: article, article:published_time, article:author) on the document; og:url and og:locale are generated per page (a site-level one is dropped) — never write them. Keys compare case-insensitively. WRITING: updateStoreDocument replaces arrays WHOLE — read the document first and send the FULL merged list (the existing entries plus yours, replacing an entry only under the same key), or the other tags are lost; clear them all with deleteStoreKeys(["seo.meta"]). seo is per locale like the rest of the document: write each locale's copy.
STRUCTURE LOCK (locked boolean on proposeSchema / writeSchema; template schemas only — an enum has no document structure): locked:true freezes the field STRUCTURE for content editors. In the admin document editor every document of that schema then only takes VALUES — no add / remove / rename / reorder of fields — and the document editor's AI is closed to structure changes the same way. It reaches through inheritance: a locked SHARED schema also freezes each per-document schema that extends it (templateBase), and a per-document schema can carry its own lock. locked:false unlocks (the key is removed); a write that does not pass locked leaves the lock exactly as it is, so resending fieldsJson never unlocks. The lock is NOT a write guard on the schema itself: the schema editor, proposeSchema and writeSchema still change a locked schema — so treat locked:true as the owner's deliberate freeze: read the schema before a structural write, restructure a locked one ONLY when the user asked for that change (keep the lock on afterwards), and never unlock unless asked. LOCK BY DEFAULT every SHARED schema of a content collection — articles, news, recipes, products (base:"product"), services, team, FAQs, references, branches: a collection with a predefined shape is filled, not restructured, by its editors. Lock it as the last step of building the collection (the documents validate, the template renders — readRule("data-collections") "Order that works"). Leave a schema unlocked only while its structure is still being worked out with the user, and leave per-document page schemas (one-off pages the owner still shapes) unlocked unless asked. A later structural change the user asks for: change it and keep the lock on. Set it through the locked argument rather than by merging the key into the schema document by hand.
VALUES MUST VALIDATE AGAINST THE SCHEMA: never save a document value whose shape contradicts the field's declared kind — and inside an element placement, every key of data must validate against the element's declared fields the same way. The renderer resolves by VALUE shape while the admin editor resolves by the DECLARED schema, so an out-of-contract value is a silent trap: the public site renders it fine, the admin shows the field as EMPTY, and the next editor save overwrites it (a live collection feed becomes a dead literal — or []). The canonical offender is a {type:"collection"} envelope stored on a field declared kind:"array". When the value you want IS the right content model, fix the DECLARATION first — e.g. array → {kind:"reference", reference_type:"collection", collection_ref:"<collection>"} via writeSchema / the element's fields — and only then write the value; never write the mismatched value and move on. The write tools ENFORCE this for element placements: updateStoreDocument refuses a data/element/product write whose placement data contradicts the element's fields, and writeStoreElement refuses a sampleJson that contradicts the fields — the refusal names the path and quotes the declaration to write first (a fieldsJson-only repair that leaves the stored sample out of shape is written with a SAMPLE CHECK warning).