Changelog
Every build cycle since genesis, newest first. Also at /feed.xml and /api/changelog.
- 2026-08-10 seo Canopy stopped failing its own SEO audit on structured data. Rotation: this is the first-ever kind:"seo" cycle and it was forced — none of the last five entries was seo, and the three-way precedence had only one rule due (the creative and organ rules were both satisfied), so discoverability was the mandatory step. The sharpest seo move available was also the most self-honest one DESIGN §10 names outright: for its entire life the product has GRADED other sites' structured data — the SEO layer's weight-10 'Structured data' check, the gateway to every rich result — while shipping NONE of its own, so Canopy's /seo layer literally warned on Canopy's own pages, the exact 'recommending a standard it fails itself' gap the seo rotation rule was written to close. Now every HTML shell carries site-identity JSON-LD — an Organization and a WebSite node, built once from SITE_URL as SITE_JSONLD and injected into all thirty page heads at the single shared stylesheet line (one edit, no copy-rot, the same derive-don't-duplicate discipline the brief demands) — and the homepage adds a SoftwareApplication node (HOME_JSONLD) describing Canopy as the free web tool it is. This is dogfooding in the most literal sense: the markup is authored to pass Canopy's OWN structured-data check by construction (Organization and WebSite are precisely the two @types its richResultsMissing advisory excludes because Google lists no required properties for them, so it flips the check warn→pass AND cannot trip the rich-results-readiness advisory), and it makes Canopy genuinely eligible for the Organization knowledge panel it was leaving on the table. Two omissions, both honesty calls the product's character requires: NO SearchAction (the homepage box scans an external URL — it does not search Canopy's own content — so claiming a sitelinks searchbox would be a rich result that lies, the 'looks configured, does nothing' class Canopy exists to surface), and NO aggregateRating on the SoftwareApplication (there are no real reviews to cite, and inventing star ratings is the exact gaming Canopy flags on other sites; offers price 0 is honest because it is genuinely free). Zero CSP cost: a <script type="application/ld+json"> is a data block, not executable JS, so the strict script-src-by-hash policy doesn't apply to it and it needs no hash — the 'CSP has no unsafe-inline' self-honesty test is unmoved, verified. Left deliberately for a later cycle and written into DESIGN §10: the homepage's Open Graph check is also a self-warn (og:title/description present, no og:image), but the honest fix needs a real rasterised ~1200x630 image — Workers have no canvas, there is no build step, and an SVG og:image mostly won't render on social, so shipping one would pass Canopy's presence-only check while doing nothing, the same dishonesty this cycle refused. Verified: the exact shipped markup run through Canopy's own collectTypes extraction reports Organization+WebSite+SoftwareApplication, zero parse failures, PASS, and no gated readiness type; the homepage serves two ld+json blocks and inner pages one; smoke.sh gained three self-honesty assertions (homepage carries the app JSON-LD, inner pages carry site identity, all of it parses) so a future cycle can't silently drop it; whole suite green. The SEO grade should re-measure up the next time the nightly sweep scans canopystack.dev. Who pays: free-tier draw, pure discoverability — a knowledge-panel-eligible, structured-data-clean front door is how a product that grades other people's craft earns the trust to be found and believed, and a site that finally passes the standard it sells is the least embarrassing possible version of itself.
- 2026-08-10 creative Look-and-feel cycle (rotation: none of the last four entries was creative — position four was product, so this one is forced; the organ rule wasn't due, /email-builder sits at position two of the last six). It gives /compare a composed first viewport — the exact gap DESIGN §9 named, which for months listed /compare, /trends, /digest, /exposure and /schema as surfaces that inherit the world through tokens but were never art-directed, while the homepage was the only composed one. /compare is the highest-value of the five to fix first: it's the agency 'drift and benchmark' surface (staging-vs-prod, or your site against a competitor's, header for header), the 'presentable' work PRODUCT.md says the design must not foreclose — and its empty state was the thinnest of all, a single grey sentence over the A/B form, teaching nothing about why you'd want two URLs side by side. It now leads with the same grid hero the homepage uses — a --t-display headline ('See exactly what drifted.', the em in signal-ink), a --t-lead lede on a readable measure naming the two jobs, and a four-item facts list under a 2px rule (Staging vs prod · You vs a competitor · Differences flagged · Free, no account) — so the first viewport now shows the value instead of asking for input cold. Below the form, a strip of one-click example comparisons (github.com vs gitlab.com, stripe.com vs paypal.com, cloudflare.com vs fastly.com, nytimes.com vs theguardian.com) turns 'what do I even type?' into a live diff — the same discoverability move the homepage's live-examples strip makes, and the sensible-next-step the UI/UX brief asks for. Reuse over fork was the discipline: the hero is the homepage's own .grid/.hero/.display/.hero-lede/.hero-facts vocabulary and the example cards are its .examples-strip/.example-card, so both are already responsive and dark-mode-proven; the only new CSS is one rule, .cmp-ex-vs, the quiet --t-micro 'vs' between an example card's two hosts. The hero shows only on the empty state — a real comparison swaps it for a one-line tagline so the grades and diff table lead, never competing with the composition. Example links carry full https:// URLs so bare-host normalisation can't bite the prefilled scan; verified live that github.com vs gitlab.com resolves and renders the two grade cards and the flagged-diff table, and that the results state drops the hero. DESIGN §9 updated from the built world: the /compare item now records the composition and re-points the still-open note at /trends, /digest, /exposure and /schema, naming the hero+examples-strip pattern as the cheap way in for each. smoke.sh gained two assertions (the empty /compare leads with .grid.hero, and it offers the one-click example pairs) so a later cycle can't quietly flatten it back to a bare form; whole suite green, and Canopy's own a11y/perf/SEO grades are unmoved (no new component, one token-only rule, reused markup). Who pays: free-tier draw with an agency edge — nobody pays for a nicer compare page, but 'compare two sites' side by side' is a benchmark an agency runs against a client's competitor, and a compare surface that looks like a considered product rather than a raw form is upstream of the presentable, branded delivery the Pro story sells.
- 2026-08-10 product The AI-readiness layer stopped rewarding a stance and started grading its substance (6 checks now). Rotation: neither hard rule was due (a creative — the scan progress bar — sits at position four of the last four; an organ, /email-builder, is the newest entry), so this is a refinement, and a deliberate break from the email/a11y-layer monoculture the last nine cycles fell into. The gap: the layer's ai-stance check rewards a site for making *a* decision about AI crawlers (naming any of them in robots.txt), but never asked whether the decision was the one the site actually wanted — and there is a distinction almost everyone gets wrong. TRAINING crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended) collect data to train models; opting out is a legitimate, common choice. ANSWER/SEARCH crawlers (OAI-SearchBot, ChatGPT-User, PerplexityBot, Perplexity-User, Claude-User, Claude-SearchBot) are the user-directed ones that fetch a page to *cite* it in a live answer and send referral traffic back — the entire upside of being in AI results. The silent, damaging, common mistake is blocking the answer crawlers while meaning to opt out of training, which quietly removes you from ChatGPT Search, Perplexity and Claude answers while robots.txt looks deliberate and correct — the exact 'looks configured, does the opposite' class Canopy exists to surface, applied to the 2026 question every site owner is now asking (AI visibility / GEO). The new sixth check, ai-answer-access (weight 15, warn-only — never a fail, because blocking answer bots is a legitimate choice for a few publishers), reuses the /robots organ's precedence-aware parser (parseRobots / robotsBlocksRoot) to find which answer crawlers a robots.txt blocks at the root, names them, and gives the fix: block the training bots instead and Allow the answer bots so your pages stay citable. It is built to never cry wolf: it fires only on a group that *explicitly names* an answer bot (the site-wide 'User-agent: *' wall is the existing blanket-block check's job, not double-flagged here), it is precedence-aware (an equally-specific Allow: / un-blocks the root, a Disallow scoped to a subpath doesn't count, an empty Disallow is 'allow all'), and it is gated to robots-served sites so a domain with no robots.txt adds no cell at all. Free per scan — it reads the robots.txt the layer already fetched, no extra subrequest — so it rides /site, the nightly sweep, badges, snapshots and history unchanged; the JSON adds aiAnswerBlocked[]. Canopy's own robots.txt is 'User-agent: * / Allow: /', so it passes and its AI grade is unmoved (the new full-weight pass firms the ratio — DESIGN §10's self-bar holds). The AI_CRAWLERS list was split into AI_TRAINING_CRAWLERS + AI_ANSWER_CRAWLERS (their union preserves the old ai-stance detection and adds the four newer user-agents ChatGPT-User, Perplexity-User, Claude-User, Claude-SearchBot). Verified live with zero false positives and real true-positives: nytimes.com blocks all six answer crawlers, bbc.com four, theverge.com five, wired.com four — every one correctly warned with the exact bots named — while github.com and reddit.com pass (they block bots by other means but never these at the root) and example.com adds no cell (no robots.txt); a nine-case deterministic harness pins the true-positive, training-only-is-not-flagged, subpath-only, Allow-un-blocks, '*'-wall, grouped-agents and empty-Disallow branches. smoke.sh gained an assertion that the check is present and passes on a live crawlable site (github.com). No DESIGN.md change — the new check renders through the existing status-row component, no new token or surface. Who pays: free-tier draw with a sharp agency/GEO edge — 'AI visibility' is a paid service in 2026, and 'your site is quietly blocking the crawlers that would cite you in ChatGPT and Perplexity, because someone confused them with the training opt-out' is precisely the non-obvious, revenue-relevant finding an agency is trusted to catch over a free robots.txt viewer that just prints the file.
- 2026-08-10 organ New organ — an SPF & DMARC record builder at /email-builder, the author-side mirror of the /email analyzer. Rotation: neither hard rule was due (an organ, /csp-builder, sits at position two of the last six; a creative, the scan progress bar, sits at position three of the last four), so an organ was a free choice, not a forced one — I grew it because six of the last nine cycles were email/a11y layer refinements and the owner warns against exactly that monoculture, and because the direction now names the pattern outright: 'every analyzer wants an author-side mirror that hands over the fix.' Shape check: the last two organs were /sitemap (fetch-and-grade) and /csp-builder (author), so the shape rule's 'both the same' condition wasn't tripped; this is a third author, which pushes further INTO the diagnose→author pattern and away from the fetch-grade default the owner cautions about. The gap it fills: for its whole life the /email layer has GRADED a domain's SPF and DMARC over DNS — is it present, does it end -all, is DMARC enforcing — but nothing AUTHORED them, and SPF/DMARC are the two DNS records people get most wrong, in ways that fail silently. Name no include: and 'v=spf1 -all' blocks every message you send; bolt a second provider on as its own TXT and it's a PermError that voids SPF entirely (the multi-record void the layer catches); leave DMARC at p=none and spoofing is reported but never stopped. So Canopy could tell you your SPF was missing or your DMARC toothless and hand you a two-line template, no more. /email-builder closes that loop the way /csp-builder closed it for CSP: tick who sends mail for the domain from an 11-provider registry (Google Workspace, Microsoft 365, Amazon SES, SendGrid, Mailgun, Mailchimp/Mandrill, Postmark, SparkPost, Brevo, Mailjet, Zoho), add any custom include: hosts, pick a DMARC policy and a rua reporting inbox — and get the exact SPF record (each provider's vendor-documented include:, deduped, ending -all or ~all) and a hardened DMARC record, paste-ready as DNS. A no-mail toggle locks a parked or brand-defensive domain down completely — v=spf1 -all, a null MX ('0 .', RFC 7505), and p=reject — the exact fully-shut posture the null-MX cycle two entries ago taught the analyzer to read. DKIM is honestly out of scope (its key is held by your provider, not generatable) with a one-paragraph 'turn it on in <your provider> and publish the record they give you' note pointing at the selector the /email layer probes. The loop is graded, not just generated: a pure emailBuilderReadiness() mirrors the layer's own SPF/DMARC rules, and — the point of grading an author tool — it REFUSES to hand over a footgun silently: pick no senders while not in no-mail mode and the readiness verdict fails, because the resulting 'v=spf1 -all' would block your own mail; ~all and p=none each earn a warn with the reason; a rua-less record warns that you're enforcing blind. Pure compute — no fetch, no subrequest, no KV, no rate limit — a GET form that renders its own output, like /schema, /security-txt and /csp-builder. New programmatic surface: GET /api/email-builder?send=google,ses&policy=reject&rua=… returns {spf, dmarc, nullMx, ready, findings} — build the records in a script or gate a policy in CI. And it wires the fourth product cure (after /schema, /security-txt and /csp-builder): the /email layer's missing-SPF and missing-DMARC findings now link to /email-builder, and because the email report builds its own check rows (it isn't a renderLayerPage layer) it gained the shared cureLinkHtml() call, so the cure shows on the /email HTML report, /api/email and the Markdown export, and rides into /site's 'fix these first' list. The provider registry is the whole surface — checkbox list, SPF merge and API all derive from it, so a new provider is one entry (the copy-rot the brief warns about, designed out). Correctness was the bar, because a wrong include: or a stray record would break a real customer's mail: smoke.sh gained six assertions pinning the exact strings — google+ses builds 'v=spf1 include:_spf.google.com include:amazonses.com -all' and 'v=DMARC1; p=reject; rua=mailto:…' and reads ready; no-mail builds 'v=spf1 -all' + null MX + p=reject; the no-senders case fails with the self-mail-blocking warning; the page renders the paste-ready record; and the /email missing-SPF finding carries the /email-builder cure — whole suite green. No new CSS: the .csp-* checkbox/opt-grid component is reused unchanged across both generators (adding only a policy <select> that already inherits the generator-form treatment), and DESIGN.md §9 is updated from the built world to record the reuse and the fourth cure. Wired into the nav (Toolkit), homepage (Fix & automate), Canopy's own sitemap, and API docs. Who pays: free-tier draw with a sharp agency edge. Nobody pays to generate one SPF record, but 'SPF record generator' and 'DMARC generator' are high-intent developer/IT search traffic that gets Canopy found, and configuring email authentication across a portfolio of client domains — without silently blocking a client's mail — is squarely agency work; an agency now has one tool that both grades a domain's email posture (/email) and authors the exact records to fix it (/email-builder), the whole loop inside the product, which is what makes the branded-report Pro story worth paying for. Every seam that keeps the fix inside Canopy instead of sending the user to a random online SPF generator is one that gets us trusted.
- 2026-08-10 product The email layer learned to read a null MX correctly — and in doing so stopped telling no-mail domains the exact opposite of the truth. Rotation: neither hard rule was due (a creative sits at position two of the last four, an organ at position five of the last six), so this is refinement, and it fixes a genuine bug in the same 'looks configured / means something else' family the DMARC and SPF series live in. RFC 7505 lets a domain publish a single MX of "0 ." (preference 0, root exchange) to declare, explicitly, that it receives no mail at all — the modern, unambiguous way to say so, because a sender can't distinguish an *absent* MX (host forgot) from a deliberate no-mail stance, but a null MX it can, and it bounces undeliverable mail immediately instead of timing out. Canopy resolves MX over DoH, which returns that record as the literal string "0 ."; the old check did `mx.length > 0` and rendered it as 'Mail is routed: 0 .' — announcing that a domain which accepts no mail is accepting mail, and then, because the same `hasMx` flag gated the inbound-transport checks, dinging it with MTA-STS and TLS-RPT warnings for failing to protect mail it will never receive. Three compounding false statements on the single most locked-down email posture there is. Now the MX check has four honest branches: a proper null MX (the only record, "0 .") passes as 'receives no mail — pair with v=spf1 -all and p=reject to shut inbound and spoofed mail alike'; a null MX published *alongside* real servers warns, because RFC 7505 §3 requires it to be the only MX RR and the mix is contradictory (a receiver may honour either, so delivery is undefined) — the classic 'added a null MX but forgot to delete the old servers' mistake; a normal MX set still reads 'Mail is routed'; and a truly absent MX still warns, now also naming null MX as the explicit way to declare no-mail. A new `receivesMail` flag (has MX and it isn't a proper null MX) replaces `hasMx` as the gate on MTA-STS/TLS-RPT/BIMI, so a no-mail domain is no longer asked to secure inbound transport it doesn't have — while the malformed mix, which *does* have real servers, still gets those checks. Zero new subrequests: it's all read off the MX answer already fetched, so it rides /site, the nightly sweep, badges, snapshots and history unchanged and costs nothing on the budget. A legal priority-0 *real* host ("0 mail.example.com.") is explicitly not mistaken for a null MX (the regex anchors the exchange to "."). Verified live: example.com and example.net — IANA-reserved domains that publish exactly this fully-locked-down shape (null MX + v=spf1 -all + p=reject + revoked DKIM) — now grade A 100 with the MX cell reading 'Null MX (RFC 7505)' and no MTA-STS/TLS-RPT cells at all, where before they carried the 'Mail is routed: 0 .' contradiction plus two phantom transport warnings; gmail.com (real MX) is untouched, still 'Mail is routed' with MTA-STS/TLS-RPT/BIMI present; and a node check confirms all four branches classify correctly including the malformed-mix and pref-0-real-host edge cases. smoke.sh gained an assertion pinned to example.com (mx reads as null MX AND the inbound transport checks are absent), whole suite green. Who pays: free-tier draw with an agency edge — parked domains, brand-defensive registrations and send-only SaaS subdomains are exactly the no-mail assets an agency manages by the dozen, and 'your domain is telling the world it accepts mail when it shouldn't, and we're wrongly nagging it about transport it doesn't have' is precisely the kind of correctness a paid audit is trusted for over a free MX lookup that just prints the raw record.
- 2026-08-10 product The email layer stopped calling a DMARC record complete when its reports go to an address that never authorized them. Rotation: neither hard rule was due — a creative sits at position two of the last four and an organ at position five of the last six — so this is refinement, deepening the DMARC series (pct<100, sp=none, the SPF-lookup and multi-record voids) with the next silent failure in the same family. The gap: when a DMARC rua=/ruf= address lives on a *different* domain than the record — an agency's shared reporting inbox, a third-party DMARC service — RFC 7489 §7.1 makes receivers first check for an authorization record, a 'v=DMARC1' TXT at <publishing-domain>._report._dmarc.<external-domain>, before they will send a single report there. Miss that one-line record and the reports are silently discarded: the policy still enforces, so the domain looks monitored, but nobody is receiving the data that tells you who's spoofing you or lets you tighten p= safely — the exact same 'looks configured, does nothing' void as pct=10 or a lookup blowout, and one of the most common agency mistakes there is (point the client's rua at your inbox, forget to publish the authorization on your side). Canopy now parses every rua/ruf mailto, keeps the ones on a foreign domain, and resolves <domain>._report._dmarc.<that-domain> over DoH for each; if the authorization record is absent it warns ('DMARC report routing'), naming the unverified domains and the one-record fix; if present it passes with a note. A domain that reports only to itself — the common case, and every strong sender that self-hosts (airbnb, shopify, stripe) — adds no cell at all, so the check never cries wolf on the default. Because it costs extra DoH lookups (one per external report domain, capped at four), it runs standalone-only — skipped on the subrequest-budgeted /site composite and the nightly sweep — at weight 0, an advisory that never moves the letter grade or fires a phantom regression on the timeline, exactly the gate and precedent the SPF-lookup walker set. Verified live: linkedin (agari + emailanalyst), cnn (Valimail) and dropbox all PASS because those third-party processors publish the wildcard authorization the RFC wants — proving zero false positives on the overwhelmingly common third-party-reporting case — a self-reporter adds no cell, and a synthetic external rua with no authorization record correctly WARNs; smoke.sh gained two assertions (the audit rides standalone /api/email for a domain with external rua=, and is absent from the composite /site — the budget gate), whole suite green. Who pays: free-tier draw with a sharp agency edge — an agency that collects DMARC reports for its clients on its own domain hits this constantly, and the finding is the reason a client's 'protected' domain has been sending its reports into a black hole for months; the kind of non-obvious catch that justifies the branded report over a free online DMARC lookup.
- 2026-08-10 creative Look-and-feel cycle (rotation: none of the last four entries was creative, so this one must be, and the organ rule wasn't due — an organ sits at position two). It targets the gap AGENT.md's UI/UX list names outright — 'loading feel on slow scans' — because it afflicts the product's single most common interaction. Every scan is a full-page GET the Worker answers synchronously; a whole-site sweep is ~43 subrequests and several seconds, and for that whole time the only feedback was the submit button dimming to opacity 0.75 with a 'Scanning…' label. A dimmed static button held for eight seconds doesn't read as working, it reads as frozen — the exact moment a first-time visitor decides the tool is broken and leaves. Now, on submit of any scan form, a 2px signal-red bar pinned to the top edge of the viewport draws toward the right and eases to a hold near the end — the standard 'request in flight' affordance every developer already recognises from GitHub and YouTube, rendered in Canopy's own vocabulary rather than a borrowed spinner: 2px is the --rule structural weight, the colour is the --signal field (not a new accent), corners stay square, and there is no box or shadow. It reads as genuine progress, not a performing ping-pong, because a single decelerating cubic-bezier (the same --ease the grade bar uses) front-loads the sweep and crawls toward 92% — the page navigation is the completion, so it never has to animate to 100%, and a fast single-layer scan just flashes it briefly while a slow /site sweep fills it convincingly. One handler covers all ~24 scan/generate forms (it rides the existing submit listener that already sets the busy label), so no route needed touching and the slow /site button keeps its honest 'Running all layers…' text underneath. Honesty about the motion budget: DESIGN §7 said the grade bar was 'the one expressive moment', so §7 is updated to admit a second — justified as confirmation of a real pending request, not decoration, and kept inside those bounds by hairline weight, the field colour, and a reduced-motion gate. Under prefers-reduced-motion the bar stays width 0 (invisible) and the pre-existing button label carries the state alone; on a bfcache back-navigation the pageshow handler now strips a stale bar alongside restoring the button. The bar is aria-hidden decoration; it costs one 2px div and ~120 bytes of the already-hashed inline script, so Canopy's own CSP honesty and its a11y/perf grades are unmoved (smoke green, self-honesty 'CSP has no unsafe-inline' still holds). smoke.sh gained three assertions — the .scanbar rule ships in app.css, its animation is gated behind a no-preference block, and a scan submit inserts the bar — so a future cycle can't quietly delete the affordance and leave slow scans feeling frozen again. Who pays: free-tier draw — nobody pays for a progress bar, but it exists so the product doesn't feel broken in the first five seconds a stranger spends with it, which is upstream of every conversion the paid tier depends on.
- 2026-08-10 product The email layer stopped grading a domain's SPF as if the second copy weren't there. Rotation: the newest entry is an organ and a creative sits inside the last four, so neither hard rule was due — this is a refinement, the right default, deepening the layer rather than adding a surface. The gap: SPF found the record with txt.find(), which takes the FIRST v=spf1 string and never notices a second — but RFC 7208 §3.2 makes publishing more than one v=spf1 record a PermError, and receivers don't pick one, they discard SPF entirely and treat the domain as having no SPF at all. So a domain with two records — each perfectly valid on its own — is fully spoofable while Canopy handed it a clean 'present and restrictive' green. It is the same silent-void failure the lookup-limit and pct/sp DMARC sharpenings caught (the record looks right, the real-world effect is zero), and one of the most common SPF mistakes there is: it's what happens when a second mail provider is bolted on as its own TXT record instead of another include: in the first. Canopy now filters the whole TXT set for v=spf1 and fails the weight-30 SPF check when there's more than one, naming the count, the PermError, the spoofability, and the fix (merge the include:/ip4:/ip6: mechanisms into one record, delete the extras). Unlike the lookup-count audit — which needs extra DoH fetches and so runs weight-0, standalone-only — this reads the TXT records already fetched, costs nothing, and is definitive, so it moves the letter grade everywhere: /site, badges, snapshots, nightly, history. Same check id ('spf'), so timelines and fingerprints carry over and a domain that was wrongly passing shows an honest re-grade, exactly the precedent the SPF-lookup and DMARC sharpenings set. The lookup walker is now gated to the single-record case so it never prints a contradictory 'resolves fine' pass beneath a multi-record fail. The /fix pack learned the difference too, mirroring its weakened-DMARC guard: a double-published domain is no longer handed a from-scratch v=spf1 record (a third would worsen the PermError) — it gets a merge note carrying the exact fix instead. Verified: a 7-case fixture test pins single-pass, two-record-fail, three-record-fail, no-record, +all and ?all permissive, and single-record-with-DMARC-noise; smoke green (no regression to the single-record live path the spf-lookups audit exercises on github.com); and a probe of ~37 prominent domains (Fortune-500 senders, .gov, .edu) found zero with multiple records and thus zero false positives — the condition is genuinely rare, which is exactly why it stays silently unfixed and why it was too fragile to anchor a smoke assertion to an external domain (the existing SPF smoke test deliberately anchors only to a reliable single-record publisher). Who pays: free-tier draw with an agency edge — an agency running the branded report on a client whose SPF was quietly doubled up now catches the reason the client's 'protected' domain is still being spoofed, the precise kind of non-obvious finding that justifies the report over a free online SPF checker.
- 2026-08-10 organ New organ — a Content-Security-Policy builder at /csp-builder, the author-side mirror of the /csp analyzer. Rotation: none of the last six entries was an organ, so this cycle grows one; the last two organs were /schema (authors) and /sitemap (fetches-and-grades), so the shape rule's condition — both the same — isn't triggered, and building a second authoring organ pushes AGAINST the fetch-grade monoculture the owner warns about rather than toward it. The gap it fills: for its whole life the /csp layer has GRADED a site's Content-Security-Policy — the single header that stops an injected <script> from running — but nothing AUTHORED one, and a CSP is the header developers get wrong most, because the moment you name any directive it silently stops falling back to default-src (name script-src and forget 'self' and your own bundle stops loading; add Stripe and you have to know its script, frame and connect origins). So Canopy could tell you your CSP was weak or missing and hand you a two-line /reference starter, no more. /csp-builder closes that loop the way /schema closed it for structured data: tick what your site actually loads — content toggles (inline styles, inline script, data: images/fonts, same-origin framing, upgrade-insecure-requests) and third-party services from a 16-entry registry (Google Analytics, Tag Manager, Fonts, Adobe Fonts, YouTube, Vimeo, Stripe, Google Maps, reCAPTCHA, Cloudflare Insights, Plausible, Fathom, Meta Pixel, Hotjar, Intercom, Sentry) — and get a hardened, paste-ready policy built up from a strict default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none' floor, loosened only where you asked. The merge is correct where hand-writing goes wrong: any fetch directive that gains a source is seeded with 'self' (so naming script-src for Stripe doesn't silently drop your own scripts), each service contributes its exact vendor-documented origins across every directive it needs (GA touches script/img/connect; Stripe touches script/frame/connect), and sources dedupe. The loop is literal, not thematic — the readiness verdict runs the SAME cspPolicyChecks the live /csp scan runs, hoisted this cycle from inside runCspScan to module scope so there is one grader: a policy that reads 'hardened' in the builder passes the live audit for the same reason, by construction, and opting into inline <script> trips the exact 'unsafe-inline' finding the analyzer would flag on a live site. Pure compute — no fetch, no subrequest, no KV, no rate limit — a GET form that renders its own output. New programmatic surface: GET /api/csp-builder?svc=ga,gfonts,stripe&… returns {csp, ready, findings} — build the header in a script or gate a policy in CI. And it wires the third product cure (after /schema and /security-txt): the /csp layer's 'Policy present and enforced' finding, when a site serves no CSP or only a report-only one, now carries a link to /csp-builder, so the diagnosis hands over the author. The service registry is the whole surface — checkbox list, merge and API all derive from it, so a new service is one entry (the copy-rot the brief warns about, designed out). Verified live in local dev: a bare build is a hardened all-pass policy; GA+Fonts+YouTube+Stripe merges every origin correctly with script-src seeded 'self'; inline-script opt-in flips the unsafe-inline check to fail exactly as /csp would; the page renders a paste-ready 'Content-Security-Policy: …' header; and the /csp cure link appears on example.com (which serves no CSP). The CSP grader refactor is behaviour-preserving — Canopy's own /csp grade and every layer check are unmoved (smoke green, self-honesty 'CSP has no unsafe-inline' still holds). Wired into the nav (Toolkit), homepage, Canopy's own sitemap, API docs, and smoke.sh (page, API, three builder assertions, and the /csp cure link). DESIGN.md §9 updated from the built world: the .csp-check checkbox component recorded, and the cure item now names the /csp-builder cure as its third shipped. Who pays: free-tier draw with a security/agency edge. Nobody pays to generate one CSP, but 'CSP generator' is high-intent developer search traffic that gets Canopy found, and an agency hardening a client's headers now has a no-URL tool that both grades the CSP (/csp) and authors the fix (/csp-builder) — the whole loop inside the product, which is exactly what makes the branded-report Pro story worth paying for. Every seam that keeps the fix inside Canopy instead of sending the user to a random online CSP generator is one that gets us trusted.
- 2026-08-10 product The cookie layer stopped calling a silently-discarded cookie 'good'. Its prefix check rewarded a session cookie for carrying a __Host-/__Secure- prefix — but it never checked whether the prefix's contract was actually met, and a broken one isn't weakened, it's DROPPED: every modern browser silently refuses to store a __Secure- cookie without the Secure attribute, or a __Host- cookie that carries a Domain, lacks Path=/, or isn't Secure. So a developer who named their login cookie __Host-session to be extra-safe and then set Domain=example.com on it has a cookie that simply does not exist in any browser — the session breaks or the security control vanishes, invisibly, while view-source shows a perfectly hardened-looking Set-Cookie. That is the exact silent-and-damaging class Canopy exists to surface, and its own cookies layer was scoring it a clean pass. A new sixth check, 'Prefixed cookies honour their contract' (ck-prefix-valid, weight 10), validates every cookie that already uses a prefix against the browser's real rejection rules and fails when one is broken, naming the cookie and the precise reason — '__Host-session (a Domain attribute (must be omitted)); __Secure-token (no Secure)' — because a failed prefix means the whole cookie is thrown away, not just downgraded. It is the mirror of the existing ck-prefix (which nudges you to ADD a prefix): this one guards the prefix you already have. Built to never cry wolf: it fires only when a __Host-/__Secure- cookie is actually present AND actually violates its contract; a response with no prefixed cookies passes with 'nothing to validate', and a valid prefixed cookie passes explaining what it verified. The Set-Cookie parser gained a Path attribute (it already read Domain/Secure) so __Host-'s Path=/ requirement can be checked. Free per scan — reads the Set-Cookie headers already fetched, no extra subrequest — and it rides the shared machinery, so it flows through /cookies, /site, the /pages sweep, badges, snapshots, history, the client report and the Markdown export with no new wiring. Verified live with zero false positives on the sites most likely to trip a naive version: accounts.google.com sets a real __Host-GAPS (Secure, Path=/, no Domain) and google.com a __Secure-STRP — both correctly pass — while github.com and cloudflare.com set no prefixed cookies and pass with 'nothing to validate'; an eight-case deterministic harness pins every true-positive branch (a __Host- with a Domain, with no Path=/, with a non-/ Path, with no Secure, and a __Secure- with no Secure) against the two valid prefixed cookies and the non-prefixed control. smoke.sh gained an assertion that the check is present on /api/cookies and passes on a prefix-less page. Canopy's own cookie grade is unmoved — it sets no prefixed cookies, so the new full-weight pass only firms up the ratio. Who pays: free-tier draw that sharpens the presentable/agency surface — a dropped __Host- login cookie is the kind of production-breaking, invisible-in-view-source bug an agency is paid to catch on a client's site, and one that keeps the diagnosis inside Canopy.
- 2026-08-10 product The accessibility layer stopped assuming an id it can see is an id something can use (15 checks now). Last cycle's fourteenth check, 'ARIA references resolve', asks whether an aria-labelledby / describedby points at an id that exists on the page; this fifteenth one asks the question that check can't — whether the id it finds is the ONLY one. An id must be unique per document, and a label's for=, an aria-labelledby/describedby/controls, and an in-page href="#id" all resolve to the FIRST element carrying the id and silently ignore every later duplicate. So a carousel or a nav rendered twice under the same ids — the classic responsive-template bug — leaves the second copy's controls pointing at the first copy's items: the markup looks perfectly wired in view-source while a screen-reader user operating the visible control moves something offscreen, or a label announces the wrong field. That is the exact silent-and-damaging class Canopy exists to surface, and its own a11y layer scored it green. The new 'Unique element IDs' check counts every id in the served HTML and grades the duplicates honestly by blast radius: a duplicated id that a for/aria/anchor actually references is a real assistive-tech mis-binding and warns naming it; a duplicated id nothing points at is invalid-but-harmless today (WCAG 2.2 softened raw parsing, SC 4.1.1) and warns low-key, because it will bite the moment a script or ARIA targets it. Weight 5, warn-only like aria-refs — never a hard fail, the honest ceiling for a static smoke test. The build caught its own would-be false positive: a bare \bid= regex also matches data-id=, item-id= and every other *-id attribute (the word boundary sits at the hyphen), which invented nine phantom duplicates on Wikipedia and one on MDN — sites that in fact have zero. The id-count regex now requires a whitespace or a prior attribute's closing quote before `id`, which excludes every *-id attribute while still catching an id packed tight against a previous value; with it, Wikipedia (195 ids), MDN (13) and github.com (26) all pass clean, exactly as the aria-refs cycle found them. The true-positive it should catch, it does: apple.com renders a nine-item gallery twice under ids endless-entertainment-gallery-item-1…9, each referenced by aria-controls, and the check flags all nine as referenced duplicates — independently verified against apple's live HTML, byte-for-byte the same nine. Free per scan (reads the already-fetched HTML, no extra subrequest) and it rides the shared machinery, so it flows through /site, the /pages sweep (where a duplicated-id template hides across a whole section), the client report, badges, snapshots and history with no new wiring, and renders on the HTML report, /api/a11y and the Markdown export automatically. Canopy's own a11y grade is unmoved — its pages carry no duplicate ids, so the new full-weight pass only firms up the ratio and DESIGN §10's self-bar holds. An eight-case deterministic harness pins every branch (clean, id-less, referenced-dup via for/aria-labelledby/describedby/href, unreferenced-dup, and a partial multi-idref) and smoke.sh gained an assertion that the check is present and passes on a unique-id page. Who pays: free-tier draw that sharpens the presentable/agency surface. Accessibility conformance is a paid deliverable and duplicate ids are a WCAG audit staple that a manual pass charges to find; an agency running the a11y report on a client site now catches the responsive-template duplicate-id bug — the kind of concrete finding with real legal exposure that justifies the branded report and keeps the diagnosis inside Canopy.
- 2026-08-10 creative The nav stopped being a wall. Rotation rule: none of the last four entries was creative, so this was a look-and-feel / UI cycle — and DESIGN.md §9 had named the exact target, the "grouping/triage of that list is the next nav question" it wrote down when the brand lockup landed. For every page Canopy has ever served, the nav was thirty-one destinations in two rows, and the split lied: the row labelled "Layers" held the four standalone organs (Redirects, robots.txt, security.txt, Sitemap) and the Reference set right alongside the twelve graded layers, while "Tools" was a fifteen-link junk drawer mixing the scan entry points, monitoring, reports, the JSON-LD generator and meta. Two dense walls, no honest label, and — the real cost — no way for a returning visitor to scan straight to the one surface they came for; recognition-over-recall, the whole point of a nav, failed. It is now five honestly-labelled groups in a funnel order that matches how the product is actually used: Scan (the three aggregate entry points — one page, whole site, many pages) → Layers (only the eleven deep-dive graded layers now, the label finally true) → Checks (the standalone single-file audits: Redirects, Sitemap, robots.txt, security.txt, Exposed files, Preview) → Monitor (Dashboard, Uptime, Trends, Digest) → Toolkit (Schema, Fix pack, Compare, Report, Reference, API, Pricing). Every one of the thirty-one destinations is preserved — nothing was dropped, nothing hidden behind a dropdown, so the everything-one-tab-away property that a power user or an agency relies on is intact; only the grouping and the labels changed. It cost no new machinery and honours the Swiss system it lives in: no new tokens, no new colours, the labels are the existing .nav-label rail, .nav-rows became a flex column with an on-scale --s1 gap so the five groups read as distinct clusters, and the brand lockup's align flipped to flex-start so it sits top-left with the first row rather than floating at the vertical centre of the taller block. The one measured change was the label inset (4rem → 4.75rem desktop, 3.4 → 4.1rem mobile) because the seven-letter MONITOR/TOOLKIT labels are wider than the old six-letter ones. The nav is not sticky — it scrolls away with the document — so five rows cost height once at the top of the page rather than as persistent chrome, which is precisely what made grouping-in-place the right answer over collapsing surfaces behind a menu to stay short. On a phone the five groups stack as five short, labelled horizontal-scroll strips instead of two long ones, which is less scrolling per group, not more. The aria-current marker (scoped to .nav-row a) and the skip link are unchanged and still work — verified: DNS marks its tab current, the brand never steals the marker on the homepage. Verified live in local dev with headless screenshots at desktop and phone widths: five labelled rows render, the brand sits top-left aligned with the Scan row, no label collides with its first link, and the full smoke suite stays green (including the nav brand/wordmark assertions and the no-literal-spacing design guards — the inset is a padding, which the scale check exempts). DESIGN.md §9 updated from the built world: the grouping item the last cycle left open is now recorded as done, with the one thing still open written down — the Layers/Checks split is an internal distinction (grade-machinery layer vs standalone organ) a first-timer can't be expected to know, and a later pass could regroup those by subject instead. Who pays: free-tier draw. Nobody buys a nav bar, but a suite that has grown to thirty-one tools and makes a stranger hunt a two-row wall for the right one loses them before the first scan — the grouping is part of what makes the breadth feel like a product instead of a pile, and a product that looks organised is one an agency trusts to hand a client.
- 2026-08-10 product The cure link stopped living only on the layer page and grew a second cure. Four cycles ago a finding learned to carry a `link` — a one-click path to the Canopy tool that authors its fix — but only /schema used it, and only renderLayerPage rendered it, so the loop DESIGN §9 sketched ("a header fail → /fix, a missing security.txt → /security-txt") was still half-built: even the SEO structured-data cure that already existed was silently dropped the moment you viewed that same check through /site instead of /seo. This cycle makes a cure follow its finding onto every actionable surface. Two concrete gains a user can see: (1) the header scan's security.txt finding — Canopy's most-scanned front-door report — now points a missing or malformed /.well-known/security.txt at /security-txt, which checks the current file and hands back a compliant, ready-to-publish one (mailto normalised, Expires dated a year out, Canonical filled) — meaningfully more than the two-line inline template; and (2) the /site cross-layer "Fix these first" to-do list, the ranked action list a site owner actually reads, now carries every cure through the synthesis, so the whole-site view hands over the fix too — in the HTML list, the JSON /api/site priorities[] (scriptable) and the Markdown export an agency pastes into a PR. The plumbing was three small joins, not a new component: runScan's check whitelist (which had dropped every field it didn't name) now preserves `link`, the /site priority object carries `link` through, and the header report + priority list each render the existing one-line cureLinkHtml() helper — the exact adoption DESIGN §9 predicted. Whether a given cure is visible on /site respects the top-12 (HTML) / top-15 (md) severity cutoff, so the to-do list still leads with the most consequential fixes; the cure rides the data regardless, verified at the API level. Verified live in local dev: example.com (no security.txt) shows the /security-txt cure on the header report HTML and /api/scan, and its /site priorities carry it through to JSON+md; smoke.sh gained a cure-links section (header API link, header .cure render, /site synthesis propagation) and the whole suite is green. DESIGN §9 updated from the built world: the cure item now records that it is no longer layer-report-only, names the security.txt cure as shipped, and leaves the aggregate /fix cure and /compare's deliberate exclusion written down for the next cycle. Who pays: free-tier draw that sharpens the presentable/agency surface — every seam that keeps the fix inside Canopy instead of sending the user to a competitor's tool is one that gets us found and trusted, and a report whose findings each link to the free tool that resolves them is exactly what makes the branded-report Pro story worth paying for.
- 2026-08-10 product The accessibility layer stopped trusting an aria-labelledby it never checked (14 checks now). Two of its existing checks — form-control labels and named links/buttons — treat the mere *presence* of aria-labelledby (or aria-describedby) as proof the element is named, and return a pass the moment they see the attribute. But those attributes name an element by pointing at another element's id, and if that id is a typo or the target was deleted, the browser resolves the reference to nothing and silently drops it: the control still looks perfectly labeled in view-source while a screen reader announces it with no name at all. That is the exact silent-and-damaging failure Canopy exists to surface, and its own a11y layer was scoring it green. A new fourteenth check, 'ARIA references resolve', collects every id in the served HTML, then walks every aria-labelledby and aria-describedby and flags the ones whose idrefs point nowhere — naming the count, the kind (a broken labelledby means no accessible name; a broken describedby means a lost description) and the first offending id. It is built, like the SEO advisories before it, to never cry wolf on the ids a framework injects at runtime: a reference counts as broken only when NONE of its space-separated idrefs exists anywhere in the document, so a partially-hydrated multi-id label passes, and a page with no ARIA references at all passes with an honest 'nothing to resolve' rather than a phantom warning. It is a weight-5 warn (never a fail) with a one-line 'if these ids are added by JavaScript after load they resolve at runtime and this is safe to ignore' caveat in the fix, because a static smoke test can't see runtime DOM — the same honesty the layer already applies to its heading and landmark checks. Free per scan (reads the already-fetched HTML, no extra subrequest) and it rides the shared machinery, so it flows through /site, the /pages sweep, the client report, badges, snapshots and history with no new wiring, and it renders on the HTML report, /api/a11y and the Markdown export automatically. Canopy's own a11y grade is unmoved (it has no dangling references, so the new full-weight pass only firms up the ratio — DESIGN §10's self-bar holds). Verified live with zero false positives against the sites most likely to trip a naive version: github.com (16 aria references, all resolve), apple.com (20), en.wikipedia.org (28) and developer.mozilla.org (3) all pass clean, w3.org passes with no references, and a six-case deterministic harness pins the true-positive path — a typo'd labelledby, a missing describedby, and a both-broken page — against the clean, ref-less and partial-resolve cases. The three stale '13 WCAG smoke checks' counts (the layer comment, the /a11y tally and the API docs) were bumped to 14 in passing. smoke.sh gained an assertion that the check is present and passes on a reference-less page. Who pays: free-tier draw that sharpens the presentable/agency surface. Accessibility conformance is increasingly a paid deliverable — an agency running the a11y report on a client site now catches broken ARIA labels that a manual WCAG audit charges for and that carry real legal exposure, the kind of concrete finding that justifies the branded report and keeps the diagnosis inside Canopy.
- 2026-08-10 product The /schema organ now teaches eligible-vs-excellent, not just pass-vs-fail. Since it shipped, both the generator's verdict and the 'Check existing' validator graded structured data against Google's *required* properties only — a binary 'ready / not ready' — and both surfaces literally apologised for it in their own copy ('this checks required fields only, not recommended ones'). But required is the floor: a Product with a name and a price is 'ready', yet with no image, no brand and no review stars it wins a thin, rarely-clicked result. The gap between eligible and excellent is Google's *recommended* properties, and that gap is precisely what an agency is paid to close on a client's markup. This cycle adds a RICH_RESULT_RECOMMENDED map (the logo and sameAs that build an Organization's knowledge panel; the photo, brand and description that flesh out a Product; the byline, lead image and dates on an Article; the tap-to-call, photo and price band on a LocalBusiness; an Event's end time; the WebSite search-box action) and surfaces each missing one as an advisory `warn` row — 'Recommended — you're eligible without it, but adding it earns <the concrete thing it earns>' — in BOTH the generator and the validator, rendered through the same schemaCheckList so the two still read identically. Deliberately scoped: recommended rows are advisory and never flip `ready`, and the live SEO layer's grade is untouched (richResultsMissing still reads required only), so no site — including Canopy's own — has its grade moved by this; DESIGN.md needs no change because it reuses the existing warn-status check row, no new token or component. It also fixes a latent conflation: /api/schema built `missing` from every non-pass row, so once warns existed they'd have leaked into the required-gaps list — the GET now splits `missing` (required, status=fail) from a new `recommended` array, and the POST validator returns `recommended` beside `missing`, both documented in the API hint. The types with no required properties at all (Organization, Article, the search box) used to early-return a bare 'Always eligible' and stop; they now carry their recommended advice too, which is the whole of the guidance those types can give. Verified live in local dev: a name+price Product reads ready yet flags image/brand/description on both the generator page and the pasted-block validator; Article (no required props) shows all four of its recommended rows; smoke.sh gained two assertions pinning that recommended surfaces without gating readiness on either surface, whole suite green. Who pays: presentable / agency. 'You're eligible, but here's what makes it show well' is advice a freelancer acts on and an agency bills for — it deepens the free authoring tool that makes the branded-report Pro story worth paying for, and keeps the richer-result guidance inside Canopy rather than sending the user back to Google's tester to discover it.
- 2026-08-10 organ New organ — a sitemap.xml auditor at /sitemap. Two existing surfaces touch the sitemap in passing (the /pages sweep reads it to pick pages to crawl; the SEO layer notes whether /sitemap.xml responds and whether robots.txt declares one), but nothing graded the sitemap itself — and a broken sitemap fails as quietly as a broken robots.txt: crawlers simply index less of your site and you never learn why. This reads it as a document. It resolves the file the way a crawler does — the URL robots.txt's Sitemap: line declares if there is one, else /sitemap.xml — then catches the silent ways a sitemap dies. First, the same masquerade the robots organ catches: a /sitemap.xml that answers 200 with an HTML page (a catch-all 404 or an SPA shell), so every crawler parses zero URLs and whatever you thought you submitted is ignored; content-verified, not trusted, and treated as a critical hit. Second, a file that isn't valid sitemap XML at all (no <urlset>/<sitemapindex> root) — every URL in it dropped. Beyond those it grades an empty sitemap (looks authoritative, tells crawlers you have nothing), Google's 50,000-URL and 50 MB hard caps (entries past them are silently never crawled), relative or http:// <loc>s (a relative one is invalid and dropped; http on an https site is the wrong canonical), cross-host <loc>s (ignored unless cross-submitted — www/bare normalised away so the common case isn't flagged), <lastmod> in W3C-datetime format (an unparseable date is ignored, losing the re-crawl freshness signal; the regex range-checks month/day/time so 2026-13-40 is caught, not just structural typos), and whether the sitemap is declared in robots.txt. A <sitemapindex> is graded as an index (its child sitemaps counted and their URLs shape-checked, with a nudge to paste a child URL to go deeper); a gzipped sitemap is graded for reachability with an honest note that its contents aren't parsed rather than emitting garbage failures on binary. A missing sitemap is valid, not failed — crawlers discover by links — so a clean absence scores well with advice, the same no-cry-wolf discipline as the robots organ; but a sitemap robots.txt *declares* that 404s is a hard fail, because a crawler follows that promise into a wall. Two fetches (robots + sitemap), deterministic, standalone like robots and security.txt: rides the shared machinery (JSON at /api/sitemap with fail_under + format=md, a live badge, 90-day snapshots) but writes no timeline and sits out the nightly sweep. Verified live: cloudflare.com (893 URLs, resolved from its robots.txt) and vercel.com (6,145 URLs) grade A clean; gov.uk (35 child sitemaps) and bbc.com (122, at the index URL its robots declares) parse correctly as indexes; example.com (no sitemap) is correctly a valid B with advice, not a red F; and a unit harness over the shipped helpers pins the HTML-masquerade, invalid-root, empty, relative/http/cross-host <loc> and W3C-datetime branches against the clean cases. Wired into the nav, homepage, Canopy's own sitemap, API docs, every report's "Also check" row, and smoke.sh (page, API, badge, CI gate, markdown export, snapshot round-trip, plus the no-false-positive assertion against a live valid sitemap). Who pays: free-tier draw with a CI-and-agency edge. Nobody pays to check one sitemap, but crawl coverage is exactly what an agency is hired to protect for a client, and a no-URL /api/sitemap that fails a build on a broken sitemap before it ships is the concrete, outcome-owning capability that makes the branded-report Pro story worth paying for — and every seam that keeps the diagnosis inside Canopy instead of sending it to a competitor's tool is one that gets us found and trusted.
- 2026-08-10 creative The nav finally has a face. Rotation rule: none of the last four entries was creative, so this was a look-and-feel cycle — and DESIGN.md §9 had named the target twice over. For every one of Canopy's ~120 cycles the nav bar carried thirty destinations and zero identity: the sprout mark and the wordmark lived only in the page header *below* the nav, so the one element on every single page had no home link and nothing that said whose product this was. A stranger who scrolled a report off the top of the screen lost the way back to the front door, and a screen-reader user tabbing the nav landmark met a wall of tool names with no anchor. This cycle gives the nav a real brand lockup: the redrawn sprout at 26px beside a 'Canopy' wordmark, left of the two link rows, linking home. It is the mark + wordmark-as-nav-anchor that DESIGN §9 twice asked for — closing both the 'nav carries no brand / no home anchor' item and the 'the lockup does not do brand work yet' one in a single move. The accessibility was built in, not bolted on: the lockup's own aria-label ('Canopy — home') overrides the sprout SVG's label, so it announces as one clean home link instead of the 'Canopy sprout Canopy' a naive nesting would read out; and the aria-current script that underlines the current page was scoped to the link rows so the brand link — which is also '/' — never steals the marker from the 'Scan' tab on the homepage. The nav became a flex row (brand fixed left, the two rows flowing in a column beside it) and it degrades honestly on a phone: below 700px it stacks, the brand taking its own line above the two horizontal-scroll strips, so a narrow screen now leads with the identity rather than a cramped logo elbowing the links. The header .brand (sprout + the page-title h1 like 'Canopy · DNS') stays and is deliberately a type-scale step larger than the nav lockup, so the two read as identity-then-page rather than a stutter — the one place they touch is the homepage's small-nav-Canopy over big-hero-Canopy, accepted as the ordinary logo-then-title pattern. No new tokens, no new colours; the lockup is the existing sprout and wordmark type at chrome scale, and the design system stayed a system. smoke.sh gained two assertions (the branded home anchor and the wordmark ship on every shell), the whole suite is green, and the change was verified live in local dev — the lockup renders with the right href and accessible name on the homepage and every layer page. DESIGN.md §9 updated from the built world: both nav-identity items resolved, with the next nav question (the *count* — 30 links still ungrouped) written down where it used to hide. Who pays: free-tier draw. Nobody buys a logo in a nav bar, but a product that grades other people's craft and gives its own front door no way home does not earn the trust the whole business runs on — the identity, on every page, is part of why a stranger believes the grades.
- 2026-08-10 product The diagnosis now hands you the cure. For three cycles the SEO layer has flagged structured-data problems — no JSON-LD, JSON-LD that won't parse, valid markup that lacks the properties Google needs for a rich result — and every one of those findings ended by sending you to *Google's* Rich Results Test, an external tool that only tells you again what's wrong. But Canopy built its own fix two cycles ago: /schema generates valid JSON-LD from a form and /schema?mode=check grades markup you paste, against the exact same requirements (richResultsMissing) the SEO layer uses. The loop was closed in the code and left open in the UI. Now a check can carry a `link` — a one-click cure pointing at the Canopy tool that fixes it — and the SEO structured-data findings use it: 'No structured data' links to the generator ('Generate valid JSON-LD in Canopy's schema builder →'), while an unparseable or incomplete block links to the validator ('Paste it into Canopy's validator to see what's missing →'). It renders as a `.cure` line under the fix (detail → fix → where-to-fix-it, a new small component in DESIGN.md §9), rides the Markdown export so the report an agency pastes into a PR or client email carries the fix link too, and appears on /api/seo as check.link so a script can follow it. The mechanism is deliberately general — one optional field, one `cureLinkHtml(c)` helper — so the next finding with a Canopy cure (a header fail → /fix, a missing security.txt → /security-txt) is a field, not a component; only the layer report renders it so far, and the other check-row renderers each adopt it with the same one line. Verified live: example.com (no structured data) shows the generator link in the HTML report, the JSON API and the Markdown export; smoke.sh gained three assertions covering all three surfaces, whole suite still green. Who pays: free-tier draw that sharpens the presentable/agency surface — an audit that both finds the structured-data gap and links to the free Canopy tool that fills it keeps the whole loop inside the product, which is exactly what makes /schema discoverable and the branded-report Pro story worth paying for. Every seam that sends a user to a competitor's tool is a leak; this closes the biggest remaining one.
- 2026-08-10 product The /schema organ learned to check, not just author — the loop closes the other way. Last cycle's generator authors valid JSON-LD; the SEO layer grades structured data on a live URL; but between them sat the developer who already has markup — hand-written, from a WordPress plugin, from a headless CMS — and no way to ask 'will this win the rich result?' without deploying it to a page and scanning that page. /schema now has a 'Check existing' mode: paste raw JSON-LD or a whole <script type="application/ld+json"> block and it grades rich-results readiness on the spot, naming the exact properties Google requires and you're missing (a Product with no offers/review/rating, an empty FAQPage, an Event with no location) — before it ships, no URL, no fetch, no rate limit. The loop is literal, not thematic: rather than a third copy of the required-property logic, the readiness walk that the live SEO advisory used inline was hoisted to one module-scope function, richResultsMissing, and all three callers — the live grade, the generator's verdict, the validator — now compute against it and the same RICH_RESULT_REQUIRED / RICH_RESULT_ONE_OF maps. So a block that checks out here passes the live audit for the same reason, by construction; verified end-to-end by feeding the generator's own output back through the validator and getting an identical verdict. The parser accepts what a developer actually copies — raw JSON, one <script> tag, or several — walks @graph and nested nodes exactly as the live scan does, and refuses to cry wolf: a page with an always-eligible type (Organization, WebSite) reads ready, a parse error is caught and explained rather than silently ignored (as Google would), and markup with no @type is named as describing nothing. It POSTs (pasted markup is too large and too private for a GET query string), which made it the product's second POST route after the waitlist; the body reader disambiguates the textarea's jsonld= field from a raw-JSON API body by the field's presence, not the content-type, because curl --data sends a form content-type too. New programmatic surface: POST /api/schema returns {ready, types, missing:[{type,properties}], parsed, invalidBlocks} — a CI gate for structured data before a deploy, the pull-shaped complement to the existing GET generator. The pill component gained a second axis (a Generate / Check mode switch above the type picker, DESIGN.md §9 updated), and smoke.sh gained five validator assertions (ready, missing-named, parse-fail, empty-400, and the check page's rendered verdict). Who pays: free-tier draw with a CI-and-agency edge. Nobody pays to validate one block, but an agency that ships schema for clients now has a no-URL way to prove markup earns rich results before it goes live — and can wire POST /api/schema into a build to fail on regressions — which is exactly the concrete, outcome-owning capability that makes the branded-report Pro story worth paying for.
- 2026-08-10 organ New organ — a structured-data (JSON-LD) generator at /schema, and the first organ that fetches nothing. Every organ before it — security.txt, exposed-files, robots.txt — is the same shape: pull a URL, grade what comes back. This one authors instead of audits, which is the point: two cycles ago the SEO layer learned to flag structured data that parses but wins no rich result because it lacks the properties Google requires (a Product with no price, a BreadcrumbList with no items, an Event with no date) — a diagnosis with no cure. Canopy could tell you your schema earns nothing and hand you no way to fix it. /schema closes that loop. Pick a type — Organization, LocalBusiness, Product, FAQ, Article, breadcrumbs, Event, or the WebSite sitelinks search box — fill a form, and get paste-ready JSON-LD wrapped in its <script type="application/ld+json"> tag with the copy button every code block gets, plus a readiness verdict that grades what you just built. The loop is literal, not thematic: the readiness check reads the SAME RICH_RESULT_REQUIRED / RICH_RESULT_ONE_OF maps the SEO layer grades live sites with — hoisted this cycle from inside the SEO scan to module scope so there is one source of truth, and the generator physically cannot emit a block Canopy's own audit would fault. A Product with a name and a price reports ready; an empty FAQ reports not-ready and names the missing mainEntity, the exact field the SEO layer would flag on a live page. Pure compute — no fetch, no subrequest, no KV, no rate limit — so it costs nothing and can't be throttled; a whole new pipeline shape (form → validated artifact) rather than another scan. JSON at /api/schema?type=…&… returns the assembled JSON-LD, a ready flag and any missing required properties, so the markup can be built in a script instead of by hand. The type registry is the whole surface — the picker, the eight forms, the builders and the readiness all derive from one SCHEMA_TYPES table, so a new rich-result type is one entry, not four edits (the copy-rot the brief warns about, designed out). Wired into the nav, homepage, sitemap and API docs; textarea/select controls and a pill-style type picker added to the design system (DESIGN.md updated). Verified live: all eight types round-trip through /api/schema with correct readiness, the page renders paste-ready markup, an unknown type is a 400 not a silent empty document, and smoke.sh gained a schema section plus page/API coverage — whole suite green. Who pays: presentable line, with an agency edge. Rich results are what an agency is hired to win for a client — review stars and FAQ accordions are the difference between a result that gets clicked and one that doesn't — and the free tool that both finds the gap (SEO layer) and fills it (/schema) is exactly the concrete, res-generating deliverable that makes the branded-report Pro story worth paying for. Free-tier draw that sharpens the paid surface.
- 2026-08-10 product Every report can now be saved to a clean PDF, not just the aggregate /report. Cycle 117 made the client roll-up print like a document; the actual graded reports — the header scan, /site and all twelve layers, the thing an agency actually attaches to a client email — never got the same treatment, so hitting ⌘P on an accessibility or email-posture report gave you the operator's screen: the 27-link nav (already stripped), then the tagline, the "Try: this site · example.com" examples, the "Also check" cross-sell to sixteen other tools, the badge-embed, the Markdown-export textarea, two "Machine-readable /api/…" hints and the marketing footer — and in dark mode the code-block surfaces printed near-black on paper. None of that belongs in a document that leaves the building. One global @media print block now finishes what the aggregate report started: it strips the operator tail, forces the light neutrals whatever theme you're in (nobody prints a near-black page, and status colour needs a light field to read against — done with !important because the dark selectors out-specify a bare :root), keeps the grade letter and every status fill in colour via print-color-adjust: exact so the verdict survives the printer rather than washing to grey, and stops a check or a site block being guillotined across a page break. It rides the shared stylesheet, so a report added tomorrow inherits clean print for free. The other half is provenance, the exact gap cycle 117 named for /report and never fixed for the reports themselves: a printed page on a client's desktop said nowhere who made it, of what, or when. A new print-only credit line — hidden on screen, last thing on the page in print — now closes each of the five graded renderers (header, /site, the nine URL layers, DNS, email): "Canopy · Accessibility report · https://example.com/ · <date> UTC · generated with Canopy — <url>", reusing the .print-credit styling the aggregate report already ships. Verified live: the credit renders with the right title, target and stamp on a layer, DNS and header report, the global block ships in /app.css, and smoke.sh gained a print-ready section (the block strips .next-steps, forces status colour, and every graded report carries a dated provenance line) — the whole suite still green. Who pays: presentable line. This is the free-tier draw that makes the paid story honest — Pro sells branded, scheduled, white-label delivery of these reports, and until the free print output looked like a deliverable rather than a screenshot of a tool, "we'll brand it for you" was selling polish on top of something that wasn't presentable yet. An agency running the header or email report on a client site can now hand over a dated PDF today; Pro puts their logo on it and mails it on a schedule.
- 2026-08-09 creative Redrew the brand mark, the one thing on every page (header and favicon) that DESIGN.md §9 still called dead weight: "emoji-scale, does almost no brand work in a world this typographic." The old sprout was a thin stem, two blobs and a dot — organism-era lineage the design system deliberately keeps (§8), but a garnish next to a bold wordmark. It now lifts its leaves in two stacked tiers so the same little plant reads twice: as a young thing reaching up, and as the product's own idea made visible — the layers of a canopy, the metaphor the whole suite is named for and organised around. The seed the project grew from survives as a bud/sun at the crown. It is one SVG string that the favicon route reuses, so header and tab update together, and it was verified by rasterising at 256px, 40px and 16px in both themes before it landed — it holds its silhouette down to the favicon and the greens/gold stay legible on the near-black dark ground. No new palette entered: the four colours are exactly the ones the old mark used, honouring §3's rule that green survives only as status and the sprout. DESIGN.md was updated from the built world, not ahead of it — §8 records the redraw, and §9 drops the grade-tile item the last cycle actually fixed and sharpens the sprout item to name what is still open: the mark now does brand work but the lockup does not, and the nav carries no brand at all (the mark and wordmark live only in the page header below it). Who pays: free-tier draw. Nobody buys a logo; a product that grades other people's craft and looks amateur at its own front door does not earn the trust that everything downstream depends on — the identity is what makes a stranger believe the grades.
- 2026-08-09 product The SEO layer stopped calling structured data "valid" when Google will silently ignore it (14 checks now). The existing check proved a JSON-LD block *parses*; it never asked whether the block carries the properties the rich result actually requires — and it doesn't have to parse-fail to be worthless: a Product with no offers/review/aggregateRating, a BreadcrumbList with an empty itemListElement, an Event with no startDate, a Recipe with no image all validate perfectly and are then quietly dropped from rich results, so the page looks marked up in view-source and earns zero review stars, breadcrumbs or job cards in search. Google doesn't error on this, it just shows nothing, which is exactly the silent-and-damaging class Canopy exists to surface. A new 'Rich-results readiness' advisory now walks the already-parsed JSON-LD (free per scan, no extra fetch) and checks the required-per-Google properties for the recognised types actually present, naming the type and the exact missing field: "Product (missing offers/review/aggregateRating); BreadcrumbList (missing itemListElement)". Built, like hreflang, to never cry wolf: it is weight 0 / advisory, so it can neither re-grade the thirteen weighted checks nor fire a phantom regression banner on the millions of pages whose markup is merely absent; the generic high-frequency types every homepage carries — Organization, WebSite, WebPage, Article — are deliberately excluded because Google lists no strictly-required properties for them, so a normal corporate homepage stays silent instead of lighting up; and Review and VideoObject are left out on purpose because both routinely appear as minimal *nested* nodes (a review inside a Product needs no itemReviewed) where a requirement would fire on markup working as intended. Only Product, Recipe, Event, JobPosting, BreadcrumbList, FAQPage and LocalBusiness — types that are unambiguous and essentially always authored top-level and on purpose — are gated. It rides the SEO layer's shared machinery, so it flows through /site, the /pages sweep (where a broken template drops rich results across a whole section), the client report, badges and history unchanged. Verified live with zero false positives: allrecipes.com's recipe page (Recipe + BreadcrumbList, complete) passes, while allrecipes/bbc/stripe/nytimes/mdn/gov.uk homepages all sit correctly silent (their structured data is the excluded generic types), and a 14-case deterministic harness pins every warn branch — the empty-list, missing-startDate, no-image, name-and-offers, one-of-satisfied-by-review and multi-type-subtype cases — against the clean passes. smoke.sh now asserts the advisory rides /api/seo and stays weightless. Who pays: a free-tier draw that sharpens the presentable surface — an agency running the client report on an e-commerce or recipe site now catches the structured-data gaps quietly costing their client rich results in search, the kind of concrete finding that justifies the branded report.
- 2026-08-09 product The grade tile now carries its own delta, closing the honesty gap DESIGN.md §9 named: a green B tile could sit mute above a red "regressed 14 points" banner, so the letter said fine and the banner said trouble and the visitor had to reconcile them. The tile states the movement itself — ▼ 14 — using exactly the conditions the banner uses (nothing on a frozen snapshot, nothing without history, nothing when the score is unchanged), so the two can never disagree about direction. It matters most where there is no banner at all: the composite site report, DNS and email each keep a timeline but render no change banner, so this is now the only thing telling a returning visitor which way their grade moved. Direction reads as an arrow plus a word, never colour alone, because the tile’s background is already a status colour — and screen readers get the full sentence rather than a glyph. Who pays: free-tier draw. Nobody buys a delta on a tile; it exists because a monitoring product that makes you reconcile two conflicting signals is not one people keep coming back to. Written by an unattended cycle that was interrupted before it could verify or record itself; finished, verified against a real regression and the full smoke suite, and committed by the owner’s session.
- 2026-08-09 product The SEO layer learned to validate hreflang (13 checks now) — international targeting, and a class of error that fails completely silently. hreflang tells Google which language/region version of a page to serve, but Google doesn't error on a broken annotation, it just drops the malformed entry: a typo'd en-UK (there is no ISO 3166 region UK — the United Kingdom is GB), an en_US underscore where a hyphen belongs, a set that never references its own URL, or two entries claiming the same locale but pointing at different pages all quietly remove locales from the cluster while every page still looks perfectly annotated in view-source. Search Console reports these by the thousand and nobody sees them otherwise. The check reads the already-fetched HTML (free per scan, no extra subrequests) and is built to never cry wolf: a page with no hreflang PASSES silently — single-language is the correct default, the same discipline that treats a missing robots.txt as valid rather than a fault — so the 13th check only warns when hreflang is actually present and actually wrong. It validates BCP-47 structure (language, optional script, region as ISO-3166 alpha-2 or UN M.49), carries an explicit list for the region typos that look structurally valid but aren't (UK→GB, EU is not a country), confirms a self-referential entry, and flags conflicting duplicates; a missing x-default is a note inside a pass, not a warn, because language-only sets legitimately omit it. It is deliberately weight 0 — advisory only — so it neither re-grades the twelve weighted SEO checks nor fires a phantom regression banner on the many sites that simply never had hreflang, the same guard the SPF-lookup and DMARC sharpenings used. Reciprocity (does the *other* locale link back to this one?) needs fetching those pages and is out of a single-page scan's budget; that limit is stated in the fix text rather than faked. Verified live with zero false positives: apple.com (137 locales, all valid, self-referenced — passes, with the x-default nudge apple actually omits) and ikea.com (115 locales plus x-default — clean pass) both pass, example.com and wikipedia.org pass silently with no hreflang, and an 8-case fixture test pins the underscore, en-UK, EU, gibberish, missing-self-reference and conflicting-duplicate warn paths against the clean and absent passes. smoke.sh now asserts the advisory rides /api/seo, is weight 0, and stays a pass on a single-language site. Who pays: a free-tier draw that also sharpens the presentable surface — an agency running the client report on a multinational site now catches the hreflang mistakes that quietly cost their client international traffic, which is exactly the kind of finding that justifies the branded report.
- 2026-08-09 product The email layer stopped handing a full green pass to a DMARC policy that's silently defeated. A p=quarantine or p=reject looks like spoofing protection, but two published settings quietly gut it and neither showed up in the grade. First, pct<100: DMARC's pct dial applies the policy to only a sample of mail and delivers the rest unfiltered, so a p=reject; pct=10 rejects a tenth of spoofed mail and lands the other 90% — it's a rollout tool, not a resting state, and it was scoring a clean pass. Second, sp=none: the subdomain policy overrides the apex one for every subdomain, so p=reject; sp=none locks the apex while leaving blog., staging. and legacy. fully spoofable — the exact includeSubDomains gap the HSTS check already catches, and the setting real Fortune-500 senders ship (ibm.com and hp.com both publish p=reject; sp=none today). Either now warns on the same weight-40 DMARC check, names the specific hole and its blast radius, and gives the one-directive fix (remove pct / set pct=100; set sp to match p). A clean enforcing policy still passes — and if it has no rua= reporting address it passes with a note, since without reports you get enforcement but no visibility into who's sending as you and can't tighten safely; surfaced, never demanded, the way HSTS preload is. The subdomain-policy comparison is rank-aware (sp stronger than or equal to p is fine; github.com's p=quarantine; sp=reject and google.com's bare p=reject both stay green), so it flags the gap without crying wolf. Same check id, so timelines and fingerprints carry over — a domain that was passing and is now correctly warned shows an honest re-grade, exactly like the HSTS and SPF-lookup sharpenings before it. The /fix pack learned the difference too: a domain already enforcing but weakened no longer gets handed a from-scratch p=quarantine starter record that would downgrade it — it gets the precise directive fix as a note instead, while a missing or p=none domain still gets the starter. Free per scan (reads the DMARC TXT already fetched) and flows through /site, badges, snapshots and history unchanged. Verified live: ibm.com and hp.com correctly warn on sp=none with the exact fix, google/github/apple/cloudflare and every other strong sender stay green, and an 11-case unit test pins the pct, sp, both-at-once, sp-stronger, pct=100-explicit, pct>100-clamped and missing-rua branches.
- 2026-08-09 organ New organ — a robots.txt validator at /robots that reads the file as a document, not a checkbox. Three existing surfaces touch robots.txt in passing (the SEO layer notes it's served and declares a sitemap; the AI layer looks for a crawler stance), but none parses it, and robots.txt is one line away from deindexing an entire site while failing completely silently. It catches the two silent betrayals no other check does. First, a /robots.txt that returns an HTML page — a catch-all 404 or a single-page-app shell answering 200 for every path — which means every crawler parses zero rules and your whole site is crawlable, so every Disallow you think you wrote is being ignored invisibly; content-verified exactly like /exposure, so a soft-404 is caught rather than trusted. Second, an accidental site-wide `Disallow: /` — the staging leftover that drops a production site from search entirely. The block detector is precedence-aware (an equally-specific `Allow: /` un-blocks the root, the way Google breaks a length tie) and scoped to the crawlers it actually matters for: blocking `*` or Googlebot/Bingbot site-wide is a critical fail that caps the grade at F, while blocking a niche SEO/AI bot (ahrefsbot, gptbot) site-wide is a deliberate, legitimate choice and stays a pass — the difference that separates a useful validator from one that cries wolf. Beyond those it grades directive hygiene (misspellings like `Dissalow` that silently expose the paths you meant to hide, the `Noindex:` directive Google dropped in 2019, Allow/Disallow rules orphaned before any `User-agent:`), an absolute-HTTPS `Sitemap:` declaration, Google's 500 KB parse cap, and `Crawl-delay` (which Googlebot ignores — a warn, not a fault). A missing robots.txt is treated as valid, not failed — the default is crawl-everything — so a clean absence still scores well with advice rather than a red grade. Single fetch, deterministic, standalone like security.txt and exposure: rides the shared machinery (JSON at /api/robots with fail_under + format=md, a live badge, 90-day snapshots) but writes no timeline and sits out the nightly sweep. Verified live: github.com and cloudflare.com validate clean (large, well-formed files with sitemaps), example.com correctly reports a missing robots.txt without penalising it, and a crafted `User-agent: *\nDisallow: /` is caught as the F-capping site-wide block while the same block scoped to ahrefsbot stays clean. Wired into the nav, homepage, sitemap, API docs, every report's "Also check" row, and smoke.sh (page, API, badge, CI gate, markdown export, snapshot round-trip, and the no-false-positive assertion against a live crawlable site).
- 2026-08-09 creative Carried the Swiss world across the remaining surfaces, so the product is one design again rather than a new homepage bolted to an old suite. Pricing, the API docs, uptime, compare, trends, the digest, the reference set and — the one that matters commercially — the client report an agency hands a customer: every boxed, shadowed, rounded card became a ruled region, every green button and pill became the signal field or ink, and zero references to the old leaf green remain anywhere in the chrome. Code blocks kept a distinct ground because they are a different material, but lost their outline for a left rule. Along the way, the cause of a recurring waste: /app.css was versioned by CHANGELOG.length, so every CSS edit inside a cycle shipped under a cache key the browser already had. A design change looked like it had done nothing until a changelog entry happened to bump the number — which cost real time in five separate cycles and, worse, could serve a visitor stale CSS against fresh markup on any deploy that added no entry. The stylesheet is now versioned by a hash of itself. DESIGN.md was then rewritten from the built world rather than from intention, as the method requires: every token in it is what /app.css actually serves, with a "Still open" section naming what this world has not solved — the 27-link nav, the client-side current-page marker, the grade tile that carries no delta, the surfaces that inherit the world without being composed in it, and a sprout mark doing almost no brand work in a world this typographic.
- 2026-08-09 creative The world changed. After five cycles of refinement the owner said the result was underwhelming — correctly — and the diagnosis was that DESIGN.md's own point of view ("a field instrument: calm, numerate, the chrome recedes") was a licence to be restrained, which every cycle then dutifully obeyed. So the direction was re-derived from scratch through a seeded roll rather than picked, mocked as real pages at real content, and chosen by the owner: a Swiss International Style grid. Chosen over louder candidates on one axis — durability. It is a system rather than a composition, so it answers surfaces nobody has designed yet; the owner's instruction was that the energy is the agent's call each cycle and should evolve with the product. This cycle lays the world at the token layer, which moves every page at once rather than stranding half the product in the old look: near-white ground, near-black ink, one signal red carried as a field (with the fill/ink split cycle 1 established — #e2231a measured 4.25:1 as text and had to become #cc1b10), square corners, elevation by rule instead of shadow, a 12-column grid, and display type at hard scale contrast. The grade tile stopped being a rounded gradient chip with a radial gauge — the old world's signature, and a widget — and became a hard field where the letter is the largest object on the page, with the score as a bar in the grid's own language. The homepage got a real first viewport. The direction contract ships as an HTML comment in every page's markup, so the world this product committed to is auditable from the rendered output rather than from a rulebook. smoke.sh caught two of this cycle's own regressions: a responsive display size that bypassed the type scale, and the earlier width ladder that only stepped by ten. DESIGN.md is deliberately not rewritten yet — it gets written from the built world once the remaining surfaces land, not defended against reality in advance.
- 2026-08-09 product The accessibility layer learned to hear autoplaying audio (13 checks now). WCAG 1.4.2 (Audio Control) requires that any sound which starts on its own and runs past ~3 seconds offer a way to stop it — because unstoppable audio plays directly over a screen reader's speech, drowning out the exact tool a blind user relies on to navigate, and traps low-vision and cognitively-impaired users too. The layer graded twelve things and never this one. Detection is markup-only and deliberately narrow to avoid crying wolf: <audio autoplay> always offends, <video autoplay> does too unless it carries muted (silent background hero video is legitimate and passes), and the split between warn and fail turns on whether the element exposes controls — a visible pause technically satisfies the SC (warn: sound still arrives unannounced) while no controls means nothing stops it (fail). The attribute matcher follows the layer's existing \shidden idiom, so data-autoplay / data-muted decoys don't false-trigger. Free per scan since it reads the already-fetched HTML, and it flows through /site, the /pages sweep, badges, snapshots and history unchanged. Two stale counts fixed in passing: the layer's own header comment still said "eleven" and the API docs still said "11 WCAG smoke checks" — both had already been wrong since the frame-title check made it twelve. Verified live: example.com, developer.mozilla.org, apple.com (which autoplays a muted hero video — correctly a pass), coca-cola.com and tesla.com all pass clean with zero false positives, and a 12-case unit test proves the fail path (<audio autoplay>, unmuted <video autoplay>), the warn path (autoplay + controls), and the muted/controls/decoy exemptions each land where they should.
- 2026-08-09 organ New organ — an exposed-files scanner at /exposure, and the first check Canopy has that reads what a site *shouldn't* be serving rather than what it should. Every existing layer grades the page you meant to publish: its headers, its HTML, its DNS. But a whole class of real breaches is one file you didn't mean to publish at all — a committed .git directory (the full source history, git-dumpable), a readable .env (database password, app key, API tokens in the clear), a config backup served as text, a database dump or a site archive left in the web root, a live phpinfo(), an open server-status leaking every request and client IP, a .DS_Store enumerating your directory. None of these show up in any header or meta check, because the failure isn't in the response you designed. The organ probes ~17 well-known sensitive paths with plain, non-destructive GETs (redirect:manual, so a catch-all login/404 isn't mistaken for the file) fired concurrently, well inside the 50-subrequest ceiling. The discipline that makes it trustworthy is that a 200 NEVER flags on its own: every path is content-verified against the fingerprint of the file it claims to be (\[core\] in .git/config, a KEY=secret line in .env, real CREATE TABLE/INSERT INTO syntax in a dump, the 0x00000001 Bud1 magic at offset 4 in .DS_Store), so a single-page app that answers 200 for every path stays clean instead of lighting up red — the #1 way a scanner like this cries wolf. A critical exposure (VCS, secrets, backups, dumps, archives, phpinfo) caps the grade at F: nine clean checks can't average away one downloadable .env. Rides the shared machinery — JSON at /api/exposure with fail_under + format=md, a live badge (/badge?url=…&type=exposure), and 90-day snapshots — but is standalone like security.txt, deliberately off the nightly sweep. Fixing that seam surfaced and killed a latent bug: saveSnapshot appended every kind to a grade timeline, and an unrecognised kind (security.txt, and now exposure) fell through historyKeyFor to the bare hist:<url> key — colliding with that URL's *headers* timeline and getting re-scanned as a header scan by the cron (the same class as the old PWA collision). Standalone kinds now skip the timeline write entirely, which retroactively stops security.txt snapshots from polluting header timelines too. Verified two ways: example.com (exposes nothing) scores a clean A with zero false positives through the live scan, and a 21-case unit test proves each signature fires on a real .git/.env/.sql/.zip/phpinfo/DS_Store payload while rejecting the benign look-alikes (an HTML shell, a README mentioning KEY=, a JSON body containing the words 'create table'). Wired into the nav, homepage, sitemap, API docs, every report's "Also check" row, and smoke.sh (clean-A assertion, badge, CI gate, markdown export, snapshot round-trip).
- 2026-08-09 creative Visual cycle 5 of 5 — accessibility and the seams, and the end of the owner's five-cycle steer. The health strip, the product's signature glance-at-the-shape device, was decorative <i> cells whose only detail lived in a title attribute: hover-only, and therefore invisible to keyboards, screen readers and every touch device. Each cell is now a link to the check it represents — same shape, but tabbable, announced as "Cookie security: fail", and genuinely useful: clicking a red cell jumps to the failure instead of making you hunt the list, and the destination row takes focus so a screen reader reads the check you asked for rather than the top of the list. Touch targets came next. Measured on a phone rather than assumed: nav links were 26px tall, past WCAG 2.2's 24px floor but well under the 44px a thumb wants, and 72 controls were under it. Coarse pointers now get a 44px minimum without costing the desktop nav its density. The padding migration cycle 2 deliberately deferred is done — 27 more values onto the scale — with two deliberate exceptions that the migration itself proved necessary: the nav row's left inset is clearance for the absolutely-positioned TOOLS/LAYERS label and tracks that label's width, not the space scale, and snapping it to 3rem overlapped the label with the first link. Sub-0.25rem padding is likewise left alone: at that size it is optical centring inside pills and badges, and snapping it changes component shape rather than page rhythm. With this the owner's five-cycle steer is spent, so it has been deleted from AGENT.md as it instructed and both rotation rules are back in force — the organ rule is overdue and takes the next cycle. DESIGN.md keeps a "Still open" section naming what these cycles did not solve, so a later one picks it up rather than rediscovering it: the nav is still 27 links on every page, the current-page marker and skip link are still client-side, and a green grade tile can still sit under a red regression banner carrying no delta of its own.
- 2026-08-09 creative Visual cycle 4 of 5 — the first five seconds. The homepage sold twelve graded layers in the tagline and then ran exactly one of them: the hero form was the security-header scan, and "or scan everything at once" was a muted line underneath it. A first-time visitor scanned, got fifteen checks, and reasonably concluded that was the product. The form now carries an explicit scope choice with Everything as the default — it hands off to the whole-site scan — and Headers only one click away for the fast path, so the front door delivers what the tagline promises. The other half was the wall. The homepage put 65+ clickable choices in front of someone who came to scan a URL: 27 nav links, four example cards, 27 feature cards across three grids, and a fifteen-item prose list of every header check. The twelve graded layers stay open, because they are the promise made concrete; "Scan & monitor", "Fix & automate" and the prose list fold to one line each until asked for — verified genuinely unreachable when closed, not merely invisible, so the reduction is real for keyboard and screen-reader visitors too. And that entire wall used to follow you onto every result page, burying the export tools, the fix pack and the next-step links under the pitch; a report now ends with one line — run every layer on this domain, or browse the suite. The rule cycle 2 added to smoke.sh caught this cycle's own regression: a hardcoded 0.4rem gap in the new scope control, failing the build for bypassing the space scale.
- 2026-08-09 creative Visual cycle 3 of 5 — the report. The critique that opened these cycles landed one observation harder than the rest: the most designed screen in the product was the one you reach by failing. The failure card tells you what happened, why, and what to do; the success path handed over a letter, a tally and a checklist and left the reading to you. Every graded report — the header scan and all twelve layers — now opens with a verdict: one derived sentence naming the thing to do first, ordered by consequence so a failing core check outranks any number of hygiene warnings. "One thing to fix first: Cookie security — Set each cookie with: Secure; HttpOnly; SameSite=Lax." "Nothing broken: 3 core checks are worth a second look — starting with Main landmark." "Clean sweep: all 9 checks pass." It is generated from the checks, never written, so it cannot drift from what the report actually says. Reading order is now grade → sentence → shape → detail. Two smaller things the same critique named: the scanned target was a <p>, so heading navigation skipped the result entirely and landed on the marketing sections below — it is an <h2> now, sized as an identifier rather than a section title. And "hygiene 72/100" sat in the tally as a second number next to the grade, measuring something the reader was never told about; it now links to its own section, which says in one line what hygiene is and why it never moves the letter.
- 2026-08-09 creative Visual cycle 2 of 5 — type and rhythm. Every one of the 133 font-size declarations in the stylesheet now resolves to one of seven scale tokens; there are zero literal sizes left. The scale itself changed in the doing: cycle 1 proposed a 0.95rem body, and reading the reports at that size against a 1.15rem h2 made the case against it — two steps that close cannot carry different jobs, which is why a report read as one undifferentiated wall of grey. Body moved to the 16px web floor, h2 to 1.5rem, and the page title to 2rem, so heading, body, secondary and metadata are now distinguishable without reading a word. The grade letter turned out not to be a type step at all: it renders at four container-dependent sizes (site hero, report tile, compare card, layer card) that already formed a coherent ~1.26 ratio, so it is now its own component ramp — --g-sm through --g-xl — tokenised as it stood rather than bent into the text scale and quietly resized. DESIGN.md was corrected on both counts rather than left describing a system the code does not have. Rhythm went the same way: 28 distinct margin and gap values collapsed onto a seven-step 4px scale, widened from six because the original left a hole between 1.5 and 2.5rem that real values had to be forced across — which is exactly how arbitrary numbers get reintroduced. Padding is deliberately left for cycle 5: it governs component internals, where a blind snap to the scale moves hit-targets and optical alignment rather than rhythm. One regression caught by looking rather than by the suite: at a 16px body the dashboard's twelve grade columns ran past the container edge, so data tables now take the dense role the scale allows and the badge wall scrolls inside its own box instead of the page. smoke.sh now fails if any literal font-size, margin or gap reappears in the stylesheet, so the scales cannot quietly erode.
- 2026-08-09 creative First of five visual cycles the owner called for, and the one that lays the spine: DESIGN.md now exists at the repo root as the design system every later cycle builds against, with a point of view rather than a mood board — Canopy is a measuring instrument and should look like one: numbers are the hero, status is a reading rather than decoration, chrome recedes. The audit behind it was unflattering in a useful way. 29 font sizes with no scale (fourteen crammed between 0.66 and 0.98rem), 19 margin values with 1/1.05/1.1/1.2 coexisting, 12 border radii, 52 hex colours, and 55 hardcoded palette values bypassing the custom properties that sat 150 lines above them. Worse, measured contrast: white on the amber warn chip was 2.59:1 and white on the pass green 3.46:1, both failing the WCAG bar Canopy's own accessibility layer grades other sites against, and light-mode --muted — most of the words in a report — sat at 3.85:1. That is an a11y analyzer failing its own audit. Fixed structurally, not by nudging hexes: every status now splits into a fill (--pass/--warn/--fail), a text ink tuned to clear 4.5:1 on both surface and page (--pass-ink et al), and an on-fill ink, so the amber chip takes near-black text instead of white (6.62:1) and the pass green deepened to #3f8049 (4.78:1 with white). Type, space, radius, elevation, motion and focus are now short token scales. The dark palette's values are defined once and referenced by both selectors that need them, instead of the block being duplicated verbatim with one copy always an edit behind. Two outright defects went with it: the nav's 27 links never marked which page you were on — no active state, no aria-current, on a product where every page wears the same shell — and there was no skip link, so a keyboard or screen-reader visitor tabbed all 27 links on every page. Both now work, marked client-side because NAV is one constant interpolated into 24 render functions that are never told their own route; moving it server-side belongs to a cycle already touching those signatures. And the /site card on the homepage claimed "Eight layers at once" while /site has run eleven for some time, two headings under a tagline promising twelve — the exact stale-count rot the brief warns about, now derived from SITE_LAYER_WEIGHTS so it cannot rot again.
- 2026-08-09 product The email layer stopped rewarding an SPF record that silently doesn't work. A "present and restrictive" SPF earned a clean green pass — but RFC 7208 caps SPF evaluation at ten DNS lookups (every include/a/mx/ptr/exists/redirect costs one, recursing into each include's own record), and a record that needs more returns PermError: receivers then ignore SPF entirely, so the domain is spoofable while every check stays green. Canopy now resolves the whole include tree over DNS-over-HTTPS and reports the count — a new "SPF DNS lookups" cell that fails past ten, warns at eight-to-ten (one more sender tips it over), and passes with headroom below. The walker is bounded so a pathological record can't run up our subrequest budget: it stops the instant the count is known to exceed ten (the failing case costs almost no fetches), caps total lookups, and skips include loops and unresolvable macros. Because counting needs those extra fetches, the audit runs only on the standalone /email report and /api/email — not the subrequest-budgeted composite /site or the nightly sweep (the same reason link health sits out /site). It is deliberately weight 0: a loud advisory that never moves the letter grade, so the deep standalone scan and the shallow, timeline-fed composite always agree on the score and no phantom regression banner can appear — the detail states the real-world effect (SPF is void) regardless of the grade. Verified live: github, ebay, oracle, macy's and intuit sit at exactly 10 (warned), paypal/wayfair/expedia at 9, and dozens of others resolve comfortably under; a mocked over-limit tree confirms the fail path and that it stops early. smoke.sh now asserts the audit is present standalone and absent from the composite, locking the budget gate.
- 2026-08-09 product Fixed the machinery gaps the security.txt organ shipped with, and one older bug it exposed. The organ returned {state, checks} but no grade or score, which quietly kept it off everything the other analyzers ride — so it now scores its ten fields by weight (Expires and Contact carry the file, a missing PGP signature does not) and the badge, snapshot, CI gate and Markdown export all work. The verdict gates the score rather than averaging into it: cloudflare.com, whose file omits the mandatory Expires and is therefore ignored by researcher tooling, was scoring 75 and wearing a B badge because nine cosmetic passes outvoted the one fatal failure. Anything but `valid` now caps at F. /api/security-txt also hand-rolled its JSON instead of going through the shared scan responder, so fail_under and format=md were silently absent on that one endpoint; it is routed through now. The older bug: /badge and /share treated an unrecognised ?type= as a cue to fall through to the header scan, so /badge?url=github.com&type=security-txt rendered "canopy: B" — github's *header* grade, presented as a security.txt result — and type=totalnonsense returned the same image. An unknown type now renders a grey "unknown type" badge and /share answers 400 instead of minting a permanent link to a report nobody asked for.
- 2026-08-09 organ New organ — a security.txt checker AND generator at /security-txt (RFC 9116, the file at /.well-known/security.txt that tells researchers where to report a vulnerability). The headers layer already answered "do you have one?" with a single Contact-present probe; this answers the harder "is it actually valid and current?", which is where the damage hides: RFC 9116 makes Expires mandatory, and an *expired* file is invalid — researcher tooling ignores it while every other check stays green. It grades ten things field by field: published, canonical well-known path, HTTPS, text/plain, Contact (and whether the values are real URIs, not bare addresses), the flagship Expires check (missing → fail, in the past → an "Expired" verdict, more than a year out → warn, single-and-current → pass), Encryption, Canonical-names-itself, unregistered/misspelled fields, and a PGP signature. Present files are shown back verbatim; missing or broken ones get a ready-to-publish starter. And because nothing else in the product authored this file, it generates a compliant one from a form — normalising a bare email to mailto:, dating Expires a year out, filling Canonical from the domain — with the copy button every code block gets. JSON at /api/security-txt returns a state (valid | expired | invalid | malformed | missing) plus every graded field. Standalone tool, not a monitored layer, so one or two subrequests per check and no timeline writes. Verified live: github.com and stripe.com pass, google.com correctly warns its Expires is set to 2030 (four years out), and cloudflare.com is correctly flagged Invalid — its published file omits the mandatory Expires field entirely. Wired into the nav, homepage, sitemap, API docs, and every report's "Also check" row; smoke test extended to cover the page, the generator and the API.
- 2026-08-09 creative Redesigned the screen a stranger is likeliest to land on: the failure. Eight analyzers each hand-rolled the same dead end — a bold "Couldn't scan …" and the raw error string, then nothing, no way forward from a typo, a slow origin or a WAF that refuses bots. There is now one shared failure card across every scanning surface, laid out like a small report: the failure class as a pill, the raw message, one sentence on why that class usually happens, and the routes out as real buttons — retry, the www./bare variant of what was typed, 'check the homepage instead' when a sub-path returned a PDF or an image, 'grade its response headers' for the layer that reads any content type. The best route was already there and never mentioned: DNS and email posture are read over DNS-over-HTTPS and never load the page, so they still return a full graded report for a site that is down or blocking us — every reachability failure now says so and links both. /trends opts out of the retry links (a www. variant of a storage key is nonsense) and keeps the diagnosis.
- 2026-08-09 product The accessibility layer learned to check frame titles (12 checks now). An <iframe> with no title is announced by a screen reader as just "frame" — a WCAG 4.1.2 failure, and the usual offenders are the exact frames sighted users skim past without thinking: an embedded YouTube video, a Google Map, an ad slot. The existing "links & buttons named" check covered icon-only controls but never frames, so a page could pass every a11y check while shipping three anonymous embeds. The new check finds every iframe and grades the ones with no accessible name — title, aria-label or aria-labelledby all count, and a hidden or aria-hidden frame is correctly exempt (a tracking pixel needs no title); a couple unnamed warns, more fails, none passes. Free per scan since it reads the already-fetched HTML, and it flows through /site, the /pages sweep, badges and history unchanged. Verified live: a page embedding two iframes where only one is titled correctly warns "1 of 2", and a page with no frames passes clean.
- 2026-08-09 product The performance layer learned to see the flash of invisible text (13 checks). A custom web font that loads with no display strategy leaves its own text blank until the file arrives — up to ~3 seconds of invisible copy on a slow connection, and a standard Lighthouse audit Canopy was missing. Two causes are detectable in the HTML alone: a Google Fonts stylesheet <link> with no &display= parameter (Google's default blocks), and an inline @font-face rule with no font-display descriptor. Either warns with the exact fix — append &display=swap, or add font-display: swap — while a page with a declared strategy, or no web fonts at all, passes; it's a text-timing nudge, never a fail, and external CSS isn't fetched so only inlined rules are visible. Free per scan since it reads the already-fetched HTML, and it flows through /site, the /pages sweep, badges and history unchanged. The tally copy also said '11 checks' while the layer already had 12 after the hero-image check was rebuilt last cycle — corrected to 13. Verified live: smashingmagazine.com correctly warns on 6 inline @font-face rules with no font-display, and sites using neither pattern pass clean.
- 2026-08-09 product Rebuilt a check that was lost: the performance layer now grades hero-image priority (12 checks). The first image in a document is almost always the hero and usually the largest thing a visitor waits for, so it is the one image that must not be deferred — yet lazy-loading everything is common advice, and a lazy hero delays the exact moment the page looks finished. Lazy on the first image fails and names the file; fetchpriority="high" or a preload link passes; default priority warns, with a note that loading="eager" prevents deferral without actually raising priority. The original version of this check was written by the 16:53 cycle and destroyed by an interactive git checkout in the shared working tree — the hazard is now documented in CLAUDE.md and the builder marks its own invocations so no cycle stands down for its own lock again.
- 2026-08-09 product The CSP deep-dive learned form-action (9 checks now) — the directive people forget because they assume default-src covers it. It doesn't: form-action, base-uri and frame-ancestors are the three directives with no default-src fallback, so a page can lock default-src down to 'self' and still let an injected or hijacked <form> POST whatever a visitor typed — a phished password, a session token pasted into a field — straight to an attacker's origin. The layer already graded the other two no-fallback directives as their own checks; form-action was the missing sibling. Present passes and names the allowed origins; explicitly absent warns (even under a tight policy, because that's exactly when the false sense of safety bites); no policy at all fails. Same shared machinery — free per scan since it reads the already-fetched policy, flows through /site, badges, snapshots and history unchanged. Verified live: github.com and cloudflare.com pass, developer.mozilla.org's otherwise-strong CSP correctly warns for the gap.
- 2026-08-09 creative The grade tile — the product's signature, the first thing your eye lands on in every report — stopped being a flat coloured square and became a radial score gauge: an arc that fills to the score around the letter grade, so a 26 reads as a quarter-full ring and a 95 as a nearly-closed one before you register the number. One shared gradeGauge() helper replaced a dozen hand-copied tiles across the headers scan, all twelve layer reports, /site's hero, /compare's two cards, the what-changed diff, /pages and the client report, and it scales from the 96px hero down to 64px cards from a single 100×100 viewBox. The arc sweeps in on load and fades gracefully to a static ring under prefers-reduced-motion. Drawn entirely with an SVG stroke-dasharray attribute rather than an inline width — so the gauge itself passes the strict style-src 'self' CSP Canopy grades everyone else on (the smoke test's own 'no inline style attributes' check still passes). The small per-layer chips stay flat on purpose, so the hero gauge leads the eye.
- 2026-08-09 product The SEO layer's Indexability check now reads the X-Robots-Tag response header, not just the <meta name="robots"> tag. This is the more dangerous of the two: an X-Robots-Tag: noindex is invisible in the HTML, so a page can pass every other SEO check — perfect title, description, self-referencing canonical — while an HTTP header quietly tells every search engine to drop it entirely, exactly the silent-and-damaging class as a canonical pointing at the wrong page. Detection is now token-based rather than a substring match, so it also catches Google's "none" shorthand (= noindex,nofollow) and bot-scoped directives ("googlebot: noindex") in both the tag and the header, while a bare nofollow correctly stays a pass. The failure names its source and gives the exact fix; the /fix pack's noindex note now mentions the header too. Same check id, so timelines and fingerprints carry over; it reads a header already fetched, so it's free per scan and flows through /site, /pages sweeps (where a templated X-Robots-Tag deindexing a whole section actually hides), badges and history unchanged. Verified end-to-end against a live header-echo endpoint.
- 2026-08-09 product The flagship HSTS check stopped rewarding a half-configured header. A bare Strict-Transport-Security: max-age=… earned a full green pass, but without includeSubDomains the lock covers only the apex — every subdomain (blog., staging., legacy.) stays reachable over plain HTTP, and a single insecure subdomain can set cookies for the parent or be used to SSL-strip it, which is precisely the attack HSTS exists to stop. That case now warns, naming the gap. The two genuinely-strong cases split too: a strong max-age with includeSubDomains passes and, when the lifetime is a year or more, notes it's preload-eligible; a header that already carries the preload directive is recognised as preload-ready. Preload is surfaced, never demanded — the list takes months to leave, so grading sites down for opting out would be dishonest. Same check id, so timelines carry over.
- 2026-08-09 product The fix pack now writes DNS records — the most copy-pasteable answer the product had been withholding. /fix runs the email and DNS scans too and lays out the exact records the domain is missing as a name/type/value table: SPF (refusing everything outright when the domain sends no mail, since that's the correct lockdown), DMARC starting at quarantine with a reporting address, MTA-STS with today's policy id, TLS-RPT, BIMI, and CAA. It is deliberately conservative about what it won't invent: DKIM keys, an SPF include and a DS record all depend on values only your provider or registrar can give you, so those are explained rather than fabricated — a confidently wrong DNS record is worse than no record.
- 2026-08-09 product The fix pack learned HTML. It handed over server config and whole files, but a whole class of failures — missing titles, meta descriptions, canonicals, viewport, favicons, Open Graph tags — live in the page's <head>, and telling someone "no meta description" is not the same as handing them the tag. /fix now runs the SEO scan alongside the header and AI scans and emits exactly the tags that failed, with the page's own URL already filled into the canonical and og:url. Two failures that can't be pasted as tags — a missing html lang attribute and a broken heading structure — are called out as notes rather than faked as markup, and a page carrying noindex is warned that nothing above will help until it comes off.
- 2026-08-09 creative Actually looked at the printed client report — the literal document an agency hands a client, built weeks of cycles ago and never once viewed as a page. Three things were wrong for a document that leaves the building: it opened with Canopy's own tool header and the operator-facing tagline "ready to print or send" rather than with the report; it identified itself nowhere, so a PDF sitting on a client's desktop had no line saying who made it, for whom, or when; and the layer tables sprawled at print width. Print now strips the app chrome, ends with a single credit line (agency · report · date · generated with Canopy, and the Pro-preview caveat when branding is previewed), and tightens the tables. The screen view is untouched.
- 2026-08-09 product Monitoring got its off switch and an honest capacity line. Enrolling a site was a one-way door: one click put twelve timelines onto a shared nightly budget and nothing could take them off again short of waiting ninety days for expiry. /unmonitor?domain= removes every timeline belonging to a domain and the site report now shows which state you're in — monitored, with links to trends, the report and stop; or not, with the offer. And because the sweep is a shared resource rather than an infinite one, the page now states the arithmetic rather than implying nightly freshness for everyone: at the current budget roughly three sites can be fully refreshed a night, so once more than that are enrolled it says so and gives the real cadence.
- 2026-08-09 product The SEO layer learned to read its canonical's destination, not just its presence. Before, any rel=canonical earned a green pass — including a canonical pointing at the wrong URL, which is the most damaging SEO bug there is: it silently tells search engines to index some other page instead of this one, so a template that canonicalizes every page to the homepage can deindex a whole site while every page looks perfectly configured. The check now resolves the href against the page's own final URL and grades it — a self-referencing canonical passes (and is praised when it drops tracking params, which is its whole job), one pointing to a different path warns ("the usual cause is a template canonicalizing every page to one URL"), a cross-host canonical warns as index-that-instead (correct only for deliberately syndicated content), and multiple or empty/unparseable canonicals warn too. Same check id, so timelines and fingerprints carry over; free per scan since it reads HTML already fetched, and it flows through /site, the multi-page /pages sweep (where templated canonical bugs actually hide), badges and history unchanged.
- 2026-08-09 product Uptime joined the client report, closing the odd gap where the one number a client actually asks — "was my site up?" — was the one thing the document an agency hands them didn't say. Each site's card now carries seven-day and 24-hour uptime, average response time and a link to its public status page, or, when nobody is watching that site, a dashed prompt to start 15-minute checks in one click. The summary line reports coverage honestly ("uptime watched on 2 of 5") rather than quietly omitting the sites nobody is watching. Read from the existing ping series, so it costs one KV read per site and no scanning.
- 2026-08-09 creative Looked at the dashboard on a phone and found every badge spending most of its width on the word "canopy" — nine times a row, under column headers that already name the layer — so reading one site's grades meant scrolling sideways past nine wide shields. Badges gained a compact form (/badge?…&style=compact): the grade letter alone in a coloured chip, no label segment. The dashboard now uses it, so a row of nine grades fits where three used to, on a phone as well as a desktop, with the layer name moved into the link title and the image alt text where it belongs for screen readers. The full shields-style badge is unchanged for READMEs, where the label is the point.
- 2026-08-09 product The email layer learned brand trust: an eighth check grades BIMI — the logo Gmail, Apple Mail and Yahoo show beside authenticated mail. One extra DoH lookup at default._bimi.<domain> reads the record, checks the l= logo is an HTTPS SVG, and crucially ties the verdict to DMARC: BIMI only displays once you're enforcing (p=quarantine/reject), so a published logo at p=none warns that it will never show, and an eligible enforcing domain with no BIMI is nudged that it's leaving inbox recognition on the table. A present, valid, enforcing setup passes, noting whether a VMC/CMC (a=) is attached for Gmail. Light weight — brand polish, not a security defect — and free per scan, flowing through /site, badges, snapshots and history unchanged. The visible payoff of getting spoofing protection right.
- 2026-08-09 product Bulk onboarding for portfolios: /onboard?domains=… enrols every layer of every site in nightly monitoring, reachable from the dashboard as "Monitor all 8 sites nightly". The interesting constraint is that it cannot be done in one request — a whole-site scan costs about 43 subrequests against a hard ceiling of 50 — so the walk scans exactly one site per request and chains to the next with a meta refresh, which also turns what would be an opaque eighty-second stall into a progress list ticking off sites as they land. Failures are marked and skipped rather than halting the run, and progress is read back from the timelines themselves rather than carried through the URL, so a refresh or a closed tab resumes truthfully.
- 2026-08-09 product Repositioned Pro around the buyer who actually pays. Every cycle made the free tier stronger — twelve layers, nightly monitoring, an RSS regression feed, print-ready client reports — until Pro's pitch (deeper sweeps, longer history, higher limits) was mostly throttles a developer would shrug at. Pro is now aimed at agencies at $29: white-label reports and status pages, scheduled delivery of both reports and alerts, private uptime slots, client-scale limits, audit-length history — and the closing line states the principle plainly, that Pro only charges for delivery, identity and scale, with a promise that anyone looking after one site is finished for free. The flagship is now visible rather than described: /report?brand=Your%20Agency renders the report under your own letter mark and name, watermarked "Pro preview" with the Canopy marks left on, so the value is something you can look at before you buy it.
- 2026-08-09 product One-click monitoring: /monitor?domain= runs the whole-site scan and writes a timeline entry for every layer at once, which is the act that enrols them in the nightly sweep. Until now getting a site monitored meant saving a snapshot on eleven separate layer pages by hand — which is precisely why /trends, /digest and /report were empty for almost every visitor, three good tools starved of the data they exist to present. The site report now offers "Monitor this site nightly →" and confirms with links into all three. Rate-limited like every other scan route; the composite scan already fetched everything, so enrolment costs writes, not requests.
- 2026-08-09 creative Walked the suite on a phone and found the navigation had quietly become the product's front page: 24 links across two wrapping rows filled roughly 450px of an 812px screen, so a first-time visitor met a wall of link text before the heading, the form, or anything they came for. Each nav row is now a single horizontally-scrollable strip below 700px — every link still reachable, the wall gone. Two more things the same walk turned up: /site led its report header with the full weighting formula (seven lines of percentages on mobile, implementation trivia as the headline) — now it reads "11 layers, one weighted grade" with the arithmetic one tap away under the cards — and the page's tagline still named six layers when eleven exist, so it no longer enumerates and cannot rot again.
- 2026-08-09 product The timeline can now explain itself. Monitoring entries stored only {grade, score}, so a regression could say "down 18 points on Tuesday" but never which check caused it — the snapshot-vs-live diff answers that only if you thought to save a snapshot beforehand. Each entry now carries a compact per-check status fingerprint (a few bytes), and the change banner names what moved: "↓ Regressed since last recorded scan: B (83/100) 2 days ago → C (68/100) now. Moved: Content-Security-Policy (pass→fail); Cookie security (pass→warn)." Regressions are listed first. The banner itself was header-scan-only despite eleven other layers keeping timelines; it is now one shared helper used by every layer page. Older entries without a fingerprint degrade to the plain score line.
- 2026-08-09 product Audited the whole surface by hand — 23 pages, 13 layer APIs, the tool APIs, CI gate, exports, badges and every snapshot round-trip, all healthy — then made the audit permanent as scripts/smoke.sh, since at fifteen autonomous cycles a day a two-minute regression check is worth more than the twenty-minute audit it replaces. It also verifies the things this project keeps rediscovering: that the CSP still carries no unsafe-inline, that no inline style attributes crept back, and that the rate limiter still trips. Writing it surfaced two real facts: wrangler dev does populate cf-connecting-ip (so the limiter is live in local testing), and a per-request counter inside $(...) silently resets — the subshell ate it and every request shared one bucket, producing six phantom failures until the identity was made stateless. AGENT.md now asks every cycle to run it before pushing.
- 2026-08-09 product The whole-site report now ends the click-through-eleven-reports chore: a "Fix these first" section synthesizes every failing and warning check across all eleven layers into one ranked action list — fails before warns, heavier layers before lighter, then each check's own weight — so a site owner sees the site's entire prioritized to-do list on one screen, each item naming its layer and the exact fix (and a clean "Nothing to fix" when it's all green). The same ranked list rides the Markdown export, so the .md an agency pastes into a PR or client email now leads with priorities, not just a per-layer grade table. Pure synthesis of data /site already computed — no new fetches. /api/site now carries a priorities[] array; smoke.sh covers both surfaces.
- 2026-08-09 creative UI/UX cycle on a seam this project kept widening: the dashboard, the weekly digest and the new client report all operate on the same list of domains, and moving between them meant retyping it. Each now carries a strip — "same 5 site(s): Dashboard · Weekly digest · Client report · RSS" — with the current view named rather than linked and each destination labelled by what it's for. Build the list once, read it four ways; the strip hides itself when printing.
- 2026-08-09 product First build under the owner's pricing steer (favour what an agency would pay for): /report?domains=…&for=Client turns stored monitoring data into a dated, print-ready site-health document — every monitored layer's grade, score and seven-day movement per site, a regressions-and-improvements summary, and print CSS that strips the navigation so ⌘P gives a clean PDF to send a client. Built from KV timelines rather than live scans, so a fifty-site report costs reads only and never nears the subrequest ceiling. Branding and scheduling are named on the page as the Pro half — the free tier does the work, Pro will do the presentation.
- 2026-08-09 product The fix pack now hands over files, not just headers. Several checks fail on a missing *file* rather than a missing header, and telling someone "you have no security.txt" is not the same as giving them one — so /fix now runs the AI-readiness scan alongside the header scan and emits ready-to-save security.txt (RFC 9116, with a year-out Expires), llms.txt (a Markdown map of the site), and a robots.txt AI-crawler stance block, each filled in with the site's own domain and labelled with the exact path to save it at. Copy button, paste, done.
- 2026-08-09 product Took the last cycle's handoff and made the site pass its own CSP honestly: the two remaining inline style= attributes are gone. The pricing note became a utility class; the Slack preview card's left rail — whose colour is the scanned site's own theme-color and so can't be a stylesheet class — is now an inline SVG rect, because an SVG fill attribute isn't a CSS declaration and survives style-src 'self' where an inline style silently would not have rendered. Zero inline styles left in the codebase; the CSP the scanner preaches is now one the site keeps everywhere.
- 2026-08-09 creative Look-and-feel cycle (rotation rule — the last four entries were all product): a report now shows its shape before you read a word. Under the grade tile sits a 'health strip' — one small cell per check, colored pass/warn/fail, so a mostly-green run with two red cells reads instantly, and hovering a cell names its check. Shared by the header report and all twelve layer reports via one helper, so every checks-based page gained it at once. CSP-clean by construction: cell colors come from status classes, never inline widths, so it survives Canopy's own strict style-src 'self' — the visual meter the letter grade always wanted.
- 2026-08-09 product The pricing page had rotted into a lie: Pro was still charging for scheduled monitoring, the multi-site dashboard and grade history — all of which shipped free weeks of cycles ago — while Free undersold itself as "15 checks" after twelve layers landed. Both tiers rewritten honestly: Free lists what actually exists, Pro now promises only what the free tier genuinely doesn't do (alert delivery, private uptime slots, deeper sweeps, longer history, higher limits), with a plain line saying Pro only ever charges for delivery, private capacity and scale. Every number on the page is now interpolated from the constants that enforce it, so the pitch cannot drift from the product again.
- 2026-08-09 product Closed a drifted seam: /compare offered eight of the twelve layers — CSP and AI readiness were registered analyzers that quietly never became comparable, because the compare list was hand-maintained beside the layer registry. It is now derived from URL_LAYERS, so registering a layer makes it comparable in the same line; a small label map keeps the friendlier wording and anything unlisted falls back to the layer's own name. Twelve layers, twelve comparable.
- 2026-08-09 product The performance layer learned to see resource hints (11 checks now): it reads <link rel=preconnect/dns-prefetch> and correlates them with the third-party origins the page already references, so the advice is specific — a page pulling from a dozen third-party origins with zero hints now warns that each first request there pays a full DNS + TCP + TLS handshake cold, while a lean same-origin page passes clean (hints aren't needed). It's the natural companion to the third-party fan-out check and a standard Lighthouse audit Canopy was missing; free per scan since it reads the HTML already fetched, and flows through /site, sweeps, badges and history unchanged.
- 2026-08-09 product Found and fixed a silent monitoring failure: whole-site composite scans grew to 43 subrequests while the nightly sweep's per-run budget stayed at 40 — so every monitored /site timeline had quietly stopped updating (trends and digests starving with it). The budget now fits the biggest target (45, under the platform's 50), the sweep runs three passes a night (3:00/4:00/5:00 UTC), and a KV cursor makes passes resumable — each one picks up exactly where the budget stopped the last, so no target can be starved permanently no matter how expensive scans get.
- 2026-08-09 creative Walked the fix-pack flow as a stranger and hit the gap between promise and mechanics: "ready to paste" code blocks with no copy button. Every <pre> on the site — fix packs, reference configs, docs examples, badge embeds — now grows a Copy button (Copied ✓ feedback, always visible on touch screens, hover-revealed on desktop). Progressive enhancement via the existing hash-allowed script, CSP hash recomputed; no-JS visitors lose nothing.
- 2026-08-09 product Proof for strangers: a "Live examples" strip on the homepage shows real whole-site grades for github.com, wikipedia.org, stripe.com, and nytimes.com — each a cached badge that is its own Worker request, so the homepage stays instant — each card clicking through to the full eleven-layer report. A first-time visitor now sees the product working on names they know before they type anything.
- 2026-08-09 product The multi-page sweep covers seven layers now: CSP, cookie security, and privacy & trackers joined a11y, performance, SEO, and headers as sweepable — "which of my pages sets an insecure cookie / loads a tracker / weakens the CSP" are exactly per-page questions. Because the sweep dispatches through the URL_LAYERS registry, the entire feature was three lines of configuration plus doc strings — the machinery thesis, still paying.
- 2026-08-09 product Privacy joined the composites, completing the established rhythm: /site now runs eleven layers (headers 20% · CSP 10% · cookies, AI, PWA, privacy 5% each · email, DNS, SEO, performance, a11y 10% each), the dashboard grew a Privacy badge column, and the sweep budget covers the extra fetch. Headers ceded five points to make room — tracker hygiene is now part of what a site grade means.
- 2026-08-09 creative Look-and-feel cycle: a real theme toggle. Dark mode was OS-only with no way to override it — now every page's nav carries an Auto / Light / Dark control (right-aligned pill) that cycles and remembers your choice in localStorage. Auto still defers to prefers-color-scheme (unchanged for anyone who never touches it); Light and Dark force the theme regardless of OS. The design tokens were refactored so the dark palette applies via both the media query (when not overridden) and an explicit [data-theme] attribute, which the tiny inline script sets before first paint to avoid a flash of the wrong theme. Same CSP discipline as before — the script rides the existing sha256-hashed bundle, no unsafe-inline.
- 2026-08-09 product Layer twelve: a privacy & tracker analyzer at /privacy. One HTML fetch is matched against a dictionary of ~40 known third-party services — Google Analytics/Tag Manager, Meta/LinkedIn/TikTok/Microsoft ad pixels, session recorders like Hotjar/FullStory/Clarity — and grades privacy *exposure* in six checks: the overall tracking surface, cross-site advertising pixels (the most invasive, named), session recording, whether a consent-management platform (Cookiebot, OneTrust, Klaro…) is present to gate them, Google-Fonts visitor-IP leakage, and a Referrer-Policy that stops URLs leaking to third parties. Privacy-friendly cookieless analytics (Plausible, Fathom, Cloudflare, Matomo…) are detected and praised, never penalized. Every tracker is named so a site owner or their DPO can act. JSON at /api/privacy (services listed by category), badge, /compare support, snapshots, nightly monitoring, markdown export and CI gate — all free via the URL_LAYERS machinery. Composite (/site) wiring follows next cycle, same rhythm as CSP, AI, cookies and PWA before it.
- 2026-08-09 product PWA installability joined the composites — and a snapshot bug got fixed along the way: /site now runs ten layers (headers 25% · CSP 10% · cookies 5% · AI readiness 5% · PWA 5% · email, DNS, SEO, performance & a11y 10% each), the dashboard grew a PWA badge column, and the PWA timeline shows in /trends and the weekly digest. The fix: PWA (shipped last cycle) was the one URL_LAYERS member missing from historyKeyFor, so its snapshots wrote to hist:<url> — colliding with the headers timeline and getting re-scanned as headers by the nightly sweep — while the PWA page read from an always-empty hist:pwa:<url>. Now keyed correctly, so PWA snapshots, history, and monitoring finally work. Site sweep budget bumped for the extra manifest fetch.
- 2026-08-09 product Layer eleven: a PWA / installability analyzer at /pwa. It reads the page plus the Web App Manifest it links to and grades the pieces a browser actually requires before it offers to install a site as an app — nine checks: a linked manifest, a manifest that loads and parses, an app name, 192px + 512px install icons (maskable noted), a standalone display mode, start_url, theme color (manifest and/or <meta>), an apple-touch-icon (iOS ignores manifest icons for the home screen), and a mobile viewport — each with the exact fix. One extra fetch (the manifest); JSON at /api/pwa with an `installable` flag and the resolved manifest URL, plus badge, /compare support, snapshots, nightly monitoring, markdown export and CI gate, all free via the URL_LAYERS machinery. Composite (/site) wiring follows next cycle, same rhythm as CSP, AI and cookies before it.
- 2026-08-09 product New tool: a social share preview at /preview. Paste a URL and see how it actually unfurls on X, Facebook/LinkedIn, and Slack/Discord — rendered mock cards built from the same Open Graph and Twitter Card tags the platforms read, with the share image shown for real (fetched through a same-origin /preview/img proxy so Canopy's own img-src 'self' CSP stays intact and SSRF is blocked by normalizeUrl). Beneath the cards, a graded checklist: it verifies the og:image genuinely loads and is an image (the #1 unfurl failure nobody sees until they post), flags relative image URLs, missing og:title/description/url/site_name, and a small twitter:card where a large-image card would earn more clicks. JSON at /api/preview; wired into the nav, homepage, sitemap, and every report's "Also check" row.
- 2026-08-09 product Every report is now a one-click deliverable: an "Export as Markdown" block on every human report page — headers, all nine layers, and the whole-site composite — shows the exact GitHub-flavored Markdown the API serves at format=md in a select-all textarea, plus a same-origin "download .md" link. Agencies and devs paste a full findings report straight into a PR, an issue, Slack, or a client email without touching the CLI. Reuses renderMarkdownReport, so the human page and the CI output can never drift; no inline script, so Canopy's own CSP grade is untouched.
- 2026-08-09 product The SEO layer learned to see structured data (12 checks now): it extracts every JSON-LD block, walks @graph and array @types, and grades rich-results eligibility — valid schema.org markup passes and names the types found (Organization, Article, Product…), a block that fails to parse fails or warns because Google silently ignores invalid structured data, schema.org microdata passes with a nudge toward JSON-LD, and a page with none warns that it's leaving review stars, breadcrumbs, and FAQ rich results on the table. Structured data is the single biggest SEO signal Canopy wasn't checking; it reads from the HTML fetch already made, so it's free per scan and flows through /site, sweeps, badges, and history unchanged.
- 2026-08-09 product Cookie security joined the composites: /site now runs nine layers (headers 25% · CSP 10% · cookies 5% · AI readiness 5% · email, DNS, SEO, performance & a11y 11% each), the dashboard grew a Cookies badge column, and the /site copy, description, and docs count nine analyzers. The nightly sweep budget for site targets covers the layer's extra fetch. Layer ten shipped last cycle; this cycle wires it into every composite surface, same rhythm as CSP and AI before it — the suite and its composites are back in sync.
- 2026-08-09 creative Design cycle (the rotation rule's first enforcement): the nav grew to 22 links overnight and the homepage grid hadn't heard about six new tools — both now have structure. The nav is two labeled rows (Tools / Layers) with API and Pricing anchored right; the homepage grid became three titled sections — Scan & monitor, Ten graded layers, Fix & automate — complete, de-staled, and each tile rewritten to one crisp line. Growth outpaced organization; this cycle caught organization up.
- 2026-08-09 product Layer ten: cookie security at /cookies. The header layer's single cookie check became a full analyzer that reads every Set-Cookie the way a browser does — Secure on every cookie, HttpOnly on session-looking ones (one XSS away from account theft without it), explicit SameSite (rejecting the SameSite=None-without-Secure combo browsers drop), __Host-/__Secure- prefixes on login cookies, lifetimes past Chrome's silent 400-day cap, and the 4 KB size limits past which cookies quietly stop existing — each fix naming the exact attribute. Sites that set no cookies on an anonymous visit get a clean A and told why. JSON at /api/cookies (with the parsed cookie list), /compare support, badge, snapshots, and nightly monitoring via URL_LAYERS; the trends page also picked up the AI-readiness timeline it had been missing. Composite wiring follows next cycle, same rhythm as CSP and AI.
- 2026-08-09 product Alerts without email: /digest.xml?domains= turns the stored timelines into an RSS feed of grade changes — every regression and improvement the nightly sweep records, across every monitored layer, newest first. Subscribe in any feed reader (or Slack's /feed) and monitoring comes to you; no payments, no inboxes, pure KV reads. Linked from the digest page and documented in /docs — the free-tier half of the alerting Pro will deliver by email.
- 2026-08-09 product AI readiness joined the composites: /site now runs eight layers (headers 25% · CSP 10% · AI readiness 5% · email, DNS, SEO, performance, a11y 12% each), the dashboard grew an AI badge column, and the homepage, docs, and site-report copy count nine report cards. The nightly sweep budget for site targets covers the layer's three extra fetches. Weighted light on purpose — AI-crawler posture is a choice, not a defect — but visible everywhere a site's health is summarized.
- 2026-08-09 product Layer nine: AI-crawler readiness at /ai. Half the web has never decided whether GPTBot may read it — this layer checks llms.txt presence and spec-format, whether robots.txt takes an explicit stance on the AI crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended, Perplexity, Bytespider, …), catches accidental blanket Disallow-everything walls, and measures whether the page's content is even readable without JavaScript (AI crawlers don't run it). Three fetches, exact fixes, JSON at /api/ai, badge, snapshots, nightly monitoring, and digest/trends timelines — all priced at "write the checks" by the URL_LAYERS machinery. Composite wiring (/site weight, dashboard column) follows next cycle, same rhythm as CSP.
- 2026-08-09 product CSP joined the composites: /site now runs seven layers (headers 25% · CSP 10% · email, DNS, SEO, performance, a11y 13% each), the dashboard grew a CSP badge column, and the CSP timeline shows up in /trends and the weekly digest. The nightly sweep budget for site targets was bumped to cover the extra fetch. Layer eight shipped last cycle; this cycle wires it into every composite surface, same as a11y and DNS before it.
- 2026-08-09 product Layer eight: a CSP deep-dive at /csp. The header layer asks "is a CSP there?"; this one reads the policy the way an attacker does — eight checks covering enforcement (report-only rehearsals detected and graded, with a nudge to promote them), script-src lockdown, unsafe-inline (recognizing when nonces/hashes neutralize it), unsafe-eval, scheme-wide sources like https: and data:, object-src, base-uri, and frame-ancestors — with the exact directive to add in every fix. Reads header and <meta> policies alike. JSON at /api/csp, badge, snapshots, and nightly monitoring all via the URL_LAYERS machinery, which priced the whole layer at "write the checks."
- 2026-08-09 product The weekly digest: /digest?domains= answers "what changed across my sites this week?" from stored timelines alone — every monitored layer's latest grade against where it stood seven days ago, regressions first with score deltas, wins next, steady layers in a quiet footnote, and a one-line headline totting up the week. Zero scans fired; it's pure KV reads. Linked from the dashboard (same domain list carries over) and the nav; Pro's future email version now has its exact body.
- 2026-08-09 product The fix pack: /fix?url= turns a header scan into a config for *your* site — it re-runs the checks and emits Nginx / Apache / Caddy / _headers-file / Workers / Express blocks containing only the headers that actually failed or warned (plus the HTTP→HTTPS redirect when missing), with the app-level findings a header line can't cure listed separately. Every header report's "fix these in one go" now links to its own tailored pack instead of the generic reference page.
- 2026-08-09 product Sweep snapshots: the multi-page sweep joined the stored machinery — every /pages result can now be frozen to a 90-day permalink (/share?url=…&type=pages&sweep=…), which also enrolls the sweep in nightly monitoring, so site-wide a11y/perf/SEO/header grades grow their own timeline per sweep type. Sweep history renders on the report page and serves as JSON via /api/history?type=pages&sweep=…; the cron budget prices a sweep at discovery + one fetch per page.
- 2026-08-09 product The multi-page sweep joined the CI machinery: /api/pages now goes through the shared scan responder, so fail_under=<grade|score> gates a deploy on the site-wide sweep grade (422 below the bar) and format=md returns a per-page Markdown report card — page, grade, score, pass/warn/fail counts — ready for $GITHUB_STEP_SUMMARY. One curl now blocks a release when any batch of pages drags the site's accessibility, performance, or SEO below your bar.
- 2026-08-09 product The /pages sweep learned SEO: pick "SEO & meta" and every discovered page gets the full 11-check SEO report — per-page titles, descriptions, canonicals, and OG tags are exactly the things that drift page by page. Made affordable by teaching the SEO analyzer to accept site-level facts (robots.txt, sitemap presence) fetched once per sweep instead of re-fetched per page, so an SEO sweep costs the same one-fetch-per-page as a11y and perf.
- 2026-08-09 product Your homepage isn't your site: new multi-page sweep at /pages. Canopy discovers up to 6 pages from the site's sitemap.xml (following one level of sitemap index, falling back to homepage links), runs the performance analyzer on every one concurrently, and rolls them into a site-wide grade with a per-page breakdown — each row linking to its full single-page report. JSON at /api/pages?url=…&type=a11y|perf, wired into the nav, docs, sitemap, rate limit, and every report's "Also check" row.
- 2026-08-09 product The dashboard answers its real question — "which of my sites regressed?": every row grew a Trend cell comparing the domain's last two recorded whole-site scans (↑ B→A green, ↓ A→B red, → steady), linking to its /trends timeline. Domains without a timeline show a "watch" link to the site report where a saved snapshot starts nightly monitoring — so the wall of grades now shows motion, not just state.
- 2026-08-09 product The email layer learned transport security: two new checks grade MTA-STS (DNS record and the policy file itself, fetched and parsed — mode: enforce passes, testing/none or a missing policy file warns) and TLS-RPT, so a domain that stops TLS-stripping of its inbound mail now outranks one that merely stops spoofing. Only applied to domains with MX records; copy across the email page, docs, and DKIM-era descriptions updated to match.
- 2026-08-09 product The performance layer learned to see compression: a tenth check reads the HTML response's Content-Encoding and fails uncompressed documents (warn when tiny) — text compresses 60–80%, so a missing gzip/brotli directive is among the cheapest real-world perf wins. Self-scans pass with a note, since Cloudflare's edge compresses Canopy's responses downstream of where a self-scan can look.
- 2026-08-09 product Snapshots answer "what changed?": every snapshot page now links to /s/<id>/diff, which re-runs the same analyzer live and renders the frozen result against the fresh one — score delta up top, then the compare machinery's check table with drifted rows highlighted. Save a snapshot before touching your config and the diff shows exactly which checks your change moved, on every layer except the composite site grade.
- 2026-08-08 product Every scan API now speaks Markdown: add format=md to /api/scan, /api/seo, /api/perf, /api/a11y, /api/links, /api/email, /api/dns, or /api/site and get the report as GitHub-flavored Markdown — grade headline, ✅/⚠️/❌ check table (or per-layer table for /api/site), and a Fixes section — built to pipe into $GITHUB_STEP_SUMMARY or a PR comment. Combines with fail_under, so the CI step that blocks a deploy also posts the report card explaining why; /docs shows the recipe.
- 2026-08-08 product The machinery's stored history became a product surface: /trends?target= charts every layer's grade timeline for one site on a single page — whole-site plus headers, email, DNS, SEO, performance, accessibility, and link health — each as a score-over-time SVG with grade-colored points and hover detail, layers without history linked to start their timelines. In the nav, the sitemap, and every report's "Also check" row.
- 2026-08-08 product New diagnostic tool: a redirect-chain tracer at /redirects. Paste a URL and see every hop it takes — status code, latency, and target per hop — with findings the graded layers can't see: redirect loops, chains that dead-end in error pages, plain-HTTP downgrades mid-chain, 3+-hop chains wasting round-trips, and 302/307s that should be 301/308. JSON at /api/redirects, wired into the nav, docs, and every report's "Also check" row.
- 2026-08-09 product The CI gate went suite-wide: fail_under=<grade|score> now works on every scan API — /api/seo, /api/perf, /api/a11y, /api/links, /api/email, /api/dns, and /api/site, not just /api/scan — answering 422 when the target scores below the bar. One curl of /api/site with fail_under=B now gates a deploy on the whole canopy at once; the eight endpoints' duplicated JSON plumbing collapsed into one shared responder along the way.
- 2026-08-09 creative First dedicated look-and-feel cycle: BASE_STYLES grew into a real design system — design tokens (canopy palette, surfaces, shadows, radii) with a tuned dark mode, an off-white page with white cards instead of flat transparent boxes, gradient grade tiles and buttons with hover lift and focus rings, a gentle rise-in animation (disabled under prefers-reduced-motion), and every card family (features, tiers, compare, layer, report) unified onto the same surface language.
- 2026-08-09 product Form error states — the last item on the UI/UX steer's original list: the uptime and dashboard add-forms used to swallow invalid input silently (redirect, nothing happens, user confusion); both now come back with a plain explanation — invalid, already added, or slots full — and a half-filled compare form says which side is missing instead of quietly re-rendering. Every form on the site now talks back when something goes wrong.
- 2026-08-09 product Mobile pass: on phone widths the dashboard's badge columns were clipped clean off the viewport and domain names wrapped mid-word. All wide tables (dashboard, compare, uptime, raw headers, history) now scroll horizontally inside their own card on narrow screens, and dashboard domains stay on one line. Checked visually at 375px in both themes. Also caught a staleness trap: /app.css is cached for an hour, so style fixes could lag deploys — the stylesheet link is now versioned by changelog length, busting caches exactly once per shipped cycle.
- 2026-08-09 product Homepage five-second teaching: the hero finally says what Canopy is now — seven graded report cards for any site, not "an HTTP security header analyzer" — the title/OG/description follow suit, the form is labeled as the header scan with "scan everything at once" beside it, and the suite grid moved above the header-check fine print. A stranger's first screen now teaches the product, not its first feature.
- 2026-08-09 product Loading feel: submitting any form now flips its button to a busy state ("Scanning…", "Running all layers…", "Joining…") and blocks double-submits while the Worker does its seconds of fetching. Done the way Canopy itself grades A: the one tiny script is allowed by its sha256 hash in the CSP — no unsafe-inline, practicing what the checker preaches.
- 2026-08-09 product Next-step prompts: every successful report now ends with an "Also check:" row carrying the same target into every other relevant layer — scan a URL's headers and one click checks its SEO, performance, accessibility, links, email, DNS, or everything at once. The moment someone gets value is the moment to show them the rest of the suite.
- 2026-08-09 product First UI/UX cycle under the new steer: a shared nav bar on every page (including the 404). Nine tools were connected only by homepage tiles and footer links — now every page shows the whole suite in one compact row: Scan · Site · Dashboard · Uptime · Compare | the seven layers | Fixes · API · Pricing. The seams between tools were exactly where the UX debt had accumulated.
- 2026-08-09 product API completeness + an abuse brake: /api/history now serves every layer's timeline (?url= with type web|seo|perf|a11y|links, or ?domain= with type email|dns|site), and scan-triggering endpoints gained a best-effort rate limit — 30 scans/minute per client IP, held in isolate memory with a 429 + Retry-After response that points at /pricing for higher limits. Cheap insurance now that badges and dashboards embed Canopy's scans across the web.
- 2026-08-09 product DNS joined the composites: /site now runs six layers (headers 25%; email, DNS, SEO, performance, a11y at 15% each) and the dashboard grew a DNS badge column. Link health deliberately stays out of the composite — 20 live probes per scan is too request-hungry for the free-tier budget — and the docs say so instead of pretending otherwise.
- 2026-08-09 product Layer seven: DNS posture at /dns. Five checks over DNS-over-HTTPS — apex resolution, DNSSEC (the resolver's AD flag proves the zone validates), CAA records (without them any CA on earth may issue certs for your domain), nameserver redundancy, and IPv6 — with the usual grade, fixes, JSON API, badge, snapshots, and nightly monitoring. The DoH machinery from the email layer made this mostly configuration.
- 2026-08-09 product /compare now compares any layer, not just headers: a selector on the form switches the side-by-side diff between security headers, SEO, performance, accessibility, and link health — same highlighted-drift table, staging-vs-prod for whatever dimension you care about. The check tables were already shape-compatible across layers, so the whole feature is a small dispatch map.
- 2026-08-09 product Uptime badges: /badge?target=…&type=uptime renders a live "canopy uptime | 99.9%" shield colored by threshold (green ≥99.5, amber ≥98, red below), cached 15 minutes, with a copy-ready Markdown snippet on every status page — put your uptime in your README. And the sprout finally became a favicon (/favicon.svg, linked on every page), which the SEO self-scan had been quietly warning us about.
- 2026-08-09 product Canopy took its own medicine: all page styles moved from inline <style> blocks (and scattered style= attributes) into one cacheable external /app.css, letting the CSP drop 'unsafe-inline' for styles; added Strict-Transport-Security and Cross-Origin-Opener-Policy to every response. The self-scan that motivated it — our own header grade — jumps accordingly, and every page got lighter and more cacheable in the bargain.
- 2026-08-09 product Self-scans actually work now: Cloudflare Workers can't fetch their own hostname, so every "scan this site" was doomed at the platform level regardless of the URL. Scanner fetches now go through smartFetch, which detects Canopy's own host and dispatches the request internally to the handler instead of the network — so Canopy grades itself for real: headers, SEO, perf, a11y, links, even its own uptime pings.
- 2026-08-09 product Fixed the site's own home address: SITE_URL pointed at a workers.dev subdomain that doesn't exist, so every self-scan link, canonical/OG URL, badge snippet, and docs example referenced a dead host (spotted by the owner clicking "Try: this site" and getting an honest "that's not an HTML page" from our own scanner). Corrected to domain-project.jjraxx.workers.dev, kept as one constant so the owner's eventual proper domain is a one-line swap. The configuration grader, misconfigured — noted for humility.
- 2026-08-09 product Every uptime target now has a public status page: /uptime?target=… shows a live headline (Operational / Down / Unreachable), 24-hour and 7-day uptime, average response, a latency sparkline with red bands where checks failed, and an incident log built from consecutive failed checks (with ~duration, marked ongoing when still down). Shareable with anyone — the classic status page, grown from the 15-minute ping series.
- 2026-08-09 product Ring 3 opens with genuinely new machinery: uptime monitoring at /uptime. A second cron cadence pings every watched URL every 15 minutes and keeps a rolling 7-day series in KV; the page shows live status, 24-hour uptime percentage, and average latency, with 8 shared community slots free (KV write budget) and an immediate first ping on add. JSON at /api/uptime. Private slots and downtime alerts are the Pro half, waiting on payments.
- 2026-08-09 product Layer six: a link-health analyzer at /links. It pulls up to 20 links from the page (same-origin first, relative URLs resolved), probes each one live with HEAD (falling back to GET where servers refuse), and grades: dead links fail with partial credit by proportion, permanent 301/308 targets warn (update the href), plain-http targets warn. Wired through the URL_LAYERS registry as usual, and the monitoring sweep's kind-dispatch was refactored from a ternary chain into an ordered prefix table while adding it.
- 2026-08-09 product Accessibility joined the composites: /site now runs all five layers and weights a11y at 20% of the whole-canopy grade (headers 30 · email 20 · a11y 20 · SEO 15 · perf 15), the dashboard grew an A11y badge column, and the nightly sweep budget for site targets was bumped accordingly. The suite and its composites are in sync again.
- 2026-08-09 product Layer five: an accessibility analyzer at /a11y. Eleven WCAG smoke checks from one fetch — image alt text, document language, page title, form-control labels, named links/buttons, pinch-zoom not disabled, heading outline (one h1, no skipped levels), a main landmark, a skip-to-content link, no positive tabindex, and descriptive link text — graded with exact fixes, JSON at /api/a11y, badge via /badge?url=…&type=a11y, plus snapshots and nightly monitoring through the shared machinery. Static HTML can only smoke-test a11y, but it catches the failures that hit real users most; the layer-page/route/badge plumbing was unified into one URL_LAYERS registry along the way.
- 2026-08-08 product The multi-site dashboard is real: /dashboard?domains=a.com,b.com renders every domain as a row of live grade badges — overall site grade plus all four layers — each badge loading as its own cached Worker request, so a ten-site wall of grades costs the page nothing and stays inside every free-tier limit. The dashboard is its own URL: bookmark it, share it, keep it open on a wall monitor. Add or remove domains from the form at the top.
- 2026-08-08 product The canopy closed over: /site?domain= runs all four analyzers concurrently — headers, email, SEO, performance — and rolls them into one weighted site grade with a four-card scorecard, each card linking to its full report. Combined badge (canopy site), snapshots, history, and nightly monitoring included; the sweep now skips over-budget targets instead of stopping, so one expensive site never starves the cheap ones. This is the multi-layer dashboard the pricing page promised, live on the free tier for one domain at a time.
- 2026-08-08 product Layer four: a performance analyzer at /perf. Nine checks from a single fetch — server response time (TTFB), HTML document weight, render-blocking scripts in <head>, total script and stylesheet counts, images without width/height (layout shift), missing lazy-loading, legacy-only image formats, and third-party origin fan-out — graded with fixes, JSON at /api/perf, badge/snapshots/monitoring via the shared machinery. Four layers in the canopy.
- 2026-08-08 product Layer three: an SEO & meta hygiene analyzer at /seo. Eleven checks — title and meta-description quality (with length guidance), canonical, exactly-one h1, html lang, viewport, noindex detection, robots.txt, sitemap discovery, Open Graph tags, favicon — graded with exact fixes, JSON at /api/seo, badge via /badge?url=…&type=seo, snapshots and nightly monitoring via the shared machinery.
- 2026-08-08 product The machinery now covers the whole suite: email scans get snapshots (/share?domain=…), grade history, nightly monitoring, and badges (/badge?domain=… renders "canopy email | A"), exactly like web scans. The cron sweep is subrequest-budgeted so mixed web+email monitoring stays inside free-tier limits, and /docs documents the email API.
- 2026-08-08 product Ring 2 opens — the header analyzer is sharp, so Canopy grew its second layer: an email security analyzer at /email. Enter a domain and it grades spoofing protection via DNS-over-HTTPS: SPF (present, no +all), DMARC (present, policy stronger than none), DKIM (probes eight common selectors), and MX — same letter-grade-with-fixes format, JSON at /api/email. The suite era begins.
- 2026-08-08 product Split the grade into core vs hygiene: the headline letter now comes from the nine security-critical checks (HTTPS, HSTS, CSP, sniffing, framing, referrer, cookies, CORS, mixed content), while the six hygiene signals (Permissions-Policy, COOP, SRI, caching, security.txt, info disclosure) roll into a separate /100 shown beside it. An A means the attacks that matter are covered; hygiene nudges instead of punishes. Reports group the two sections; the API returns both scores.
- 2026-08-08 product Added a security.txt check (15 checks): Canopy now probes /.well-known/security.txt (RFC 9116) — the standard file telling researchers where to report vulnerabilities. Present with a Contact line passes; present but malformed or missing warns, with a ready-to-paste template in the fix. Cron sweep trimmed to 12 URLs/night to stay comfortably inside free-tier subrequest limits.
- 2026-08-08 product Added a Subresource Integrity check (14 checks now): the body scan finds scripts and stylesheets loaded from third-party origins without an integrity attribute — the gap that lets a compromised CDN inject code into your page — and names them, with the sri-hash fix in the recommendation. Same-origin and integrity-tagged resources pass.
- 2026-08-08 product Gave the homepage a proper landing-page treatment: scan form up top, then a six-tile feature grid (compare, snapshots & monitoring, CI gate, badges, reference configs, API) that shows the whole product at a glance instead of burying it in a link list. The full build changelog moved to its own /changelog page — still on the homepage in spirit via a footer link, still at /feed.xml and /api/changelog.
- 2026-08-08 product Canopy now works as a CI gate: /api/scan accepts fail_under=<grade or score> and answers 422 when the site scores below the bar, so one curl in a pipeline blocks deploys that regress security headers. Added /api/history?url= (the grade timeline as JSON) and /docs — a developer page documenting every endpoint with a copy-paste GitHub Actions recipe.
- 2026-08-08 product Made regressions visible: timeline entries now carry ↑ improved / ↓ regressed markers against the previous scan, and a live report whose score moved since the last recorded entry opens with a change banner — "↓ Down from B (85/100), last seen 3 days ago" — so a header regression jumps out the moment you look. The in-page half of Pro's future alerting.
- 2026-08-08 product Monitoring is live: a daily cron trigger now re-scans every URL that has a saved snapshot (up to 15 per night, newest histories first) and appends the result to its grade timeline, marked as an automatic scan. Save a snapshot once and Canopy quietly watches the site from then on — timelines now grow on their own. Alerts on regressions (email/webhook) remain the Pro feature to build once the owner wires payments.
- 2026-08-08 product Added a grade-history timeline: every saved snapshot now also appends a {grade, score, date} entry to a per-URL history in KV, and scan reports for a URL with history show its grade over time — each entry linking to the frozen snapshot that recorded it. Save a snapshot before and after a fix and the report itself now shows the improvement. This is the free, manual seed of Pro's automated monitoring timeline.
- 2026-08-08 product Built the monetization scaffolding up to the payment line: /pricing lays out Free (everything today, forever) vs Canopy Pro (scheduled monitoring with alerts on grade regressions, multi-site dashboard, grade history, higher API limits, priority badges), and a Pro waitlist form stores emails to KV. FLAG FOR OWNER: per the brief no real payments are wired — when you're ready to charge, the waitlist in KV (prefix waitlist:) is your launch list and the pricing page is where checkout goes.
- 2026-08-08 product Added shareable scan permalinks — Canopy's first stored feature (the owner unblocked KV). Every report now has a "Save snapshot" link that freezes the full result to KV for 90 days and redirects to /s/<id>: a stable URL that renders the report exactly as scanned, stamped with when it was taken and a one-click rescan link to compare against live. Snapshots are the seed of the paid tier's grade-history timeline.
- 2026-08-08 product Added a cache-control sanity check, completing the checks menu: pages with no caching policy at all get a warn (browsers and shared caches will guess), as do HTML documents marked public with a long max-age (stale or personalized content served from shared caches). Explicit policies — no-store, private, or a deliberate max-age — pass.
- 2026-08-08 product Added a mixed-content check — Canopy's first body-aware check. On HTTPS pages it now reads the HTML (capped at 256 KB) and finds subresources still loaded over plain http://: scripts, stylesheets, iframes and objects fail (browsers block them or attackers can hijack them), images and media warn (they break the padlock), with the first few offending URLs named in the report.
- 2026-08-08 product Added /reference — a "what good looks like" library: the full A-grade header set Canopy checks for, as copy-ready config for Nginx, Apache, Caddy, Netlify/Vercel _headers files, Cloudflare Workers, and Express + Helmet. Every scan report now links to it, so a failed check goes straight to the exact block to paste into your stack.
- 2026-08-08 product Added a compare view at /compare: scan two URLs at once and see every security-header check side by side, with the rows that differ highlighted and both letter grades up top. Built for staging-vs-prod drift checks and benchmarking against a reference site; both scans run concurrently, and each column links out to its full report.
- 2026-08-08 product Added an HTTPS-enforcement check: Canopy now actively probes the site's plain-HTTP origin to see whether it redirects to HTTPS. It passes when HTTP 301s to https:// (or the HTTP port is closed), fails when a site answers over plain HTTP with no upgrade (or is served over HTTP outright), and warns when HTTP is reachable but neither serves nor redirects — the most fundamental transport check, previously missing.
- 2026-08-08 product Added embeddable grade badges: /badge?url=… returns a shields-style SVG ("canopy | A") rendered live from a scan and colored by grade, cached for an hour. Every report now shows the badge with copy-ready Markdown and HTML snippets so people can drop a self-updating Canopy grade into their README or site.
- 2026-08-08 product Added a CORS check: the scan now sends a throwaway Origin header so Canopy can tell whether a site blindly reflects the requesting origin. It fails on the critical reflected-origin-plus-credentials pattern (any site can read a logged-in visitor's authenticated responses), warns on origin reflection or wildcard Allow-Origin, and passes when sharing is pinned to a specific origin or absent.
- 2026-08-08 product Added a cookie-security check: Canopy now inspects every Set-Cookie for the Secure, HttpOnly, and SameSite flags, names any weak cookies, and fails the check when Secure/HttpOnly are missing (session-theft risk) — a warn when only SameSite is absent.
- 2026-08-08 product Pivoted to a real product: Canopy, an HTTP response & security-header analyzer. Paste a URL and get a graded report (HSTS, CSP, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, framing, info disclosure) with plain-English fixes, plus a JSON API at /api/scan. The organism retires to a sprout mark.
- 2026-08-08 creative The organism came alive: it's now a real-time canvas simulation that grows before your eyes, bends phototropically toward your cursor (its light), and lets you click to plant new shoots. The changelog is its DNA; the static SVG stays as a no-JS/reduced-motion fallback.
- 2026-08-08 creative The organism now grows different forms per change kind: infra work sprouts small green buds, creative work blooms full flowers, and the base is a seed — so the creature's shape now tells you what kind of work built it.
- 2026-08-08 creative Declared a direction: the homepage now renders a procedurally generated organism (SVG vine) with one leaf per changelog entry, recolored daily by a date-seeded PRNG — the site's history is now something you can see.
- 2026-08-08 infra Added ETag support and 304 Not Modified responses for all cacheable routes, so repeat visitors and feed readers stop re-downloading unchanged content.
- 2026-08-08 infra Added a resources nav linking the machine-readable endpoints from the homepage, security headers on every response, and proper 405 handling for non-GET/HEAD requests.
- 2026-08-08 infra Added robots.txt, sitemap.xml, Open Graph/Twitter meta tags, and cache-control headers so the site is properly crawlable and cacheable.
- 2026-08-07 infra Added an RSS feed (/feed.xml) so the changelog can be followed outside the browser.
- 2026-08-07 infra Added basic routing: /api/changelog (JSON), /health, and a 404 page.
- 2026-08-07 genesis Genesis. Project created, deploy pipeline wired up.