https://vocalcom.colibrity.com - graded against every rule this standard publishes. The rules this project does not meet are listed first.
80 pass 14 fail 70 unverified 11 n/a 0 waived
Unverified is not a pass. It is the verdict for a rule nobody has checked yet, and each one below carries what would settle it. On a build still in progress most of the list is expected to sit there; a rule moves out of it by being measured, not by being assumed.
Not a plain pass
| Result | Rule | Title | Reason |
|---|---|---|---|
| fail | a11y.axe-clean |
No automated accessibility violations | 7 of 7 page(s) scanned by axe at WCAG 2.1 A and AA, at rest under reduced motion - / color-contrast x38 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /blog/ color-contrast x26 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /contact/ color-contrast x26 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /deploying-without-drama/ color-contrast x26 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /design-system/ color-contrast x90 (.mega-nav__demo > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /product-page/ color-contrast x44 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1) |
| fail | a11y.contrast |
Text and interface contrast meets AA | Measured, and open with Colibrity as this project's own recorded decision. The primary button ships Figma's own Button/Text Light #f7f5f2 on Corail at 3.17:1 against the 4.5:1 this rule asks for, and Figma's hover and pressed fills LIGHTEN, taking it to 2.91 and 2.64 - under even the 3:1 floor for UI. tests/contrast.test.mjs pins all three ratios, so the moment the palette moves the suite goes red. A fix exists (Marine text at 5.82:1, which also makes the ramp run the right way) and was deliberately reverted: changing how a client's primary button looks is Colibrity's call. Two more pairs joined it on 2026-08-28, both drawn on the services-1 homepage section and both reported to Colibrity on /design-system/ rather than corrected: the card's audience label is the text button's hover orange on white at 2.55:1, and the card's outline pill puts base Corail on white at 3.45:1. Same open decision, more instances. A third joined on 2026-09-25, measured by axe on every page with a footer: the footer's column links are Figma's muted-soft #6c667f on the panel's 80% white over Marine, which composites to #ceccd4 at 3.43:1. Figma's colour, so reported rather than corrected. Not waivable - this is a blocker - so it stands as failing until Colibrity answers. The decision is recorded in the theme repository, which is not a document anybody outside the build can open, so it is written out here rather than referred to by number. |
| fail | delivery.real-content |
No placeholder content survives to launch | 42 URL(s) from the sitemap index at /sitemap_index.xml and its 4 readable child sitemap(s): 42 carry placeholder content - /blog/ (ipsum, placeholder); /deploying-without-drama/ (ipsum, placeholder); /motion-that-respects-people/ (ipsum, placeholder); /forms-that-fail-well/ (ipsum, placeholder); /fluid-type-without-tears/ (ipsum, placeholder); /the-lazy-loading-trap/ (ipsum, placeholder), and 36 more - tried /sitemap_index.xml, /wp-sitemap.xml, /sitemap.xml, /sitemap_index.xml; /blog/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; /deploying-without-drama/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; /motion-that-respects-people/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; /forms-that-fail-well/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; scope: visible text, alt/title/aria-label/aria-description/aria-placeholder, description metas and application/json islands for the words; src/srcset/data-src/poster, rel=preload as=image and url() in style attributes and <style> blocks for the assets. Comments and <template> bodies were blanked first. Both lists are FIXED: /\b(lorem|ipsum|placeholder)\b/ case-insensitive, /\b(TODO|FIXME)\b/ as written, /(demo-image|dummy|placeholder|lorem|ipsum)/ on filenames. A stylesheet served from its own file is not read |
| fail | design.contrast-in-mockup |
Contrast is proven in the mockup, not found in the browser | The design-side pair of a11y.contrast, which is already an open blocker on this project: the primary button ships Figma's own light text on Corail at 3.17:1 against the 4.5:1 this rule asks for, and the hover and pressed fills lighten to 2.91 and 2.64. A fix at 5.82:1 exists and was reverted, because changing a client's primary button is Colibrity's call. The services-1 section adds two more of the same pairing on 2026-08-28 - a 2.55:1 card label and a 3.45:1 pill label, both orange on white - so the design-side finding is now a pattern in the file rather than one component. Grading this as unverified while its measured twin fails would understate a defect we have measured. |
| fail | design.motion-spec |
Every animated surface has a defined motion spec | The handoff file carries NO motion sheet and no motion data at all, measured 2026-08-28: get_motion_context on the whole Home section (767:26121, recursive) returns an empty node list, and the file's only motion expressions are two text annotations (the hero waveform's "animation with code"; the primary button hover naming osmo button pack 077, a reference rather than values). The project's own decision record carries the finding. Mitigated on our side, not fixed on theirs: DESIGN.md's motion key now holds the sheet - the standard's eight surfaces plus the waveform and marquee, each with trigger, duration in ms and named easing or a stated not-applicable - rendered at /design-system/#motion and pinned to the shipped code literals by tests/motion.test.mjs. The rule grades THEIR file, so it stays failing until Colibrity adds the sheet there; ours is the draft they are welcome to copy. |
| fail | forms.anti-spam |
Forms carry three layers of spam defence | Measured 2026-09-25: two of the three layers. The honeypot is on and stores nothing when filled. The per-IP rate limit is built (inc/forms-core.php, vocalcom_forms_rate_limited(), 5 accepted submissions per 10 minutes across every form, keyed on an HMAC of the address): six POSTs from one IP to the local /wp-json/vocalcom/v1/contact answered 200 five times and 429 the sixth, with 5 messages and 5 consent records stored, the refused POST writing none; the same on staging in Chrome. tests/forms-rate-limit.test.mjs pins it. What still fails is the last clause: the scored gate is off ('recaptcha' commented out in functions.php, no key), and inc/recaptcha.php passes on a transport error, so an unreachable verifier is not a refusal. Turning the gate on and failing it closed is on the production list. |
| fail | html.buttons-vs-links |
A link goes somewhere, a button does something | Measured 2026-09-25 on the seven sampled templates at https://vocalcom.colibrity.com . The markup half passes: 0 clickable div or span, 0 role=button on a non-button, and every control reached and operated with Tab and Enter in the keyboard pass (a11y.keyboard-operable). What fails is resolving hrefs: 278 anchors per page carry href="#" - the mega nav's dummy tabs (102 links, 21 cards, 6 banners and their 102 mobile twins), 43 footer column links, 2 footer legal links and the Request Demo button. They are the placeholder navigation STATUS 2e records, waiting on Colibrity's sitemap, and they are the reason this is a fail rather than a pass: a link that goes nowhere is exactly what the rule is about. |
| fail | onpage.core-fields |
Core CMS fields carry the content, not only the builder | 2 public type(s) read on colibrity-staging - post: 7 of 13 with no excerpt; page: 30 of 30 with no featured image (page does not support excerpt) |
| fail | ops.fatal-alerts |
A fatal error reaches a mailbox somebody actually reads | the fatal-error notice goes to an address on vocalcom.test (from admin_email), a reserved domain no mailbox is read on |
| fail | perf.enqueue-hygiene |
Every asset loads once, has content, and only where used | Read in Chrome on staging 2026-09-25 across the seven sampled templates: 8 to 23 resource hints in the head against the limit of 4 (most are Vite modulepreloads for the block chunks; 23 on the home page, 20 on the product page); one script under 100 bytes (block-text-anim, 90 bytes, on /contact/); no URL requested twice except reviews- demo.mp4 on the home page, which is two media range requests. The library clause was read in inc/vite.php: per-page block stylesheets and a registry chunk loaded only for the layouts a page renders. |
| fail | perf.image-formats |
Images ship in a modern format at the size they display | /blog/ the image audits estimate 150 ms off LCP or FCP (13 KB) - 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, image audits read on every run, flagged images re-measured in Chrome at 412 x 823, 1.75x: / 15 KB of estimated savings that move no metric; /blog/ 13 KB of estimated savings that move no metric; /contact/ no savings; /privacy-policy/ no savings; /deploying-without-drama/ 31 KB of estimated savings that move no metric; articles-1-768x544.jpg.webp fits its 364x258 slot at 1.75x (768x544); Lighthouse's 31 KB is lighthouse#17080; /design-system/ no savings; /product-page/ 15 KB of estimated savings that move no metric |
| fail | perf.lcp |
LCP at or under 2.5 seconds on mobile | 4 template(s) over the line (/, /blog/, /deploying-without-drama/, /product-page/) - 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 2.84 s (2.75 s-3.12 s) LCP div.block-hero__media > img.block-hero__image; /blog/ 3.37 s (3.36 s-3.39 s) LCP div.post-card__media > img.post-card__image; /contact/ 1.65 s (1.65 s-2.73 s) LCP div.block-text__body > p; /privacy-policy/ 1.50 s (1.35 s-2.53 s) LCP div.legal-page__section-body > p; /deploying-without-drama/ 2.77 s (2.38 s-2.81 s) LCP figure.post-single__media > img.post-single__image; /design-system/ 2.48 s (2.10 s-2.50 s) LCP div.sg-intro > p; /product-page/ 2.87 s (2.38 s-2.91 s) LCP div.block-hero-b__media > img.block-hero-b__poster |
| fail | perf.video-weight |
Video is deferred and never blocks the first paint | /product-page/ reviews-demo.mp4 fetched 236 KB before LCP, on the critical path - 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, video read off every run and off the served page: / 1 video file(s) under 2 MB; /blog/ no video on the page; /contact/ no video on the page; /privacy-policy/ no video on the page; /deploying-without-drama/ no video on the page; /design-system/ no video on the page; /product-page/ 1 video file(s) under 2 MB |
| fail | security.csp-enforced-baseline |
A Content-Security-Policy is enforced, not only reported | 7 sampled page(s) read with curl - / sends no report-only header beside the enforcing one; / enforcing header lacks object-src; / enforcing header lacks base-uri; / enforcing header lacks form-action; / enforcing header lacks frame-ancestors; /blog/ sends no report-only header beside the enforcing one; /blog/ enforcing header lacks object-src; /blog/ enforcing header lacks base-uri; /blog/ enforcing header lacks form-action; /blog/ enforcing header lacks frame-ancestors; /contact/ sends no report-only header beside the enforcing one; /contact/ enforcing header lacks object-src; /contact/ enforcing header lacks base-uri; /contact/ enforcing header lacks form-action; /contact/ enforcing header lacks frame-ancestors; /privacy-policy/ sends no report-only header beside the enforcing one; /privacy-policy/ enforcing header lacks object-src; /privacy-policy/ enforcing header lacks base-uri; /privacy-policy/ enforcing header lacks form-action; /privacy-policy/ enforcing header lacks frame-ancestors; /deploying-without-drama/ sends no report-only header beside the enforcing one; /deploying-without-drama/ enforcing header lacks object-src; /deploying-without-drama/ enforcing header lacks base-uri; /deploying-without-drama/ enforcing header lacks form-action; /deploying-without-drama/ enforcing header lacks frame-ancestors; /design-system/ sends no report-only header beside the enforcing one; /design-system/ enforcing header lacks object-src; /design-system/ enforcing header lacks base-uri; /design-system/ enforcing header lacks form-action; /design-system/ enforcing header lacks frame-ancestors; /product-page/ sends no report-only header beside the enforcing one; /product-page/ enforcing header lacks object-src; /product-page/ enforcing header lacks base-uri; /product-page/ enforcing header lacks form-action; /product-page/ enforcing header lacks frame-ancestors |
| unverified | a11y.alt-text |
Images carry alt text that earns its place | Half measured 2026-09-25: 0 images without an alt attribute on all seven sampled templates at https://vocalcom.colibrity.com (21 to 111 images each), and axe's image-alt rule passed on every one. The other half - that reading the alt text conveys what the picture does - cannot be judged while the images and their alt text are placeholders; it waits on Colibrity's copy and imagery. |
| unverified | a11y.media-alternatives |
Video and audio carry captions and controls | A caption mechanism, which does not exist: `<track>`, "captions" and "subtitles" appear nowhere in template-parts or src. The hero background video is muted and decorative, but the video block and webinar recordings can both carry speech, so this is a real gap rather than a technicality. |
| unverified | a11y.screen-reader-pass |
One page and one form are driven with a screen reader | Not checked against this build. Settled by: the report names the screen reader and browser used, lists the headings and landmarks in the order they were announced, and confirms the invalid submit was announced. Zero unlabelled controls were met on the two pages. |
| unverified | a11y.statement |
The site publishes an accessibility statement | A published accessibility statement naming the target, the known gaps and a contact route. None exists, and the open contrast blocker above means there is already a known gap to name. |
| unverified | a11y.zoom-and-reflow |
The page survives 200 percent zoom and a 320px viewport | A pass at 200 percent zoom and at a 320px viewport across the built templates. Not done. |
| unverified | admin.editor-language |
The admin reads in the language the editor set | Not checked, and it matters more here than anywhere: this project is four regions by four languages. Settled by: switching the wp-admin display language switches the labels of the site's own post types, columns and meta boxes. No slug, meta key or field name changes with it. |
| unverified | admin.one-control-per-setting |
One control writes one setting | Not checked. Settled by: for each setting the client can change, exactly one control in the admin writes it. Where a plugin supersedes a theme feature, the theme's control is not rendered while that plugin is active, checked with the plugin on and off. This theme has both a Theme Settings page and per-module tabs appended to it, which is one page, but the Yoast overlap is now the live case: Yoast has been the SEO engine since 2026-09-02 and functions.php:268-276 stands the theme's own engine down while it is active, but whether the Theme Settings SEO tab is also withheld while Yoast is on has not been looked at. |
| unverified | admin.translation-declared |
Every field and every fixed string says how it translates | Field half proved: inc/acf-fields.php gives every registered field a wpml_cf_preferences value and tests/wpml-acf-prefs.test.mjs (4 of 4) fails the suite if one is missing. String half: theme strings route through vocalcom_t() and String Translation, and node bin/lang-parity-check.mjs (2026-09-25, local) reads no default-language block copy on the translated pages beyond its three written exceptions. Open: a scan of each secondary-language template for fixed theme strings still in English. |
| unverified | colibrity.js-hooks-are-data-attributes |
JavaScript targets data attributes, never CSS classes | 254 DOM quer(ies) across 83 script(s), 17 state class(es) the scripts set themselves, none by a styling class among those read, but 12 selector(s) are built at runtime and were not read: src/js/blocks/block-features-anim.js:435, src/js/blocks/block-reviews-anim.js:319, src/js/core/entrance-store.js:110, src/js/modules/dialog-a11y.js:32, src/js/modules/form-validate.js:288, src/js/modules/head-sync.js:99, src/js/modules/head-sync.js:100, src/js/modules/header-metrics.js:18, src/js/modules/header-scroll.js:128, src/js/modules/video-tile.js:157, src/js/modules/video-tile.js:166, src/js/modules/waveform-lab.js:29 |
| unverified | comms.delivery-alerting |
A broken form or CRM connection raises an alert | Not checked against this build. Settled by: break the CRM credential on purpose and submit once. An alert naming the site and the failure reaches the named recipient within 24 hours, and the handover names that recipient and the date they took it on. |
| unverified | comms.email-authentication |
Automated mail arrives with SPF, DKIM and DMARC passing | DNS read 2026-09-25: vocalcom.colibrity.com, staging's sending domain, publishes no SPF and no DMARC; colibrity.com publishes SPF (include:_spf.google.com) and DMARC p=none. The production domain's records are not set up yet. Open: a dated Authentication-Results header from a real send. |
| unverified | comms.email-rendering |
Transactional emails render in the clients people use | Not checked against this build. Settled by: the same message is opened in Gmail, Outlook and iPhone Mail, with a dated screenshot of each: no broken layout, no clipping notice, every link working. The raw source carries a text/plain part. |
| unverified | comms.sender-identity |
Automated email carries a correct From, Reply-To and language | Read 2026-09-25 on staging: notifications go From WordPress <wordpress@vocalcom.colibrity.com>, WordPress's defaults, with Reply-To set to the submitter (inc/rest-contact.php step 8). The production sending domain and name are not decided, and there is no autoresponder. Open: a received notification from each locale, read for its From, Reply-To and language. |
| unverified | crawl.breadcrumb-trail |
Every page below the home page shows one breadcrumb trail | Not checked against this build. Settled by: each sampled page below the home page shows exactly one trail, its root item in the page's language, its last item not a link, and its BreadcrumbList lists the same items in the same order under the same names. The Rich Results Test reports 0 BreadcrumbList errors. |
| unverified | crawl.indexability-decided |
Every URL-minting template has a recorded index decision | docs/EDITOR-HANDOVER.md section 11 carries the dated table (2026-09-25) of every URL- minting template and non-HTML route, measured on the local site: every noindex row answers noindex (date archives, search, the two dev pages; /wp-json/ and the sitemaps by X-Robots-Tag) and none of them is in a sitemap. Open: two rows have no decision yet - /elements/ is indexable today, and /blog/?category=<one> is indexable while duplicating /category/<one>/. |
| unverified | crawl.internal-reachability |
Every indexable page is reachable by a static HTML link | Not checked against this build. Settled by: the raw-HTML crawl reaches every URL in the sitemap: 0 orphans. No indexable page sits more than 3 clicks from the home page. A page found only in the sitemap counts as an orphan, not as reachable. |
| unverified | crawl.search-console |
Search Console is verified and the build team has access | Not checked against this build. Settled by: a dated screenshot taken from the build team's own Google account with the Coverage report open on the production property. Requested at kickoff, not at launch. After go-live the same account shows the sitemap submitted, with a read date. |
| unverified | crawl.slug-change-redirect |
A renamed slug leaves a redirect from its old URL | Not checked against this build. Settled by: every URL in the sitemap snapshot taken before the restructure, plus every old URL rebuilt from _wp_old_slug, returns 200 or exactly one 301 to a 200. 0 of them 404, and 0 chains of two hops or more. |
| unverified | crawl.url-format |
Slugs are readable, hyphenated and in the page's language | Not checked against this build. Settled by: every URL in the crawl is under 115 characters, separates words with hyphens rather than underscores, carries no numeric id or date segment, and is in the same language as the page it serves. 0 exceptions across the sampled templates. |
| unverified | delivery.browser-matrix |
Every template is opened in all three browser engines | Not checked against this build. Settled by: every template on the template list has been opened in Chrome, Firefox and desktop Safari - three engines, none checked in fewer - and each difference found is either fixed or written down. |
| unverified | delivery.client-handover |
The client leaves handover able to run the site | Not checked against this build. Settled by: the client has a dated recording of the session covering editing content, adding a page and what must not be touched, naming at least 2 attendees. The wider account table is not graded anywhere since ops.account-inventory was withdrawn - raise it, do not check it. |
| unverified | delivery.editor-test |
The admin has been tested by someone who did not build it | The kit exists and the test has not been run: docs/editor-test/task-script.md is the script and docs/editor-test/observation-sheet.md the per-tester sheet, and no filled sheet exists. Settled by: the handover carries a dated observation sheet naming the tester, the tasks attempted and each point of friction with what was done about it. The tester is not a developer on the build. |
| unverified | delivery.monitoring-and-backups |
Uptime monitoring and a restorable backup are live at launch | Not checked against this build. Settled by: the monitor's public status URL shows checks on the home page at five minutes or better, and the handover names the person it alerts and the date they took it on. If nobody will own it, the handover records that and it is not ticked. |
| unverified | design.mockup-coverage |
Every unique section and page has its own mockup | Not checked against this build. Settled by: on the handoff date, the frame count in the file matches the sections and page types in the agreed sitemap: 0 sections and 0 page types without a named frame. Record both counts and that date in the handoff message. |
| unverified | design.naming-convention |
Layer and component names are agreed with the developer | Not checked against this build. Settled by: the kickoff note lists the agreed name of every component and section frame and is countersigned by the developer. At handoff the names in the file are that same list: 0 renames between the agreed list and the delivered file. |
| unverified | design.state-set |
The design specifies every state of every interactive element | Not checked against this build. Settled by: every interactive component in the kit has all six states present as variants, with 0 components missing one. The focus variant is identical across components and matches the kit's single focus style. |
| unverified | design.text-over-media |
Text over imagery carries a scrim the design specifies | Not checked against this build. Settled by: the mockup names the overlay colour and opacity, or the gradient stops, for every text-over-media block. A dated export shows each block over the lightest and darkest plausible photo, body copy at 4.5:1 and display text at 3:1 in both. |
| unverified | design.ui-kit |
The UI kit is built and published before the build starts | Not checked against this build. Settled by: colibrity shares a published library URL at kickoff. Opening it, every colour and text value used in the mockups resolves to a published variable or style, and the file holds 0 detached instances of a kit component. |
| unverified | env.deploy-provenance |
What production is running is a commit you can name | Not checked against this build. Settled by: the live site exposes an identifier that resolves to one commit on the default branch of the repository handed over. A deployment naming no commit anywhere in its response or in a stamped file does not pass. |
| unverified | env.env-config-not-in-db |
Environment switches are enforced in code, not in the database | Measured 2026-09-25 on colibrity-staging: wp eval prints VOCALCOM_ENV staging, which mu- plugins/vocalcom-env.php derives from a wp-config.php constant, never a database row; WP_DEBUG_DISPLAY is false in wp-config.php; the gate's perf.third-party read zero third- party requests on all seven templates. The 2026-08-30 local-to-staging database push left staging still reporting staging. Open: the clause is a production database pushed onto staging, and there is no production host yet. |
| unverified | env.no-production-data-off-production |
Production personal data does not travel to staging or a laptop | Waiting on a production host. There is none yet, so there is no production personal data that could travel; the rule becomes checkable the day production takes its first submission. |
| unverified | env.staging-parity |
Staging matches production where the checks depend on it | no production host was supplied, so staging cannot be compared with it |
| unverified | env.staging-sends-nothing-live |
Staging never writes to a production system | Page half measured 2026-09-25: zero analytics, tag or advertising requests on all seven sampled templates (the gate's perf.third-party). A marked gate probe submission on staging stored nothing and sent nothing (message and consent counts 1 and 1 before and after). Nothing reaches a CRM: there is no connector and the vocalcom_form_submitted seam has no listener. Open: a real staging submission arriving in a test inbox. |
| unverified | forms.confirmation |
A successful submit produces a confirmation nobody can miss | Not checked against this build. Settled by: after a successful submit, focus is on the confirmation, a screen reader announces it, and it is still on screen 30 seconds later. Where it is a thank-you page, the handover names its URL. |
| unverified | forms.value-formats |
Email and phone are checked without rejecting valid values | Not checked against this build. Settled by: test@example is refused, test@example.com is accepted, and no address in the reviewer's list of 10 valid addresses is refused. +33 6 12 34 56 78 is accepted and stored as +33612345678. |
| unverified | html.lists-and-tables |
Lists are lists and tables are tables | An axe run. The standard's own site failed this on all 25 of its table headers, so it is not a safe assumption. |
| unverified | html.no-duplicate-ids |
Ids are unique on the rendered page | none of the 7 template(s) could be validated: the validator answered 429 |
| unverified | html.valid-markup |
Every template produces valid HTML | none of the 7 template(s) could be validated: the validator answered 429 |
| unverified | legal.consent-record-context |
Consent records carry the context of the consent | Not checked against this build. Settled by: two consents captured by one submit produce two records with different purposes. Each record names the URL it was given on and a collection method matching how it was actually collected, not a default string. |
| unverified | legal.consent-retention |
The consent log is exportable and has a retention period | Not checked against this build. Settled by: the period is stated in writing by the client or their DPO and appears in the privacy policy. An export of the log produces a file carrying every stored key. A record aged past the period is gone after the scheduled job has run once. |
| unverified | legal.documents-published |
The privacy policy is a real page, linked where data is taken | Measured 2026-09-25 on https://vocalcom.colibrity.com : /privacy-policy/ returns 200 on its own permalink, the footer links it on every sampled page, and both forms that collect personal data link it from their consent label (/contact/ and the register form on /product-page/). The footer's subscribe inputs and the hero phone field submit nowhere yet (src/js/modules/inline-form-validate.js dispatches an event and stops), so they collect nothing; they need the link when they are wired. Two inputs outstanding: staging is noindex by environment (x-robots-tag: noindex, nofollow, mu-plugins/vocalcom-env.php), so the no-noindex half can only be read on production, and Colibrity has not confirmed in writing that the text is the version their counsel approved. |
| unverified | migration.redirect-map |
Every inventoried URL reaches its replacement in one hop | no old-site URL inventory was supplied, so redirects could not be mapped |
| unverified | migration.url-inventory |
The old site's indexed URLs are counted before anything is built | Waiting on the redirect map's scope. A crawl of vocalcom.com plus Colibrity's Search Console export, reconciled into one dated list by language and template, is the input; neither has been started or supplied. |
| unverified | onpage.site-identity |
The site's identity is set on purpose and the share card is seen | inc/seo-defaults.php:47 makes the tagline the homepage hero heading, Figma's own copy, and :84 adds it to the front-page title; inc/seo-yoast.php:145-148 merges the Theme Settings social links into the Organization node's sameAs. Whether Colibrity has set the site title, the tagline and the separator on staging, which official accounts belong in sameAs, and the rendered share card in three platforms are theirs and undone: the project's decision record says Colibrity writes the copy. Settled by: the home page prints a deliberate title and tagline, every page uses one separator, the Organization node lists each official account in sameAs, and a dated screenshot of the home and one article preview exists for each of the three platforms. |
| unverified | ops.backup-coverage |
Backups cover the database and the uploads, on a stated schedule | Not checked against this build. Settled by: a dated screenshot of the backup listing shows a database backup under 24 hours old and a file backup under 7 days old. The handover states retention in days and names the copy held outside the hosting account. |
| unverified | ops.cron-runs |
Scheduled tasks actually fire on the production host | 21 scheduled event(s) on colibrity-staging at 2026-09-25T15:29:29Z, DISABLE_WP_CRON not set, none overdue - but DISABLE_WP_CRON is not set, so WP-Cron runs on visits and this run's own requests drained the queue before it was read |
| unverified | ops.db-write-backup |
No write to a live database without a fresh dump of what it replaces | Not checked against this build. Settled by: every path in the repository that writes to a remote database dumps it first, stops when that dump fails a size and gzip check, and prints the restore command. The dump from the last production write is on disk, dated. |
| unverified | ops.recovery-drill |
The site has been restored and rolled back, once, on purpose | Not checked against this build. Settled by: the handover carries dated terminal output from two runs: a restore of a backup no more than 7 days old into a non-production target, ending with that target's home page returning 200, and a rollback to the previous release. A tick with no output does not settle it. |
| unverified | perf.animation-cost |
Animation is cheap and stops when off screen | A read of the GSAP effects and scroll modules for properties other than transform and opacity, and for animation that keeps running off screen. |
| unverified | perf.optimiser-output-fresh |
Optimiser output is regenerated and state classes safelisted | Not checked against this build. Settled by: every class toggled from JavaScript is present in the production stylesheet: open the menu, a modal and one filter on the live URL and confirm the styling applies. The critical CSS is no older than the build it shipped with. |
| unverified | perf.split-points-asserted |
Every deliberate split point is asserted in the build | bin/assert-chunks.mjs reads the build manifest at :27, positive assertion at :33, negative at :37, exits non-zero at :44; run by `deploy`, `deploy:prod`. Whether the build actually FAILS when either assertion is broken cannot be established by reading, and this gate may not break one: breaking an assertion is a write into the repository under review and watching a build fail is a build. To settle it a person widens the allow-list (or empties the required-chunk list) on a scratch branch, runs the project's own build, confirms it exits non-zero, and reverts. Everything a reader can settle is met: scripts calling assert-chunks.mjs: build:check (runs), chunks:check (runs), deploy (runs), deploy:prod (runs); scripts that build or deploy: build, deploy, deploy:prod, release, release:prod; shipping files carrying it: none; dist/.vite/manifest.json declares 45 dynamic import(s); 13 dynamic import(s) in 83 source file(s); the clause that no module is bundled into two chunks is not measured by this check |
| unverified | practices.empty-states |
Every dynamic component handles empty, long and broken | The four self-filling region blocks are queried through vocalcom_region_query() so they are never empty by construction, and tests/region-blocks.test.mjs fails if a block builds its own WP_Query. The authored blocks have not been exercised with zero items, one, a very long title or a missing image. |
| unverified | practices.mobile-viewport-chrome |
The layout survives the phone's real viewport | Not checked against this build. Settled by: on a real phone at 390x844: no section clipped by the browser toolbar in either orientation, no text or control under the notch or home indicator, and a sticky header at 15 percent of viewport height or less in landscape. |
| unverified | practices.no-hardcoded-content |
Editable content is editable, not baked into templates | A read of the templates for copy, images, links or order baked in rather than coming from fields. inc/block-defaults.php deliberately ships default copy through ACF default_value, which is the right mechanism, but the audit has not been done. |
| unverified | practices.one-change-per-deploy |
Performance and behaviour changes ship one at a time | Judgement over the release history rather than a check. Recommended, so it never blocks. |
| unverified | practices.real-device-check |
Anything animated or video-driven is checked on a real device | A pass on a real iPhone and a real mid-range Android. The theme has GSAP animation and autoplay video, so it is squarely in scope. Not done. |
| unverified | practices.responsive-breakpoints |
The layout holds at every width, not just the design widths | Partially measured 2026-08-27 during the scaling rework in the project's decision record: the front page and /design-system/ at 375, 768, 1440, 1710, 1920, 2560 and 3840, horizontal scroll asserted programmatically (scrollWidth vs clientWidth) at every stop and zero found, with the root ladder and the 75.5 percent column verified at each rung. Still owed before this reads as proof: 320 itself, the inner templates, and an overlap pass beyond the two pages checked. |
| unverified | security.admin-accounts-at-handover |
The administrator list is named and owned before handover | Not checked against this build. Settled by: wp user list --role=administrator names no account called admin and no build account above editor, and a dated written confirmation from the client - an email reply or a countersigned handover line - claims every remaining administrator. |
| unverified | security.no-error-disclosure |
No response leaks a PHP error, a stack trace or a server path | Measured 2026-09-25 on staging: none of the seven sampled pages carries Fatal error, Warning:, Notice:, Stack trace or the host's absolute path; wp-config.php defines WP_DEBUG_DISPLAY false (WP_DEBUG and WP_DEBUG_LOG are on), and /wp-content/debug.log answers 403. Open: a deliberate fatal returning 500 without those strings, which needs a write to staging. |
| unverified | security.public-post-rate-limit |
Login and every public form endpoint are rate limited | Not built in the theme: no login throttle exists in the theme, its mu-plugins or its plugins (git grep for a 429 or a per-IP counter finds only the analytics transport). Whether Hostinger's LiteSpeed limits wp-login.php was deliberately not measured on 2026-09-25: twenty wrong-password POSTs risk blocking the reviewer's IP. Building the limit is on the production list, with the form rate limit and the enforcing CSP. |
| unverified | security.versions-current |
Core, plugins, themes and PHP are current, and updates have an owner | Read 2026-09-25 on staging: wp core check-update 0; two plugin updates available (ACF Pro 6.8.9 to 6.8.10, Yoast SEO 28.4 to 28.5); web PHP is 8.3, under php.net security support to the end of 2027, while the host's CLI PHP is 8.0, past its end of life. Judged on handover day, so these counts are a reading, not a verdict. Open: the handover naming who applies updates for twelve months, which is the client's. |
| unverified | seo.headings-describe-content |
The heading outline reads as an outline | A read of the heading outline once the copy is real. Recommended, so it never blocks. |
| unverified | seo.open-graph |
Open Graph and Twitter card tags are complete | 7 of 7 sampled page(s), 2 distinct og:image, 0 fault(s), but og:url could not be compared: / has no canonical to compare og:url against; /blog/ has no canonical to compare og:url against; /contact/ has no canonical to compare og:url against; /privacy-policy/ has no canonical to compare og:url against; /deploying-without-drama/ has no canonical to compare og:url against; /design-system/ has no canonical to compare og:url against; /product-page/ has no canonical to compare og:url against |
| unverified | seo.redirects-on-launch |
Replaced URLs redirect to their new home | Waiting on the redirect map, which does not exist and is not budgeted. The old vocalcom.com carries roughly 2,362 indexed blog posts plus about 52 pages per language. Nothing can be checked until migration.url-inventory produces the list. |
| unverified | seo.structured-data |
Structured data is present, valid and singular | Measured 2026-09-25 on staging: each of the seven sampled templates emits exactly one Organization node, named Vocalcom. A post's author now credits that Organization when the WordPress account has no real name (inc/seo-yoast.php vocalcom_yoast_author_fallback). Open: the Rich Results Test clause, which has no API. |
| unverified | states.current-page |
The current page is marked in the navigation | the main navigation links 0 page(s) on this host, and the rule is judged on 5: 7 page(s) read from the served document; 0 page(s) the main navigation links |
| unverified | states.error-page-design |
The 404 and 500 pages are designed, not server defaults | Not checked against this build. Settled by: the 404 opens with the site header, footer and type styles and at least one working link back into the site. Forcing a 500 on staging returns a page in the same chrome, not the raw server or PHP default. |
| unverified | states.interactive-set |
Every interactive element ships all of its states | Not checked against this build. Settled by: sample one control of each kind on three templates: hover, active, disabled and open each render a visible change. An element inserted by a filter or a load-more shows the same four states as one present at first load. Nothing a hover reveals is the only route to it on touch. |
| unverified | states.loading-feedback |
Anything that takes time says so | Not checked against this build. Settled by: throttle to Slow 3G, then submit a form and change a filter. Each shows a busy state within 500 ms. Force one to fail and the busy state clears rather than leaving a spinner running forever. |
| unverified | states.sticky-chrome |
Sticky header and menu scroll behaviour is decided up front | Not checked against this build. Settled by: scrolling past the stick point shifts the page by 0 pixels. A link to #section leaves its heading fully below the header. With the menu open the page behind it does not scroll, and closing it returns to the same position. |
| n/a | colibrity.paged-canonical |
Paged views are canonical to themselves | page two /blog/page/2/ is noindex with no canonical, which is how a closed host serves it. Re-run against the production host |
| n/a | comms.crm-field-map |
Every form field lands in the right CRM field | Submissions are stored and emailed. Nothing reaches a CRM: there is no connector in `inc/`, and the `vocalcom_form_submitted` seam has no listener. |
| n/a | crawl.signals-agree |
Canonical, robots, sitemap and meta tell one story | every sampled URL is noindex and robots.txt is a blanket Disallow: /, so the whole host is closed deliberately rather than sending conflicting signals. Re-run against the production host |
| n/a | forms.file-uploads |
File uploads declare what they accept and why they refuse | No form accepts a file. The only `type="file"` in the theme is a styleguide demo at inc/dev-styleguide.php:744, wired to nothing. |
| n/a | legal.newsletter-separate |
Newsletter consent is its own optional checkbox | This project has no newsletter. `cpt-subscriber` is commented out in functions.php:195 with the reason written beside it - a subscriber screen on a site with no newsletter is clutter the client has to ignore forever. |
| n/a | legal.terms-consent-checkbox |
The terms box starts unchecked and the server enforces it | No form asks a visitor to accept terms or a contract. Every registered form (contact in inc/forms-core.php:186, register in inc/forms-register.php:55) carries only the registry's default privacy consent, inc/forms-core.php:56-62, and the rule's own do-text says a terms box proves acceptance of a contract and is not a consent. Checked 2026-09-25. The server-side half was measured anyway for the privacy box: a POST to the local /wp-json/vocalcom/v1/contact with the consent unticked returned HTTP 400 "You must accept the Privacy Policy." and vocalcom_message and vocalcom_consent_log both stayed at 0 rows. |
| n/a | seo.canonical |
Every indexable page declares a self-referencing canonical | every sampled page is noindex and carries no canonical (/, /blog/, /contact/, /privacy-policy/, /deploying-without-drama/, /design-system/, /product-page/), which is how a closed host serves them. Re-run against the production host |
| n/a | seo.no-noindex-leak |
Production is indexable and staging is not | staging half holds: 7 of 7 sampled URLs noindex and robots.txt Disallow: /. The production half needs the production host |
| n/a | seo.robots-txt |
robots.txt exists and points at the sitemap | 200, 1 Sitemap: line(s), 131 same-origin asset path(s) and 7 template(s) tested against the * group, sitemap lines sound. robots.txt is a blanket Disallow: / and all 7 sampled URLs are noindex, so this host is closed deliberately and whether a ranking route or an asset is blocked cannot be judged here. Re-run against the production host |
| n/a | seo.sitemap |
The sitemap lists every indexable URL and nothing else | https://vocalcom.colibrity.com/sitemap_index.xml (index of 4), 42 URL(s), 10 sampled, every structural clause holds, but all 10 sampled URLs are noindex under a blanket Disallow: /: a closed host. Re-run the noindex clause against production |
| n/a | states.preloader-budget |
The preloader has a hard cap and a fallback | No preloader. `site-transition.php` is a route-change wipe, not a first-load gate, and nothing defers the first paint. |
Passing
Where a pass needs evidence rather than an assertion, the evidence is named. An unproved pass is what the gate itself calls unverified.
| Result | Rule | Title | How it is proved |
|---|---|---|---|
| pass | a11y.keyboard-operable |
Everything works with a keyboard alone | Measured 2026-09-25 on https://vocalcom.colibrity.com , keyboard only. / at 1440: 109 Tab stops, skip link first, every control reached. /contact/ at 1440: 90 stops through the whole form. / at 390 with the burger open: focus stays inside the panel and the header's logo, Request Demo and burger, and Escape closes it and returns focus to the burger. Outcomes tabs move with ArrowRight and show their panel; the reviews Next button pages on Enter and Space and keeps focus; the video lightbox is aria-modal, puts focus on Close, keeps six further Tabs inside, and Escape closes it and returns focus to the play button that opened it; the footer accordion toggles on Enter and Space. One thing to know: 23 stops on / land on a card whose entrance has not run yet, at opacity 0 for an instant - measured, a focused one is at 0.26 after 50 ms and 0.97 after 400 ms, because focus scrolls it in and the scroll reveals it. That is the trade src/scss/blocks/_shared.scss:18-35 documents over visibility:hidden, which took whole sections out of the tab order. |
| pass | a11y.focus-visible |
A visible focus indicator on every focusable element | Measured 2026-09-25 in the same keyboard pass as a11y.keyboard-operable (/ and /contact/ at 1440, / at 390 with the burger open, https://vocalcom.colibrity.com). Every stop showed a ring: 2px #0a0029 at an offset over a 6px rgba(255,255,255,0.9) halo, from mix.focus-ring (the theme's focus-ring decision). The two inputs whose own outline is none carry it on their pill wrapper through :focus-within - the hero phone field (.btn-input__field) and the footer subscribe input (.subscribe). The ring is ink against its own white halo, 20.1:1 over white and 16.1:1 where the 90% halo sits on Marine, so it holds its contrast over images, gradients and the Marine footer, which is why the theme chose a halo over a per-surface colour. |
| pass | a11y.skip-link |
A working skip link is the first focusable element | wp-starter/skip-link present: header.php, src/scss/base/_a11y.scss. Project stamped wp-starter 1.23.0, verified against wp-starter 1.10.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-11 |
| pass | a11y.form-labels |
Every form control has a real label | Measured 2026-09-25 on https://vocalcom.colibrity.com / , /contact/ and /product-page/ after the deploy: 0 inputs, selects or textareas without a label element, counted through each control's labels list at 1440. The footer's subscribe prompt is now a real label, one per form, each shown with its own form and pointing for= at its own input (template-parts/global/site-footer.php:146-147), with no aria-label left on either input; tapping the one visible label focuses the visible input at 390 and at 1440, checked in a browser. Every registered form prints "Fields marked * are required." once, before its first field (template-parts/global/form-body.php:105), in all four languages on staging. tests/form-labels.test.mjs pins all of it. Required fields carry both the required attribute and a * in the label, as before. |
| pass | a11y.form-errors |
Errors say what is wrong and how to fix it | 1 of 1 form(s) submitted empty |
| pass | a11y.reduced-motion |
prefers-reduced-motion is honoured everywhere | src/js/core/motion-guard.js, the verified wp-starter/motion-guard component. tests/motion- guard.test.mjs asserts prefers-reduced-motion is read in exactly one file and fails if a block stub regenerates a second reader, which is the failure mode that made the component necessary. |
| pass | a11y.dialog-behaviour |
Modals trap focus, return it, and close on Escape | src/js/modules/dialog-a11y.js, the verified wp-starter/dialog-a11y component, wired in src/js/main.js and used by modal-factory.js, legal-modals.js and success-popup.js rather than each rolling its own trap. |
| pass | a11y.link-text |
Link text makes sense out of context | 7 of 7 template(s) read for link text |
| pass | a11y.icon-semantics |
Icons are hidden when decorative and named when they act | Measured 2026-09-25. All 27 partials in template-parts/icons/ carry aria-hidden="true" and focusable="false", and none carries a <title>. Rendered on https://vocalcom.colibrity.com / , /contact/ , /product-page/ and /blog/ : 0 SVGs exposed to the accessibility tree without a name out of 89 to 143 per page, 0 leftover titles, and 0 icon-only links or buttons without an accessible name (28 to 34 per page, named by aria-label). The two role=img SVGs are the logo lockups, aria-hidden inside links that carry aria-label. axe's button-name and link-name rules passed on all seven sampled templates the same day. |
| pass | a11y.target-size |
Tap targets are big enough and far enough apart | 7 of 7 page(s) read at 390px for target size, collapsed menus opened |
| pass | colibrity.filter-url-state |
Filters write their state to the URL | wp-starter/filter-url-state present: inc/listing.php, tests/listing.test.mjs, tests/listing-fetch.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.17.0. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-13 |
| pass | colibrity.filter-history-noise |
Filter changes do not litter the back button | wp-starter/filter-url-state present: inc/listing.php, tests/listing.test.mjs, tests/listing-fetch.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.17.0. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-13 |
| pass | colibrity.pagination-in-url |
Every page of a listing has its page number in the URL | wp-starter/archive-state present: template-parts/post/pagination.php, inc/blog-seo.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.10.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-11 |
| pass | colibrity.pagination-crawlable |
Pagination controls are links, not buttons | wp-starter/archive-state present: template-parts/post/pagination.php, inc/blog-seo.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.10.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-11 |
| pass | colibrity.filter-indexability |
Filter combinations do not become a crawl trap | Measured on the local site 2026-09-25: /blog/?tags=wordpress and /blog/?category=notes&tags=wordpress canonicalise to /blog/. The one indexable filter, /blog/?category=notes, now canonicalises to /category/notes/ (inc/listing.php, vocalcom_listing_seo_canonical()), the archive that renders the same listing and that Yoast's category sitemap lists, so no filtered URL is its own canonical. Any other combination collapses to /blog/, so the set of indexable filtered URLs is bounded by the categories. Staging answers noindex everywhere and prints no canonical, so this is proven locally; tests/listing.test.mjs pins it. |
| pass | colibrity.block-file-triple |
A block is three files sharing one slug | 24 block(s) read from 123 tracked file(s), 24 registry entries: template, stylesheet, script and data-block agree for every block, and no stylesheet or script is orphaned |
| pass | colibrity.dev-surfaces-hidden |
Developer surfaces stay, and their exposure is decided in code | wp-starter/dev-pages present: inc/dev-pages-noindex.php, inc/admin-dev-pages.php, inc/dev-blocks.php, tests/dev-pages-noindex.test.mjs, tests/admin-dev-pages.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.23.0. Presence and version only - the behaviour was not exercised - 5 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-09-15 |
| pass | colibrity.adding-a-block |
Adding a block shows a picture first and never lands empty | inc/admin-blocks.php:140-173 paints a screenshot of each block on ACF's picker card, keyed on its data-layout anchor, from the 25 images under src/assets/images/blocks that npm run blocks:thumbs regenerates from /sandbox/; inc/block-defaults.php:1-12 pre-fills every authored block with real copy and gives every new page a starter set. The thumbnails are inherited from wp-starter; the defaults are this build's own. |
| pass | colibrity.tokens-from-the-design-file |
Design values reach the code as generated tokens, not typed numbers | 0 colour literals in 70 stylesheet(s) outside src/scss/tokens/. wp-starter/design-tokens present: DESIGN.md, scripts/build-tokens.mjs, src/scss/tokens/_tokens.generated.scss, theme.json. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 4 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25 |
| pass | html.doctype-and-lang |
Doctype and a correct lang attribute on every page | 7 of 7 template(s) carry the HTML5 doctype and a lang that agrees with the language root the URL sits under (en-US x7) - the sample sits under 1 of the 4 configured language root(s) (/), and the visible copy itself was not read - / -> doctype ok, lang="en-US", root / (en); /blog/ -> doctype ok, lang="en-US", root / (en); /contact/ -> doctype ok, lang="en-US", root / (en); /privacy-policy/ -> doctype ok, lang="en-US", root / (en); /deploying-without-drama/ -> doctype ok, lang="en-US", root / (en); /design-system/ -> doctype ok, lang="en-US", root / (en); /product-page/ -> doctype ok, lang="en-US", root / (en); the sample sits under 1 of the 4 configured language root(s) (/); the visible copy itself was not read, so the language of the words on the page is inferred from the config, not measured |
| pass | html.single-h1 |
One h1 per page, and no skipped heading levels | 7 of 7 template(s) have exactly one h1, none shared between pages, no heading above it and a heading outline with no skipped level (6 to 46 headings per page) - / -> 1 h1, 23 headings, outline 12233332223332233323333; /blog/ -> 1 h1, 18 headings, outline 122222222222223333; /contact/ -> 1 h1, 9 headings, outline 122323333; /privacy-policy/ -> 1 h1, 10 headings, outline 1222223333; /deploying-without-drama/ -> 1 h1, 6 headings, outline 123333; /design-system/ -> 1 h1, 46 headings, outline 123332333233232322332333333333332333233...; /product-page/ -> 1 h1, 29 headings, outline 12333222333322344423332323333; read from the server's HTML, so a heading or an id that a script writes into the page afterwards is not counted |
| pass | html.landmarks |
Page regions use landmark elements | 7 of 7 page(s) checked for landmarks |
| pass | html.bem-naming |
Class names follow BEM and match their block slug | 24 block root(s) and every class attribute in 184 template file(s) read, 217 design-system class(es): every root class is its slug, and no class outside the design system names a colour or a size |
| pass | html.no-inline-handlers |
No inline event handlers or javascript: URLs | 7 of 7 template(s) read, 2826 KB of rendered HTML: 0 of the 120 known on* event attributes and 0 javascript: hrefs, in the document the browser would build. A template sample, not every rendered page - /: 471 KB, clean; /blog/: 350 KB, clean; /contact/: 271 KB, clean; /privacy-policy/: 340 KB, clean; /deploying-without-drama/: 328 KB, clean; /design-system/: 615 KB, clean; /product-page/: 451 KB, clean; scope: 7 of 7 template(s), which is a SAMPLE and not every rendered page - which URLs a site serves is not readable from a template list. Handler names are a closed list of 120; comments and script bodies were blanked before scanning, and a soft 404 that answers 200 with real HTML is indistinguishable from the template it replaced |
| pass | perf.cls |
CLS at or under 0.1 | 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 0.001 (0.000-0.001); /blog/ 0.000 (0.000-0.000); /contact/ 0.000 (0.000-0.000); /privacy-policy/ 0.000 (0.000-0.000); /deploying-without-drama/ 0.015 (0.000-0.015); /design-system/ 0.000 (0.000-0.000); /product-page/ 0.000 (0.000-0.054). The slow-network reload watched by eye was not exercised |
| pass | perf.inp-tbt |
TBT at or under 200ms and INP at or under 200ms | TBT 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 125 ms (51 ms-140 ms); /blog/ 18 ms (7 ms-37 ms); /contact/ 0 ms (0 ms-0 ms); /privacy-policy/ 2 ms (0 ms-51 ms); /deploying-without-drama/ 19 ms (0 ms-43 ms); /design-system/ 78 ms (76 ms-318 ms); /product-page/ 25 ms (21 ms-189 ms); no field data for INP |
| pass | perf.score-floor |
Mobile performance score at or above 90 | 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 91 (90-93); /blog/ 90 (90-90); /contact/ 99 (94-99); /privacy-policy/ 100 (95-100); /deploying-without-drama/ 94 (94-95); /design-system/ 94 (89-97); /product-page/ 93 (93-94) |
| pass | perf.bundle-budgets |
JavaScript and CSS stay inside their budgets | 10 of 10 assertions pass in tests/bundle-size.test.mjs, 0 skipped - the run printed no byte figure; the ceilings tests/bundle-size.test.mjs asserts are 16450, 1800, 12350, 55000, 14000, 8000 bytes - node --test --test-reporter=tap tests/bundle-size.test.mjs (cwd /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom) -> exit 0; 10 tests, 10 pass, 0 fail, 0 skipped; newest built file 2026-09-25 (dist/.vite/manifest.json), newest source 2026-09-25 (src/scss/layout/_content-grid.scss). The accept's other branch is not settled here: a RAISED budget is allowed when the same commit says what the extra bytes bought, and whether a sentence explains a number is a reviewer's judgement, not a measurement. A raise made with no note leaves a green test behind it and reads as a pass above. This check also measures the budget test's BEHAVIOUR rather than its pin - it never reads `.wp-starter-version`, so a vendored copy that has drifted from `wp-starter/bundle-size-test` is graded as whatever it now contains. |
| pass | perf.no-render-blocking |
Nothing render-blocking that does not have to be | 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, render-blocking audit read on every run: / nothing listed; /blog/ nothing listed; /contact/ nothing listed; /privacy-policy/ nothing listed; /deploying-without-drama/ nothing listed; /design-system/ nothing listed; /product-page/ nothing listed |
| pass | perf.lazy-and-priority |
Below-the-fold images lazy load, the LCP image does not | 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, LCP element read off every run, the fold read in Chrome at 412 x 823: / LCP img.block-hero__image; /blog/ LCP img.post-card__image (first of 10 in the page); /contact/ LCP is p, not an image, so only the fold was judged; /privacy-policy/ LCP is p, not an image, so only the fold was judged; /deploying-without-drama/ LCP img.post-single__image; /design-system/ LCP is p, not an image, so only the fold was judged; /product-page/ LCP img.block-hero-b__poster |
| pass | perf.fonts |
Fonts are subset woff2, self-hosted, with a swap policy | 7 of 7 template(s) read for fonts |
| pass | perf.caching-and-compression |
Static assets are compressed and cached far-future | html gzip, css inlined in the HTML, js br (block-registry-rleWQ2PH.js); 3 hashed asset(s) checked |
| pass | perf.third-party |
Third-party scripts are counted and justified | 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, third-party summary read on every run: / no third party; /blog/ no third party; /contact/ no third party; /privacy-policy/ no third party; /deploying-without-drama/ no third party; /design-system/ no third party; /product-page/ no third party |
| pass | perf.page-weight |
Each template has a transfer and DOM budget | 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 519 kB, 2646 elements (over 1400, passed by measurement: TBT 125 ms on the only page this template renders); /blog/ 469 kB, 1483 elements (over 1400, passed by measurement: TBT 18 ms, and this is the largest page its template renders); /contact/ 333 kB, 1406 elements (over 1400, judged on /product-page/, the largest page its template renders: TBT 25 ms); /privacy-policy/ 339 kB, 1388 elements; /deploying-without-drama/ 380 kB, 1372 elements; /design-system/ 423 kB, 4865 elements (over 1400, passed by measurement: TBT 78 ms, and this is the largest page its template renders); /product-page/ 418 kB, 2127 elements (over 1400, passed by measurement: TBT 25 ms, and this is the largest page its template renders) |
| pass | perf.ttfb |
Server response starts inside 800 ms | 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: slowest /blog/ 19 ms (10-19); /contact/ 14 ms, /deploying-without-drama/ 14 ms, /design-system/ 14 ms, / 10 ms, /product-page/ 10 ms, /privacy-policy/ 8 ms |
| pass | practices.no-secrets |
No credential ever reaches the repository or the web root | 4 environment path(s) probed (/.env -> 403, /.env.local -> 403, /.env.production -> 403, /.env.bak -> 403), all refused or redirected away; 0 credential-shaped file(s) among the 813 ever added across 57 ref(s) of a full checkout of /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom, .env ignored by .gitignore:8:.env. Filenames only, and a fixed list of them: .env, .env.local, .env.production, .env.bak over HTTP, and .env* / .envrc / *.env / auth.json / wp-config.php in the history. A credential hard-coded in an ordinary tracked file, or served from a path not on that list, is not covered - /.env -> 403; /.env.local -> 403; /.env.production -> 403; /.env.bak -> 403; git -C /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom log --all --no-renames --diff-filter=A --name-only, toplevel /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; Filenames only, and a fixed list of them: .env, .env.local, .env.production, .env.bak over HTTP, and .env* / .envrc / *.env / auth.json / wp-config.php in the history. A credential hard-coded in an ordinary tracked file, or served from a path not on that list, is not covered |
| pass | practices.output-escaping |
All dynamic output is escaped at the point of output | Both halves measured 2026-09-25. Code: every echo, print and <?= of a $ expression in the theme's PHP that carries no esc_*, wp_kses*, absint or json_encode on the same line - 206 lines, from git grep - was traced to its source: 206 safe, 0 unsafe. They are escaped where the variable is built, clamped to an enum, integers, core-escaped image helpers, or pass through helpers read and confirmed to escape every dynamic part (inc/helpers.php vocalcom_btn, vocalcom_render_image, vocalcom_video, vocalcom_icon). Live: a published local page carried <script>alert(1)</script> in the hero, text, CTA and features-3 fields, the button title and a javascript: button URL; https://vocalcom.test/xss-probe-tmp/ served 0 raw script tags and 13 escaped as <script>, the wysiwyg field stripped the tag, and the javascript: URL did not render. The probe page was deleted. Re-read after the same day's hook refactor: git grep -E '(echo|print|<?=)[[:space:]]*$' without an escaper gives the same 100 lines as before it, differing only in static data-* attributes, and the new outputs escape or validate (the waveform and video hook argument is matched against ^data-[a-z0-9-]+$, the Styleguide srcset goes through esc_url and esc_attr, the pagination is core's own escaped markup). |
| pass | practices.console-clean |
Production loads with a clean console | 7 of 7 page(s) loaded, scrolled and navigated from |
| pass | practices.self-cleaning-modules |
Scripts clean up after themselves on page transition | / to /design-system/ and back five times: 51 listeners on window and document and 31 ScrollTriggers, the same as the first load |
| pass | practices.handover-notes |
The project ships with the notes needed to run it | docs/EDITOR-HANDOVER.md plus the runnable kit in docs/editor-test/, in the repo rather than in a chat thread. CLAUDE.md and .claude/DECISIONS.md carry the developer half, with DECISIONS.md stated as the single source so the same decision cannot be recorded in six places again. |
| pass | practices.soft-navigation-parity |
A soft navigation leaves the document as a hard load would | / to /design-system/ by transition: title, canonical, og:url, og:type, body classes and stylesheets match a hard load, and one pushState fired, which is what a history-change page-view trigger fires on |
| pass | practices.tests-before-deploy |
The theme's contracts are tests, and a deploy runs them first | Measured 2026-09-15: package.json:26 deploy and :27 deploy:prod run npm run build:check && npm test && node deploy.mjs, so the build, the chunk assertion and the suite (87 test files, 1391 tests in about 8 s) all run before the upload and a red exit stops it. Proven the same day: the first deploy that ran the assertion refused to upload on a real defect, the preload helper split out of the entry since 2026-08-31, and uploaded only after the fix landed. The suite reads the built output rather than skipping it, because the deploy builds first: tests/bundle-size.test.mjs measures dist/ as just built. |
| pass | seo.title-description |
Titles and descriptions are present, unique and the right length | 7 of 7 sampled page(s) read, 0 fault(s) - title/description length: /: 43/109, /blog/: 50/110, /contact/: 53/144, /privacy-policy/: 60/160, /deploying-without-drama/: 34/154, /design-system/: 59/129, /product-page/: 58/152 |
| pass | seo.https-and-mixed-content |
HTTPS is enforced and nothing loads over http | http://vocalcom.colibrity.com/ -> 301 -> 200; 7 page(s) scanned, 0 http:// asset(s) - element attributes only; url() inside inline style blocks is not scanned |
| pass | seo.internal-links-resolve |
No internal link is broken | 88 internal link(s) walked from 7 page(s), 0 broken or chained |
| pass | seo.hreflang |
Multilingual sites declare reciprocal hreflang | 4 language root(s), 60 alternate pair(s) walked, 16 distinct URL(s) read, 0 fault(s) - walked from the home page and each language root; inner templates are not walked |
| pass | crawl.sibling-hosts |
Only the production host serves an indexable copy of the site | 1 sibling host(s), 2 probes: 0 indexable, 0 gated, 2 noindex - https://vocalcom.tastysites.pl/ -> noindex; https://vocalcom.tastysites.pl/blog/ -> noindex |
| pass | crawl.language-urls |
Each language version has its own crawlable URL | 4 of 4 language root(s) answered when requested with Accept-Language: ja; 4 answer 200 with no redirect, 4 are linked as anchors from the home page; 3 of 3 region root(s) answered when requested with Accept-Language: ja, 3 answer 200 with no redirect, 3 findable by anchor, hreflang or sitemap - GET / (Accept-Language ja) -> 200; GET /fr/ (Accept-Language ja) -> 200; GET /es/ (Accept-Language ja) -> 200; GET /pt/ (Accept-Language ja) -> 200; region GET /af/ (Accept-Language ja) -> 200 [anchor no, hreflang yes, sitemap not read]; region GET /la/ (Accept-Language ja) -> 200 [anchor no, hreflang yes, sitemap not read]; region GET /me/ (Accept-Language ja) -> 200 [anchor no, hreflang yes, sitemap not read] |
| pass | crawl.content-in-server-html |
The content that must rank is in the server HTML | 7 of 7 template(s) carry an h1, body copy and nav with JavaScript off - /: h1 "AI-first contact center platform", 4 nav links in nav and header; /blog/: h1 "Blog", 6 nav links in nav and header; /contact/: h1 "Contact", 5 nav links in nav and header; /privacy-policy/: h1 "Privacy Policy", 5 nav links in nav and header; /deploying-without-drama/: h1 "Deploying without drama", 6 nav links in nav and header; /design-system/: h1 "Design system", 7 nav links in nav and header; /product-page/: h1 "The AI contact center built for banks th", 5 nav links in nav and header |
| pass | crawl.published-urls-resolve |
Every published URL answers at its own address | 49 URL(s): 42 from the sitemap at /sitemap_index.xml, 29 navigation link(s) from 10 page(s), every one 200 within one redirect |
| pass | crawl.language-parity |
Every language publishes the same set, or says what it leaves out | node bin/lang-parity-check.mjs against the local site on 2026-09-25 after the fix: 25 things checked across en, fr, pt-pt and es, 0 failed, 9 expected, each expected gap with a written reason, block stacks and copy included. The English primary menu is back to 4 items: bin/seed-starter.php no longer seeds that menu, which is what had re-added Elements, Design System and Kitchen Sink after the 2026-09-10 run. /elements/ is an internal page, noindexed and out of the sitemap, declared English-only. Staging after the db:push carries 4 primary items in all four languages. |
| pass | onpage.schema-fidelity |
Structured data says what the page actually says | 7 sampled page(s) with JSON-LD read against their own copy (/, /blog/, /contact/, /privacy-policy/, /deploying-without-drama/, /design-system/, /product-page/): every headline, author, date, rating, price and count agrees with the page |
| pass | onpage.favicon |
The site ships a favicon Google can crawl | 96x96 png at /wp-content/themes/vocalcom/favicon-96x96.png (robots.txt is a blanket Disallow: /, so the indexability half cannot be judged on this host) - GET https://vocalcom.colibrity.com/wp-content/themes/vocalcom/favicon-96x96.png -> 200; robots.txt Disallow: / |
| pass | admin.client-mode |
The client's admin is trimmed, and the trim can always be undone | Done as per-user self-service rather than as a site-wide switch, and the rule allows both. inc/admin-cleanup.php hides menus per user from that user's own Profile screen; empty meta means every menu is visible, so nothing is hidden by default and an administrator can un-hide anything they hid. That satisfies the reversal clause by construction: there is no state a user can put themselves in that they cannot get out of. It also satisfies the clause this project is the reason for. The theme replaced its old single site-wide toggle with the per-user one precisely because the toggle exempted administrators, and administrator is the role Colibrity's own people hold on this site. |
| pass | admin.capability-boundaries |
What the client must not do is refused, not hidden | The three inboxes are browse-only by capability, not by CSS: cpt-message.php:105-107, cpt-subscriber.php:89-91 and cpt-consent-log.php:115-117 each register with map_meta_cap => true and create_posts => do_not_allow. region-content.php:69-76 does the same for the region taxonomy with manage_terms. The URL half of the acceptance has not been run against a live account, so this is the mechanism verified rather than the refusal observed. |
| pass | admin.fields-in-code |
Field definitions live in the repository, and a field name is the data | Every field group is registered in PHP under version control - inc/acf-fields.php, inc/acf-options.php and inc/legal-docs.php - and none is authored in the ACF UI, so a field change arrives as a reviewable diff. The rename half of the rule is a discipline going forward rather than something a first grading can evidence: there is no previous release to diff field names against yet. |
| pass | admin.duplicate-content |
An editor can duplicate any content item, translations included | wp-starter/duplicator present: inc/duplicate-core.php, inc/duplicate-post.php, bin/duplicate-check.php, tests/duplicate-post.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.23.0. Presence and version only - the behaviour was not exercised - 4 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-09-15 |
| pass | comms.submission-delivery |
A submission is stored before anything is emailed | Measured locally 2026-09-25: a temporary mu-plugin made every wp_mail() fail (pre_wp_mail returning false) and one contact form was POSTed to https://vocalcom.test/wp-json/vocalcom/v1/contact with the consent ticked. It answered 200, and in the same request a vocalcom_message row was written with vocalcom_msg_mail_sent = no, shown as the Email sent column of the Messages list (inc/cpt-message.php:198), beside a vocalcom_consent_log row: purpose contact, type privacy, status accepted, version 1.0, text "I accept the Privacy Policy.". Nothing was lost when mail failed. The mu-plugin was trashed and both probe rows deleted. Worth knowing: the visitor is still told the message was sent, which is true of the storage and not of the email. |
| pass | comms.conversion-events |
The events that count are agreed, fired once, and observable | wp-starter/conversion-events present: inc/analytics-events.php, inc/analytics-capi.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25 |
| pass | design.not-colour-alone |
No information is carried by colour alone | Measured 2026-09-25 on https://vocalcom.colibrity.com / , /contact/ and /product-page/ with filter: grayscale(1) on <html> and reduced motion so every section had entered. Exports: docs/evidence/2026-09-25/greyscale-home.jpg, greyscale-contact.jpg, greyscale-product-page.jpg, greyscale-contact-error.jpg and greyscale-reviews-dots.png. Links inside running text are underlined; the Learn more text links differ by weight (600 against 400) and a trailing mark; required fields carry a * in the label; an error shows its message text and sets aria-invalid; the active tab is a filled dark pill with light 600 text against light pills; the active reviews dot is opacity 1 against 0.3. There are no charts, map keys or status chips on these pages. 0 distinctions left to hue alone. |
| pass | forms.server-side-validation |
Every validation rule runs again on the server | wp-starter/form-registry present: inc/forms-core.php, inc/rest-contact.php, src/js/modules/form-validate.js. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25 |
| pass | forms.error-timing |
An error appears on blur, not while someone is typing | 1 of 1 form(s) typed into, left and corrected |
| pass | forms.input-font-size |
Form inputs are 16 pixels so iOS does not zoom on focus | 7 of 7 page(s) read at 390px |
| pass | forms.input-type-and-autocomplete |
Every field declares its type and its autocomplete token | 7 of 7 page(s) with every identity field read |
| pass | forms.submit-lifecycle |
A submit happens once and loses nothing | Measured 2026-09-25 on https://vocalcom.colibrity.com/contact/ in Chrome after the deploy: a double click on Send sent one POST, and replaying that same payload from the page answered 200 with the same success message and stored nothing, so the pair left exactly 1 message and 1 consent record (5 and 5 across the whole run of five distinct submissions). The server-side guard is a fingerprint of the form, its sanitised data and the consents given, written only after a successful store (inc/rest-contact.php, step 4b); locally two identical curl POSTs stored 1 and 1, where the same test stored 2 and 2 before the fix. A refused submit (the 429 above) left every typed value and the ticked consent in the form. All test rows were deleted afterwards. tests/forms-rate-limit.test.mjs pins the guard. |
| pass | forms.cache-safe-submit |
A form still submits from a cached page | wp-starter/form-registry present: inc/forms-core.php, inc/rest-contact.php, src/js/modules/form-validate.js. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25 |
| pass | legal.consent-record |
Every consent is recorded with what it takes to prove it | wp-starter/consent-log present: inc/cpt-consent-log.php, inc/privacy-exporters.php, inc/rest-contact.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25 |
| pass | legal.cookie-banner |
Refusing cookies is as easy as accepting them | Fixed 2026-09-25 and checked in a browser locally, because the banner renders only when analytics is on and staging has no tag. Reject all is now the same primary, small button as Accept all (template-parts/global/cookie-bar.php:108-128), both one click from the first screen. With tracking forced on for one page load through a throwaway mu-plugin that wrote no option row, the two computed identically: background #ff4400, text #f7f5f2, weight 600, 16px, 36px tall, differing only in width by label length. Settings and Save choices stay ghost. tests/analytics.test.mjs:550 fails if Reject ever stops matching Accept's variant and size. |
| pass | legal.no-tags-before-consent |
No analytics or marketing tag fires before a choice | wp-starter/analytics-registry present: inc/analytics-vendors.php, inc/analytics-head.php, src/js/modules/tracking.js, inc/consent-cookie.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 4 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25 |
| pass | legal.no-pii-in-urls |
No personal data travels in a URL | 7 of 7 page(s) with every form submitted (3 submit(s), 11 valid form(s) that send nothing, 2 POST(s) sent to the real server as acknowledged gate probes and their responses followed); no analytics request fired on this host, so the page-path report can hold none |
| pass | states.no-js-fallback |
Content is visible when JavaScript does not run | Measured failing and fixed the same day, 2026-09-25, on https://vocalcom.colibrity.com with JavaScript disabled. Before: / and /product-page/ each left 38 text elements at opacity 0 - every section below the first - because .anim-hidden is lifted only by a block module, by reduced motion or by the first-screen rule; /blog/ was clean. The fix is @media (scripting: none) beside the reduced-motion override in src/scss/blocks/_shared.scss, pinned by tests/blocks-split.test.mjs. After the deploy: 0 of 237 text elements invisible on /, 0 of 256 on /product-page/ and 0 of 104 on /blog/, and with JS on the entrance still runs (measured locally: the articles header at opacity 0 before scroll, 1 after). Inactive tab panels and review slides stay hidden without JS by design, so each such section shows its first state. |
| pass | states.error-pages |
A missing URL returns a real 404 in the right language | 4 language root(s): / 404 lang=en-US, /fr/ 404 lang=fr-FR, /es/ 404 lang=es-ES, /pt/ 404 lang=pt-PT - GET https://vocalcom.colibrity.com/colibrity-gate-no-such-page/ -> 404 lang="en-US"; GET https://vocalcom.colibrity.com/fr/colibrity-gate-no-such-page/ -> 404 lang="fr-FR"; GET https://vocalcom.colibrity.com/es/colibrity-gate-no-such-page/ -> 404 lang="es-ES"; GET https://vocalcom.colibrity.com/pt/colibrity-gate-no-such-page/ -> 404 lang="pt-PT" |
| pass | states.scroll-reveal-recovery |
Scroll reveals fire on deep links and on back navigation | nothing hidden above the scroll position after loading /product-page/#wps-form-1-hp, or after leaving and returning by a bfcache restore |
| pass | ops.deploy-target-pinned |
A destructive deploy refuses to run against the wrong target | deploy.mjs:83-92 reads PROD_EXPECTED_URL_HOST from the environment file with no hardcoded fallback, and :135-142 refuses the production deploy unless the resolved site host matches it. An empty pin refuses too, which is the right default before go-live, and db-sync.mjs carries the same rails. The deliberate-misfire half of the acceptance has not been run, so this is the refusal read in the code rather than observed. |
| pass | security.rest-route-authorisation |
No REST route writes without an authorisation check or a stated bound | 3 register_rest_route call(s), 3 live route(s) under vocalcom/v1, 3 row(s) in docs/EDITOR-HANDOVER.md; registered in code, not live: /csp-report (inc/csp-report.php:124): every live route is capability-gated or listed with its bound |
| pass | security.submission-store-private |
Stored form submissions are unreachable without logging in | 2 submission type(s): vocalcom_message, vocalcom_consent_log; vocalcom_message #1510 answers 200 at its permalink and 404 at its REST route |
| pass | security.upload-types-and-execution |
Uploads cannot execute, and no script-capable type is allowed | read on colibrity-staging as an administrator: no type added to core's list, an uploads .htaccess: no script-capable type is added, and .php and .phtml are refused in /home/u835041983/domains/vocalcom.colibrity.com/public_html/wp-content/uploads |
| pass | security.response-headers |
Every HTML response carries the baseline security headers | 3 of 3 sampled URL(s) carry all five headers with the rule's values (/ max-age=31536000; /blog/ max-age=31536000; /contact/ max-age=31536000) - GET / -> 200 [nosniff=nosniff, x-frame-options=sameorigin, referrer-policy=strict-origin-when-cross-origin, permissions-policy=geolocation=(), camera=(), microphone=(), hsts max-age=31536000]; GET /blog/ -> 200 [nosniff=nosniff, x-frame-options=sameorigin, referrer-policy=strict-origin-when-cross-origin, permissions-policy=geolocation=(), camera=(), microphone=(), hsts max-age=31536000]; GET /contact/ -> 200 [nosniff=nosniff, x-frame-options=sameorigin, referrer-policy=strict-origin-when-cross-origin, permissions-policy=geolocation=(), camera=(), microphone=(), hsts max-age=31536000]; no response was marked a cache HIT, so the cached-response half of the acceptance test is not settled here |
| pass | security.wp-attack-surface |
WordPress's default attack surface is closed at setup | xmlrpc.php POST 403, ?author=1 301 -> /, /wp-json/wp/v2/users 401, DISALLOW_FILE_EDIT true at runtime |
| pass | security.comments-closed |
Comments are closed at every layer unless the brief asks for them | wp-starter/comments-off present: inc/comments-off.php, tests/comments-off.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.23.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-09-15 |