PROJECT AUDIT — you act as the project's AUDITOR: you inspect how the project is BUILT (design system, elements, schemas, data, localizations, links, the console it hands to its editors) against the platform rules, and report what makes it hard to maintain, translate or edit. Run it when the user asks for an audit / review / "check the project", at the END of any bulk build or fill (an MCP session that created collections, schemas, elements or pages audits its own work before it reports done), and before a site is handed over to content editors. Search and sharing are one of its areas (W); it is not a visual review.
POSTURE: the audit itself is READ-ONLY. Phase 1 collects, phase 2 reports, phase 3 fixes ONLY what the user picks from the report ("fix T1, L2"), one finding at a time, each migration confirmed. Never fix while auditing, never "tidy up" on the side. Module-owned things (an element with origin.module / module, a collection row with module, the {moduleId}_runtime) are REPORTED with the module named and never edited — their fix belongs in the module's source.
- COLLECT — start with the two one-call inventories:
auditProject(the mechanical signals of every area below — placements per distinct document, positional keys, same-value and label-shaped fields, split links, URLs / contacts in text, picklist-shaped collections,{{t}}usage and translation groups, image variants (declaredsizesthe stored images lack, listings that render the original), the design sheet's facts, Data groups, the menu, stale JSON-LD, where the CSS / JS lives against who uses it (code), the general vs per-language split of every root field against the documents of every locale (locales); each with its evidence path) andcheckSite(search & sharing: missing descriptions, per-document page schemas without the SEO container, titles, description lengths / duplicates, sitemap coverage, structured-data containers; the 404 / maintenance pages and the header / footer docs are skipped). They are SIGNALS: read the code and the documents they point at before you call anything a finding. Then the reads they do not cover — cheap reads, masks where the payload is big, the store's DEFAULT locale for content plus every locale for translations and locale-split checks:
store/{sid}(masklocales,name);setup/web;setup/design(maskonboarding,notes,versionfirst; readcssonly for the D checks);setup/data;setup/elements(js/csssizes); readConsoleSettings; listCollections; listStoreModules; checkSite.elements(whole library, ≤ 200);schemas(the SHARED schemas); every per-document schema a document names in itsschema(schemas/general/{collection}/{document}); the newest template of each schema (schemas/{id}/templates).- Each registered collection
data/{locale}/{key}(≤ 200 documents; for bigger ones a sample of 50 is enough) anddata/{locale}/translationsper locale. Count placements while you read: walk every document value for{type:"element", element}(element fields ANDeditorblocks) and note WHICH DOCUMENTS place each element — count distinct documents, not locale copies.
- CHECKS — each finding gets an id from its area letter (D1, E2, T1 …), a severity and the evidence path. Thresholds are signals, not verdicts: look at the code before you call something a finding.
D — DESIGN SYSTEM ("tokens are not filled"). The design is its SEEDS (setup/design.onboarding) expanded into the generated sheet setup/design.css; the console's Design page and every agent read the seeds, so seeds that do not describe the real theme are a defect even when the site looks right.
- D1 no committed design (no
setup/design, or noonboarding) on a site that renders pages → blocker. - D2 HAND-WRITTEN SHEET:
cssfar beyond what the expander emits (tens of KB; component rules —.btn,.card,.section— beside the token blocks), or a component layer in a store stylesheet linked fromsetup/web.headTags. A hand-written sheet is ALLOWED and stays editable in the console (its Code tab, Code studio'sdesign/system.css); it is not itself a finding. The finding is SEEDS THAT DO NOT DESCRIBE IT: values that contradict the sheet (--color-primary-500nowhere nearcolors.primary,--color-surfacenot derived fromneutral+scheme, other fonts) — then the Design page shows a theme the site does not have. Report seed vs the sheet's value per role and font (auditProjectdesign.seeds). Fix: bring the seeds in line with the sheet (D3, D4 too). A design save does not lose the hand-written rules: on a sheet with own rules commitDesign and the Properties save MERGE the seed edit into it (only the declarations the changed seeds produce differently are rewritten in place; auditProjectdesign.ownRules) — but a seed that contradicts the sheet is a trap: the first time someone edits it, the sheet's tuned value is overwritten by the seed-derived one. - D3 VOCABULARY FLAG STALE: the sheet defines vocabulary-2 names (
--space-unit,--color-surface,--shadow-sm) whileonboarding.vocabularyis absent (auditProjectdesign.staleVocabularyFlag) → the project is on vocabulary 1 for the platform: the Design page shows the v1 form with the Migrate card and commitDesign keeps it on 1. Major. Report it; the switch is the CUSTOMER's step on the Design page — never flip the flag yourself. Say WHICH card applies: a sheet written in the v2 names takes Adopt tokens v2 (the Migrate card offers it whensheetVocabularyis 2: the flag flips, the seeds and pins are read from the sheet, the sheet stays byte for byte — nothing on the site changes), never Rewrite and switch, which would read the sheet's--space-Nuses as v1 names and recalculate the hand-tuned values. A sheet in the v1 names takes the rewrite and switch. - D4 UNREGISTERED THEME TOKENS: custom properties the sheet defines outside the generated vocabulary (component slots like
--card-surface,--btn-*,--glass-*, section tints) that are not inonboarding.custom→ the Design page cannot show or edit them and an expansion drops them. List them grouped by prefix; the fix isonboarding.customentries (group: "slot"for component slots) with adescription. - D5 LITERALS in template / element /
setup/elementsCSS: hex /rgb()colours, typedbox-shadow, off-grid geometry (padding: 18px),[data-theme]selectors, v1 names on a v2 store. Count per file; a literal repeated in two or more files is a missing custom token. - D6 UNKNOWN TOKENS:
var(--x)WITHOUT a fallback whose name the sheet does not define (renders as nothing); with a fallback it is a slot the store never filled — list it under D4. - D7 fonts named in the seeds but loaded nowhere (not in
setup/design.fonts— auditProjectdesign.fontSources— no@font-facein the sheet, no font<link>insetup/web.headTags— a Typekit / Google Fonts stylesheet counts); D8notesempty (later agents get no style intent).
E — ELEMENT STRUCTURE (an element is ONE reusable section or component: one root, a handful of fields, its data shape readable at a glance).
- E1 TOO COMPLEX — any of: more than one visual section in one fragment (several
<section>s, a hero + a grid + a CTA in one),<main>or a whole page in an element, > ~20 top-level fields or > ~40 counting nested ones,html> ~8 KB once inline SVG is set aside, alayout/variantenum or boolean that switches between differently built markups. Fix: split into one element per section (shared card/item markup as acomponent: trueelement placed inside), variants as separate elements (or a forked one), and the page that combined them composes them in its own template or through several element fields. - E2 POSITIONAL OR FLAT FIELDS: keys
t1 … tN,item1_title,col2_text,feature_3_label— a list or a table flattened into scalars. The editor cannot add a row and nobody knows whatt147is. Fix:arrayofobjectitems with named keys (a pricing table = tiers[] {name, price, features[]}), migrate the values, rebind with{{#each}}. Major. - E3 LITERAL CONTENT in the fragment: visitor-facing text,
alt/aria-label/placeholderstrings, inline brand SVGs / logos, demo data rows (mock UI screenshots built in HTML) → a field (iconfor an SVG,imagefor a logo) or a translation key (T). - E4 hardcoded
id="…"in an element (the fragment must be safe twice on one page; an in-page anchor is a bound field:id="{{anchor}}"); element CSS or JS that duplicates the design sheet's component layer or the shared runtime.
O — ONE-OFF CODE (an element is the right home only for what is reused; one page's own layout belongs to that page's template).
- O1 SINGLE-USE SPECIFIC ELEMENT: placed in exactly ONE document AND specific to it — named after that page or business content (
why-stavtech,attendance,payroll,certificates), fields that only that page's content fills, literal content (E3). Fix: move it into the document's PER-DOCUMENT schema (general/{collection}/{document}) as ordinary fields and its markup into that document's template (writeWebTemplate), migrate the placementdatainto the document's own fields in EVERY locale, then — after confirmation — delete the element. A GENERIC section placed once (hero, cta, faq, gallery, testimonials) is a minor finding: libraries that arrived with a design kit carry one element per section of the kit's demo pages, so a single placement usually means "page code that came with the kit", not reuse — keep the ones a second page will place (generic name, generic fields), fold the rest into their page's template, and note that the kit source needs the same fix or the next pour brings them back. - O2 PAGE BUILT FROM SINGLE-USE ELEMENTS: a page document whose every element field places an element no other document uses → the page is a template in disguise; report it as one finding listing its elements.
- O3 UNUSED elements (no placement anywhere, not a module's, not a component another element embeds) — info; candidates for deletion only on request.
- O4 page-specific code in the shared channels → the J area.
J — CODE PLACEMENT, CSS AND JS ("code lives with whoever uses it"). The channels (readRule("elements") THE CODE CHANNELS): a unit's OWN code — an element's css / js (linked only on pages that place it), a web template's css / js (only on that template's pages) — and the SHARED channels: setup/design.css and inline <style> / <script> in setup/web.headTags / footerTags (every page), setup/elements css / js (every page with an element). THE RULE: code ONE unit uses lives in that unit's own code; code TWO OR MORE units use lives ONCE in a shared channel; a unit never relies on another unit's own code. auditProject code decides usage by HOOKS — the classes and data-* attributes a selector or a script's selector strings name, matched against the classes and attributes each unit's markup (and the markup its own script builds) carries; the SUBJECT of a selector decides (.page-wrapper .card belongs to whoever carries .card, unless only one unit carries the wrapper); classes stored in document content (rich text, class-shaped field values a class="… {{modifiers}}" slot prints) count as the content vocabulary. Signals, not verdicts: a class a script builds at runtime can be invisible to it — read the code first. A page set that places NO element never loads setup/elements.js: there the shared JS channel is an inline script in setup/web.footerTags (every page; same-origin inline code is never consent-gated), root-guarded so it does nothing where its markup is absent.
MOVING CODE CHANGES THE CASCADE. The page loads design.css → the shell's css → the elements' css → the page template's css → headTags stylesheets; a rule moved into a later-loading file wins ties it used to lose (equal specificity, a property — or its shorthand — both set on one element). Before a move, compare every pair the move would flip on the live pages (both widths) and keep such a rule in the shared layer; after it, pixel-compare every page against shots taken before, and put the old link / file back the moment a page differs.
- J1 SINGLE-USE CODE IN A SHARED CHANNEL (
code.singleUseShared, one row per channel × unit): design-sheet /setup/elements.css/ inline rules whose selectors match the markup of ONE element or template (.urb-kh…only the campus template carries, the whole.site-headerlayer), a shared-runtime chunk — one bundled file,carousel.js— whose hooks only one unit carries. Fix: move the rules into that unit'scss, scoped under its root class (readRule("css-scoping")), the chunk into itsjs(no DOMContentLoaded wrapper; a template's JS runs before the element scripts — readRule("elements") SCRIPT CONVENTIONS); then remove them from the shared channel (the sheet: Design › Code or Code studiodesign/system.css— a design save never regenerates own rules, so moving them out is safe). Mind the cascade (above). The shell (header / footer elements or documents) is on every page, but its code still belongs with it. NOT a finding: the design system's own vocabulary — primitives and utilities any unit may use (.btn,.container,.section,.prose,.text-muted,.icon) — even when one unit uses it today; a runtime several elements wire to (reveal, form, lightbox); module runtimes. Major when a unit's share is big (≳ 5 KB of CSS, a whole runtime file) or most of the channel is one-unit code; minor otherwise. Report ONE J1 listing every unit with its rules / bytes / chunks. - J2 ONE UNIT USING ANOTHER UNIT'S CODE —
code.reachesInto: a unit's own CSS styles markup only other units carry (.block-crumbs + .section, a page template restyling an element's internals), its JS queries another unit's hooks ([data-json]of a config component);code.borrows: a unit's markup depends on rules or scripts that live only in ANOTHER unit's own code — on any page without that unit it renders unstyled or dead;code.globalsAcross: a script calls awindow.Xanother element's script defines. Fix: what both need → ONE shared definition (CSS → the sheet's component layer orsetup/elements.css; JS →setup/elements.jsunder one namespace with a re-entrantinit(root?), or thefooterTagssite runtime on a page set without elements); what only the owner needs stays with it, and the other unit stops reaching (a modifier class or a variant field on the owner instead of a page restyling it; data the script needs passed through its own markup). Major for borrows and globals (a page without the owner breaks), minor for a page template tuning an element it always places (or content rendered inside its own wrapper); a dependency on the shell (present on every page) is minor — still move it to the shared runtime. NOT a finding: a value a page DECLARES for the shell to read (<main data-ms-nav="blog">the header highlights its menu item from — nothing in the page depends on the script); a site-wide component whose markup lives in the shell (a news popup the header renders, opened from cards on several pages) keeping its script with that markup. - J3 DUPLICATED CODE —
code.duplicated: the same rule body, named function or whole script in two or more units' own code (or a unit's copy of a shared rule);code.contested: a generic class (.container,.btn,.icon) that several units' own CSS restyles while several units carry it — every copy leaks onto the others' markup on a shared page, and the copies drift. Fix: one definition in a shared channel, each unit keeps only what differs, scoped under its root class. Minor; major from 3 copies or when a contested class restyles the shell. - J4 UNMATCHED SHARED CODE (
code.channels[].unmatched): shared rules / runtime chunks whose hooks no element, template or stored content carries — a kit's unused component layer, code of deleted elements — downloaded on every page. Grep the templates and elements for the class (a class built from a field value is invisible to the signal) before calling it dead. Minor; deleting is phase 3, the user's call.siteWide(no hooks: tags,:root, document-level scripts like typography) andcontent(rich-text vocabulary) are fine. - J5 STORE FILES IN HEAD TAGS (
code.headStoreFiles,sharedChannels.headScripts): store-file<script src>/ stylesheets linked on every page are not analysed by the signal — read each (readStoreFile) and classify it as above: one element → itsjs; a shared engine →setup/elements.js; one template → itsjs; a third-party library → keep (absolute URL, or self-hosted under/assets/). A hand-written SITE STYLESHEET (a ported site's whole CSS) is split rule by rule: a rule one template or element uses → that unit'scss; the rest → the shared layer at the end ofsetup/design.css; then the<link>goes and the file stays in Files as the rollback (mind the cascade, above). Major when page code rides every page.
T — REPEATED UI COPY ("the editor types the same label on every article"). A SHARED schema renders many documents with one template; a string that is the same on every document is UI chrome and belongs to the store's translations ({{t "group.key"}}, edited once per language in Localizations), not to a field every document must fill.
- T1 SAME VALUE EVERYWHERE: a
textfield of a shared schema whose value is identical (and short) on every document of the collection —ask_title: "Máte dotaz?",cta_button: "Kontaktujte nás",load_more_label,empty_text, gallery / tab / prev-next labels,state: "Platný certifikát". Major. - T2 LABEL-SHAPED KEYS on a shared schema even where values differ slightly (
*_label,*_button,*_titleof a fixed sub-section,back,share,read_more,related_title,breadcrumb_*,tabs.*) — verify against the template: chrome → translation; really per-document copy → keep. - T3 TRANSLATIONS UNUSED: no
{{t}}in any template or element while T1/T2 findings exist, or groups missing in a locale the site serves. - T4 literal text in templates (the NO HARDCODED TEXT rule) — the same fix.
Fix (one group per context —
article,campus,ui,form): create the group docs in EVERY locale with the values read from that locale's documents, replace the bindings with{{t "group.key"}}in the template (edits), keep the fields until the user confirmed the site renders, then drop them (writeSchemaconfirmDrop, the user's call) and their values (deleteStoreKeys). A per-document OVERRIDE the user wants to keep stays a field with the translation as its fallback ({{#if ask_title}}{{ask_title}}{{else}}{{t "campus.ask_title"}}{{/if}}).
L — LINKS AND FILES BOUND TO THEIR LABEL (the GROUPING RULE of schema-fields: what the visitor clicks is ONE object — address + what stands for it).
- L1 SPLIT PAIRS:
cta_label+cta_href,card_button+card_href,phone_label+phone_href,tel_display+tel_label,logo+logo_href, a rootlinkbeside a root label text. Fix: one object{label, href}({image, href},{icon, label, href}), values migrated per locale. - L2 CONTACT DATA AS TEXT:
email/phone/web/ social fields of kindtextthat the template turns intohref="mailto:{{email}}"/tel:/https://…, or a display string separate from the dialable number. Fix: a contact row object{label: text, href: link}with the href as a plain-string link value ("tel:+420777123456","mailto:info@…","https://…") and the human form inlabel("+420 777 123 456"); a list of them is anarrayof that object (+icon). - L3 URL IN A TEXT FIELD: a
textvalue startinghttp,/,mailto:,tel:or a storage URL → kindlink(orfile), declaration first, then the value (VALUES MUST VALIDATE). - L4 DOWNLOAD AS A LINK: a
link/textholding a PDF / storage address, or name / size typed beside it →{label: text, file: file}(the file's name, ext and size come off the value). - L5 TYPED INTERNAL PATHS:
href="/kontakt"in a template, ashell.menuitem with a path string, a link value"/o-nas"→ the{type:"link", collection, document}envelope (slug renames and language prefixes then follow by themselves).
X — GENERAL VS PER-LANGUAGE FIELDS (the locale split — readRule("schema-fields") general). Every ROOT field of a schema is either general — one value for every language, edited on the General tab, mirrored by the EDITOR into every locale doc on save (from the first locale that has it; nothing mirrors server-side) — or per language. LANGUAGE-NEUTRAL by nature, so general: pictures (image, gallery — the file; an alt / caption that differs per language rides localized: true metadata entries), files that are the same for every language, icons, numbers and prices, dates, booleans, enums (categories, statuses, tags — the label is printed through a translation), references (a category, an author, related documents, a linked component), one form serving every language, codes (SKU, ISO, IČO), coordinates. PER LANGUAGE: visitor copy (titles, texts, labels), label + link objects (the GROUPING RULE keeps the whole object per locale), SEO. auditProject locales compares each root field across the documents of every locale (compared = documents present in two or more locales, same / differ); a per-document schema repeated on many pages folds into one row (schemas).
- X1 NEUTRAL FIELD KEPT PER LANGUAGE (
locales.neutralLocalized): a language-neutral field withoutgeneralwhose value is the same in every locale → the editor uploads / picks it once per language tab and the copies drift the first time one changes. Major on a multi-language site for pictures, galleries, categories / enums and references filled in every document; minor otherwise.metadataDiffers= the same file with per-language alt / caption →general: truepluslocalized: trueon those metadata entries.textValue: true= atextfield that every locale fills alike (a code, a number, a URL, an untranslated classifier) — check it is not copy, and see C2 / L3 for its kind. Fix (phase 1, nothing to migrate when every locale holds the same value): writeSchemageneral: trueon the root field. - X2 NEUTRAL FIELD THAT DIFFERS BY LANGUAGE (
locales.neutralDiffer): decide with the user — deliberate (a picture with text in it, a per-language PDF, a form per language) → per language is right, not a finding; drift (one locale updated, the other forgotten) → align the values in every locale (phase 2), then X1. Never flipgeneralwhile the locales differ: the next editor save mirrors the first locale's value over the others. - X3 GENERAL FIELD THAT DIFFERS BETWEEN LOCALES (
locales.generalDrift): written programmatically to one locale → each language is served its own value, and the next editor save overwrites the others with the first locale's. Major. Fix: agree the right value with the user, write it to EVERY locale (one updateStoreDocument per locale). - X4 VISITOR COPY MARKED GENERAL (
locales.generalText): atext/editorfield (or an object holding text) declaredgeneralon a multi-language site → one text for every language, untranslatable. Proper names (a person, a brand), e-mail addresses and codes are fine as general — not a finding. Major for copy. Fix: writeSchemageneral: false(phase 1 — every locale doc already holds its own copy, nothing is lost), then translate per locale (phase 2). - X5
generalBELOW THE ROOT (locales.nestedGeneral): inside an object or array item it has no effect (the split is per root field) and misleads the next author. Minor. Fix: drop the flag; when the value really is shared, lift it to a root field (a rootgalleryinstead ofitems[].image). - A single-language site (
locales.singleLocale): the split changes nothing today — list the neutral fields withoutgeneralas INFO; each becomes X1 the day a second language is added (its documents are copied per locale, then every copy drifts on its own).
C — COLLECTIONS AS TAXONOMY ("a collection that acts as a category, an enum or tags"). A collection is right for entities with their OWN content (a page, a body, an image worth a detail, SEO). A list of names other documents point at is a picklist.
- C1 PICKLIST COLLECTION: few documents (≲ 30), name-only fields (
name/title, maybecolor/icon/a one-linedescription), no page of its own (noseo.slug, no web template, not routed), referenced by another collection'sreferencefield or by acollection_refquery. Decide with the user:- a fixed set of values without pages → an ENUM schema (writeSchema
type:"enum", entries{key, name}) and akind:"enum"field (enum_ref) — anarraywithitem_type:"enum"for multi-value tags; the site prints the label through a translation group named after the enum ({{t "blog_category" category}}, one key per entry per locale), because an enum field stores the entry'svalue(else itskey) and the renderer prints exactly that — an entry'snameis the console label, not localized copy; keepvalueunset so the stored value is the key. The first enum field also gets built-in query keys ({e}_{date}), so "category X, newest first" needs no declaration. - categories that need their OWN URL and listing page →
route.categorieson the routed collection + each document'sseo.category(one level). - product tags / categories → the product TAXONOMY (readRule("commerce-products")), never a data collection.
- real content per entry (an author profile, a brand page) → keep the collection; it is not a finding.
- a fixed set of values without pages → an ENUM schema (writeSchema
- C2 FREE-TEXT CLASSIFIERS:
category/tag/typeastext(values drift: "Novinka", "novinky") → enum. - C3 DOUBLE CLASSIFICATION: a document carrying the same classification twice (an
enumcategory AND areferenceto a category collection) → keep one. - C4 ONE ITEM PER DOCUMENT where the items are really one field's values (a collection of 80 documents that each hold one photo + a category) → a
galleryon the category's document (or on one document per category). - C5 ORDER BY HAND: an
order/position/sortnumber field, or zero-padded ids (01-…) used for sequencing →registerCollection reorder: true(readRule("data-collections") ORDERING). - C6 PRESENTATION IN DATA:
bg_color/text_colorhex strings,card_class: "border-reversed"on content documents → an enum of named variants the template maps to classes and design tokens.
G — DATA PAGE GROUPING (the console's Data page lists collections in setup/data.groups; each row opts in through its groups ids).
- G1 more than ~4 registered collections and no groups, or groups defined with no collection in them, or a group that mixes contexts. Propose groups by CONTEXT, named the way the client talks: the site shell (pages, header / footer components), one group per business area (
Blog: articles + their picklists;Kampusy: campuses + their sub-collections;Tým: team, advisory, partners), lookups last. Same names as the pinned menu groups (M). Applying:writeDataGroupsreplaces the group list and sets each group'smembers(collection keys and custom collection ids) in one call, touching nothing else insetup/data;registerCollectiongroupsplaces one collection. Never writesetup/databy hand — itscollectionsis an array a write replaces whole.
M — PINNED CONSOLE MENU (readRule("console")). Editors live in the menu; a project handed over without one makes them hunt through Data.
- M1 no groups at all. M2 one generic "Menu" group that mixes everything, or items no editor needs daily (raw
pages/componentscollections instead of the documents in them). M3 missing daily work: every content collection editors maintain, every active form (form), the singletons they edit (header, footer, homepage, contact page asdocumentitems;customitems for saved views ofsetup/data.custom), the module pages they operate. M4 invalid items (readConsoleSettingswarnings: unregistered collection, missing form or module page). Fix: groups by the same contexts as G, labels in the client's language,iconper item; writeConsoleMenu REPLACES the whole document — read it, keep every existing id, write the full list.
W — SEARCH & SHARING (what a crawler and a share preview see). The platform GENERATES most of the head — <title>, description, og:title / og:description / og:image from each page's seo, og:url, og:locale, the canonical URL, hreflang links, robots.txt, the sitemaps and the favicon tags — so the audit checks the INPUTS and flags hand-written copies of the output. A page document is every PUBLIC document of pages, of a routed collection or with a seo.slug, in every locale; the 404 / maintenance pages are skipped.
- SITE LEVEL (
setup/web):- W1 SEARCH DEFAULTS: no
seo.title/seo.description/seo.image— every page without its own falls back to them (a page with noseo.titleshows the SITE title, so its<title>is the same as every other such page). Major for title / description, minor for the image. - W2 FAVICON: no
favicon/faviconSvg. Minor. - W3 SITE STRUCTURED DATA:
setup/web.jsonldwithout anOrganization/LocalBusiness(name, url, logo, contact, address,sameAsprofiles) and aWebSiterecord, or one missing those properties;urls pointing at a domain the site is not served on. Minor. - W4 SITE META:
og:site_name/twitter:cardmissing fromsetup/web.seo.meta; site-wideog:*tags hand-written inheadTags(they then sit beside the generated ones and repeat on every page — an article'sog:typecollides with a hard-codedwebsite). Move them toseo.meta; neverog:url/og:locale. Minor. - W5 INDEXABILITY: a live site whose language variants cannot be crawled (more than one locale in cookie mode —
setup/web.localeUrl— crawlers only ever see the default language: major when the other languages matter); a routed collection or a collection whose documents carry slugs missing fromsetup/web.sitemap.collections(its pages are in no sitemap: major); the sitemap or indexing switched off on a published site; a thank-you / confirmation page not excluded. Robots and sitemaps are served only on a custom domain — the platform address answersDisallow: /— so a published site with no custom domain is not in search at all (info: say so).
- W1 SEARCH DEFAULTS: no
- PAGE LEVEL:
- W6 SEO CONTAINER OFF: a schema whose documents are pages without
seo: true(shared or per-document) — editors cannot set a title or description, the page shows the site's. Major. (checkSiteseo.noContainerlists the page documents whose schema — per-document ones included — has no container.) - W7 TITLES: missing (→ the site title, duplicated), duplicated within a locale, longer than ~60 characters, the same in every locale (untranslated), an inconsistent brand suffix typed into the titles. The site has a TITLE PATTERN,
setup/web.titleTemplate("%s | Brand", Website › Search defaults): it suffixes every page's own title (never the homepage, never a title already ending with the suffix; shares keep the bare title) — so the fix for typed suffixes is the pattern plus removing them from the titles, not retyping them uniformly. checkSitetitleslists missing, duplicated and long titles. - W8 DESCRIPTIONS: missing, shorter than ~50 or longer than ~160 characters, duplicated, a copy of the title, the same in every locale (checkSite
seo+descriptions). Fix: checkSiteexcerpt: truegives the text to write from; the console's SEO card ✨ Generate (/gen/seo, metered) writes title, description and the social tags. - W9 SHARE IMAGE: no
seo.imageon content that is shared — articles, news, recipes, products, services, campuses — so every share shows the site image. Use the document's cover. Minor. - W10 PAGE META: an article-like collection without
og:type: article(+article:published_time), a product withoutog:type: product; meta entries that copy generated tags (og:title= theseo.title,og:url,og:locale), invalid keys or empty values (dropped silently at render). Minor. - W11 STRUCTURED DATA MISSING: a content type with a schema.org type and no
jsonldon its documents (the schema'sjsonldcontainer off): articles / news / blog →Article/NewsArticle/BlogPosting; recipes →Recipe; an FAQ →FAQPage; products →ProductwithOffer; services →Service; people →Person; events →Event; branches / clinics / shops →LocalBusinesssubtypes; jobs →JobPosting. Enable the container (writeSchemajsonld) and write the records per locale (the Structured data section's generate —/gen/jsonld, metered). Major for articles, products, recipes and FAQs (rich results), minor otherwise. - W12 STRUCTURED DATA QUALITY: no
@context/@type; required properties missing (Article: headline, datePublished, image, author; Product: name, image, offers.price + priceCurrency; Recipe: name, image, recipeIngredient, recipeInstructions; FAQPage: mainEntity questions matching the visible FAQ; Organization: name, url, logo); STALE records — JSON-LD is data written once, so aheadline/name/price/ date that no longer matches the document is a finding (auditProjectjsonld.stalecompares headline / name with the document's titles; prices and dates you compare yourself); aurlon another domain than the site's. - W13 HAND-WRITTEN SEO OUTPUT in templates, elements or head tags:
<link rel="canonical">,<link rel="alternate" hreflang>,<meta name="description">,og:*tags,<script type="application/ld+json">— the platform emits these, so a hand-written copy duplicates or contradicts them. Move the data intoseo/seo.meta/jsonldand delete the tag. (hreflangon the language switcher's<a>links is fine.)
- W6 SEO CONTAINER OFF: a schema whose documents are pages without
S — SITE WIRING (short; the other rules hold the detail): setup/web.headTags with store-file <script src>, <style> or absolute storage URLs; a hand-rolled cookie bar instead of setup/web.consent; third-party trackers with consent off; a page template without <main>; menu items with typed paths (L5).
I — IMAGE VARIANTS ("every card loads the full photo"). An image / gallery field declares its renditions in sizes (thumb, preview, card, mobile …); they are generated AT UPLOAD and stored on the value (variants: {name: {path, width, height}}). The renderer never fails on a missing one: {{cover.thumb}} silently falls back to the original and {{cover.srcset}} renders empty — so the site looks right while a listing of 24 articles downloads 24 hero-sized originals. The audit checks both halves: the variants are FILLED, and the places that show many pictures at once USE them. (auditProject images: variantGaps, loopOriginals, srcsetWithoutSizes, variantNames.)
- I1 DECLARED, NOT FILLED: a field with
sizeswhose stored values lack some or all of the declared variants (variantGaps: per shared schema, per-document schema and element placement — images, how many carry novariantsat all, the count missing per name, a sample path; SVG values are not counted — vector, no raster variants exist or are needed). Typical causes: the images were uploaded before the sizes were declared, picked from the Files browser, imported or generated withoutfieldKey+schemaId, or written as a bare URL string. Major when the field is shown in a listing, a card grid or a gallery (I2 then cannot be fixed by the template alone); minor otherwise. Fix: per image, the console's "Generate size variants" button on the field (gallery pictures too) fills them from the stored original; an agent re-imports the original (importImageFromUrlwith the image's public URL{storageBase}/{path}+fieldKey+schemaId) and writes the returnedimageValueback, keeping the value'smetadataandvariant— this makes a new file, the old one stays in Files. Every locale whose value is its own. - I2 LISTING RENDERS THE ORIGINAL: an
<img src>or CSSurl()inside{{#each}}— a collection listing, related items, a card grid, a gallery, a slider's thumbnails — bound to the bare field ({{this.cover}},{{this}}for a gallery picture,{{this.cover.original}}) with nosrcset(loopOriginals,where+expr). Major on a listing or gallery page, minor for a short list (≤ 4 items). Fix: bind the smallest sufficient variant (<img src="{{this.cover.thumb}}">), or the responsive pair (src="{{this.cover.thumb}}" srcset="{{this.cover.srcset}}" sizes="(min-width: 64rem) 33vw, 100vw"); a gallery shows{{this.thumb}}in the grid and links the full picture (<a href="{{this}}">— the lightbox target IS the original). When the field declares no variant that fits the card, add one tosizesfirst (F1), then I1 fills it. Also covers element fragments. - I3 VARIANT FIELD MISSING: a listing / gallery image field with no
sizesat all → F1 plus I2; a SECOND image field holding a smaller copy of the same picture (thumbbesidecover,image_mobilebesideimage) → one field with asizesvariant (schema-fields: ONE image field per picture), values migrated, the copy dropped in phase 3. - I4
srcsetWITHOUTsizes: an<img srcset="{{…srcset}}">with nosizesattribute — the browser assumes100vwand picks the largest rendition for a 300-px card (srcsetWithoutSizes). Minor. Fix: asizesattribute describing the slot's real width. - I5 PINNED VARIANT IN THE WRONG PLACE: a value whose
variantpins a small rendition (thumb) while a template renders{{field}}large (a detail hero) — the pin applies wherever{{field}}is printed. Minor; fix the listing to address.thumbexplicitly and drop the pin. Templates and elements that a MODULE owns are reported with the module named, never edited.
F — FIELD HYGIENE: F1 an image/gallery without max + sizes (a listing or gallery field without them is also I3); F2 a photo set as an array of {image, alt} instead of gallery; F3 an alt / caption text field beside an image (→ the image's metadata); F4 a text field whose values carry HTML tags without editor:"html"; F5 a long body as text instead of editor; F6 a FLAT schema used by exactly one document (→ per-document general/{collection}/{document}); F7 element samples that no longer validate.
- F8 SCHEMA NOT LOCKED: the SHARED schema of a content collection with documents — articles, recipes, products (a
base:"product"schema the catalog uses), services, team, FAQs — withoutlocked: true(auditProjectschemas.locks.unlocked) → every editor can add, rename or drop fields on one document and the collection drifts out of its template's shape. Minor; major for a collection editors fill daily (articles, products) on a handed-over site. Fix: writeSchemalocked: true(additive, phase 1); per-document page schemas are not a finding. - F9 REFERENCE WITHOUT A COLLECTION: a
referencefield (schemas, array items, element fields) withoutcollection_ref(auditProjectschemas.unscopedReferences, withtargets= the collections its stored values point at) → in every document the editor first picks the collection, then the document. Minor (major when the field is filled in every document — an author, a category). Fix: setcollection_refto the one collection the values point at (phase 1); whentargetsnames several collections, ask whether the field is really multi-collection before scoping it — a scoped picker keeps existing values from other collections but cannot pick them again.
SEVERITY: BLOCKER — something is broken or one console action destroys work (no design on a live site, submissions that cannot work). MAJOR — editors repeat work or cannot edit something safely, or every visitor pays for it (T1, L1, E1, E2, C1, D2, D3, D4, I1 and I2 on listings and galleries, J1 when big, J2 borrows, X1 for pictures / categories / references, X3, X4). MINOR — inconsistency that costs later (literals, ordering tricks, missing sizes, a missing share image). INFO — worth knowing, no action needed (unused library elements, module findings).
- REPORT — in the user's language, in this order, nothing invented (every finding cites a path you read):
- Header: project, env, locales, date; the inventory in one line (collections, documents read, schemas, elements, templates, translation groups, modules).
- A per-area verdict (D E O J T L X C G M W I S F): OK / n findings, worst severity.
- The findings, most severe first:
ID · severity · where (path, field, element) · evidence (the value / count you saw) · why (the rule) · fix (the concrete change and the tools) · effort (S/M/L) · reversible?. Group repeats (one T1 listing every same-value field ofkampusy, not twelve). - The FIX PLAN in phases: (1) additive and safe — translation groups, menu, Data groups, enum schemas, design
customtokens,reorder, schema locks (F8), reference collections (F9), missing imagesizes(F1 / I3),generalon neutral fields whose locales already agree (X1) and off on copy (X4); (2) migrations — move values, rebind templates, split elements, move code to its user or into one shared definition (J1–J3, J5), align drifted locale values (X2, X3), fill image variants (I1) before the listings are rebound to them (I2, I4), per collection and per locale; (3) cleanup — drop the old fields, delete one-off elements, picklist collections and unmatched shared code (J4). Phase 3 only after the user saw phase 2 working.
FIXING (phase 3 of the audit, on the user's pick): read every locale before writing; a code move (J) adds the code to its new home first, checks the affected pages in the Studio preview, and only then removes the old copy; build the NEW shape first (schema, enum, translation keys, element), migrate the values for EVERY locale, rebind the template with edits, then ask the user to check the Studio preview; removing the old fields, elements or collections is a separate, confirmed step (writeSchema confirmDrop, deleteStoreKeys, deleteStoreDocument — all the user's call, never in the turn that asked). On a store with a D2 finding fix the SEEDS first (from the sheet's values); commitDesign merges into the sheet, and removing its own rules is the user's call. Never touch module-owned elements, collections or the runtime. Re-run the affected checks after each fix and report the before / after counts.