Learn

One guide per check, written to be enough on its own. Your report links straight to the ones it found.

Every guide follows the same shape, because the question is always the same one: what is this, why does it matter, how do I fix it, and how do I know the fix worked. Each one ends with the mistakes people actually make on that check, which is usually the part that saves the second attempt. They are written to be read without a scan, so you can send one to whoever owns the fix rather than sending them a whole report.

Exposure

exposure

Restrict or rotate the exposed Google API key

A Google API key that isn't restricted to specific APIs and referrers can be used by anyone who finds it to make billable calls against the associated Google Cloud project. It's less immediately severe than a secret credential, since it can't authenticate as a user or bypass access control, but an unrestricted key is still a standing invitation to run up usage charges or exhaust a shared quota.

Read more
exposure

Revoke the exposed GitHub token

A GitHub token inherits whatever scopes it was issued with, from read-only access to a single repository up to full account or organization control. A token shipped to the browser is exposed to anyone who loads the page, and its blast radius (private source code, CI secrets, the ability to push commits) depends entirely on what it was scoped to do.

Read more
exposure

Revoke the exposed private key

A private key is the half of a key pair that is never supposed to leave the system that generated it. It is what a TLS certificate, an SSH host, or a signing system uses to prove its identity. Once a private key has been public, even briefly, it must be treated as permanently compromised: there is no way to know who copied it before it was removed.

Read more
exposure

Revoke the exposed Slack token

A Slack token lets whoever holds it act as the app or user it was issued for: reading and posting messages in every channel it can reach, and depending on scope, pulling files and message history. A token shipped to the browser hands that same access to anyone who loads the page.

Read more
exposure

Rotate the exposed AWS access key

An AWS access key pair (an access key ID plus its paired secret) grants whatever IAM permissions are attached to it, from read-only access to a single bucket up to full account administration. A key pair that ends up in client-side JavaScript is exposed to anyone who loads the page, with whatever blast radius its permissions allow.

Read more
exposure

Rotate the exposed SendGrid API key

A SendGrid API key can send email as the account's verified sender and read delivery and activity data. In the wrong hands that is enough to run a phishing campaign that appears to come from a trusted domain, or to quietly read who the business communicates with.

Read more
exposure

Rotate the exposed Stripe secret key

A Stripe secret key (sk_live_...) is meant to run exclusively on a server: it can create charges, issue refunds, and read customer and payment data on the account's behalf. When a secret key ends up in a browser bundle instead of Stripe's publishable key (pk_live_...), anyone who loads the page can extract it and act with that same access.

Read more
exposure

Rotate the exposed Supabase service-role key

Supabase issues two JWTs for a project: an anon key, meant to be public and safe in a browser because row-level security (RLS) policies govern what it can actually do, and a service_role key, meant to stay server-side because it bypasses RLS entirely and can read or write any row in any table. Confusing the two, or shipping the service_role key to the client, removes the database's access control altogether.

Read more
exposure

Stop shipping source maps publicly

A source map lets browser devtools, or anyone else who fetches it, reconstruct the original, unminified source from a production JavaScript bundle, including comments, file and variable names, and code structure that was never meant to ship. It isn't a leaked credential, but it hands an attacker a much easier starting point for finding other vulnerabilities in the same codebase.

Read more

Security

security

Add a Content-Security-Policy header

A Content-Security-Policy header is the browser-enforced allowlist for what a page is permitted to load and execute. It is the strongest single defense against cross-site scripting (XSS), because even if an attacker manages to inject a `<script>` tag, a well-configured CSP stops the browser from running it.

Read more
security

Add a Permissions-Policy header

Permissions-Policy (formerly Feature-Policy) lets a page explicitly declare which powerful browser features (camera, microphone, geolocation, USB, payment APIs) it and any embedded iframes are allowed to use. Without it, any script that ends up running on the page, including a compromised third-party ad or widget, can request access to those features.

Read more
security

Add a Referrer-Policy header

By default, browsers send the full URL of the page a visitor is leaving, including query strings, as the Referer header on every outbound link click and resource request. A Referrer-Policy header controls how much of that URL is shared with the destination, which matters whenever URLs can contain session tokens, password-reset codes, or internal identifiers.

Read more
security

Add clickjacking protection

Clickjacking protection stops another site from loading your page inside an invisible or disguised iframe and tricking a visitor into clicking something they cannot see: a "Confirm Purchase" or "Delete Account" button, for example, hidden under a decoy UI. X-Frame-Options and the CSP frame-ancestors directive both control who is allowed to embed a page in a frame.

Read more
security

X-Content-Type-Options nosniff: how to add it

Without X-Content-Type-Options: nosniff, some browsers will try to guess a resource's type by inspecting its contents rather than trusting the declared Content-Type. That "MIME sniffing" behavior is a legacy compatibility feature, but it also gives attackers a way to get a browser to execute a file (like a user-uploaded image) as if it were a script.

Read more
security

Ask the tracking vendor to mark their cookies Secure

Some cookies that pass the first-party check by domain are still not the site's own to fix in code: an analytics or ads vendor's script wrote them via document.cookie while running on the site's pages, and only that vendor's own configuration controls the attributes it sets. Google Analytics' _ga, Microsoft Clarity's _clck, and Meta Pixel's _fbp are common examples. Each lives on the site's domain but is entirely managed by its vendor's snippet.

Read more
security

HSTS: enable Strict-Transport-Security

HSTS (HTTP Strict Transport Security) closes the gap that plain HTTPS leaves open: the very first request a browser makes to a domain, before it has ever seen the redirect to HTTPS, can still be intercepted and downgraded on an untrusted network. The header tells the browser to skip HTTP entirely for future visits, removing that window.

Read more
security

Enforce the existing Content-Security-Policy

Content-Security-Policy-Report-Only runs the exact same policy as an enforcing CSP, but only logs violations instead of blocking them. It exists so a team can watch a real policy against real traffic before it can break anything. A site with this header live has already done the hard part (writing the policy and validating it); the browser just is not acting on it yet.

Read more
security

Mixed content warning: how to fix it

Mixed content happens when an HTTPS page loads a resource (a script, stylesheet, image, or iframe) over plain HTTP. Modern browsers block "active" mixed content like scripts outright and often show a broken-padlock warning even for "passive" mixed content like images, undermining the trust HTTPS is supposed to provide.

Read more
security

Fix the Strict-Transport-Security max-age

A Strict-Transport-Security header only does its job for as long as its max-age lasts, and only if a browser can actually parse a max-age value out of it. A header with max-age=0 is not a weak policy, it is HSTS being actively switched off: browsers that already trusted the site are told to forget that immediately. A header with no valid max-age at all (missing, or too malformed to parse) is invalid per RFC 6797 and is ignored outright, so it protects nobody despite being present in every response.

Read more
security

Get the report-only CSP ready to enforce, safe directives first

A Content-Security-Policy-Report-Only header is a policy on trial: the browser reports what it would block and blocks nothing. Switching it to enforcing is safe only once those reports have been read, so a policy with no report-uri or report-to has never really been tried. And a policy whose script rules allow 'unsafe-inline' or 'unsafe-eval' would let an injected script run even when enforced. Some directives restrict things an ordinary page never relies on, so they can be enforced today.

Read more
security

Keep up security hardening

A clean security scan is a snapshot, not a guarantee: headers, cookie flags, and TLS configuration are all things a later deploy, a new third-party script, or an infrastructure change can quietly weaken without anyone noticing, since none of them fail loudly the way a broken build does.

Read more
security

Let the CSP allow what the page loads, or stop loading it

An enforcing Content-Security-Policy refuses every request its directives do not allow, including the site's own choices: an analytics pixel, a font stylesheet, a vendor's redirect to a host nobody listed. The browser logs each refusal as a console error and the request fails, so on a scan or in the network tab it looks like broken code or a service that is down. The fix is in the policy, one directive at a time.

Read more
security

Mark cookies as HttpOnly

The HttpOnly attribute hides a cookie from JavaScript: document.cookie simply will not return it. That matters because it is the last line of defense if the page ever has an XSS vulnerability: without HttpOnly, a single injected script is enough to read and exfiltrate every session cookie on the page.

Read more
security

Mark cookies as Secure

The Secure attribute on a cookie tells the browser to only ever transmit it over an HTTPS connection. Without it, the same cookie a user's browser sends on a secure page could also be sent in plaintext if the user (or an attacker) ever triggers a request to the HTTP version of the site.

Read more
security

Migrate Feature-Policy to Permissions-Policy

Feature-Policy was the original name for the header that later shipped as Permissions-Policy: same purpose (restricting which browser features a page and its embedded frames may use), a slightly different syntax. Chromium removed support for parsing the Feature-Policy header years ago, so a site that still ships only Feature-Policy is not protected in Chrome, Edge, or any other Chromium-based browser, despite having done the real design work of deciding what to restrict.

Read more
security

Serve the site over HTTPS

HTTPS encrypts everything that travels between a visitor's browser and the server, so it is the baseline every other security control assumes is already in place. Modern browsers actively punish sites that lack it: Chrome and Firefox both mark plain-HTTP pages as "Not Secure" in the address bar, and any page that collects a form field is flagged even more aggressively.

Read more
security

Set up dependency vulnerability scanning

Read more
security

Stop exposing server software versions

Response headers like Server and X-Powered-By often reveal the exact web server, framework, or language version in use. That information is not directly exploitable on its own, but it is the first thing an attacker looks up to find publicly known vulnerabilities for that specific version, turning a general scan into a targeted attack.

Read more
security

Tighten the Referrer-Policy value

unsafe-url is a legal Referrer-Policy value, but its name describes exactly what it does: it sends the complete URL a visitor is leaving, including any query string, to every destination, on every request, even when that destination is reached by downgrading from https to http. A site with this value has a Referrer-Policy header in every response; it just tells browsers to share the maximum amount of information rather than the minimum.

Read more
security

Upgrade vulnerable JavaScript libraries

Most sites ship third-party JavaScript libraries, and every version of a popular library eventually has publicly documented vulnerabilities. Because the advisories (and often proof-of-concept exploits) are public, an outdated library is one of the cheapest things for an attacker to find and abuse: it can turn into cross-site scripting or prototype pollution in your visitors' browsers even when your own code is sound.

Read more

Performance

performance

Add cache-control headers to static assets

Cache-Control headers tell a returning visitor's browser (and any CDN in between) how long a static asset can be reused without re-downloading it. Without them, every visit, even a second visit five minutes later, re-fetches the same images, scripts, and stylesheets from scratch, wasting bandwidth and slowing the page down for no reason.

Read more
performance

Add real-user performance monitoring

Read more
performance

Enable text compression

Gzip and Brotli compress text-based responses (HTML, CSS, JS, JSON) before they go over the wire, typically shrinking them several times over since text compresses extremely well. It is one of the highest-leverage, lowest-effort performance wins available because it is usually a single server or CDN configuration setting rather than a code change.

Read more
performance

Give images their size, and load them at the right time

This check reads every <img> the page showed: whether it reserves its space (width and height attributes), and whether it loads at the right moment for where it sits. It is informational and never scored, but it is usually the cheapest layout-shift and first-screen fix a page has.

Read more
performance

Keep an eye on performance

Performance regressions rarely arrive as one dramatic change: they accumulate one added image, one new tracking pixel, one un-lazy-loaded video at a time, until a site that was fast a quarter ago quietly isn't anymore, with no single commit anyone would think to blame.

Read more
performance

Optimize large images

Oversized images are consistently one of the single biggest and easiest-to-fix causes of slow pages, because a handful of unoptimized photos can outweigh every other resource on the page combined. The fix is rarely "compress more" alone: the dimensions served usually need to match the dimensions actually displayed.

Read more
performance

Reduce page weight

Total page weight is the sum of every byte a browser has to download to render the page: HTML, CSS, JS, images, fonts, and third-party embeds. It is a blunt but powerful metric: heavier pages take longer to become usable on every connection, and the effect is most punishing on mobile networks where bandwidth is limited and variable.

Read more
performance

Reduce render-blocking resources

Render-blocking resources are CSS and JS files the browser must download and process before it can paint anything on screen. A page can have a perfectly reasonable total size and still feel slow if too much of that weight sits in the critical path between the request and the first pixel appearing.

Read more
performance

Reduce the number of requests

Every distinct file the browser fetches (scripts, styles, fonts, icons, tracking pixels) adds a round trip, and even small requests carry latency overhead that adds up fast, especially on connections with high latency like mobile networks. A page can look lean by total size and still feel slow purely because of how many separate requests it makes.

Read more
performance

Reduce third-party request volume

Once a site is served over HTTP/2 or HTTP/3, the number of requests a browser makes stops being the dominant cost, because multiplexing over one connection removed the old per-host connection limit that made bundling and sprite sheets worthwhile. What still matters is how many of those requests go to origins the site owner does not control: every analytics tag, ad pixel, chat widget, and web font from a third-party CDN adds its own DNS lookup, connection, and script-execution cost that no amount of first-party bundling can remove.

Read more
performance

Resize the oversized images already in a modern format

This check fires when every oversized image on the page is already served in a modern format (WebP/AVIF). The format work is done, so the remaining fix is purely about dimensions: an image can be the right format and still be delivered several times larger than the space it's displayed in.

Read more
performance

Speed up page load time

Navigation time (how long it takes from clicking a link to the page becoming usable) is one of the strongest predictors of whether a visitor stays or leaves: studies consistently show abandonment rising sharply past just a couple of seconds. It is also the most common check that requires investigating multiple causes at once rather than one single fix.

Read more

SEO

seo

Canonical link: what it is and how to add it

A canonical link tells search engines which URL is the authoritative version when the same or near-identical content is reachable at multiple URLs, with tracking parameters, trailing slashes, or http vs https, for example. Without it, ranking signals (links, authority) get split across duplicates instead of consolidating on one page.

Read more
seo

Add a favicon

The favicon is a small icon shown in browser tabs, bookmarks, and history, and it is one of the first visual details a returning visitor notices. A missing favicon makes even a polished site look unfinished or, worse, makes it harder to spot among a dozen open tabs.

Read more
seo

Add a meta description

The meta description does not directly affect rankings, but it is frequently used verbatim as the result snippet in search engines, making it one of the highest-leverage pieces of copy on the page for click-through rate. Without one, the search engine auto-generates a snippet from body text, which is often an awkward or irrelevant excerpt.

Read more
seo

Add a page title

The <title> tag is the single most important on-page SEO signal and the text search engines usually display as the clickable headline in results. It also becomes the browser tab label, so a missing title hurts both discoverability and everyday usability.

Read more
seo

Add an hreflang x-default

hreflang links tell search engines which language version of a page to show which visitors. The x-default value names the page for everyone whose language is not listed. Without it, search engines pick one of the listed versions themselves. This check reads the hreflang links in the page head and asks whether one of them is x-default.

Read more
seo

Add an llms.txt file

llms.txt is a proposed standard (llmstxt.org): a Markdown file at the root of a site that gives AI assistants a short summary of the site and links to its most useful pages. It is to AI tools roughly what a sitemap is to search engines, but written for reading rather than crawling. This check asks whether the site serves one; an HTML page or an empty body at /llms.txt does not count. Because the standard is still a proposal, a missing llms.txt is shown as a recommendation but does not lower your score.

Read more
seo

Add an Open Graph image

og:image is the Open Graph property that supplies the preview image a link shows when it is shared on social platforms and in chat apps. The broader seo.open-graph check passes as soon as any og: property exists, so a page can pass that and still share as a plain, imageless card; this check answers specifically whether that preview will carry an image, which is the single biggest driver of click-through on a shared link.

Read more
seo

Add Open Graph tags

Open Graph tags control how a link looks when it is shared on social platforms, in chat apps, and in messaging previews. Without them, most platforms fall back to a generic or broken preview card, which measurably suppresses click-through on shared links. This check passes as soon as any og: property is present and requires no particular one, so it answers "has this page been given Open Graph markup at all", not "will the preview look right". The three that decide how the preview actually renders are og:title, og:description and og:image.

Read more
seo

Add structured data

Structured data is schema.org markup, usually shipped as a JSON-LD script block, that describes the page in a machine-readable form. It is what makes a page eligible for rich results in search: review stars, breadcrumb trails, FAQ accordions, product prices, and similar enhancements that take up more space on the results page and typically earn a higher click-through rate. This check reports whether any JSON-LD is present at all, not whether the schema is valid or the right type for the page.

Read more
seo

Allow the page to be indexed

Three separate things can keep a page out of search, and this check reads all three. A noindex directive (via the robots meta tag or the X-Robots-Tag response header) explicitly tells search engines to exclude the page from results, overriding everything else on the page; content="none" is defined as exactly equivalent to noindex, nofollow, and a directive addressed to a single crawler (name="googlebot", name="bingbot") removes the page from that engine. A Disallow rule in robots.txt works differently: it stops the page being fetched at all, so its content never enters the index. Where directives conflict, search engines apply the most restrictive one. Each is a deliberate tool for pages that should stay out of search (internal tools, thank-you pages), but each is also often left over accidentally from a staging configuration.

Read more
seo

Connect Google Search Console

Read more
seo

Meta description length: the right range

Search engines display roughly 150-160 characters of a meta description before truncating it with an ellipsis, and descriptions under about 50 characters generally waste the space available to sell the click. Staying in that range keeps the full message visible in results.

Read more
seo

Title length: how long should a page title be?

Search engines truncate title tags at roughly 50-60 characters (about 600 pixels) when rendering results, and titles under about 10 characters usually mean a missed opportunity to communicate what the page is about. Staying within a sensible range keeps the title fully visible and meaningful.

Read more
seo

Fix skipped heading levels

Headings form the document outline that both search engines and screen readers rely on to understand a page. That outline only holds together when levels descend one step at a time: an h1 followed directly by an h3, with no h2 between them, is a skipped level that breaks the structure. This check complements seo.single-h1, which counts the top-level heading but says nothing about how the levels beneath it nest.

Read more
seo

Point your canonical, share tags and sitemap at your own domain

A site tells search engines and social networks its own address in several places: the canonical link, the og:url and og:image share tags, the Sitemap line in robots.txt, and the URLs in its sitemap. When those name another host, such as the hosting platform's default address (main.<id>.amplifyapp.com, <project>.vercel.app) or an old domain, search engines are told that host is the real one. This check reads each of them and flags any that leave the site's own domain or its www/apex twin. A share image on an image CDN is fine; one on a platform's default host is not.

Read more
seo

Publish a valid robots.txt

robots.txt is a plain-text file at the root of a site that tells crawlers which paths they may fetch, grouped by User-agent, and usually where the sitemap lives. This check asks whether the site serves a real one, and lists what it blocks: the paths blocked for search crawlers (the Googlebot group if there is one, otherwise User-agent: *) and any crawler shut out of the whole site. It fails when the file is missing, is not a robots.txt at all, or blocks the whole site for search crawlers. Whether this particular page is blocked is reported separately by the Indexable check.

Read more
seo

Redirect your other hosts to your main domain

A site is usually reachable on more than one host: its www and bare forms, and the default address its hosting platform gave it. Each of those should answer with a permanent redirect to the main domain. This check asks the root of the twin, and of any platform host the page itself names, once, without following the answer. A host that serves the page instead of redirecting is a second copy of the site.

Read more
seo

Stop serving your home page twice

Multilingual sites often keep each language under a prefix (/en/, /th/) and still serve the home page at /. When / shows the same English page as /en/ and both call themselves canonical, search engines see two copies of one page. This check reads the page head: an unprefixed page whose hreflang links list a prefixed version in its own language, and whose canonical does not point at that version, is reported. No extra request is made.

Read more
seo

Use exactly one H1 per page

The <h1> is the strongest heading signal on a page for both search engines and assistive technology, and both assume there is exactly one that names the page's primary topic. Zero H1s leaves that signal missing entirely; more than one dilutes it and makes the document outline ambiguous.

Read more

Accessibility

Runtime Errors

errors

Fix console errors

Console errors are the browser telling you something failed while the page was running: a broken API call, a missing dependency, a third-party script that did not load correctly. They are easy to ignore because the page usually still renders something, but they very often correspond to a feature that is silently not working for real users.

Read more
errors

Fix failed network requests

A request that comes back 4xx or 5xx, or that never reaches a server at all, means something the page depends on (an API call, an image, a script, a font) failed to load. Depending on what failed, the visible impact ranges from a missing icon to an entire feature (like checkout or search) silently not working. The two shapes usually have different causes: an error status means the server answered and refused, while a connection that never opened points at a hostname that no longer resolves, a service that is down, or a certificate the browser rejected.

Read more
errors

Fix the hydration mismatch

Frameworks such as Svelte, React and Vue render a page on the server, then "hydrate" it in the browser by attaching their code to that HTML. When the browser renders something different from what the server sent, the framework logs a hydration mismatch warning and patches or rebuilds the page. Visitors see content flash or jump, and the page may not respond until it is done. This check reads those warnings on the site's own pages.

Read more
errors

Fix uncaught JavaScript errors

Uncaught JavaScript exceptions can stop execution partway through a script, silently breaking whatever functionality was supposed to run after the failure point (a form that no longer submits, a menu that no longer opens) without any visible error to the user. They are functionally worse than console errors that come from optional third-party scripts, because they usually indicate a bug in the site's own code.

Read more

Best Practices

best-practices

Viewport meta tag missing: how to add it

A responsive viewport tag is what lets the rest of a site's responsive CSS actually take effect on a phone or tablet. Without it, mobile browsers render the page at a fixed desktop-like width and scale it down, making text unreadably small and forcing visitors to pinch and zoom just to read anything.

Read more
best-practices

Missing doctype: add <!DOCTYPE html>

The <!DOCTYPE html> declaration tells the browser to render the page in standards mode, following the CSS and HTML specifications consistently. Without it (or with an outdated doctype), browsers fall back to "quirks mode", which reproduces decades-old rendering inconsistencies that can make layout and CSS behave unpredictably across browsers.

Read more
best-practices

Add uptime monitoring & alerting

Uptime and error monitoring is what tells a team a site is down or broken before customers have to tell them. Without it, outages and error spikes are typically discovered through complaints, support tickets, or lost revenue, all of which take far longer than an automated alert and cost more trust in the process.

Read more
best-practices

Character encoding was not declared: the fix

Declaring <meta charset="utf-8"> tells the browser exactly how to interpret the bytes of the page before it starts parsing content. Without an early, correct charset declaration, browsers have to guess, and a wrong guess turns accented characters, quotation marks, or symbols into visibly garbled text (mojibake).

Read more
best-practices

Establish a regular release cadence

Deploy frequency is a strong proxy for how healthy a team's delivery process is. Shipping less than roughly once a month means fixes and improvements queue up into larger, riskier batches, and it becomes much harder to isolate which change caused a problem when something does break.

Read more
best-practices

Fix HTTPS on your www/apex twin

People type a domain both with and without www. If the form you do not use has no valid certificate, their browser shows a security warning instead of your site. This check asks the www/apex twin of the scanned host for its root over HTTPS, once, and reports a failed TLS handshake or a rejected certificate. A twin that does not exist at all is not reported.

Read more
best-practices

Redirect HTTP to HTTPS: Nginx, Apache and more

Even after HTTPS is available, visitors and search engines who land on the plain-HTTP URL (from an old link, a bookmark, or a direct type-in) will stay there unless the server actively redirects them. Without that redirect, the insecure version keeps getting used indefinitely, and SEO authority can end up split between the HTTP and HTTPS versions of the same content.

Read more
best-practices

Run your tests automatically on every change (CI)

Read more
best-practices

Set up a staging environment

A staging environment is a production-like place to verify a change before it reaches real users. Without one, every deploy is tested for the first time in production, which turns routine changes into higher-risk events and makes it much harder to catch integration bugs before customers do.

Read more

Ready to see where you stand?

Scan your site and get your Engineering Score with a prioritized roadmap in under a minute, no signup required.

Scan your site

Session expired

Your session has expired for security. Please log in again to continue.