Blog

Practical guides for agencies managing client brand assets and compliance.

Your Single-Page App Serves an Empty Page to an AI Crawler

A client-rendered SPA ships an empty <div id="root"> and a script bundle. A browser runs the bundle and paints your content; an AI crawler that does not execute JavaScript reads the raw response and sees nothing. That is the JS-only-shell problem. How to tell what a non-rendering fetch actually receives, the three states of your initial HTML, why "but Googlebot renders JS" does not save you with answer-engine crawlers, and the fix.

Read article →

Detecting a Meaningful Page Change: A Content Hash, a Line Diff, and Why the AI Summary Runs Last

A page-change monitor that fires on every re-render is noise; one that never fires is theater. The hard part is not diffing two strings — it is the order of the pipeline. Why you normalize before you hash, gate the alert on the hash AND the diff, fetch defensively even on trusted URLs, and keep the LLM summary as the last, fully optional layer that fails back to the raw diff.

Read article →

Hidden Text an AI Agent Reads but a Human Can't: Detecting Page Cloaking Without False-Flagging Your Accessibility Markup

An AI agent reads your page’s DOM, not its pixels — so text you hid with opacity:0, off-screen positioning, or a transparent color is invisible to a visitor and perfectly legible to a model. That is the hidden-text prompt-injection surface (OWASP LLM01). The hard part is that most hidden text is benign accessibility markup — sr-only labels, skip links, collapsed menus. How to separate cloaked injection from your a11y layer: a strong-vs-weak signal split, a substantive-character floor, why it needs the computed DOM instead of raw HTML, and a three-verdict model.

Read article →

Redact Before the LLM Sees It: A Deterministic Floor, an AI Ceiling, and Never the Reverse

The moment your app sends a user-supplied string to an external LLM, every email, token, private key, and IP in it leaves your trust boundary. Why redaction must run before the call with a deterministic regex floor, why that floor fails closed on anything it cannot scan, and why the AI second-opinion guard is ordered so it can only make the decision stricter — never looser — and falls back to the safe verdict on any error.

Read article →

Verifying a Webhook Signature: The Raw-Body Trap, Verify-Before-Parse, and When a Timestamp Window Is Just Theater

A webhook signature is an HMAC over the exact bytes the sender signed. Re-serialize the JSON before you hash and the digest will not match — the outage that looks like a rotated key but is really a reformatted body. Why you must hash the raw body, verify before you parse, compare in constant time, and why a replay-timestamp window is only real when the timestamp is inside the signed bytes (Svix) and security theater when it is not.

Read article →

An HTTP 200 Is Not Uptime: The Monitor Went Green While the Page Served an Error

A status-code check answers "did the server respond?" — not "did it respond with the right thing?" A checkout page that starts rendering a 200 error page, a login that silently 302s to the wrong host, a JSON API that returns 200 with an empty body: every one of those is down to a user and green to a naive monitor. Why real uptime gates on the status match AND every configured assertion (keyword, redirect target), why an unconfigured assertion must stay null instead of failing closed, and the dogfooded bug where we computed the assertion, stored it, labeled it in the UI — and read it with nothing.

Read article →

Your Certificate Is Valid and Your Site Loads Fine — and Your TLS Still Grades Weak

A certificate can be valid, unexpired, and trusted by every browser while the handshake underneath it is still graded weak: you negotiate TLS 1.0/1.1, a cipher with no forward secrecy or no AEAD, or ship a SHA-1 signature somewhere in the chain. None of that throws an error or shows a padlock warning — the site works. The dangerous change is a silent reconfiguration to a weaker cipher on the same certificate, which a cert-expiry monitor and a cert-identity diff both miss. Why a graded handshake has three axes, why the worsening transition is the only thing worth paging on, and the honest limits of assessing posture without a bundled trust store.

Read article →

A Dead-Man’s Switch That Pages Once and Goes Quiet Is Worse Than None. Ours Went Silent for 43 Days.

A heartbeat monitor watches for absence, not presence — a cron or backup that stops running throws no error to catch. The subtle failure is not the check; it is the alert dedupe key. Key a "still dead" alert on the asset forever and never resolve it, and the monitor pages once per outage-lifetime instead of once per outage. Ours did exactly that: one page, then 6,052 silent "dead" rows across 43.3 days. Why a heartbeat needs three states, why "never beat yet" must not alert, and why the dedupe key has to move on every outage.

Read article →

A Dangling CNAME Is a Subdomain Takeover Waiting to Happen — and DNS Still Resolves Fine

When you delete the SaaS app a subdomain points at but leave the CNAME behind, the record still resolves and every uptime check stays green — while the target becomes claimable by anyone. The dangerous DNS events are silent edits to the delegation, not outages. Why a CNAME that resolves is not a CNAME that is safe, why you should alert on the first hop in your own zone and never on the CDN chain below it, and why "removed" and "not measured" are different facts.

Read article →

A Port Check Has Three Answers, Not Two — 'Open', 'Closed', and 'I Don't Know'

A port monitor that reports only open or closed will page you at 3am for an outage that never happened. A refused connection means the port is closed; a timeout, a DNS hiccup, or a blocked probe means you could not determine anything — and collapsing the second into the first is how a monitor learns to cry wolf. Why a TCP port check needs a third state, why a port monitor that accepts a hostname is one DNS rebind away from scanning your cloud metadata, and the Cloudflare Workers gotcha that turned 13,489 checks into 13,489 shrugs.

Read article →

Your security.txt Has an Expiry Date — and It Is Probably Already Stale

RFC 9116 made the Expires field mandatory: a security.txt whose Expires date has passed MUST be treated as stale. Most published files are already past it, and no header grader or uptime monitor tells you — an expired security.txt looks identical to a valid one to every tool that only checks for presence. How RFC 9116 actually works, why a 200 with no Contact field is not a real security.txt, the difference between "the file is gone" and "I could not reach it," and why the Expires date is a renewal deadline you have to monitor like a certificate.

Read article →

Your Certificate Auto-Renewal Will Fail Silently One Day. The 47-Day Cliff Makes It a When.

Automating certificate renewal with ACME does not mean you will notice when it stops. A blocked port 80, a stale CAA record, an exhausted rate limit, or a hook that exits 0 without reloading all fail quietly — and the CA/Browser Forum schedule to 47-day certificates by 2029 turns ~1 renewal a year into ~8, multiplying every silent failure surface. Why "it exited 0" is not proof, the failure modes to expect, and why independent expiry monitoring is the safety net automation cannot be.

Read article →

A CAA Record Decides Who Can Issue Your Certificates. Almost Nobody Watches It.

A CAA record tells certificate authorities which CAs may issue for your domain — and because CAA inherits down the DNS tree, an apex record permitting only DigiCert will silently make a Let’s Encrypt renewal on a subdomain fail with no certificate issued. The failure surfaces weeks later as an expired certificate, not a CAA error. How CAA and RFC 8659 inheritance work, why "missing" is not "permits any," why a lone semicolon is the opposite of an empty record, and how to check the policy that governs your next renewal.

Read article →

DNSSEC Fails Closed: A Broken Signature Is a SERVFAIL, Not a Warning

Most security controls fail open — DNSSEC is the opposite. A broken signature, an expired RRSIG, or a DS record that no longer matches your DNSKEY makes every validating resolver return SERVFAIL, so your domain goes dark for everyone on 1.1.1.1, 8.8.8.8, or Quad9 while still resolving fine from a non-validating resolver — which is why whoever broke it usually can’t reproduce it. Why it breaks, the silent signed→unsigned downgrade nobody notices, and how to check.

Read article →

Someone Can Get an SSL Certificate for Your Domain. CT Logs Are How You Find Out.

A certificate issued for your domain by someone else never touches your origin, so your uptime and SSL-expiry monitoring stay green while it exists — but it is public in the Certificate Transparency logs. How CT works, how an unauthorized certificate gets issued, how to check your own logs with crt.sh and the certspotter API, why CAA records constrain future issuance, and why catching one means watching for a new issuance, not remembering to look.

Read article →

Your PageSpeed Score Is a Lab Test. Google Ranks You on Field Data.

A Lighthouse/PageSpeed score is one throttled synthetic run. Google’s page-experience signal uses field data — the 75th percentile of Core Web Vitals from real Chrome users over a trailing 28 days. They routinely disagree, and knowing which is which tells you whether a red number is a ranking problem or an early warning. How lab and field Core Web Vitals differ, why INP is field-only, and how to use the lab number as the regression signal it actually is.

Read article →

A Broken-Link Check Counts 404s. The Resource That Breaks Your Padlock Returns 200.

A broken-link checker follows your <a href> links and flags the ones that 404. But the resource most likely to visibly break a page — an http:// image, script, or stylesheet loaded by an https:// page — returns 200. It is not "broken" by status, so a status-code checker passes it, while the browser blocks it and downgrades the padlock. Why mixed content is invisible to a link check, and how to actually find it.

Read article →

Is My Domain Blacklisted? Why a Public DNSBL Check Can Show a False Positive

A blocklist checker that reads any 127.x answer as "listed" will tell you you’re on Spamhaus when you aren’t. Spamhaus and Barracuda refuse queries from large shared resolvers and signal that refusal with a 127.255.255.x answer — which is exactly what browser tools and cloud functions query through. How a DNSBL lookup actually works, the sentinel that trips naive checkers, and which lists still carry real signal.

Read article →

A Security-Headers Grade Counts Headers. It Doesn’t Test Them.

A security-headers scanner grades a domain on how many of six headers are present — and present is not the same as effective. An HSTS header with max-age=0 counts toward your grade and does nothing. A CSP with unsafe-inline is present and toothless. And the flags most likely to leak a session — Secure, HttpOnly, SameSite on your cookies — aren’t graded at all. Where a good grade hides a real exposure, header by header.

Read article →

SPF, DKIM, and DMARC: Why “Valid” Records Still Let Your Domain Be Spoofed

Having SPF, DKIM, and DMARC records is the easy half — and the half most checkers stop at. Whether they actually stop a forged email is a different question, decided by a handful of qualifiers most green-tick tools ignore: an SPF that ends in ~all instead of -all, a DMARC stuck at p=none, a DKIM you cannot fully verify from outside, an MTA-STS policy in testing. Presence versus enforcement, term by term, and how to check which one you have.

Read article →

Structured Data for AI Answer Engines: Why JSON-LD Decides Whether You Get Cited

Being crawlable gets an AI answer engine to your page. Being parseable is what gets you cited. JSON-LD structured data states, in machine terms, what a page is — and the common failure is not a missing schema but a shallow one: an Article with no author, a Product with no description, a block of malformed JSON the engine silently skips. The types that matter, the fields that make an entity citable, and how to check yours.

Read article →

robots.txt for AI Crawlers: Why Blocking GPTBot Doesn’t Remove You From ChatGPT

Most "block the AI crawlers" robots.txt snippets block the wrong bots. Training crawlers and answer-engine crawlers are different user-agents with opposite consequences: blocking GPTBot only opts you out of training, while OAI-SearchBot is the one that fetches your pages to answer a live question. The per-vendor user-agent tokens, the four ways robots.txt gets this wrong, and why a robots.txt "allow" can still 403 at the edge.

Read article →