inmake DOCS
Docs / Generative / Audit website

Audit website

The "audit-website" rule fragment — composed into the CRM & Studio agents' prompts and served as drop-core://rules/audit-website.

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.

  1. 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 (declared sizes the 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) and checkSite (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} (mask locales, name); setup/web; setup/design (mask onboarding, notes, version first; read css only for the D checks); setup/data; setup/elements (js/css sizes); readConsoleSettings; listCollections; listStoreModules; checkSite.
  • elements (whole library, ≤ 200); schemas (the SHARED schemas); every per-document schema a document names in its schema (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) and data/{locale}/translations per locale. Count placements while you read: walk every document value for {type:"element", element} (element fields AND editor blocks) and note WHICH DOCUMENTS place each element — count distinct documents, not locale copies.
  1. 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 no onboarding) on a site that renders pages → blocker.
  • D2 HAND-WRITTEN SHEET: css far 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 from setup/web.headTags. A hand-written sheet is ALLOWED and stays editable in the console (its Code tab, Code studio's design/system.css); it is not itself a finding. The finding is SEEDS THAT DO NOT DESCRIBE IT: values that contradict the sheet (--color-primary-500 nowhere near colors.primary, --color-surface not derived from neutral + 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 (auditProject design.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; auditProject design.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) while onboarding.vocabulary is absent (auditProject design.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 when sheetVocabulary is 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-N uses 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 in onboarding.custom → the Design page cannot show or edit them and an expansion drops them. List them grouped by prefix; the fix is onboarding.custom entries (group: "slot" for component slots) with a description.
  • D5 LITERALS in template / element / setup/elements CSS: hex / rgb() colours, typed box-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 — auditProject design.fontSources — no @font-face in the sheet, no font <link> in setup/web.headTags — a Typekit / Google Fonts stylesheet counts); D8 notes empty (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, a layout/variant enum or boolean that switches between differently built markups. Fix: split into one element per section (shared card/item markup as a component: true element 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 what t147 is. Fix: array of object items 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 / placeholder strings, inline brand SVGs / logos, demo data rows (mock UI screenshots built in HTML) → a field (icon for an SVG, image for 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 placement data into 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-header layer), a shared-runtime chunk — one bundled file, carousel.js — whose hooks only one unit carries. Fix: move the rules into that unit's css, scoped under its root class (readRule("css-scoping")), the chunk into its js (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 studio design/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 a window.X another element's script defines. Fix: what both need → ONE shared definition (CSS → the sheet's component layer or setup/elements.css; JS → setup/elements.js under one namespace with a re-entrant init(root?), or the footerTags site 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) and content (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 → its js; a shared engine → setup/elements.js; one template → its js; 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's css; the rest → the shared layer at the end of setup/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 text field 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, *_title of 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 (writeSchema confirmDrop, 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 root link beside 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 kind text that the template turns into href="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 in label ("+420 777 123 456"); a list of them is an array of that object (+ icon).
  • L3 URL IN A TEXT FIELD: a text value starting http, /, mailto:, tel: or a storage URL → kind link (or file), declaration first, then the value (VALUES MUST VALIDATE).
  • L4 DOWNLOAD AS A LINK: a link/text holding 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, a shell.menu item 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 without general whose 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: true plus localized: true on those metadata entries. textValue: true = a text field 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): writeSchema general: true on 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 flip general while 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): a text / editor field (or an object holding text) declared general on 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: writeSchema general: false (phase 1 — every locale doc already holds its own copy, nothing is lost), then translate per locale (phase 2).
  • X5 general BELOW 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 root gallery instead of items[].image).
  • A single-language site (locales.singleLocale): the split changes nothing today — list the neutral fields without general as 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, maybe color/icon/a one-line description), no page of its own (no seo.slug, no web template, not routed), referenced by another collection's reference field or by a collection_ref query. Decide with the user:
    • a fixed set of values without pages → an ENUM schema (writeSchema type:"enum", entries {key, name}) and a kind:"enum" field (enum_ref) — an array with item_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's value (else its key) and the renderer prints exactly that — an entry's name is the console label, not localized copy; keep value unset 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.categories on the routed collection + each document's seo.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.
  • C2 FREE-TEXT CLASSIFIERS: category/tag/type as text (values drift: "Novinka", "novinky") → enum.
  • C3 DOUBLE CLASSIFICATION: a document carrying the same classification twice (an enum category AND a reference to 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 gallery on the category's document (or on one document per category).
  • C5 ORDER BY HAND: an order/position/sort number field, or zero-padded ids (01-…) used for sequencing → registerCollection reorder: true (readRule("data-collections") ORDERING).
  • C6 PRESENTATION IN DATA: bg_color / text_color hex 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: writeDataGroups replaces the group list and sets each group's members (collection keys and custom collection ids) in one call, touching nothing else in setup/data; registerCollection groups places one collection. Never write setup/data by hand — its collections is 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 / components collections 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 as document items; custom items for saved views of setup/data.custom), the module pages they operate. M4 invalid items (readConsoleSettings warnings: unregistered collection, missing form or module page). Fix: groups by the same contexts as G, labels in the client's language, icon per 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 no seo.title shows 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.jsonld without an Organization / LocalBusiness (name, url, logo, contact, address, sameAs profiles) and a WebSite record, or one missing those properties; urls pointing at a domain the site is not served on. Minor.
    • W4 SITE META: og:site_name / twitter:card missing from setup/web.seo.meta; site-wide og:* tags hand-written in headTags (they then sit beside the generated ones and repeat on every page — an article's og:type collides with a hard-coded website). Move them to seo.meta; never og: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 from setup/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 answers Disallow: / — so a published site with no custom domain is not in search at all (info: say so).
  • 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. (checkSite seo.noContainer lists 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. checkSite titles lists 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: checkSite excerpt: true gives 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.image on 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 without og:type: product; meta entries that copy generated tags (og:title = the seo.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 jsonld on its documents (the schema's jsonld container off): articles / news / blog → Article / NewsArticle / BlogPosting; recipes → Recipe; an FAQ → FAQPage; products → Product with Offer; services → Service; people → Person; events → Event; branches / clinics / shops → LocalBusiness subtypes; jobs → JobPosting. Enable the container (writeSchema jsonld) 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 a headline / name / price / date that no longer matches the document is a finding (auditProject jsonld.stale compares headline / name with the document's titles; prices and dates you compare yourself); a url on 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 into seo / seo.meta / jsonld and delete the tag. (hreflang on the language switcher's <a> links is fine.)

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 sizes whose stored values lack some or all of the declared variants (variantGaps: per shared schema, per-document schema and element placement — images, how many carry no variants at 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 without fieldKey + 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 (importImageFromUrl with the image's public URL {storageBase}/{path} + fieldKey + schemaId) and writes the returned imageValue back, keeping the value's metadata and variant — 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 CSS url() 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 no srcset (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 to sizes first (F1), then I1 fills it. Also covers element fragments.
  • I3 VARIANT FIELD MISSING: a listing / gallery image field with no sizes at all → F1 plus I2; a SECOND image field holding a smaller copy of the same picture (thumb beside cover, image_mobile beside image) → one field with a sizes variant (schema-fields: ONE image field per picture), values migrated, the copy dropped in phase 3.
  • I4 srcset WITHOUT sizes: an <img srcset="{{…srcset}}"> with no sizes attribute — the browser assumes 100vw and picks the largest rendition for a 300-px card (srcsetWithoutSizes). Minor. Fix: a sizes attribute describing the slot's real width.
  • I5 PINNED VARIANT IN THE WRONG PLACE: a value whose variant pins 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 .thumb explicitly 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 — without locked: true (auditProject schemas.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: writeSchema locked: true (additive, phase 1); per-document page schemas are not a finding.
  • F9 REFERENCE WITHOUT A COLLECTION: a reference field (schemas, array items, element fields) without collection_ref (auditProject schemas.unscopedReferences, with targets = 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: set collection_ref to the one collection the values point at (phase 1); when targets names 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).

  1. 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 of kampusy, not twelve).
  • The FIX PLAN in phases: (1) additive and safe — translation groups, menu, Data groups, enum schemas, design custom tokens, reorder, schema locks (F8), reference collections (F9), missing image sizes (F1 / I3), general on 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.