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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Add X-Content-Type-Options header
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.
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.
Enable HSTS
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.
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.
Fix mixed content
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.
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.
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.
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.
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.
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.
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.
Set up dependency vulnerability scanning
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.
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.
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.
Add real-user performance monitoring
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.
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.
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.
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.
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.
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.
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.
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.
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.
SEO
Add a canonical link
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.
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.
Add a language attribute
A lang attribute on the <html> tag declares the page's primary language to both search engines and browsers. Search engines use it to match pages to the right locale of searcher, and it also drives how screen readers pronounce the content, the same signal accessibility relies on.
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.
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.
Add a viewport meta tag
The viewport meta tag tells mobile browsers how to scale the page instead of rendering it at desktop width and shrinking everything down. It is a prerequisite for any responsive CSS to actually take effect on a phone, and Google has used mobile-friendliness as a ranking factor for years.
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.
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.
Connect Google Search Console
Fix meta description length
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.
Fix page title length
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.
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.
Accessibility
Add a language attribute
Screen readers use the lang attribute on <html> to choose which pronunciation rules and voice to use. Without it, or with an incorrect value, a screen reader may read English content with a different language's pronunciation rules, making the page difficult or impossible to understand for blind and low-vision visitors.
Add accessibility testing to your process
Add alt text to images
Alt text is how screen reader users perceive the content of an image: without it, an image is either skipped silently or announced only by its filename, which is rarely meaningful. It is also read by search engine image crawlers, and in many jurisdictions accessible images are a legal requirement (e.g. under the ADA or EN 301 549), not just a best practice.
Keep accessibility standards up
Accessibility is easy to regress a component at a time: a new button built without a screen-reader pass, an image added without alt text, a color chosen without checking contrast; each one small, each one a real barrier for someone relying on assistive technology, and none of them break the build.
Runtime 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.
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.
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.
Best Practices
Add a responsive viewport tag
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.
Add an HTML5 doctype
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.
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.
Declare a character encoding
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).
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.
Redirect HTTP to HTTPS
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.
Run your tests automatically on every change (CI)
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.
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.
EngineeringScore