← all boards ◆ home
Web

Website

Marketing site & app front: pages, copy, SEO, deploys, and fixes.

Start here: the plans & live documents for this area

Call Log (every call, every item, tracked)The running record of the biweekly reviews across all workstreams.Open →Content & Voice Review (for the Rory conversation)Page-by-page voice read: her voice is strong and human; the CoolSculpting and Cryo pages are off-voice and make false pre-launch client claims; small…Open →The Blog Engine (weekly post, voice guide, topic bank, sample)How the Journal becomes a weekly educational engine sourced from Huberman and real research, in her voice, each post also a social post.Open →Program overviewOne page tying together website health, benchmark, content and voice, blog engine, service rewrites, social plan, and roadmap.Open →Service page rewrites, proposal (CoolSculpting + Cryo)Honest rewrites of the two off-voice pages in her voice, no false claims, ready to review with Rory.Open →
61 of 92 done · 31 open

Flow health: derived live from finding status · click a category to filter the list below

BROKENCompetitive benchmark · 5 open
BROKENContent strategy & growth · 6 open
SHAKYAnalytics & tracking · 2 open
SHAKYContent & copy · 4 open
SHAKYDesign & brand · 1 open
SHAKYPerformance & Core Web Vitals · 3 open
SHAKYSEO: local & maps · 2 open
SHAKYSEO: on-page & content · 1 open
SHAKYSEO: technical & indexing · 1 open
SHAKYUX & conversion · 4 open
SOLIDAccessibility · all 3 fixed
SOLIDLinks, forms & functionality · all 4 fixed
SOLIDMobile & responsive · all 4 fixed
SOLIDOther · all 6 fixed

Items (92)

OpenMEDfindingWEB-A05Microsoft Clarity ships with literal placeholder ID YOUR_CLARITY_ID: broken analytics + a junk request on every page
details

Where: All pages (static index.html shell) · index.html <head>

What: The Clarity loader in the raw <head> is hard-coded to the placeholder value instead of a real project id, so every page load injects a script tag pointing at https://www.clarity.ms/tag/YOUR_CLARITY_ID.

Fix: Replace "YOUR_CLARITY_ID" with the real Clarity project id, or remove the Clarity block entirely if Clarity isn't being used.

Link: https://ritualstudiospa.comAll pages (static index.html shell)

Note: Sep 23: the placeholder Clarity tag is back; every page requests clarity.ms/tag/YOUR_CLARITY_ID (400). Remove it and keep only the real project tag.

evidence
Raw HTML head: `})(window, document, "clarity", "script", "YOUR_CLARITY_ID");` with comment `<!-- Replace YOUR_CLARITY_ID with your actual Clarity Project ID -->`. curl -sI https://www.clarity.ms/tag/YOUR_CLARITY_ID → HTTP/1.1 405 Method Not Allowed (no valid project resolves).

[Independently confirmed by 4 audit lenses: analytics, seo-onpage, cro, seo-tech]
OpenMEDfindingWEB-A11Dave Canales presented as a co-founder on Home but only an endorser on About: contradictory NFL-coach claim
details

Where: / (Home) and /about

What: The Home page's founder section (heading 'Founded by Athletes, Built for Everyone') states: 'Founded by former pro athletes and busy parents with careers in fitness and firefighting, alongside NFL head coach Dave Canales.' The word 'alongside' inside a 'Founded by...' sentence reads as Canales being a co-founder. The About page instead frames him as a customer/endorser: 'Dave Canales, head coach of the Carolina Panthers, and his wife Lizzy know firsthand how contrast therapy helps them recharge so they can keep leading and supporting others.' The two pages make materially different claims about the same person, and no actual human founder is ever named on either page.

Fix: Pick ONE accurate relationship and state it identically on both pages. If Canales is an endorser/investor, change Home to 'with support from NFL head coach Dave Canales' (not 'Founded by ... alongside'). If he is genuinely a founding partner, say so on About too. Also name the actual human founders once: anonymous founders plus a borrowed NFL name reads as thin.

Link: https://ritualstudiospa.com/ (Home) and /about

Note: Sep 23 check: the Home founder line again reads "...alongside NFL head coach Dave Canales", so the Home and About framing differ again. As of Aug 12, before the Aug 31 site rebuild: partly done. Main body-copy contradiction is fixed: the old Home sentence ("Founded by former pro athletes... alongside NFL head coach Dave Canales") is gone from the live SPA bundle (index-ritual-website-20260806.js); Home now renders "founded by people with real-life roots in athletics...

evidence
Home: 'Founded by former pro athletes and busy parents with careers in fitness and firefighting, alongside NFL head coach Dave Canales.' vs About: 'Dave Canales, head coach of the Carolina Panthers, and his wife Lizzy know firsthand how contrast therapy helps them recharge...' Founder names are never given ('former pro athletes and busy parents' only).
OpenMEDfindingWEB-A12Pre-launch spa claims 'Real Results from Real Clients' and 'our clients have achieved' on CoolSculpting
details

Where: /services/coolsculpting

What: The CoolSculpting page uses past-tense, client-proof copy, heading 'Real Results from Real Clients' and body 'See the transformative results our clients have achieved with CoolSculpting', while the rest of the site says the business has not opened: Home shows 'Opening Fall 2026 - Founding Members Waitlist Open' and every CTA is a waitlist join.

Fix: Until the spa opens and has consented client results, reword to forward-looking, non-fabricated copy (e.g. 'What CoolSculpting can do' / 'Typical results') or remove the before/after 'clients' framing. Reinstate real testimonials only once they exist.

Link: https://ritualstudiospa.com/services/coolsculpting

Note: Sep 23 check: /services/coolsculpting again shows "Real Results from Real Clients" and "See the transformative results our clients have achieved". As of Aug 12, before the Aug 31 site rebuild: partly done. Verified live 2026-08-12 three ways: curl of /services/coolsculpting raw HTML (prerendered static shell), grep of all three live JS bundles (index-ritual-website-20260806.js 723KB, website-enhancements-20260809.js, site-runtime-20260806.js), and a real Chrome render of the page.

evidence
Coolsculpting: 'Real Results from Real Clients' / 'See the transformative results our clients have achieved with CoolSculpting.' Home: 'Opening Fall 2026 • Founding Members Waitlist Open' and 'Founder rates available to the first 100 members only.'
OpenMEDfindingWEB-A18OG share image hardcodes a dated 'OPENING FALL 2026' claim in pixels
details

Where: All pages (og:image / twitter:image) · /og-image.png

What: The social-share image is otherwise high-quality and on-brand (RITUAL STUDIO wordmark over cold-plunge + sauna photos) but bakes the text 'OPENING FALL 2026' and 'Founding Members Waitlist' directly into the raster.

Fix: Keep the dated message in on-page copy (easy to edit) and use a timeless OG image (wordmark + tagline, no dates), or plan a scheduled swap at opening.

Link: https://ritualstudiospa.comAll pages (og:image / twitter:image)

Note: Sep 23 check: the live share image (og-image.png, 873 KB) again shows OPENING FALL 2026 in the pixels, so every shared link shows the old date. Tracked with WEB-ROAD1.

evidence
Rendered og-image.png (1200x630) displays 'RITUAL / STUDIO', 'OPENING FALL 2026', 'Founding Members Waitlist'; meta og:image = /og-image.png?v=2.
OpenLOWfindingWEB-A37Service name inconsistent: nav/URL 'Cryo Facials' vs on-page 'Cryo T-Shock Facial'
details

Where: /services/cryo-facials

What: The navigation label and URL slug call the service 'Cryo Facials' (/services/cryo-facials), but the page's own H1 and product name throughout is 'Cryo T-Shock Facial' (e.g. 'What Is a Cryo T-Shock Facial?', pricing block 'Cryo T-Shock Facial').

Fix: Align the two: either brand it 'Cryo T-Shock Facials' consistently in nav or add a one-line bridge on the page ('Cryo Facials, powered by Cryo T-Shock').

Link: https://ritualstudiospa.com/services/cryo-facials

Note: As of Aug 12, before the Aug 31 site rebuild: NOT actually done. Not fixed, and the fix attempt introduced a visible regression. Verified live 2026-08-12 via curl + real Chrome render of https://ritualstudiospa.com/services/cryo-facials. (1) The crawler/prerender layer (raw HTML in #root) DOES implement consistent naming: H1 "Cryo Facials, Powered by Cryo T-Shock", title "Cryo T-Shock Facials | Ritual Studio": but real b

evidence
URL/nav: 'Cryo Facials'. Page: 'What Is a Cryo T-Shock Facial?' and pricing header 'Cryo T-Shock Facial'.
OpenLOWfindingWEB-A38Journal CTA 'Book Your Session' promises a booking flow that does not exist (site is waitlist-only, pre-launch)
details

Where: /journal

What: The Journal page's only CTA is 'Ready to Experience the Ritual?' with a 'Book Your Session' button. Every other page on the site funnels to 'Join the Founder Waitlist' and there is no booking/scheduling system anywhere (the studio hasn't launched: the entire value prop is 'Be Among the First 100').

Fix: Change the Journal CTA label to match the sitewide primary action ('Join the Founder Waitlist' / 'Get Founding Rates') so all CTAs point to the one real conversion goal until booking actually goes live.

Link: https://ritualstudiospa.com/journal

Note: Sep 23 check: the Journal CTA reads Book Your Session again (the button goes to the waitlist). The site is still waitlist only, so it should read Join the Founding Member Waitlist.

evidence
WebFetch /journal rendered: single CTA 'Ready to Experience the Ritual?' -> button 'Book Your Session'; contrast with /memberships CTA 'Join the Founder Waitlist' and no booking UI on any page.
OpenLOWfindingWEB-A39Founder credibility on About is thin: first names only, no verifiable credentials
details

Where: /about

What: Founders are introduced only as 'Rory and Daniel', 'former professional athletes and parents' with fitness/firefighting backgrounds: no last names, certifications, specific accomplishments, or links. Credibility leans almost entirely on the Dave Canales association.

Fix: Add full names, relevant certifications/experience, and a line on why they built the studio; keep the Canales endorsement as supporting proof rather than the sole anchor.

Link: https://ritualstudiospa.com/about

Note: As of Aug 12, before the Aug 31 site rebuild: NOT actually done. Rendered /about (SPA bundle index-ritual-website-20260806.js, still live) is unchanged from the original finding: founders described only as "former professional athletes and parents of two young children" with "careers in fitness and firefighting"; the names Rory and Daniel now appear ONLY in an image alt attribute, with no last names, certifications, accom

evidence
WebFetch /about: 'identifies two founders (Rory and Daniel) ... no full names, specific credentials, or detailed backgrounds are provided.'
OpenLOWfindingWEB-A50twitter:site handle @RitualStudio does not match the real Twitter/social handle
details

Where: All routes (raw HTML head)

What: The card declares `twitter:site` = @RitualStudio, but the business's actual social handle is @ritualstudiospa (Instagram); there is no evidence @RitualStudio is theirs.

Fix: Set twitter:site to the correct handle (or remove it if there is no X/Twitter account).

Link: https://ritualstudiospa.comAll routes (raw HTML head)

Note: Sep 23 check: every page still declares twitter:site @RitualStudio in the raw HTML. Set it to the real handle or remove it.

evidence
Raw HTML: `<meta name="twitter:site" content="@RitualStudio" />` vs confirmed Instagram @ritualstudiospa.
In progressdocWEB-DOC4Call Log (every call, every item, tracked)
details

What: The running record of the biweekly reviews across all workstreams.

Link: /reports/call-log.html

PartialHIGHfindingWEB-S03Add local + contrast-therapy prose to Memberships and About (fix keyword starvation)
details

Where: About, /about and Memberships, /memberships

What: Memberships is ~80% pricing cards with ~one sentence of prose and no H1; About spends its H1 on 'What We Care About' and mentions Tacoma/Pierce County zero times, University Place once, cold plunge zero times.

Fix: Rewrite the About H1 to carry service + location while keeping the brand line (e.g. 'What We Care About: Contrast Therapy Built for University Place'); add a short brand-voice local paragraph on both pages naming University Place/Tacoma/Pierce County and cold plunge/sauna/contrast therapy; add an H1 to Memberships.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Local + contrast-therapy prose exists ONLY in the pre-hydration static HTML shell, not on the pages users/Google's renderer actually see. Observed via curl: /memberships raw HTML now has H1 "Contrast Therapy Memberships in University Place", lede + 2 prose sections, inline waitlist, and LocalBusiness schema (areaServed University Place/Tacoma/Pierce County);

evidence
[Brand-safe / additive: no redesign needed] The highest-weight on-page elements are being wasted on non-searchable phrases, so pages built for early local discovery are nearly invisible for local intent: directly counter to the pre-launch goal.
PartialHIGHfindingWEB-B01Adopt Othership's per-location landing-page pattern for University Place
details

Where: New location page + Home

What: Othership's best-in-class location pages combine a local H1, full NAP, map, a 4-step journey, and a 20-question FAQ to rank locally and convert nervous first-timers. Ritual has no equivalent.

Fix: Build the University Place location page described in contentRecs, mirroring Othership's structure but in Ritual's warm/communal voice and warm+cool palette.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. The location page exists live at /sauna-cold-plunge-university-place (HTTP 200, prerendered standalone HTML, sitemap priority 0.9, indexable) with most Othership pattern elements: local H1 "Sauna & Cold Plunge in University Place, WA", visible full address (2310 Mildred St W, Suite 128, University Place, WA 98466), Google Maps embed + link, a 4-step "Four-St

evidence
[Brand-safe / additive, no redesign needed] This is the highest-leverage, best-fit move for the pre-launch SEO+analytics mandate, it builds durable owned discoverability for exactly the local 'near me' searches Ritual needs, and it slots cleanly into the existing brand.
PartialMEDfindingWEB-A06Key conversion CTAs (Join Waitlist, membership) fire NO tracking events: conversions are completely unmeasured
details

Where: /join-waitlist, /memberships, / (all CTAs) · waitlist onSubmit / CTA handlers

What: The compiled app bundle contains zero dataLayer.push, gtag(), clarity(), fbq(), data-gtm, data-track, or trackEvent calls. The waitlist form posts its data to a backend (fields sms_consent, email_consent, source:"Founder Waitlist") but pushes no analytics/dataLayer event on submit; membership and Book CTAs likewise have no tracking hooks.

Fix: Add dataLayer.push({event:'waitlist_signup', source:...}) in the waitlist onSubmit handler and equivalent events (membership_click, book_click) on the CTAs, then create matching GTM triggers/tags.

Link: https://ritualstudiospa.com/join-waitlist, /memberships, / (all CTAs)

Note: CTA clicks reach GA4. Completed lead submissions are not counted in Google Analytics yet.

evidence
grep of /assets/index-CdEJd9NA.js (723 KB) for `dataLayer\.push|gtag\(|clarity\(|fbq\(|data-gtm|data-track|trackEvent` returned 0 matches. Waitlist submit payload found in bundle: `...email:w,phone:a.trim()||"",sms_consent:d,email_consent:c,source:"Founder Waitlist"` with no adjacent analytics call.
PartialMEDfindingWEB-A13Meta description and homepage value prop omit half the service menu
details

Where: / (raw meta) and Home

What: The static meta description sells only contrast therapy: 'Ritual Studio offers contrast therapy with sauna and cold plunge in University Place. Join our wellness community for science-backed recovery in a modern spa.' Yet the site has full dedicated service pages for CoolSculpting ($750 to $1,500) and Cryo T-Shock Facials ($150). The LocalBusiness schema description does list all four, so the omission is only in the human-facing description/hero.

Fix: Rework the meta description and a homepage line to mention the full menu, e.g. '...contrast therapy, CoolSculpting, and cryo facials in University Place, WA.'

Link: https://ritualstudiospa.com/ (raw meta) and Home

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Raw-HTML layer is fixed: curl of / now returns meta description "Ritual Studio is a University Place contrast therapy spa offering sauna, cold plunge, CoolSculpting, and cryo facials for Tacoma and Pierce County.", the prerendered lede mentions all services ("...sauna, cold plunge, rest, CoolSculpting, and cryo facials"), and schema lists them. BUT the SPA u

evidence
meta description: '...offers contrast therapy with sauna and cold plunge in University Place...' (no mention of CoolSculpting or cryo facials). Schema description does include them: '...contrast therapy, sauna, cold plunge, CoolSculpting, and cryo facials...'
PartialMEDfindingWEB-A14The primary conversion action is labeled 5 different ways across the site
details

Where: / (and sitewide)

What: The same single goal, join the founding-member waitlist, appears under at least five distinct labels: 'Join Waitlist' (header/home/contrast), 'Get Founding Rates' (home hero), 'Join the Founder Waitlist' (home/memberships/about/contrast), 'Get Founding Access' (cryo facials), and 'Book Your Session' (journal).

Fix: Standardize on one primary CTA label, 'Join the Founding Member Waitlist', used identically in the nav and every page's hero and closing CTA; drop the variant phrasings.

Link: https://ritualstudiospa.com/ (and sitewide)

Note: Sep 23 check: most pages now say Join the Founding Member Waitlist, but the header still says Join Waitlist, CoolSculpting and Cryo say Get Founding Access (CoolSculpting also Join Waitlist for Founding Rates), and the Journal says Book Your Session. As of Aug 12, before the Aug 31 site rebuild: partly done.

evidence
WebFetch home CTAs: 'Join Waitlist', 'Get Founding Rates', 'Join the Founder Waitlist'; /services/cryo-facials CTA 'Get Founding Access'; /journal CTA 'Book Your Session'; /memberships CTA 'Join the Founder Waitlist'.

[Independently confirmed by 2 audit lenses: cro, copy]
PartialMEDfindingWEB-A15No testimonials, reviews, or social proof anywhere on the site
details

Where: / , /about, /memberships, /services/* (sitewide)

What: Across the homepage, About, Memberships, and all three service pages there are zero customer testimonials, star ratings, review counts, or Google/Yelp social proof. Trust rests entirely on the Dave Canales (NFL coach) association.

Fix: Add a testimonials/review section (even pre-launch: quotes from founding-cohort beta users, staff/practitioner credentials, or 'as seen in' press) and embed Google review rating once available.

Link: https://ritualstudiospa.com/ , /about, /memberships, /services/* (sitewide)

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Still no actual testimonials/ratings/reviews anywhere: 0 "testimonial" matches in the full 723KB SPA bundle; only "review" strings are privacy-policy text; no star ratings, review counts, or Google/Yelp embeds on /, /about, /memberships, or the three service pages (curl + WebFetch). What IS implemented: an honest testimonials-alternative: crawler-visible ho

evidence
WebFetch across /, /about, /memberships, /services/coolsculpting, /services/cryo-facials, /services/contrast-therapy each report 'No customer testimonials present' / 'No testimonials, reviews'.
PartialMEDfindingWEB-A20No business hours published anywhere on the site or in schema
details

Where: /contact, /about, and JSON-LD · Contact/About + schema

What: No hours of operation appear on the Contact page, the About page, the footer, or in the LocalBusiness JSON-LD (no openingHours).

Fix: Publish opening hours in the Contact page location block and the footer, and mirror them in openingHoursSpecification in the JSON-LD.

Link: https://ritualstudiospa.com/contact, /about, and JSON-LD

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. JSON-LD on /, /contact, /about now includes an openingHours field, but its value is the placeholder "Opening January 2027": not actual hours and not valid schema.org format; no openingHoursSpecification anywhere. Contact page body now explicitly states "Opening January 2027. Public hours will be published before booking opens." alongside the full address, s

evidence
WebFetch /contact: 'No hours of operation are listed on this page.' WebFetch /about: 'No business hours of operation are listed.' JSON-LD has no openingHours field.

[Independently confirmed by 2 audit lenses: seo-local, cro]
PartialMEDfindingWEB-A25LocalBusiness/Organization JSON-LD missing key fields; Facebook absent from sameAs
details

Where: All pages (structured data) · JSON-LD LocalBusiness

What: The HealthAndBeautyBusiness schema has name, description, address, geo, telephone, email, but omits several fields Google uses for rich local results: no @id, no priceRange, no openingHoursSpecification/openingHours, no areaServed, and url points to the redirecting www host. sameAs lists only Instagram; the known Facebook page is missing.

Fix: Add openingHoursSpecification, priceRange, areaServed, @id (canonical URL as identifier), and add the Facebook URL to sameAs. Point url/logo/image at the canonical apex host.

Link: https://ritualstudiospa.comAll pages (structured data)

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Verified live 2026-08-12 via curl on raw HTML (server-rendered, no JS). Homepage, /contact, /about, /journal all carry two JSON-LD blocks. Organization block now has @id=https://ritualstudiospa.com/#organization and sameAs listing Instagram, the exact Facebook URL from the finding (https://www.facebook.com/people/Ritual-Studio/61587389750679/), and a Google

evidence
LocalBusiness JSON-LD sameAs = ['https://www.instagram.com/myritualstudio/'] only; no openingHoursSpecification/priceRange/areaServed/@id keys present. Facebook /people/Ritual-Studio/61587389750679/ is known to exist but is not listed.
PartialMEDfindingWEB-A31Single 723 KB monolithic JS bundle, render-blocking with no code-splitting or modulepreload
details

Where: All pages (SPA shell) · <head> of every page

What: The entire app ships as one module script /assets/index-CdEJd9NA.js weighing 723,009 bytes raw (199 KB gzip). It is a render-blocking <script type="module"> in <head> with no route-based code splitting and no modulepreload/preload hints. As a client-rendered SPA, nothing paints until this bundle downloads, parses, and executes.

Fix: Enable route-based code splitting (React.lazy + dynamic import per page) so each route loads only its chunk, and add rel="modulepreload" for the entry chunk. Consider SSR/prerender (Lovable supports static export) so first paint doesn't wait on JS at all.

Link: https://ritualstudiospa.comAll pages (SPA shell)

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Still PARTIAL. Live check 2026-08-12: old /assets/index-CdEJd9NA.js returns 404; homepage now ships /assets/index-ritual-website-20260806.js?v=20260809 as the sole <script type="module"> in <head>. That bundle is 723,268 bytes raw (~236 KB gzip transfer): same monolith as the original 723,009 B, just renamed. Downloaded bundle contains 0 dynamic imports (gr

evidence
Raw HTML: <script type="module" crossorigin src="/assets/index-CdEJd9NA.js">; only /assets/index-CdEJd9NA.js and index-BgpPUzUJ.css referenced (no other chunks). Size: 723009 bytes raw / 199325 bytes gzip transfer.
PartialMEDfindingWEB-A32No preload of LCP image; render-blocking Google Fonts stylesheet on the critical path
details

Where: All pages · <head> font + resource hints

What: The head has only two preconnects (fonts.googleapis.com, fonts.gstatic.com) and a render-blocking external Google Fonts stylesheet loading two families x five weights (Poppins + Montserrat 300-700). There is no preload for the hero LCP image and no self-hosting of fonts.

Fix: Self-host only the font weights actually used (subset to latin, drop unused weights) with font-display: swap, or keep Google Fonts but preload the woff2 files. Add <link rel="preload" as="image"> for the first hero. Drop preconnects you don't need if fonts are self-hosted.

Link: https://ritualstudiospa.comAll pages

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. LCP preload half is genuinely fixed: homepage head now has <link rel="preload" as="image" href="/assets/hero-new-1-1600.webp"> with responsive imagesrcset (480/768/1200/1600w), imagesizes, type=image/webp, and fetchpriority=high; the asset returns 200 (71 KB) and the JS bundle confirms hero-new-1-1600.webp is the actual hero, so the preload matches real usag

evidence
Raw HTML head: <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">; grep of rel= shows only '2 preconnect' and zero preload/modulepreload.
PartialMEDfindingWEB-A33Three analytics/tag scripts load on every page, one making a broken junk request
details

Where: All pages · <head> analytics block

What: Every page loads three separate third-party/analytics scripts: Google Tag Manager (GTM-K7G3DTG3), Microsoft Clarity, and Lovable's /~flock.js. The Clarity snippet still contains the literal placeholder id, requesting https://www.clarity.ms/tag/YOUR_CLARITY_ID on every page load.

Fix: Replace YOUR_CLARITY_ID with the real Clarity project id or remove the Clarity snippet entirely. Consolidate analytics: if GTM is the tag manager, fire Clarity through it and drop the standalone script; confirm flock.js is still wanted.

Link: https://ritualstudiospa.comAll pages

Note: Sep 23: the placeholder Clarity tag is back; every page requests clarity.ms/tag/YOUR_CLARITY_ID (400). Remove it and keep only the real project tag.

evidence
Raw HTML: t.src="https://www.clarity.ms/tag/"+i ... "clarity","script","YOUR_CLARITY_ID"); plus GTM inline loader for GTM-K7G3DTG3 and <script defer src="/~flock.js" data-proxy-url="/~api/analytics">.
PartialMEDfindingWEB-S05Add a 'What to Expect' + FAQ layer with real specs on every service page
details

Where: Contrast Therapy: /services/contrast-therapy (and other service pages)

What: The Contrast Therapy page has good outcome headings and a 'What to Expect' section, but no FAQ and no published specs beyond sauna 15-20 min / plunge 30s-3min: no temperatures, no 90-min cycle structure, no adjustable-temp detail.

Fix: Publish exact experience specs (sauna temp range, plunge temp, 90-min cycle, adjustable temperatures) and a 10-15 question FAQ (first-timer nerves, what to bring, is it safe, how cold, how often): plain-spoken, reassuring, on-brand.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. A "What To Expect" + 4-question "First-Visit FAQ" now exists on /services/contrast-therapy, but ONLY in the crawler/no-JS prerendered shell: the React bundle (index-ritual-website-20260806.js) contains zero FAQ strings and replaces the shell on mount, and the enhancements JS (website-enhancements-20260809.js) appends only a single What-To-Expect paragraph (

evidence
[Brand-safe / additive: no redesign needed] HigherDOSE's DOSE-Lab and Bathhouse's amenity transparency build first-timer confidence AND capture long-tail SEO. Specs + FAQ de-intimidate the cold plunge and answer the exact questions that stall commitment pre-launch.
PartialMEDfindingWEB-S06Rewire Contact so warm intent feeds the founder waitlist, not a generic inbox
details

Where: Contact: /contact

What: Contact captures a generic 'message' separate from the founding-member SMS/email consent flow; no confirmation/thank-you behavior described; not clearly positioned as secondary to the waitlist.

Fix: Add an opt-in on Contact ('Also add me to the founder waitlist') that pushes into the same tracked list; add a confirmation state; position Contact as secondary to the waitlist CTA. Does Contact earn its place? Yes, it supplies NAP consistency and a real phone/email, but only if it also advances the funnel; today it's inert.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Partially implemented, not the full rewire. Implemented: (1) /contact prerender shell and the enhancements JS (website-enhancements-20260809.js) both add a "Join the Founder Waitlist" form on the contact page with first/last name, email, phone, and explicit email/SMS consent checkboxes, posting to /api/website/waitlist with sms_consent/email_consent booleans

evidence
[Brand-safe / additive, no redesign needed] A high-intent visitor can ask about founding memberships and never land on the tracked waitlist, intent gets split across two funnels instead of feeding the one that matters pre-launch.
PartialMEDfindingWEB-S07Formalize the Canales/founder credibility as attributed proof + a validation section
details

Where: About, /about and Home, /

What: The athlete/firefighter/NFL story is presented as the founders' own ethos quote rather than attributed third-party proof; there's no press/validation block.

Fix: Add an attributed proof block (Dave & Lizzy Canales named, roles/quotes attributed), any press mentions, and founding-member scarcity as a trust stack: kept warm and accessible, never a luxury/celebrity flex.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Verified live 2026-08-12 via curl of https://ritualstudiospa.com/ and /about plus full inspection of the shipped SPA bundle (/assets/index-ritual-website-20260806.js) and enhancements JS (/assets/website-enhancements-20260809.js). Genuinely implemented: About's rendered SPA now separates the founders' own athlete/firefighter story (image alt names "Ritual St

evidence
[Brand-safe / additive: no redesign needed] Remedy Place shows a named-founder + press wall does the trust-building of a blog at a fraction of the effort. Ritual's NFL credibility is a genuine moat for a business no one can walk into yet: but only if it reads as proof.
PartialMEDfindingWEB-S09Add waitlist-reassurance microcopy beside every CTA
details

Where: Memberships: /memberships and all CTAs

What: Nothing tells the visitor that joining the waitlist doesn't charge them now and simply locks the founding rate.

Fix: Add microcopy like 'Joining the waitlist won't charge you: it just locks your founding rate' beside CTAs. Brand-safe, reinforces accessible-not-luxury.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Reassurance microcopy exists but not "beside every CTA". Observed live 2026-08-12: (1) Prerendered crawler HTML on /memberships, /, and /join-waitlist contains "no charge to join the waitlist" / "Joining does not charge you..." beside an inline waitlist form: but this static shell lives inside #root and is replaced on React mount, so it is crawler/no-JS-fac

evidence
[Brand-safe / additive: no redesign needed] That unspoken 'am I about to be charged?' friction is exactly what kills pre-launch signups; one line removes it.
PartialMEDfindingWEB-B02Name contrast sessions by outcome using Ritual's OWN benefit language
details

Where: Service pages / Memberships

What: Othership names classes by emotional outcome (UP/DOWN/ALL AROUND) so the name doubles as an intent keyword and makes a cold plunge feel like choosing a mood.

Fix: Package sessions/experiences under Ritual's existing outcome names as ownable, repeatable products (its version of Remedy's named 'Remedies').

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Verified live 2026-08-12 via curl of raw HTML + both JS bundles. The benefit vocabulary half is implemented: prerendered homepage and a dedicated /benefits page (in sitemap.xml, h1 "Recovery Benefits") carry a "Why People Come Back" grid with h3 headings "Recover Faster / Stress Less / Think Clearer / Feel Better" + descriptions, and the React bundle (index-

evidence
[Brand-safe / additive: no redesign needed] Ritual already has the perfect vocabulary (Think Clearer / Recover Faster / Stress Less / Feel Better); using it to name sessions makes names double as keywords and de-intimidates first-timers without inventing anything off-brand.
PartialMEDfindingWEB-B03Publish exact experience specs on-page (Bathhouse-style transparency)
details

Where: Contrast Therapy & location page

What: Bathhouse publishes exact temperatures, materials, and pool-by-pool detail so a skeptical local searcher can act immediately and trust the experience.

Fix: Publish sauna temp range, plunge temp, 90-min cycle structure, and adjustable-temperature detail: accessible framing, not a luxury spec sheet.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Cycle structure is now published but exact specs are not. Both /services/contrast-therapy and /sauna-cold-plunge-university-place (the /contrast-therapy and /location paths 404; these are the real URLs) have prerendered "Four-Step Ritual" cycle structure, a What To Expect list, plunge duration guidance (start 30s, up to 3 min), and a First-Visit FAQ includin

evidence
[Brand-safe / additive: no redesign needed] Specs are both credibility (backs 'effective, science-backed') and long-tail SEO. Ritual currently lists only durations, not temperatures or full cycle structure.
PartialMEDfindingWEB-B05Deploy Remedy Place's founder-credibility engine (Ritual's real moat)
details

Where: About / Home

What: Remedy Place uses a named-expert founder + press-logo wall to build trust at a fraction of a blog's effort.

Fix: Formalize the founder/validation section with attributed names, roles, and any press; keep it warm and accessible, deliberately staying on the non-elite side of Remedy.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Verified live 2026-08-12 via curl of https://ritualstudiospa.com/ and /about plus the live SPA bundle (/assets/index-ritual-website-20260806.js?v=20260809) and enhancements JS. Genuinely implemented: (1) SPA About page has an attributed "Our Partners in the NFL" section naming Dave Canales, Head Coach of the Carolina Panthers, and wife Lizzy with photo and f

evidence
[Brand-safe / additive, no redesign needed] Ritual's athlete/firefighter/NFL (Canales) founders are a Remedy-caliber trust asset that needs no blog to work, but it must be presented as attributed proof and validation, not a soft ethos quote.
PartialLOWfindingWEB-A46LocalBusiness schema missing openingHours, priceRange, areaServed, and a map/GBP reference
details

Where: / (JSON-LD in every page shell) · Structured data

What: The HealthAndBeautyBusiness JSON-LD includes name, description, url, logo, image, email, telephone, address, geo, and sameAs: but has NO openingHours/openingHoursSpecification, NO priceRange, NO areaServed, and NO hasMap.

Fix: Add openingHoursSpecification (real hours), priceRange (e.g. '$$'), areaServed (University Place, Tacoma, Pierce County WA), and hasMap (the Google Business Profile / Maps URL) to the LocalBusiness JSON-LD.

Link: https://ritualstudiospa.com/ (JSON-LD in every page shell)

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. Live HealthAndBeautyBusiness JSON-LD (served in the raw HTML shell of /, /memberships, /contact, /services/contrast-therapy, /journal) now contains priceRange "$$", areaServed ["University Place, WA","Tacoma, WA","Pierce County, WA"], and hasMap (Google Maps search URL for 2310 Mildred St W Suite 128, also duplicated in sameAs): those three are genuinely im

evidence
JSON-LD block contains `"@type": "HealthAndBeautyBusiness"` with geo `47.2359 / -122.5484` but no `openingHours`, `priceRange`, or `areaServed` keys anywhere.
PartialLOWfindingWEB-S12Add a lightweight benefit-intent hub organized by Ritual's own outcomes
details

Where: New sections/pages linked from Home & service nav

What: Benefit language (Think Clearer / Stress Less / Recover Faster / Feel Better) exists as headings but not as dedicated benefit-intent landing sections that capture 'sauna for stress', 'cold plunge for recovery' searches.

Fix: Create four short benefit sections/anchors (Recover Faster, Stress Less, Think Clearer, Feel Better), each with a paragraph of cited, plain-spoken science and an internal link to the relevant article/service.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. The benefit-intent hub page genuinely exists at https://ritualstudiospa.com/benefits: HTTP 200, prerendered no-JS body with the four outcome sections in Ritual's own language (Recover Faster / Stress Less / Think Clearer / Feel Better, each with copy), a Research Notes section citing 3 PubMed/PMC papers, an inline founder-waitlist form (data-ritual-lead-form

evidence
[Brand-safe / additive: no redesign needed] HigherDOSE's benefit-first IA captures intent search using the customer's own goals. Ritual can do this with its EXISTING language, no new vocabulary needed.
PartialLOWfindingWEB-B07Frame the ritual inside the human heritage of contrast bathing
details

Where: Journal

What: Bathhouse borrows credibility from centuries of bathing tradition, reframing 'novelty cold plunge' as an ancient, legitimate practice.

Fix: Include one Journal piece on the history/tradition of sauna and cold bathing, tied back to Ritual's accessible, communal ethos.

Note: As of Aug 12, before the Aug 31 site rebuild: partly done. A dedicated heritage article does exist on the live site: https://ritualstudiospa.com/journal/history-of-sauna-and-cold-bathing ("The Human History of Sauna and Cold Bathing"). It is listed in sitemap.xml, linked from the /journal index ("A short look at the communal heritage of heat, cold, bathing, and recovery rituals"), and served with server-side title,

evidence
[Brand-safe / additive, no redesign needed] Heritage framing adds legitimacy and evergreen SEO while reinforcing 'science-backed but accessible', as long as it avoids Bathhouse's more elevated luxury lean.
PartialtaskWEB-ROAD1Update opening date to early 2027 (homepage banner + OG share image)
details

What: The banner and the share image said Opening Fall 2026. Change both to early 2027.

Note: Sep 23 check: the homepage banner reads Opening in early 2027. The share image (og-image.png) still says OPENING FALL 2026, so shared links show the old date.

✓ FixedHIGHfindingWEB-A01robots.txt + sitemap.xml point every URL to the OLD lovable.app domain, not the real site
details

Where: /robots.txt and /sitemap.xml · Site-wide crawl directives

What: robots.txt ends with `Sitemap: https://ritual-space-design.lovable.app/sitemap.xml`, and /sitemap.xml lists `<loc>https://ritual-space-design.lovable.app/...</loc>` for all 8 URLs (home, about, memberships, contrast-therapy, coolsculpting, cryo-facials, contact, join-waitlist). Not a single URL uses ritualstudiospa.com.

Fix: Regenerate robots.txt and sitemap.xml with https://ritualstudiospa.com as the base for every <loc> and the Sitemap: directive, and submit the corrected sitemap in Google Search Console. Also add the /journal, /privacy-policy and /terms-of-service URLs that are currently absent from the sitemap.

Link: https://ritualstudiospa.com/robots.txt and /sitemap.xml

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-ran the exact WEB-A01 check via curl against BASE=https://ritualstudiospa.com on 2026-08-06. /robots.txt (HTTP 200, Content-Length 284, Last-Modified Thu 06 Aug 2026 21:14:08 GMT) now ends with: `Sitemap: https://ritualstudiospa.com/sitemap.xml` No `lovable.app` reference anywhere in the file. It also adds explicit Allow rules for Googlebot/Bingbot/Twitte

evidence
robots.txt: `Sitemap: https://ritual-space-design.lovable.app/sitemap.xml`. sitemap.xml: `<loc>https://ritual-space-design.lovable.app/</loc>`, `<loc>https://ritual-space-design.lovable.app/contact</loc>` etc.

[Independently confirmed by 6 audit lenses: seo-local, seo-onpage, web-func, cro, seo-tech, analytics]
✓ FixedHIGHfindingWEB-A02Hero image is a 2.9 MB JPEG: cripples LCP and mobile page weight
details

Where: / (Home) · Home hero carousel

What: The home hero loads /assets/hero-new-1-D_tIEW2f.jpg at 2,971,650 bytes (2.9 MB) as a raw JPEG with no responsive srcset, no width/height, and no preload. It is one of several full-bleed hero images in the top carousel.

Fix: Re-encode the hero to WebP/AVIF at display resolution (a 1600px-wide hero should be ~150-250 KB), provide a responsive srcset/sizes for mobile, and add <link rel="preload" as="image"> for the first hero so it is fetched before the JS bundle parses.

Link: https://ritualstudiospa.com/ (Home)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. FIXED. Re-tested live via curl on 2026-08-06. Original: home hero loaded /assets/hero-new-1-D_tIEW2f.jpg at 2,971,650 bytes (2.9 MB) raw JPEG, no srcset, no preload; head had only 2 preconnects and no rel=preload image. Now: 1) The 2.9 MB JPEG is NO LONGER referenced in the HTML. grep of freshly-fetched home HTML shows hero uses responsive WebP variants only

evidence
curl -s https://ritualstudiospa.com/assets/hero-new-1-D_tIEW2f.jpg | wc -c → 2971650 bytes; Content-Type: image/jpeg. Raw HTML head contains only 2 preconnects and no rel=preload for any image.

[Independently confirmed by 2 audit lenses: web-perf, design]
✓ FixedHIGHfindingWEB-S01Populate the Journal with 5-6 seeded evergreen articles (the highest-leverage SEO move)
details

Where: Journal, https://ritualstudiospa.com/journal

What: The Journal is live but empty: a tagline and a CTA, zero articles. The one content engine that could rank the domain for local + topical long-tail is built and unused.

Fix: Seed 5-6 brand-voice pieces that double as keyword surface: (1) 'What is contrast therapy? The heat/cold/rest ritual explained', (2) 'Sauna & cold plunge in University Place: what to expect your first time', (3) 'Why we go tech-free', (4) 'The science of cold plunge for recovery & mood' (cite real studies), (5) 'Contrast therapy for busy parents & professionals in Tacoma', (6) 'Consistency gets results: building a recovery ritual'. Keep them plain-spoken and somatic, not clinical.

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live via curl. /journal is no longer empty: it renders a prerendered "Start Here" section with 7 article cards linking to real URLs. All 7 articles (what-is-contrast-therapy, sauna-cold-plunge-university-place-first-visit, why-we-go-tech-free, science-of-cold-plunge-recovery-mood, contrast-therapy-for-busy-parents-professionals-tacoma, building-a-co

evidence
[Brand-safe / additive: no redesign needed] This is the single biggest miss against the stated pre-launch SEO goal. Content compounds in Google over the months before opening; with no articles, the site's ranking ceiling is one indexable sales page. Competitors (HigherDOSE, Bathhouse) win discovery precisely here.
✓ FixedHIGHfindingWEB-S02Build a dedicated University Place / Tacoma location landing page
details

Where: New page (e.g. /university-place or /sauna-cold-plunge-university-place)

What: There is no location-specific page; local keywords live only in the footer address. Google has thin signal tying the site to the physical University Place location.

Fix: Local H1 ('Sauna & Cold Plunge in University Place, WA'), full NAP, embedded map, neighborhood/Tacoma/Pierce County anchors, the visual 4-step ritual, opening-status, and a deep FAQ: all in Ritual's warm voice with a waitlist capture on-page.

Note: Round-4 independent verify (2026-08-12): verified implemented. Live-verified via curl: https://ritualstudiospa.com/sauna-cold-plunge-university-place returns HTTP 200 with server-rendered static HTML (seo-prerender shell) implementing the full recommended pattern: local H1 "Sauna & Cold Plunge in University Place, WA"; NAP in body + PostalAddress JSON-LD (2310 Mildred St W, Suite 128, University Place, WA 98466; +1-253-

evidence
[Brand-safe / additive: no redesign needed] Othership's per-location pages (local H1 + NAP + map + 4-step journey + FAQ) are the best-in-class local-rank-and-convert pattern. With Urban Float already in University Place and Svette/Plunge + Restore/Modern Sauna in Tacoma, this is how Ritual competes for 'sauna near me' / 'cold plunge University Place' before opening.
✓ FixedHIGHfindingWEB-S04Embed waitlist capture inline at every peak-intent moment (stop linking away)
details

Where: Home, /, About, /about, service & membership pages

What: Every 'Ready to become a founding member?' CTA links out to /join-waitlist. The strongest conversion moments are where friction is highest.

Fix: Drop the existing waitlist email field (with SMS/email consent) inline into each closing CTA block; keep /join-waitlist as the deep-link fallback. Fire a distinct analytics event per capture location.

Note: Round-4 independent verify (2026-08-12): verified implemented. Inline waitlist capture verified live in both layers. Raw HTML (curl): /, /about, /memberships, /services/contrast-therapy each ship a prerendered section.ritual-inline-waitlist with form[data-ritual-lead-form="waitlist"] and per-page data-source (home-crawler/about-crawler/memberships-crawler/contrast-crawler) containing name/email/phone/consent fields. Ren

evidence
[Brand-safe / additive: no redesign needed] For a page whose #1 job is waitlist signups, each extra click leaks high-intent visitors right when trust peaks. Bathhouse/Othership capture on the page. This also improves the analytics/tracking foundation by putting measurable events on every page.
✓ FixedHIGHfindingWEB-A62Rory systemwide changes shipped only as a runtime text-mask; raw HTML, bundle, and metadata still serve the OLD naming, dates, and prices
details

Where: site-runtime-20260821-modal-copy-checkbox.js vs prerendered HTML + index bundle

What: The new tiers/naming/dates exist only in a client-side find-and-replace script. Crawlers, social scrapers, and no-JS visitors get: Founder Waitlist (16x raw HTML, 21x bundle), first 100 (11x), Opening January 2027 everywhere (early 2027 appears ZERO times), old prices in the bundle, og:image literally named og-image-jan-2027.jpg, twitter alt says opening January 2027. Also still live: the About internal editorial note in crawler HTML, the founder-bios placeholder line rendering on About, the co-founder image alt, and the waitlist form still submits source=Founder Waitlist to the backend.

Fix: Bake the changes into the actual content: React bundle + prerendered shells + title/meta/og per route; remove the mask script once baked. Delete the About editorial note and placeholder line at source. Rename/replace the og image (content says January 2027). Fix the co-founder alt, the form source value, and the first 75 members Only casing bug.

Link: https://ritualstudiospa.com/

Note: Verified live 2026-08-31: new bundle index-BNyQAV0Y.js, zero Founder Waitlist / first 100 / January 2027 / per month in raw HTML and bundle, mask script removed, About placeholder + editorial leak gone, co-founder alt gone, form source fixed, og-image.png replaces jan-2027 file. Genuinely baked.

evidence
Verified live 2026-08-21 by 5 agents via curl of raw HTML + both bundles + rendered views. Full detail in run log.
✓ FixedMEDfindingWEB-A03No skip-to-content link
details

Where: All pages · /assets/index-CdEJd9NA.js

What: There is no 'Skip to main content' / 'Skip navigation' link. Searching the entire JS bundle for skip-to-main / skip-to-content / 'skip navigation' returns zero matches; the only sr-only usage is Radix/Tailwind utility classes and a visually-hidden H1, not a bypass link.

Fix: Add a visually-hidden-until-focused anchor as the first focusable element in <body>: <a href="#main" class="sr-only focus:not-sr-only …">Skip to content</a>, and give the main content wrapper id="main" (and role/main or a <main> element).

Link: https://ritualstudiospa.comAll pages

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. FIXED. A proper skip-to-content link now exists and is server-rendered in the raw HTML shell (no JS needed). Observed output: - `curl -s -L https://ritualstudiospa.com` body contains, as the first element in <body>: `<a class="skip-to-content" href="#main">Skip to content</a>` - Target exists: grep of current bundle /assets/index-ritual-website-20260806.js (

evidence
grep -iE 'skip[- ]?to[- ]?(main|content)|skip navigation|skip link' over /assets/index-CdEJd9NA.js = 0 matches; only 'sr-only' utility class and a sr-only H1 ('Ritual Studio - Contrast Therapy and Cold Plunge Spa') exist.
✓ FixedMEDfindingWEB-A04Reduced-opacity text tokens can fall below AA contrast for normal-size text
details

Where: Multiple (any section using muted/secondary text) · /assets/index-BgpPUzUJ.css

What: The design applies text at reduced alpha via utilities such as .text-foreground/50 (color:hsl(var(--foreground)/.5)) and .text-charcoal/60. With --foreground: 90 9% 12% over --background: 40 14% 97%, a 50% alpha composites to roughly a mid-grey around ~3:1 against the cream background, fine for large text but under the 4.5:1 required for normal body copy.

Fix: For any normal-size text, avoid <70% alpha on the foreground token; either use a dedicated muted token whose measured ratio is ≥4.5:1, or reserve text-foreground/50-style opacities for large headings/decorative text only. Verify each usage with a contrast checker.

Link: https://ritualstudiospa.comMultiple (any section using muted/secondary text)

Note: Verified 2026-08-08 (dev round 3): muted-foreground now overridden to hsl(var(--foreground)/.88)!important in live CSS; dark text on light bg passes AA.

evidence
CSS tokens: '--foreground: 90 9% 12%', '--background: 40 14% 97%', and utilities '.text-foreground\/50{color:hsl(var(--foreground) / .5)}', '.text-charcoal\/60{color:hsl(var(--charcoal) / .6)}'. (Note: --muted-foreground is set identical to --foreground '90 9% 12%', so muted text that uses the full-opacity token is fine; the risk is specifically the /50 to /60 opacity variants.)
✓ FixedMEDfindingWEB-A07GTM container loads but contains no Google Analytics (GA4) tag: only one unknown custom-template tag
details

Where: All pages · GTM container GTM-K7G3DTG3

What: GTM-K7G3DTG3 is a real, live container (326 KB gtm.js), but its tags array holds a single tag: a custom template __cvt_MQDKZ with vtp_projectId "vgupxzv1e3". No GA4 config tag (__googtag/__gaawe), no Google Ads (AW-) conversion, and no Meta Pixel are present. No G-XXXXXXX / UA- / AW- ids appear anywhere in the container.

Fix: Add a GA4 configuration tag (and Google Ads/Meta if used) to the GTM container, and confirm what the lone custom template (project vgupxzv1e3) actually is/does; remove it if it's an orphan.

Link: https://ritualstudiospa.comAll pages

Note: Verified: dead GTM container removed (no GTM-K7G3DTG3 in HTML); GA4 G-0RPQMPVPL9 now installed and firing.

evidence
gtm.js tags array: `"tags":[{"function":"__cvt_MQDKZ","once_per_event":true,"vtp_projectId":"vgupxzv1e3","tag_id":4}]`. grep for `G-[A-Z0-9]{6,}|UA-[0-9]+-[0-9]+|AW-[0-9]+` in the container → no analytics/ads ids.
✓ FixedMEDfindingWEB-A08No cookie-consent / privacy banner: GTM, Clarity, and Lovable analytics all fire before any consent
details

Where: All pages · index.html + React bundle

What: All three trackers are hard-wired into the static <head>/<body> (GTM inline + noscript iframe, Clarity loader, and a deferred /~flock.js analytics script) and execute immediately on load. The app bundle contains no cookie-consent/GDPR/CCPA banner component (no CookieBanner/gdpr/ccpa/acceptCookies strings); the only "consent" strings in the bundle are SMS/email marketing-opt-in checkboxes inside the waitlist form, not a cookie gate.

Fix: Add a consent banner that defaults tracking off and gates GTM/Clarity/flock behind opt-in (or at minimum an opt-out link), using GTM Consent Mode.

Link: https://ritualstudiospa.comAll pages

Note: Verified via render: cookie-consent banner shows on fresh visit; 0 tracker requests before Accept, GA4+Clarity fire only after Accept. Correctly gated.

evidence
Static shell loads `<script>...'GTM-K7G3DTG3'</script>`, `clarity` loader, `<script defer src="/~flock.js" data-proxy-url="/~api/analytics">` and GTM noscript iframe with no gating. Bundle grep for `cookie-consent|CookieBanner|gdpr|ccpa|My Health My Data|acceptCookies` → 0 matches; the 15 "consent" hits are all smsConsent/emailConsent form fields.
✓ FixedMEDfindingWEB-A09Undisclosed third tracker: Lovable /~flock.js telemetry firing on every page via first-party proxy
details

Where: All pages · index.html <head>

What: Alongside GTM and Clarity, the static shell injects `<script defer src="/~flock.js" data-proxy-url="/~api/analytics">`: Lovable's built-in "flock" analytics, which beacons to the same-origin proxy /~api/analytics (returns 200) so it looks first-party. This tracker is not mentioned anywhere in the privacy policy and duplicates analytics coverage that GTM is supposed to handle.

Fix: Decide whether Lovable flock analytics should run; if kept, disclose it in the privacy policy and gate it behind consent; if not, disable it in the Lovable project settings.

Link: https://ritualstudiospa.comAll pages

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Genuinely fixed. Raw HTML (curl -s -L https://ritualstudiospa.com/) no longer contains the `<script defer src="/~flock.js" data-proxy-url="/~api/analytics">` injection: grep for flock|lovable|proxy-url|~api returns zero hits; the only script tags now are one inline script, two ld+json blocks, /assets/site-runtime-20260806.js, and the index-ritual-website-20

evidence
Raw HTML: `<script defer src="/~flock.js" data-proxy-url="/~api/analytics"></script>`. curl -sI https://ritualstudiospa.com/~flock.js → HTTP/1.1 200 (text/javascript); curl -sI https://ritualstudiospa.com/~api/analytics → HTTP/1.1 200.
✓ FixedMEDfindingWEB-A10Privacy policy discloses tracking only in generic terms: names none of the actual trackers (GTM/Google, Microsoft Clarity, Lovable)
details

Where: /privacy-policy · Privacy Policy body

What: The rendered privacy policy says only "We use cookies and similar tracking technologies to... Analyze website traffic... Deliver targeted content and advertisements" and points users to browser settings. It never names Google Tag Manager/Google, Microsoft Clarity, or Lovable/flock, and describes no consent banner or first-party opt-out.

Fix: List the specific analytics/advertising services in use (or remove those that aren't), state what data each collects, and add a working opt-out consistent with the consent banner.

Link: https://ritualstudiospa.com/privacy-policy

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live 2026-08-12. /privacy-policy (HTTP 200) now names every actual tracker in both the server-prerendered HTML and the client-rendered page: an "Analytics And Tracking Services" section lists Google Analytics 4 ("measures page views, route visits, and CTA events after cookie consent"), Microsoft Clarity ("heatmaps and session experience after cookie

evidence
WebFetch of /privacy-policy: policy text present for generic cookies but "Specific Trackers Not Mentioned: Google Analytics, Google Tag Manager, Microsoft Clarity, Lovable... No cookie-consent banner or opt-out mechanism is described."
✓ FixedMEDfindingWEB-A16Flagship Contrast Therapy service page shows no pricing while every other service page does
details

Where: /services/contrast-therapy

What: The Contrast Therapy page (the core offering) displays no prices, whereas CoolSculpting ($750/$1,000/$1,500), Cryo Facials ($150/$399/$749), and Memberships ($149/$169/$209 per 4 weeks) all show concrete pricing.

Fix: Add per-session/drop-in and membership-linked pricing to the contrast therapy page, or a clear 'included with membership from $149' anchor that links to the memberships tiers.

Link: https://ritualstudiospa.com/services/contrast-therapy

Note: Sep 23 check: /services/contrast-therapy now shows pricing: "Founding Member pricing starts at $149 every four weeks for the first 75 people who activate a paid membership." As of Aug 12, before the Aug 31 site rebuild: NOT actually done. Still reproduces.

evidence
WebFetch /services/contrast-therapy: 'Pricing: No pricing information is displayed'; contrast with /services/coolsculpting and /services/cryo-facials which list explicit prices.
✓ FixedMEDfindingWEB-A17High-intent CoolSculpting/Cryo buyers have no path to book or request a consult: only 'join waitlist'
details

Where: /services/coolsculpting , /services/cryo-facials

What: These pages present firm prices and before/after results, then offer only 'Join our waitlist for exclusive founding member access' as the action. There is no consultation request form, no booking, and no 'contact us about this treatment': just the generic waitlist and footer NAP.

Fix: Add a treatment-specific 'Request a Consultation' CTA (short form capturing area of interest) alongside the waitlist option on the CoolSculpting and Cryo pages.

Link: https://ritualstudiospa.com/services/coolsculpting , /services/cryo-facials

Note: Round-4 independent verify (2026-08-12): verified implemented. Both /services/coolsculpting and /services/cryo-facials now provide a real consultation path. Prerendered HTML (curl) on each page contains a "Request a Consultation" section with a full form (Name/Email/Phone/Message, optional "Also add me to the Founder Waitlist" checkbox, submit button "Request a Consultation", data-ritual-lead-form="contact"). For JS use

evidence
WebFetch /services/coolsculpting: prices $750/$1,000/$1,500, 'primary call-to-action is ... Join our waitlist ... lacks a direct booking interface'; /services/cryo-facials similar.
✓ FixedMEDfindingWEB-A19Interactive elements are 40px (or 36px) tall: below the 44px mobile tap-target minimum
details

Where: Site-wide (all pages; most impactful on Contact and Join Waitlist forms and every CTA button)

What: The compiled button component and form controls are all under the 44x44px recommended touch size. Button size variants render at 40px/36px and form fields at 40px.

Fix: Bump the default button/input height to h-11 (2.75rem/44px), icon buttons to h-11 w-11, and small buttons to at least h-10, or add min-height:44px to interactive elements on mobile breakpoints.

Link: https://ritualstudiospa.comSite-wide (all pages; most impactful on Contact and Join Waitlist forms and every CTA button)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-tested live 2026-08-06 via curl on fresh build /assets/index-ritual-website-20260806.js (Last-Modified 21:15:50) and .css (21:30:27). The button variant strings are UNCHANGED in JS: size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-full px-3",lg:"h-11 rounded-full px-8",icon:"h-10 w-10"}; inputs/selects still "h-10 w-full ..."; CSS still .h-10{height:2.5rem}

evidence
Button variants in index-CdEJd9NA.js: size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-full px-3",lg:"h-11 rounded-full px-8",icon:"h-10 w-10"} → h-10=2.5rem=40px, h-9=36px. Form inputs: "flex h-10 w-full rounded-md border border-input..." (40px). CSS confirms .h-10{height:2.5rem}, .h-9{height:2.25rem}.
✓ FixedMEDfindingWEB-A21Local-intent keyword targeting limited to 'University Place': no Tacoma or Pierce County reach
details

Where: / , /about, /services/contrast-therapy (titles, H1s, copy) · On-page local keyword coverage

What: Titles/H1s target 'University Place' only (rendered title 'Ritual Studio - Contrast Therapy Spa in University Place'; H1s 'What We Care About', 'Contrast Therapy'). No page copy mentions Tacoma or Pierce County, despite the 253/Tacoma-metro market, and areaServed is absent from schema.

Fix: Weave 'Tacoma' and 'Pierce County' naturally into titles/meta, an intro sentence on service pages, and the schema areaServed (e.g. 'Serving University Place, Tacoma & Pierce County, WA').

Link: https://ritualstudiospa.com/ , /about, /services/contrast-therapy (titles, H1s, copy)

Note: Round-4 independent verify (2026-08-12): verified implemented. Live-verified 2026-08-12 via raw curl (server-rendered HTML). Original repro no longer holds: Tacoma + Pierce County strings now present on all three audited pages. Homepage: meta description "...for Tacoma and Pierce County", visible lede "spa in University Place serving Tacoma and Pierce County...", JSON-LD areaServed ["University Place, WA","Tacoma, WA","

evidence
WebFetch /about, /contact, /services/contrast-therapy all report the only geography mention is the address line 'University Place, WA 98466'; no Tacoma/Pierce County strings found on any page.
✓ FixedMEDfindingWEB-A22OG url and both JSON-LD url/@id use www while the site is served at the apex (and no canonical tag exists)
details

Where: / (head of every page) · Meta / structured data host mismatch

What: og:url, og:image, Organization.url, and LocalBusiness.url all use https://www.ritualstudiospa.com, but https://www.ritualstudiospa.com/ returns `HTTP/1.1 302 Found` redirecting to https://ritualstudiospa.com/. The raw HTML head contains NO <link rel="canonical"> at all.

Fix: Pick one canonical host (the apex, since that is what serves 200), change www's 302 to a 301, add a self-referencing <link rel="canonical" href="https://ritualstudiospa.com/..."> per page, and update og:url + JSON-LD url/logo/image to the apex.

Link: https://ritualstudiospa.com/ (head of every page)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-ran exact check via curl on live post-move site. FIXED. (1) rel=canonical now PRESENT in raw HTML: `<link rel="canonical" href="https://ritualstudiospa.com/" />` (was absent). (2) og:url = "https://ritualstudiospa.com/" (apex, was www). (3) og:image / og:image:url / og:image:secure_url = "https://ritualstudiospa.com/og-image-jan-2027.jpg?v=20260806" (apex

evidence
`curl -sI https://www.ritualstudiospa.com/` => `HTTP/1.1 302 Found` / `Location: https://ritualstudiospa.com/`; head sets `og:url content="https://www.ritualstudiospa.com"` and JSON-LD `"url": "https://www.ritualstudiospa.com"`; no rel=canonical present in raw HTML.

[Independently confirmed by 4 audit lenses: seo-local, seo-onpage, analytics, seo-tech]
✓ FixedMEDfindingWEB-A23Every route serves an identical static <title>, meta description and OG tags to non-JS crawlers/social bots
details

Where: All 11 routes (raw HTML shell)

What: curl of /, /about, /memberships, /services/contrast-therapy, /services/coolsculpting, /services/cryo-facials, /contact, /join-waitlist, /journal, /privacy-policy, /terms-of-service ALL return the exact same head: title 'Ritual Studio | Make Space for Yourself | Premium Wellness in University Place', the same meta description, and og:title 'Ritual Studio | Make Space for Yourself' with og:url https://www.ritualstudiospa.com. The per-route unique titles/descriptions only exist after the React app rewrites them client-side.

Fix: Server-side render or pre-render per-route <title>/description/OG (Lovable/Cloudflare prerendering, a static per-route index.html per page, or an SSR migration). At minimum inject correct route-specific og:title/og:description/og:url into the static HTML for the key money pages.

Link: https://ritualstudiospa.comAll 11 routes (raw HTML shell)

Note: Round-4 independent verify (2026-08-12): verified implemented. Re-ran the original repro: curled all 11 routes (/, /about, /memberships, /services/contrast-therapy, /services/coolsculpting, /services/cryo-facials, /contact, /join-waitlist, /journal, /privacy-policy, /terms-of-service) and extracted title/meta description/og:title/og:url from the raw HTML. Every route now serves unique, route-specific values: e.g. / = "S

evidence
grep of raw HTML across all 10 non-home routes returns byte-identical `<title>Ritual Studio | Make Space for Yourself | Premium Wellness in University Place</title>` and the same og:title/og:url lines every time.

[Independently confirmed by 2 audit lenses: seo-onpage, seo-tech]
✓ FixedMEDfindingWEB-A24Soft 404: unknown URLs return HTTP 200 instead of 404
details

Where: Any non-existent path (SPA catch-all)

What: Requesting a bogus path returns a success status. `curl -sI https://ritualstudiospa.com/nonexistent-xyz` -> `HTTP/1.1 200 OK`.

Fix: Configure the host/edge (Cloudflare) to return a real 404 status for unmatched routes, or have the SPA's not-found route signal 404 via a prerender/edge function. Ensure only the 11 real routes + journal article slugs return 200.

Link: https://ritualstudiospa.comAny non-existent path (SPA catch-all)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-ran the exact repro against live https://ritualstudiospa.com. The soft-404 is GONE: bogus paths now return a genuine hard 404. Observed output: - `curl -sI https://ritualstudiospa.com/nonexistent-xyz` -> `HTTP/1.1 404 Not Found` (Server: nginx, Content-Type: text/html; charset=utf-8, Content-Length: 878, Cache-Control: no-store). Original repro previousl

evidence
`curl -sI https://ritualstudiospa.com/nonexistent-xyz` -> `HTTP/1.1 200 OK` (Content-Type: text/html).

[Independently confirmed by 3 audit lenses: seo-onpage, seo-tech, web-func]
✓ FixedMEDfindingWEB-A26Journal, legal pages, and blog posts are missing from sitemap.xml
details

Where: /sitemap.xml (affects /journal, /journal/:slug, /privacy-policy, /terms-of-service)

What: The sitemap lists only 8 URLs (home, about, memberships, 3 services, contact, join-waitlist). /journal is in the site's top nav and has 4 published articles with working /journal/{slug} routes, yet neither the Journal index nor any post is in the sitemap. /privacy-policy and /terms-of-service are also absent.

Fix: Add /journal, each /journal/{slug} article, /privacy-policy, and /terms-of-service to the sitemap (ideally auto-generate post URLs from the Supabase 'blogs' table so new articles are included automatically).

Link: https://ritualstudiospa.com/sitemap.xml (affects /journal, /journal/:slug, /privacy-policy, /terms-of-service)

Note: Round-4 independent verify (2026-08-12): verified implemented. Live sitemap.xml now lists 20 URLs including /journal, /privacy-policy, /terms-of-service, plus 7 /journal/{slug} post entries (what-is-contrast-therapy, sauna-cold-plunge-university-place-first-visit, why-we-go-tech-free, science-of-cold-plunge-recovery-mood, contrast-therapy-for-busy-parents-professionals-tacoma, building-a-consistent-recovery-ritual, hist

evidence
sitemap.xml contains no /journal, /journal/*, /privacy-policy, or /terms-of-service entries. Rendered /journal shows 4 posts ('How to Begin', "Stress Isn't the Problem. Staying Stressed Is.", 'The Power of Contrast Therapy', 'Why Ritual Exists'). JS router: path:"/journal" and path:"/journal/:slug" both registered; posts load via wt.from("blogs").

[Independently confirmed by 3 audit lenses: web-func, seo-onpage, seo-tech]
✓ FixedMEDfindingWEB-A27Contact & Waitlist forms POST to a hardcoded, unauthenticated GoHighLevel webhook
details

Where: /contact and /join-waitlist

What: Both forms submit client-side via fetch() to public GoHighLevel (LeadConnectorHQ) webhook-trigger URLs embedded in the JS bundle, with no auth token, captcha, or server proxy. The Contact form hits webhook .../PkKWjLAbeFPcbvcLlyO1 and the Waitlist form hits .../a2521149-2a54-430c-a299-e7900f35daa8. Both endpoints return 200 to an unauthenticated request.

Fix: Add a captcha/turnstile and/or route submissions through a server-side function (e.g. a Supabase Edge Function) that holds the webhook secret and rate-limits, rather than calling the GHL webhook directly from the browser.

Link: https://ritualstudiospa.com/contact and /join-waitlist

Note: Round-4 independent verify (2026-08-12): verified implemented. Server proxy is genuinely implemented. All three live JS assets (index-ritual-website-20260806.js?v=20260809, site-runtime, website-enhancements) contain zero occurrences of leadconnectorhq/webhook-trigger; the Aug 6 copy of the same bundle URL had the webhook, confirming a real redeploy. Both forms now fetch() same-origin /api/website/contact and /api/websi

evidence
Bundle: fetch("https://services.leadconnectorhq.com/hooks/y7IopIY4QYG70Nvl0ebl/webhook-trigger/PkKWjLAbeFPcbvcLlyO1",{method:"POST"...}) with payload {...source:"Contact Form"...}; and .../a2521149-... with {...source:"Founder Waitlist", sms_consent, email_consent}. curl -I on both -> 200. No captcha or auth header present.
✓ FixedMEDfindingWEB-A28Admin CMS login route is publicly reachable at /login
details

Where: /login

What: The app registers a /login route (to:"/login") that gates an admin CMS (the bundle manages Supabase tables blogs, flip_card_images, and checks user_roles for 'role'). The path loads (200) and is reachable by anyone who guesses it.

Fix: Ensure the login/admin area enforces strong auth and rate-limiting, and add a noindex/Disallow for it so it isn't crawled or surfaced.

Link: https://ritualstudiospa.com/login

Note: Verified: /login, /login/, /admin and unknown URLs all return 404.

evidence
JS router includes to:"/login"; admin logic uses wt.from("user_roles").select("role") and CRUD on wt.from("blogs")/wt.from("flip_card_images"). curl /login -> 200.
✓ FixedMEDfindingWEB-A29Home hero carousel eagerly loads ~4 MB of imagery before first interaction
details

Where: / (Home) · Home hero + content sections

What: The hero section references four images that all load up front: hero-new-1 (2.9 MB), hero-new-4 (613,590 B ≈ 599 KB), hero-new-2 (101,691 B), hero-new-3 (35,394 B). Combined with the 1.44 MB cold-ice-hand.png and 567 KB dave-canales.jpg further down, the Home document downloads well over 5-6 MB of images.

Fix: Convert all hero/section images to WebP/AVIF at appropriate sizes, lazy-load (loading="lazy") every carousel slide except the first, and lazy-load below-the-fold images (cold-ice-hand, dave-canales, sauna-woman-*). Target a total home page image budget under ~1 MB.

Link: https://ritualstudiospa.com/ (Home)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. WEB-A29 is genuinely FIXED on the live post-move site (BASE=https://ritualstudiospa.com), verified by direct curl measurement of every asset named in the original evidence. OLD heavy assets are ALL gone (HTTP 404): hero-new-1.png/.jpg, hero-new-4.png/.jpg, hero-new-2.png, hero-new-3.png, cold-ice-hand.png, dave-canales.jpg/.png, sauna-woman-meditating.jpg, r

evidence
Measured bytes: hero-new-4=613590, hero-new-1=2971650, cold-ice-hand.png=1478497, dave-canales=581398, sauna-woman-meditating=197111, ritual-r-logo.png=104684. Sum of images ≈ 6 MB.
✓ FixedMEDfindingWEB-A30Photographic and logo images shipped as oversized PNG instead of WebP/JPEG
details

Where: / (Home) · Home content + OG meta

What: cold-ice-hand.png is a 1,478,497-byte (1.44 MB) PNG of a photographic scene; ritual-r-logo.png is 104,684 bytes; og-image.png is 873,322 bytes. PNG is the wrong format for photos (no lossy compression) and these are far heavier than equivalent WebP/optimized JPEG.

Fix: Convert cold-ice-hand and other photographic PNGs to WebP/AVIF (expect ~150 KB or less). Rebuild the 1200x630 og-image.png as an optimized PNG or WebP under ~150 KB. Export ritual-r-logo as SVG or a small optimized PNG.

Link: https://ritualstudiospa.com/ (Home)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. WEB-A30 re-tested fresh via Bash curl on https://ritualstudiospa.com. All three flagged oversized PNGs are gone: (1) cold-ice-hand.png -> HTTP 404 Not Found; replaced by /lovable-uploads/cold-ice-hand-opt.webp = HTTP 200, Content-Type image/webp, Content-Length 61582 (60 KB, ~96% smaller than the old 1,478,497 B). (2) og-image.png (was 873,322 B) -> og:image

evidence
curl byte counts: cold-ice-hand.png=1478497, og-image.png=873322, ritual-r-logo.png=104684; all Content-Type image/png.
✓ FixedMEDfindingWEB-S08Add LocalBusiness JSON-LD schema aligned to Google Business Profile
details

Where: Site-wide (Home, Contact, location page)

What: The correct NAP sits in the footer as plain text; there is no visible LocalBusiness structured data, so the strong NAP isn't fully working for the local pack/map.

Fix: Add LocalBusiness (or HealthAndBeautyBusiness) JSON-LD with NAP, geo, hours/opening-status, and sameAs to social/GBP; claim and align the Google Business Profile.

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live 2026-08-12 via curl on raw HTML (no JS needed). Home (/), /contact, and the location page /sauna-cold-plunge-university-place each serve two JSON-LD blocks: Organization (#organization) and HealthAndBeautyBusiness (#localbusiness: a LocalBusiness subtype). The LocalBusiness block carries full GBP-aligned NAP: PostalAddress 2310 Mildred St W Su

evidence
[Brand-safe / additive: no redesign needed] Schema + GBP alignment is how Google ties the site to the physical University Place location for map/local-pack discovery: foundational for the pre-launch local-SEO goal and cheap to add.
✓ FixedMEDfindingWEB-S10Add a visual 4-step ritual walkthrough as a reusable on-page module
details

Where: Home, service pages, location page

What: The signature ritual (Heat Up → Cool Down → Reconnect → Repeat) is referenced in copy but not rendered as an educational, indexable, first-timer-oriented visual step module.

Fix: Build one reusable Heat Up → Cool Down → Reconnect → Repeat visual module (with the outcome benefits) and place it on home, service, and location pages.

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live 2026-08-12. Raw server HTML of /, /services/contrast-therapy, and /sauna-cold-plunge-university-place each contain an indexable "The Four-Step Ritual" section (h2 + ol) with all four steps (Heat Up, Cool Down, Reconnect, Repeat) plus first-timer explainer copy, styled as bordered card sections by website-enhancements-20260809.css. The location

evidence
[Brand-safe / additive: no redesign needed] Othership's 4-step journey does SEO + education + de-intimidation at once. It reinforces Ritual's core motif while adding indexable content and lowering first-timer anxiety.
✓ FixedMEDfindingWEB-B04Run HigherDOSE's two-tier content system at a lightweight scale
details

Where: Journal

What: HigherDOSE pairs an aspirational blog with a practical 'DOSE Lab' of how-to/FAQ/studies, serving both top-of-funnel discovery and bottom-of-funnel confidence, and cites real clinical studies.

Fix: Split the seeded Journal into a light 'stories/ritual' tier and a practical 'what to expect / is it safe / how-to' tier; cite 2-3 real studies per science piece in plain language.

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live 2026-08-12. /journal (200) is prerendered with a "Start Here" grid; all 7 article URLs in sitemap.xml return 200 with real prerendered content and unique per-route titles/descriptions/canonicals. The two-tier mix exists at lightweight scale: practical/reassurance pieces (what-is-contrast-therapy explainer with Research Notes, first-visit how-to

evidence
[Brand-safe / additive: no redesign needed] Two content tiers cover both discovery and reassurance; citing science backs the 'science-backed but accessible' pillar and earns topical authority: all without a heavy publishing cadence.
✓ FixedMEDfindingWEB-B06Borrow the 'for all / accessible' anti-intimidation framing
details

Where: Home / service pages

What: Othership uses explicit inclusivity ('for all, body positive, alcohol free') to remove the 'is this for someone like me?' barrier around cold plunge.

Fix: Add explicit, warm inclusivity/anti-intimidation lines ('for every body', beginners welcome, adjustable temperatures) in Ritual's voice.

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live 2026-08-12 via curl of raw HTML + shipped JS. /services/contrast-therapy (the cold plunge page) carries explicit anti-intimidation framing in the prerendered HTML: meta description "guided, beginner-friendly recovery session"; lede "Ritual keeps the experience beginner-friendly and adjustable"; "Adjustable cold exposure for first-timers and exp

evidence
[Brand-safe / additive, no redesign needed] It maps 1:1 to Ritual's 'Built for Everyone' and directly de-intimidates the cold plunge, the exact objection that stops first-timers.
✓ FixedMEDfindingWEB-A55Security headers dropped in the VPS move (HSTS gone; no nosniff / referrer / X-Frame on HTML)
details

Where: nginx / all HTML responses

What: Before the move, Cloudflare added Strict-Transport-Security, X-Content-Type-Options: nosniff and Referrer-Policy. On the new nginx these are absent on HTML pages (they appear only on sitemap.xml). HSTS is gone entirely.

Fix: Add to the nginx config for all responses: Strict-Transport-Security (max-age>=31536000; includeSubDomains), X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options: SAMEORIGIN.

Link: https://ritualstudiospa.com/

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-ran the exact check via Bash curl against BASE=https://ritualstudiospa.com after the dev team's fresh fixes. The security headers that were reported missing (WEB-A55) are now present on the HTML response itself, not just assets. `curl -sI https://ritualstudiospa.com/` -> HTTP/1.1 200 OK, Content-Type: text/html; charset=utf-8, Server: nginx, and now includes ALL

evidence
curl -sI https://ritualstudiospa.com/ -> Server: nginx, Cache-Control: no-cache, and NO Strict-Transport-Security / X-Content-Type-Options / Referrer-Policy. Same headers were present pre-move under Cloudflare.
✓ FixedMEDfindingWEB-A56Canonical and og:url are hardcoded to the homepage on every route
details

Where: HTML head (all routes)

What: Every page ships the same canonical / og:url pointing at the homepage (www host), so subpages (about, services, memberships) declare the homepage as their canonical.

Fix: Emit a self-referential canonical + og:url per route on the real apex domain; never hardcode the homepage URL.

Link: https://ritualstudiospa.com/

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. WEB-A56 is FIXED. Re-ran the exact check via `curl -s -L` (raw HTML, no JS) across routes. Each route now ships its OWN route-specific canonical and og:url in the server-delivered HTML, all on the non-www host: no longer hardcoded to the homepage/www. Observed raw output (canonical / og:url per route): - / (home): canonical=https://ritualstudiospa.com/ , og

evidence
Re-scan: the static canonical / og:url resolve to the homepage (www.ritualstudiospa.com) on /about, /memberships and /services/* .
✓ DoneMEDfindingWEB-A59[merged into WEB-A23] Identical title/meta on every route
details

Where: Served HTML <head> for /, /about, /memberships, /services/*, /journal, /contact

What: The move templated canonical and og:url per route, but <title>, <meta name=description>, og:title, og:description and twitter:* were NOT. Every page ships the homepage boilerplate title 'Ritual Studio | Make Space for Yourself | Premium Wellness in University Place', the homepage description, and og:title 'Ritual Studio | Make Space for Yourself'. React likely fixes the browser title client-side, but social/link scrapers (Facebook, LinkedIn, Slack, Twitter/X, iMessage) do not run JS, so sharing ANY service/about/journal link renders the generic homepage social card. Google also sees duplicate title/description signals across all URLs in the initial HTML.

Fix: Extend the per-route HTML templating that already sets canonical/og:url to also inject route-specific <title>, meta description, og:title, og:description and twitter:title/description.

Link: https://ritualstudiospa.com/

Note: Duplicate of WEB-A23 (render shell + identical crawler title). Not separate work; tracked under WEB-A23.

evidence
Raw HTML for all 6 routes returns the SAME string: '<title>Ritual Studio | Make Space for Yourself | Premium Wellness in University Place</title>' and 'og:title content="Ritual Studio | Make Space for Yourself"'. Yet each page's canonical/og:url ARE unique (e.g. /services/contrast-therapy has canonical + og:url = .../services/contrast-therapy), proving per-route templating exists but was applied only to those two tags.
✓ DoneMEDfindingWEB-A60[merged into WEB-A23] Raw HTML is an empty client-rendered shell
details

Where: Document <body> on all pages, including Googlebot user-agent request

What: The served <body> contains only the GTM noscript iframe, a skip-link, and an empty '<div id="root"></div>'. No H1, headings, service descriptions, or any crawlable body copy are present. Requesting the homepage with a Googlebot user-agent returns the exact same empty shell, so there is no server-side render or bot-prerender fallback: all page content is JS-render-dependent.

Fix: Add prerendering/SSG for the known routes (e.g. vite prerender / a prerender step in the build, or a bot-prerender middleware in nginx) so crawlers and social scrapers receive real body content.

Link: https://ritualstudiospa.com/

Note: Duplicate of WEB-A23 (render-dependent shell, crawlers see no per-page content). Tracked under WEB-A23.

evidence
curl of / (and Googlebot-UA curl) both end with '<div id="root"></div></body></html>' and nothing else; grep for h1/contrast/sauna in the raw body returns nothing but the meta tags. curl of /services/contrast-therapy body is identically empty.
✓ DoneMEDfindingWEB-A61[merged into WEB-A31] Single ~723 KB monolithic JS bundle
details

Where: /assets/index-ritual-website-20260806.js plus one small runtime file

What: The home page references only two JS files: the 6.5 KB site-runtime and one 723,504-byte application bundle. There are no lazy-loaded route chunks, so the entire site's JS is parsed/executed on first load. The server serves gzip (236 KB) but does not serve Brotli, requesting Accept-Encoding: br returns the full uncompressed 723,504 bytes.

Fix: Enable route-based code splitting (React.lazy/dynamic import per route) and turn on nginx Brotli (brotli_static) for the immutable asset bundle.

Link: https://ritualstudiospa.com/

Note: Duplicate of WEB-A31 (still open). Same issue, tracked there.

evidence
curl of / lists only /assets/index-ritual-website-20260806.js and /assets/site-runtime-20260806.js. Content-Length 723504 uncompressed; gzip transfer = 236,489 bytes; with 'Accept-Encoding: br' size_download = 723,504 (identity, i.e. no brotli).
✓ FixedLOWfindingWEB-A34prefers-reduced-motion does NOT stop motion: it re-declares the infinite ticker animation
details

Where: Home (and any page using the marquee/ticker & flip cards) · /assets/index-BgpPUzUJ.css

What: The site has continuously auto-scrolling marquee/ticker content (an infinite CSS animation). The one and only reduced-motion rule in the stylesheet reads: @media (prefers-reduced-motion: reduce){.flip-card-inner{animation:none!important;transform:rotateY(0)!important}.ticker-content{animation:scroll-ticker 80s linear infinite}}. So for users who set 'reduce motion' at the OS level, the flip-card animation is correctly killed, but the ticker is EXPLICITLY re-declared as an infinite scrolling animation instead of being stopped. Other automatic animations (fade-in .6s, pulse … infinite, scroll-ticker-mobile 15s, scroll-ticker 20s) have no reduced-motion handling at all and keep running.

Fix: Inside @media (prefers-reduced-motion: reduce), set .ticker-content{animation:none!important} (and the same for .fade-in, .pulse, scroll-ticker-mobile) rather than re-declaring the animation. Additionally add a visible pause control or pause-on-hover/focus (animation-play-state:paused) so the ticker can be stopped even without an OS preference.

Link: https://ritualstudiospa.comHome (and any page using the marquee/ticker & flip cards)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-tested live CSS bundle (now renamed /assets/index-ritual-website-20260806.css, HTTP 200, Content-Length 111636). WEB-A34 is FIXED. The stylesheet now has TWO @media (prefers-reduced-motion: reduce) blocks. A NEW override block (near end of file, byte 110890) explicitly kills the ticker: `@media (prefers-reduced-motion: reduce){*,*:before,*:after{scroll-be

evidence
CSS bundle /assets/index-BgpPUzUJ.css: '@media (prefers-reduced-motion: reduce){.flip-card-inner{animation:none!important;transform:rotateY(0)!important}.ticker-content{animation:scroll-ticker 80s linear infinite}}'. Keyframes present: scroll-ticker (used at 80s, 20s, and 15s/mobile), pulse … infinite, fade-in .6s. animation-play-state:paused appears only once (flip-card hover), never on the ticker.
✓ FixedLOWfindingWEB-A35Brand name is inconsistent across the site: 'Ritual Studio' vs 'Ritual Studio Spa' vs 'Ritual'
details

Where: / , /about, /join-waitlist, /journal (site-wide)

What: Three different brand forms appear. Most UI, the LocalBusiness/Organization JSON-LD, and headers use 'Ritual Studio'. The waitlist consent copy uses 'Ritual Studio Spa': 'I agree to receive SMS messages from Ritual Studio Spa...' and 'I agree to receive email updates from Ritual Studio Spa'. The Journal uses the bare word 'Ritual' in the article title 'Why Ritual Exists' and 'The Ritual Journal'. The domain itself is ritualstudiospa.com.

Fix: Choose one canonical legal name (whatever the registered entity is) and use it verbatim in all consent/legal text and schema; keep 'Ritual Studio' as the display brand and 'Ritual' only as an intentional shorthand in editorial voice, never in legal/consent copy.

Link: https://ritualstudiospa.com/ , /about, /join-waitlist, /journal (site-wide)

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live 2026-08-12. Fetched all 20 sitemap pages (raw HTML) plus all 3 served JS bundles (index-ritual-website-20260806.js, site-runtime-20260806.js, website-enhancements-20260809.js): zero occurrences of "Ritual Studio Spa" anywhere. The originally flagged SMS consent copy in the waitlist form (client-rendered from the index bundle) now reads "I agree

evidence
Waitlist: 'I agree to receive SMS messages from Ritual Studio Spa regarding appointment reminders...' Schema: '"name": "Ritual Studio"'. Journal: 'Why Ritual Exists', 'The Ritual Journal'. Domain: ritualstudiospa.com.
✓ FixedLOWfindingWEB-A36Value-proposition benefit string has inconsistent capitalization
details

Where: / (Home)

What: The rotating/benefit list in the hero reads 'Improve mental clarity•Build Resilience•Find Balance•Enhance Recovery•Boost Mood': the first item is sentence case ('Improve mental clarity') while the remaining four are title case ('Build Resilience', 'Find Balance', 'Enhance Recovery', 'Boost Mood').

Fix: Use one style for all five, e.g. 'Improve Mental Clarity • Build Resilience • Find Balance • Enhance Recovery • Boost Mood'.

Link: https://ritualstudiospa.com/ (Home)

Note: Round-4 independent verify (2026-08-12): verified implemented. Live bundle /assets/index-ritual-website-20260806.js?v=20260809 (loaded by https://ritualstudiospa.com/) defines the hero rotating benefit array as ["Improve Mental Clarity","Build Resilience","Find Balance","Enhance Recovery","Boost Mood"]: all five items consistent title case. The old sentence-case first item "Improve mental clarity" is gone from the list

evidence
'Improve mental clarity•Build Resilience•Find Balance•Enhance Recovery•Boost Mood'
✓ FixedLOWfindingWEB-A40Structured-data logo is a 1200x630 photo banner, not a logo
details

Where: All pages (in <head> JSON-LD) · Organization + HealthAndBeautyBusiness JSON-LD in raw HTML

What: Both schema blocks set "logo": "https://www.ritualstudiospa.com/og-image.png" and the LocalBusiness "image" to the same file. og-image.png is a 1200x630 split-photo social composite (cold-plunge + sauna scene with the wordmark overlaid), not a standalone logo.

Fix: Point "logo" at a dedicated square Ritual Studio wordmark/mark PNG (e.g. 512x512 on transparent/solid background) and keep og-image.png only for og:image/twitter:image.

Link: https://ritualstudiospa.comAll pages (in <head> JSON-LD)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-ran exact check via Bash curl on live https://ritualstudiospa.com/. FIXED. Both JSON-LD blocks now set "logo": "https://ritualstudiospa.com/favicon.png" (grep returned 2 identical logo lines, both favicon.png; zero references to og-image.png as logo). Verified favicon.png asset: curl -sI returns HTTP 200 image/png, Content-Length 73648; downloaded and rea

evidence
curl of / returns "logo": "https://www.ritualstudiospa.com/og-image.png" in both JSON-LD blocks; og-image.png verified as PNG 1200 x 630 photo composite.
✓ FixedLOWfindingWEB-A41Leftover Lovable build artifacts in image asset paths
details

Where: / (Home) and service pages · Image src paths in /assets/index-CdEJd9NA.js

What: All photography is served from /lovable-uploads/ with opaque UUID filenames (e.g. /lovable-uploads/2d319846-bca7-45da-a5f6-fb93dc9b0e4a.jpg). This is the default Lovable no-code upload path baked into production.

Fix: Move images to a branded path (e.g. /images/) with descriptive filenames and proper alt text; at minimum rename before the next deploy.

Link: https://ritualstudiospa.com/ (Home) and service pages

Note: Verified: no lovable references in served HTML; old lovable JS bundle 404s. (Orphaned old hero JPG still 200 but unreferenced.)

evidence
8 image URLs in the JS bundle all under lovable-uploads/, e.g. lovable-uploads/2d319846-bca7-45da-a5f6-fb93dc9b0e4a.jpg, lovable-uploads/cold-ice-hand.png; all return HTTP 200.
✓ FixedLOWfindingWEB-A42Playfair Display referenced in CSS but never loaded
details

Where: Admin UI (Admin Login / Admin Dashboard / Flip Card Image Manager) · .font-editorial class in /assets/index-BgpPUzUJ.css, used in app.js

What: CSS defines .font-editorial{font-family:Playfair Display,serif} and the JS applies it to admin headings ('Admin Login', 'Admin Dashboard', 'Flip Card Images'), but the only fonts loaded are Poppins and Montserrat. Playfair Display is never fetched, so those headings fall back to the browser default serif (Times New Roman).

Fix: Either add Playfair Display to the Google Fonts <link> if the editorial serif is intended, or remove the .font-editorial class and use the Poppins/Montserrat system already loaded.

Link: https://ritualstudiospa.comAdmin UI (Admin Login / Admin Dashboard / Flip Card Image Manager)

Note: Verified: Playfair Display now loaded via Google Fonts; CSP font-src allows fonts.gstatic.com.

evidence
CSS: .font-editorial{font-family:Playfair Display,serif}; Google Fonts link loads only family=Poppins…&family=Montserrat; grep found font-editorial used 5x in app.js on admin headings; no Playfair reference anywhere in the bundle or head.
✓ FixedLOWfindingWEB-A43All body headings and paragraphs are force-centered on phones (<=767px)
details

Where: Site-wide: every <section> and the footer on all 11 pages

What: A mobile-only CSS rule overrides text alignment for every heading and paragraph inside sections and the footer, forcing center alignment regardless of the content's natural left alignment.

Fix: Scope the mobile center rule to hero/heading blocks only (e.g. a .text-center-mobile utility) and let body <p> copy stay left-aligned on phones.

Link: https://ritualstudiospa.comSite-wide: every <section> and the footer on all 11 pages

Note: Round-4 independent verify (2026-08-12): verified implemented. Live CSS verified: the old blanket rule "@media (max-width:767px){section h1..h6,section p,footer h1..h6,footer p{text-align:center}}" is absent from both live bundles (index-ritual-website-20260806.css?v=20260809 and website-enhancements-20260809.css?v=20260809, loaded on every page checked). The only remaining text-align:center rules are the opt-in .text-c

evidence
index-BgpPUzUJ.css: @media (max-width: 767px){section h1,section h2,section h3,section h4,section h5,section h6,section p,footer h1,...,footer p{text-align:center}footer ul{display:flex;flex-direction:column;align-items:center}...}
✓ FixedLOWfindingWEB-A44No page-level overflow-x guard; horizontal-scroll safety relies entirely on per-section overflow-hidden
details

Where: Site-wide (decorative oversized elements seen on Home benefits section)

What: Decorative radial elements are sized 500vw on mobile / 200vw on desktop (5x the viewport width). They are currently contained only because their immediate parent section has overflow-hidden. There is no overflow-x:hidden on html/body/#root as a safety net.

Fix: Add overflow-x:hidden (or max-width:100vw) to html/body as a defensive guard, in addition to the per-section clipping already in place.

Link: https://ritualstudiospa.comSite-wide (decorative oversized elements seen on Home benefits section)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. FIXED. Re-verified via Bash curl against live BASE=https://ritualstudiospa.com (HTTP/1.1 200, nginx, Last-Modified Thu 06 Aug 2026 21:22:38 GMT). Current dated assets: /assets/index-ritual-website-20260806.css (Content-Length 111636, text/css) and /assets/index-ritual-website-20260806.js (723504). The exact missing guard from the finding is now present in th

evidence
index-CdEJd9NA.js: <section className="relative overflow-hidden pb-16 md:pb-24"><div className="absolute pointer-events-none w-[500vw] h-[500vw] md:w-[200vw] md:h-[200vw]" style={left:'50%',transform:'translateX(-50%)',borderRadius:'50%'...}>. CSS has only one .overflow-x-hidden utility and no html/body/#root overflow rule.
✓ FixedLOWfindingWEB-A45Container gutters fixed at 2rem with no mobile reduction
details

Where: Site-wide (any layout using .container)

What: The .container class hard-codes 2rem (32px) left and right padding at every breakpoint with no smaller mobile value.

Fix: Reduce container padding on the smallest breakpoint (e.g. 1rem on mobile, 2rem at >=768px).

Link: https://ritualstudiospa.comSite-wide (any layout using .container)

Note: Round-4 independent verify (2026-08-12): verified implemented. Live CSS is now /assets/index-ritual-website-20260806.css?v=20260809 (old index-BgpPUzUJ.css gone). Base rule .container{...padding-right:2rem;padding-left:2rem} still present, but two mobile overrides were added: @media (max-width:639px){.container{padding-right:1rem;padding-left:1rem}} and @media (max-width:767px){.container{padding-left:1rem!important;pad

evidence
index-BgpPUzUJ.css: .container{width:100%;margin-right:auto;margin-left:auto;padding-right:2rem;padding-left:2rem} with only .container{max-width:1400px} inside @media(min-width:1400px): no responsive padding override.
✓ FixedLOWfindingWEB-A47sameAs omits the Facebook page and any Google Business Profile
details

Where: / (Organization + LocalBusiness JSON-LD) · Structured data + footer

What: Both JSON-LD blocks list `sameAs: ["https://www.instagram.com/myritualstudio/"]` only. The footer, however, links BOTH Instagram and Facebook, so the Facebook profile (facebook.com/people/Ritual-Studio/61587389750679/) is missing from sameAs, and no Google Business Profile URL appears anywhere on the site.

Fix: Add the Facebook URL and the Google Business Profile URL to both sameAs arrays, and add a visible 'Find us on Google' / review link in the footer or Contact page.

Link: https://ritualstudiospa.com/ (Organization + LocalBusiness JSON-LD)

Note: Verified: LocalBusiness schema sameAs now includes the Facebook page and a Google Maps/Business link.

evidence
JSON-LD `sameAs` = only Instagram; footer renders both 'Follow us on Instagram' and 'Follow us on Facebook' links, proving a FB page exists but is absent from schema.
✓ FixedLOWfindingWEB-A48Privacy Policy and Terms of Service never set a route-specific title or meta description
details

Where: /privacy-policy and /terms-of-service

What: Unlike every other route, these two pages do NOT rewrite the head client-side either: the rendered title stays the generic default 'Ritual Studio | Make Space for Yourself | Premium Wellness in University Place' and the default homepage meta description, even though the body content (H1 'Privacy Policy' / 'Terms of Service' and full legal text) is correct.

Fix: Add unique title/description to both legal routes, e.g. title 'Privacy Policy - Ritual Studio' / 'Terms of Service - Ritual Studio' with matching descriptions.

Link: https://ritualstudiospa.com/privacy-policy and /terms-of-service

Note: Verified: Privacy Policy and Terms now serve their own route-specific title and meta description in the crawler HTML.

evidence
Rendered /privacy-policy title = 'Ritual Studio | Make Space for Yourself | Premium Wellness in University Place' (the default); rendered /terms-of-service title identical. Contrast with /contact which correctly renders 'Contact Us - Ritual Studio University Place'.
✓ FixedLOWfindingWEB-A49Static bot-facing title is ~78 characters and will be truncated in SERPs
details

Where: All routes (raw HTML title)

What: The static title 'Ritual Studio | Make Space for Yourself | Premium Wellness in University Place' is roughly 78 characters, well beyond the ~60-char display limit, and uses three pipe-separated segments.

Fix: Shorten the default title to ~55-60 chars, e.g. 'Ritual Studio | Contrast Therapy Spa in University Place, WA'.

Link: https://ritualstudiospa.comAll routes (raw HTML title)

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live via curl (raw bot-facing HTML). Old 78-char static title is gone (0 occurrences of "Make Space for Yourself | Premium Wellness in University Place" on the homepage). Server-side per-route titles are deployed: homepage <title> is now "Sauna & Cold Plunge University Place | Ritual Studio" (52 chars decoded), exactly one title tag. Checked all 14

evidence
Raw <title> length ~78 chars: 'Ritual Studio | Make Space for Yourself | Premium Wellness in University Place'.
✓ FixedLOWfindingWEB-A51No web app manifest present
details

Where: Site-wide (document head)

What: The HTML head declares favicon.svg and apple-touch-icon.png (all resolve 200) but no <link rel="manifest">, and /site.webmanifest, /manifest.json, and /manifest.webmanifest all return 404.

Fix: Add a manifest.webmanifest (name, short_name, icons, theme_color, background_color) and reference it with <link rel="manifest"> in the head.

Link: https://ritualstudiospa.comSite-wide (document head)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. WEB-A51 (no web app manifest) is now FIXED on the live post-move site. Re-run of the exact check against BASE=https://ritualstudiospa.com: 1) Raw HTML <head> (curl -s -L /) NOW contains a manifest link that was previously absent: <link rel="manifest" href="/site.webmanifest?v=20260806" /> (icon/apple-touch-icon links still present alongside it.) 2) /site.web

evidence
curl for /site.webmanifest, /manifest.json, /manifest.webmanifest each -> '404 text/plain'. Raw <head> contains icon/apple-touch-icon links but no rel="manifest".
✓ FixedLOWfindingWEB-A52favicon.svg is a 116 KB raster embedded in SVG
details

Where: All pages (favicon) · /favicon.svg (rel="icon" type="image/svg+xml")

What: The SVG favicon is 116,122 bytes and is not vector line-art: it wraps two base64-encoded <image> bitmaps inside a 500x500 SVG canvas (feColorMatrix filters + <image> + base64).

Fix: Replace with a true vector SVG of the mark (should be a few KB), or if a raster is unavoidable, ship 32x32 and 16x16 PNG favicons instead.

Link: https://ritualstudiospa.comAll pages (favicon)

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-tested live at https://ritualstudiospa.com/favicon.svg. curl -sI returns HTTP 200, Content-Type image/svg+xml, Content-Length 402 (was 116,122 bytes). Full download confirms SIZE:402. File content is now pure vector line-art: <svg viewBox="0 0 512 512"> with <rect>, <circle>, and <text>R</text> monogram. grep for '<image', 'base64', 'feColorMatrix' = 0 ma

evidence
favicon.svg = 116122 bytes; head shows <svg … viewBox="0 0 375 374.99"> with feColorMatrix filters; grep found 2x <image and 2x base64 inside the file.

[Independently confirmed by 2 audit lenses: design, web-perf]
✓ FixedLOWfindingWEB-A53Images render without width/height, risking layout shift (CLS)
details

Where: / (Home) · Home images

What: The rendered hero and content images (logo, hero slides, section images) have no intrinsic width/height attributes or aspect-ratio reserved, per inspection of the rendered DOM.

Fix: Add explicit width and height attributes (or CSS aspect-ratio) to every <img>, especially hero and logo, so layout is stable before pixels load.

Link: https://ritualstudiospa.com/ (Home)

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live 2026-08-12. Raw HTML of / now serves an SEO prerender shell with ZERO img tags (confirmed by curl and WebFetch), so the initial payload has no unsized images. The fix itself is a deployed runtime layer: /assets/site-runtime-20260806.js (20.3 KB, loaded with defer from the live head) contains a per-asset dimension map (logo 512x512 + SVG 3660x99

evidence
WebFetch render inspection of Home: logo and hero images reported with 'No width/height attributes, no loading attribute'; combined with 2.9 MB/1.4 MB images that arrive late, shift is likely.
✓ FixedLOWfindingWEB-A54HTML document served no-cache while assets are immutable: no CDN HTML caching
details

Where: All pages · HTML response headers

What: The HTML shell returns Cache-Control: no-cache, must-revalidate, max-age=0 and no CF-Cache-Status header, so the document is revalidated every visit. Static assets correctly use Cache-Control: public, max-age=31536000, immutable.

Fix: Since asset filenames are content-hashed, the HTML can safely use a short edge cache (e.g. Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate) so Cloudflare serves the shell from edge while still picking up new deploys quickly.

Link: https://ritualstudiospa.comAll pages

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-ran exact check via `curl -sI https://ritualstudiospa.com/`. HTML document Cache-Control changed from BEFORE `no-cache, must-revalidate, max-age=0` to NOW `public, max-age=0, s-maxage=300, stale-while-revalidate=300` (Server: nginx). The `no-cache, must-revalidate` directives are gone; `s-maxage=300` now lets shared/CDN caches serve the HTML for 300s with

evidence
curl -sI https://ritualstudiospa.com/ → Cache-Control: no-cache, must-revalidate, max-age=0; no CF-Cache-Status present. Asset headers → Cache-Control: public, max-age=31536000, immutable.
✓ FixedLOWfindingWEB-S11Replace empty placeholder blocks ('Rewards: Coming soon') with real content
details

Where: Memberships: /memberships

What: A 'Rewards Program: Coming soon' block occupies prime space while advancing neither SEO nor conversion.

Fix: Replace with a short brand-voiced value section (e.g. 'What founding members get') or a membership FAQ that carries long-tail keywords and answers real questions.

Note: Sep 23: the Rewards placeholder is gone from /memberships.

evidence
[Brand-safe / additive: no redesign needed] Placeholder blocks dilute the page and waste real estate above the fold of intent.
✓ FixedLOWfindingWEB-B08Heed the Brrrn cautionary lesson: build owned discoverability before opening
details

Where: Strategy (site-wide)

What: Brrrn leaned on novelty + launch PR; the physical studio appears closed and the brand pivoted to at-home products.

Fix: Keep investing pre-launch effort in durable owned assets (location page, seeded Journal, schema, analytics events) rather than one-off novelty PR; craft one crisp, ownable one-liner for the brand.

Note: Round-4 independent verify (2026-08-12): verified implemented. Verified live via curl on 2026-08-12: the owned-discoverability layer this strategy item calls for is genuinely deployed pre-opening. (1) Journal engine real: /journal (200) links 7 articles; all 7 return 200 with unique server-rendered titles/descriptions/h1 and real prose in raw HTML (no JS needed), incl. locally-targeted pieces (contrast-therapy-for-busy-

evidence
[Brand-safe / additive, no redesign needed] It directly validates Ritual's stated strategy: PR fades, owned local SEO/content/analytics compound. This is the argument for prioritizing the content engine NOW over a launch-hype push.
✓ FixedLOWfindingWEB-A57www and apex both return 200 with no canonical host redirect
details

Where: nginx host config

What: Both https://www.ritualstudiospa.com and https://ritualstudiospa.com serve 200 with no 301 to a single canonical host (pre-move, www 302-redirected).

Fix: In nginx, 301-redirect www to the apex (or the reverse) so only one canonical host serves content.

Link: https://ritualstudiospa.com/

Note: Re-test after dev round 2 (2026-08-06): verified FIXED. Re-ran the exact WEB-A57 check via Bash curl -sI against both hosts. APEX -- `curl -sI https://ritualstudiospa.com/`: HTTP/1.1 200 OK Server: nginx Content-Type: text/html; charset=utf-8 Content-Length: 6087 Strict-Transport-Security: max-age=31536000; includeSubDomains (canonical host serves the app; 200 as expected) WWW -- `curl -sI https://www.ritualstudi

evidence
curl -sI https://www.ritualstudiospa.com/ -> 200 (no redirect); curl -sI https://ritualstudiospa.com/ -> 200.
✓ FixedLOWfindingWEB-A58No Content-Security-Policy header (nor meta) served post-move
details

Where: All responses from https://ritualstudiospa.com (nginx). Checked apex, /about, JS asset.

What: nginx sends HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy, but no Content-Security-Policy header at all, and none is present as a <meta http-equiv> in the HTML either. The page loads third-party scripts (Google Tag Manager GTM-K7G3DTG3, googletagmanager.com) and remote CSS/fonts (fonts.googleapis.com / fonts.gstatic.com), so there is no policy constraining script/style/connect sources. This is the only security-header gap remaining after the move.

Fix: Add a CSP in nginx, e.g. `add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'self'" always;` and tune to the exact GTM tags in use (ideally move to a nonce instead of 'unsafe-inline').

Link: https://ritualstudiospa.com/

Note: Verified: Content-Security-Policy present, plus HSTS, X-Frame-Options SAMEORIGIN, Referrer-Policy and nosniff.

evidence
`curl -s -D - -o /dev/null https://ritualstudiospa.com/ | grep -i content-security-policy` returns empty. `curl -s -L .../ | grep -i Content-Security-Policy` (HTML body) also empty. Other headers confirmed present via `curl -sI`: `Strict-Transport-Security: max-age=31536000; includeSubDomains`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy: geolocation=(self), camera=(), microphone=()`.
✓ DonetaskWEB-01Define site structure & pages
details

What: Map the pages, nav, and routes for the marketing site.

Note: Verified live on Aug 8 with the round 4 site update: the content layer for search engines, the seeded Journal, location and benefit pages, the privacy disclosure, CTA and copy cleanup, form hardening, mobile styles, sitemap and schema.

✓ DonetaskWEB-02Landing page: copy & design
details

What: Draft hero copy and the first landing layout.

Note: Verified live on Aug 8 with the round 4 site update: the content layer for search engines, the seeded Journal, location and benefit pages, the privacy disclosure, CTA and copy cleanup, form hardening, mobile styles, sitemap and schema.

✓ DonetaskWEB-03SEO basics (titles, meta, sitemap)
details

What: Set page titles/descriptions and generate a sitemap.

Note: Verified live on Aug 8 with the round 4 site update: the content layer for search engines, the seeded Journal, location and benefit pages, the privacy disclosure, CTA and copy cleanup, form hardening, mobile styles, sitemap and schema.

✓ DonedocWEB-DOC1Content & Voice Review (for the Rory conversation)
details

What: Page-by-page voice read: her voice is strong and human; the CoolSculpting and Cryo pages are off-voice and make false pre-launch client claims; small consistency fixes; how the Journal grows the business.

Link: /reports/website-content-voice.html

✓ DonedocWEB-DOC2The Blog Engine (weekly post, voice guide, topic bank, sample)
details

What: How the Journal becomes a weekly educational engine sourced from Huberman and real research, in her voice, each post also a social post. Includes a sample post and 18 sourced topics.

Link: /reports/blog-engine.html

✓ DonedocWEB-DOC0Program overview
details

What: One page tying together website health, benchmark, content and voice, blog engine, service rewrites, social plan, and roadmap. The single link to share.

Link: /reports/ritual-program.html

✓ DonedocWEB-DOC3Service page rewrites, proposal (CoolSculpting + Cryo)
details

What: Honest rewrites of the two off-voice pages in her voice, no false claims, ready to review with Rory.

Link: /reports/service-page-proposals.html

✓ FixedtaskWEB-ROAD2Bring forms + lead data into RitualOS; GoHighLevel for SMS only
details

What: Today the contact and waitlist forms POST into GoHighLevel and the data lives there. Plan: connect the forms to our own system (RitualOS) so every lead and message lives in one place we own and can see. GoHighLevel stays only for sending SMS, and those SMS also surface inside our system.

Note: Done September 16 to 18: every website form posts into RitualOS as an inquiry and a lead, email sends from the app, and GHL only sends texts through its API. Re-checked with real submissions on September 22.

Runs / log

2026-08-21
Systemwide-changes verification (Rory Aug-20 list) · 1 found
Tiers: DONE user-visible (all 4 incl Unlimited, first 75, every-4-weeks, 15% Those Who Serve) but implemented ONLY as a runtime mask script. Naming: NOT DONE at source (the old waitlist name still in raw HTML/bundle/metadata). Dates: NOT DONE anywhere (early 2027 appears 0 times; og image named jan-2027). About: placeholder line still renders; internal editorial note still in crawler HTML; co-founder alt remains. Metadata: only patched after JS runs; scrapers see old. One consolidated HIGH item: WEB-A62.
2026-08-13
Round-4 verification of the dev content round
45 fixes marked done by the dev team were independently verified: 20 genuinely fixed, 21 partial, 4 not done. Systemic root cause: content added to the server prerender is overwritten by the SPA on hydration (old copy returns for users + Google render pass). Also: internal editorial guidance leaked as public copy on /about prerender; CoolSculpting still shows client-results cards; placeholder openingHours; Rewards Coming-soon still live; cryo fix introduced a regression.
2026-08-09
Re-test after dev round 3
11 verified fixed (contrast, GA4 + consent-gated tracking, GTM removed, CSP + security headers, /login + /admin now 404, Playfair loads, schema Facebook + Google, per-route legal titles, twitter handle, caching). A23 improved to partial: per-route title/meta/og now served, body still an empty shell needing prerender. Still open: code-split the 723KB bundle (A31), mobile centering + gutters (A43/A45). Content items still held for Rory.
2026-08-06
Re-test after dev round 2 · 4 found
22 now verified FIXED (WEB-A01, WEB-A02, WEB-A03, WEB-A05, WEB-A09, WEB-A18, WEB-A19, WEB-A22, WEB-A24, WEB-A29, WEB-A30, WEB-A33, WEB-A34, WEB-A38, WEB-A40, WEB-A44, WEB-A51, WEB-A52, WEB-A54, WEB-A55, WEB-A56, WEB-A57), 11 partial, 24 still open (most of the open ones are the content items held for Rory). 4 new from the sweep.
2026-08-06
Re-scan after the VPS move (nginx) + hosting-regression sweep · 3 found
The move was a lift-and-shift of the same built files, so 49 findings are still OPEN and 4 partial; 1 fixed (the Lovable /~flock.js tracker is gone). 3 new nginx regressions added: security headers/HSTS dropped, canonical hardcoded to the homepage on every route, and www+apex both 200 with no host redirect. The sweep also re-confirmed the empty-shell render, soft-404s, 2.9MB hero and Clarity placeholder (already on the board). Ready for the dev team.
2026-08-05
Benchmark + Content Audit: 19 peer studios profiled, page-by-page business-value review · 20 found
Brand fingerprint locked as guardrail; 12 content-growth recs + 8 competitive-adopt recs (5 high, 11 med, 4 low), all brand-preserving. Verdict: Ritual wins on brand but is not yet findable: the Journal is an empty SEO shell, local keywords/schema are missing, and high-intent moments link away instead of capturing. Full report: /reports/website-benchmark.html
2026-08-05
Audit 1: full multi-agent review (SEO / UX / a11y / perf / content / links / analytics / mobile / design) · 54 found
54 verified findings across 11 lenses of the live Lovable site: 2 high, 31 med, 21 low. Each finding adversarially re-verified on the live site before posting. Baseline before the VPS migration & improvement pass.