Updated

Keyword monitoring checks that a specific string is still present in a page or API response on every run, instead of trusting the HTTP status code. It catches the failures that uptime checks sleep through: a deploy that renders an empty template, a CMS entry someone unpublished, a checkout page serving "Something went wrong" with a perfectly healthy 200.

In Hyperping, the simple version is a text body assertion on an HTTP monitor and takes about ten seconds to set up. The harder version, content that only exists after JavaScript runs or after a login, needs a browser check running Playwright. This post covers both and where the line between them sits.

Key takeaways

  • A keyword check matches text against the raw HTTP response body. No JavaScript runs, so anything rendered client-side is invisible to it.
  • Text body assertions are on every Hyperping plan including Free, and run at the monitor's normal interval from 18 regions.
  • Browser checks run a Playwright script in headless Chromium every 5 minutes. Essentials includes 3, Pro 10, Business 25.
  • curl -s https://yoursite.com | grep -c "your keyword" in 5 seconds tells you which of the two you need. Zero matches on a page where the text is clearly visible means the content is client-side.
  • Bad keyword choice is the top cause of false alarms. Prices, timestamps and A/B tested headlines change without anything being broken.

Which check do you need?

Keyword check Browser check
What it inspects Raw HTTP response body Fully rendered DOM in Chromium
JavaScript Not executed Executed
Login and multi-step flows No Yes
Interval Monitor interval, down to 30s 5 minutes
Setup One text field A Playwright script
Debug artifacts Response body in logs Screenshot, video, trace
Cost Included on every plan, Free too 3 to 25 checks depending on plan

The rule I use: if curl can see the text, use the keyword check. Server-rendered marketing pages, Next.js and Nuxt pages with SSR, WordPress, Shopify storefronts, and every JSON API fall in this bucket. If curl cannot see it, or the content lives behind a login, a search box, or three clicks, it needs a browser.

Setting up a keyword check in Hyperping

The check lives inside an HTTP monitor, not as a separate monitor type.

  1. Create an HTTP monitor or edit an existing one.
  2. Open the advanced HTTP settings.
  3. In Text body assertion, enter the text the response body must include.
  4. Save.

From then on, every check fetches the page and verifies the string is in the body. When it is missing, the monitor is treated exactly like a failed status code: Hyperping double-checks from your other selected regions to rule out a regional glitch, then opens an outage and fires your notification channels. Full field reference is in the keyword monitoring docs.

Two details that matter in practice. The match is exact, so casing and punctuation have to line up with what the server actually sent. And it reads the body of the final response, so a page behind a redirect chain is fine, but a page behind a cookie consent wall may return the wall instead of the page.

Choosing text that will not page you at 3am

The failure mode of keyword monitoring is not missing real incidents. It is waking someone up because the marketing team rewrote a headline.

Good candidates:

  • A product or brand name in the hero that has been there for a year.
  • A fixed line in the footer, such as the company legal name.
  • "status":"ok" or another structural field in a JSON response.
  • A specific SKU or product title on an ecommerce category page.

Bad candidates:

  • Prices and stock counts. They move on their own.
  • Timestamps, "last updated" strings, and personalized greetings.
  • Any copy that is currently in an A/B test, since half the checks will see the variant.
  • Text that is also present on your error page. If your 500 page includes the site name in the header, checking for the site name proves nothing.

That last one is the trap worth repeating. Ask what the broken version of this page looks like, then pick text that exists in the healthy version and not in the broken one.

What it catches that a status code does not

I have hit all four of these on real projects:

  • A deploy shipped with a broken template. The route returned 200 with a valid HTML skeleton and an empty <main>.
  • A CMS editor unpublished a pricing block. Page loaded, pricing table gone, sales did not notice for a day and a half.
  • A third-party product feed timed out and the storefront rendered its fallback: the category page with zero items, at 200.
  • An API gateway returned {"data":[]} instead of 502 when the upstream was down, which the API content validation approach covers in more depth.

None of those move a status code. All of them fail a keyword check on the next run.

Where the keyword check stops working

Three situations force you into a real browser.

The content is rendered client-side. A React or Vue SPA ships an HTML shell and fills it in after hydration. Your keyword lives in a JSON payload or a JS bundle, so the string match either fails permanently or matches something irrelevant. Test it with curl -s https://yoursite.com | grep "keyword" before you assume it works.

The content is behind authentication. Dashboard content, order history, anything per-account. An HTTP monitor with a bearer token can reach an API, but not a session-based web app with a login form.

The content depends on interaction. Search results, a filtered product list, the cart page after adding an item. There is no URL you can curl that proves the search index is returning results.

There is also a subtler case: you want to check that text is absent. Hyperping's text body assertion fails when the expected text is missing, which is the right default for "my best seller must be visible". For the inverse, alerting when "Application Error" or "We'll be right back" shows up, write the assertion in a browser check.

Keyword assertions in a browser check

Browser checks run a Playwright script in headless Chromium on the 2026.05 runtime (Node 24, Playwright 1.59.1). The keyword assertion itself is one line:

await expect(page.getByText('KEYWORD')).toBeVisible()

A complete check for an ecommerce storefront that must always show its best seller:

import { test, expect } from '@playwright/test';

test('best seller is visible on the storefront', async ({ page }) => {
  await page.goto('https://shop.acme.com/');
  await expect(page.getByText('The Detonation Factor')).toBeVisible();
  await expect(page.getByRole('button', { name: 'Add to cart' }).first()).toBeVisible();
});

getByText matches any element containing that text, of any type, case-insensitively, and matches substrings. A button labeled "click here for KEYWORD" satisfies it. There is no waitFor because Playwright's web-first assertions retry until the element appears or the timeout fires, which is both faster and less flaky than a fixed sleep.

Exact matching

To require the element's full text to match, including case:

await expect(page.getByText('Sign up', { exact: true })).toBeVisible()

When the keyword appears many times

Assert on a string like a price that repeats down a listing page and the run fails with:

expect.toBeVisible: Error: strict mode violation: getByText('9.95') resolved to 30 elements

Playwright is refusing to guess which of the 30 you meant. If some were visible and some were not, there is no single right answer. Take the first match when you only care that the text exists somewhere:

await expect(page.getByText('9.95').first()).toBeVisible()

I add .first() by default for content assertions. The exception is single-instance UI: a promo banner, a cookie bar, a primary CTA. If your banner renders twice, something has gone wrong in a way you want to hear about, so leave strict mode on for those and let the duplicate fail the check.

Matching several possible strings

Marketing rewrites CTAs. If "Sign up", "Create an account" and "Register" are all acceptable, a regex covers them:

await expect(page.getByText(/Sign up|Create an account|Register/i).first()).toBeVisible()

Playwright regexes are case-sensitive unless you pass the i flag, and CSS text-transform: uppercase does not change the underlying text, so the DOM may hold "Sign up" while the screenshot shows "SIGN UP". The .or() locator does the same job when the alternatives are different kinds of element:

await expect(
  page.getByRole('link', { name: 'Sign up' })
    .or(page.getByRole('button', { name: 'Get started' }))
    .first()
).toBeVisible()

Asserting that text is gone

await expect(page.getByText('Application Error')).toHaveCount(0);
await expect(page.getByText(/undefined|NaN|\[object Object\]/)).toHaveCount(0);

The second line is the one I would add to any check on a page with a JS-heavy frontend. undefined rendering into a heading is a real bug that ships and that nothing else in your monitoring will notice.

Checking content users actually reach

The reason to spend a browser check on a keyword is rarely the homepage. It is the content two steps in:

import { test, expect } from '@playwright/test';

test('search returns results', async ({ page }) => {
  await test.step('open the shop', async () => {
    await page.goto('https://shop.acme.com/');
  });

  await test.step('search for a known product', async () => {
    await page.getByRole('searchbox').fill('detonation');
    await page.keyboard.press('Enter');
  });

  await test.step('results contain the product', async () => {
    await expect(page.getByText('The Detonation Factor').first()).toBeVisible();
    await expect(page.getByText('No results found')).toHaveCount(0);
  });
});

Each test.step shows up as its own row with a duration in the run view, so a failure points at the stage that broke instead of dumping a log wall on whoever is on call. Failed runs also attach a screenshot, a video and a Playwright trace you can replay at trace.playwright.dev. Credentials go in the monitor's environment variables and are read with process.env, never in the script body. The browser checks docs cover runtimes, timeouts and the bundled packages, and there are twelve ready-made scripts in the browser check templates.

How to use Hyperping for keyword monitoring

The setup I would run on a content-driven site, cheapest check first.

1. Put a text body assertion on every important server-rendered page

Homepage, pricing, the top three category pages, the login screen. One string each, chosen with the rules above. These cost nothing beyond the monitors you already have, run as often as your plan allows (30 seconds from Essentials up, 5 minutes on Free), and rotate through the 18 regions you selected, with a double check from a second region before an outage opens.

2. Confirm the keyword is really in the HTML

curl -s https://yoursite.com/pricing | grep -c "Starting at"

A count of zero on text you can see in the browser is the signal to move that page to a browser check. Do this once per page when you set the monitor up and you will avoid a monitor that has been silently passing on the wrong thing.

3. Spend browser checks on the JavaScript and logged-in paths

Essentials gives you 3 browser checks, Pro 10, Business 25, all at a 5 minute interval, so treat them as a scarce budget. Mine go to: search results, the logged-in dashboard rendering real data, and the checkout step where the payment form actually mounts. Each one asserts on visible content, not on a URL. The synthetic monitoring page shows how these run alongside uptime checks.

4. Route missing content to a human

A failed keyword check opens an outage like any other failure, so bind an escalation policy to the monitor and let your on-call schedule decide who hears about it. Content failures are worth paging for precisely because nobody else will find them: there is no 500 in the logs and no spike in the error tracker.

5. Publish when customers are affected

If the missing content is user visible, connect the monitor to a status page so the incident is communicated without anyone writing a manual update.

Where to start

Add a text body assertion to your highest-traffic page today, pick a string that only exists when that page is genuinely working, and run the curl | grep check once to prove the assertion is testing what you think it is. That covers most of the silent failures for zero extra cost.

Then look at the pages where curl comes back empty, or where the content only exists after a login. Those are the ones worth a browser check. If you are new to Playwright, the Learn Playwright guides cover locators and assertions from scratch, and 10 end-to-end Playwright test ideas has more flows worth monitoring once the simple checks are in place.