Task: Configure Open Graph / meta tags for correct page previews in messengers

Configure Open Graph / meta tags for correct page previews in messengers

Explicitly define preview metadata (title, description, image) for website pages so that messengers and social networks correctly generate link cards.

What needs to be done

Add explicit metadata to website pages for generating link previews in messengers and social networks. First of all, Open Graph (og:title, og:description, og:image, og:url, og:type), and Twitter Cards if necessary.

Currently, the preview image is not explicitly set. Therefore, Telegram, WhatsApp, VK, Facebook, and other services try to figure out themselves which title, description, and image to use when a link is sent. They might take <title>, meta description, the first suitable image from the page, a large picture from the content, or even keep a previously found option. As a result, the preview may look unpredictable and differ from what we want to show the user.

Why this matters

A link preview is essentially a mini-snippet of the page inside the messenger. It affects how the user perceives the link before clicking: whether they see a clear title, a relevant description, and a normal product/category image instead of a random picture.

This is not a direct SEO ranking factor, but it relates to proper page markup and affects CTR, brand awareness, and the quality of link sharing. For search engines and external services, it is also better when key page metadata is defined explicitly rather than determined heuristically.

What should be in the markup

For each indexed page, it is advisable to generate at least:

<meta property="og:title" content="Page Title">
<meta property="og:description" content="Short page description">
<meta property="og:image" content="https://happybaby2000.ru/path/to/image.jpg">
<meta property="og:url" content="https://happybaby2000.ru/current-page/">
<meta property="og:type" content="website">

For product cards, you can use og:type=product if it matches the current implementation and does not cause support issues.

og:image must contain an absolute publicly accessible image URL. For products, it is desirable to use the main product photo; for categories and information pages, a predefined relevant image or a general fallback.

Also, make sure that the image is available without authorization, is not blocked by robots/firewall/CDN, and is correctly served to external bots.

Image Selection Logic

A clear priority needs to be defined:

  1. Product card β€” main product image.
  2. Category β€” category image, if specified.
  3. Information page β€” separate page image, if specified.
  4. If no suitable image exists β€” use the site's general fallback.

It is important to explicitly output og:image rather than relying on the messenger to find the "correct" picture in the HTML on its own.

How Messengers Generate Previews

When a user sends a link for the first time, the messenger's server usually accesses the page itself, reads the HTML, and saves the found metadata. That is, the preview is often generated not on the user's phone, but on the side of Telegram/WhatsApp/VK/another service.

If Open Graph markup is present, the service usually relies on it first. If it is missing or some fields are missing, the service uses its own rules: it may take a regular <title>, description, the first large image, an image from the content, etc. These rules differ across services, so without explicit markup, the result cannot be considered stable.

Separately on Preview Caching

It must be taken into account that messengers cache the URL parsing result. For example, if the link https://happybaby2000.ru/catalog/example/ was already sent today and the messenger saved the old image for it, then after replacing the image on the site, this same URL may continue to show the old preview for some time.

The reason is that the messenger is not required to re-download the page and image every time a link is sent. It sees an already familiar URL and uses the saved result from its cache. The lifetime of such a cache depends on the specific service and is usually not controlled by us.

Therefore, after changing og:image, you cannot check the result simply by re-sending the same URL and concluding that the markup is not working.

How to Test a New Preview Bypassing the Old Cache

To check, you can add an arbitrary GET parameter to the URL, for example:

https://happybaby2000.ru/catalog/example/?preview=2

or:

https://happybaby2000.ru/catalog/example/?v=20260916

For the browser and the site, this is still the same page if these parameters are not used in the page logic. But for the messenger, this is a different URL that is not yet in its cache. Therefore, it is much more likely to request the page again and generate a new preview based on the current Open Graph tags.

This does not "clear" the messenger's old cache. It's just that a new URL with a different query string creates a separate cache entry and allows you to test the actual markup without waiting for the old cache to expire.

During testing, it is advisable to change the parameter every time you need to guaranteed test a new option, for example ?preview=1, then ?preview=2.

Definition of Done

  • Open Graph tags are explicitly output on main page types.
  • og:title, og:description, and og:image correspond to the content of the specific page.
  • og:image uses an absolute URL.
  • A fallback is provided for pages without their own image.
  • It is verified that external bots can retrieve the image.
  • Preview generation in popular messengers has been manually verified on several page types.
  • The cache was taken into account when checking changes: the new option is additionally tested via a URL with a unique GET parameter.

Π’ΠΎΡ€ΠΊΠ»ΠΎΠ³ΠΈ

Enhanced og:image validation following Google errors

Google reported an "Invalid URL in image field" error for some product cards. The suspected cause is that the original image paths contained unencoded Cyrillic characters and spaces, which the previous validation did not catch.

The script agent/scripts/check-page-image/index.ts in the happybaby2000.ru project has been updated:

  • the original og:image value is checked prior to any normalization;
  • the URL must be an absolute HTTP(S) address with a host;
  • unencoded spaces, Cyrillic characters, control characters, and other invalid characters are detected;
  • the correctness of %-escape sequences is verified;
  • unencoded square brackets in the path/query/fragment and duplicate # characters in the fragment are detected, while IPv6 addresses in square brackets are permitted;
  • all og:image tags on the page are checked, not just the first one;
  • diagnostics indicate the image number, original URL, and the reason for the error; for invalid characters, the Unicode code and position are also output;
  • on errors, the script returns exit code 1, and on success β€” 0.

A regression test agent/scripts/check-page-image/index.test.ts using Vitest has been added alongside it. The Vitest configuration has been extended to cover scripts/**/*.test.ts. The test suite covers 28 scenarios, including valid and invalid URLs, encoded characters, IPv6, missing images, multiple og:image tags, and diagnostic precision.

The check runs successfully: 28 tests passed, ESLint, Prettier, and git diff --check β€” no errors.

Test command:

npm run test -- scripts/check-page-image/index.test.ts

Validation limitation: it validates the syntax of the URL from og:image, but does not check the availability of the image itself and does not guarantee that the URL will be accepted by Google services. The script does not perform automatic URL correction.

Progress: First an Executable Check, Then Implementation

Before making any changes, a separate terminal TypeScript script was written first. It makes an HTTP request to the page, parses the HTML via cheerio, and checks for required Open Graph tags. This provided an objective baseline before making edits and a ready-made acceptance criterion for the AI agent.

Verification Script

import * as cheerio from "cheerio";

const REQUIRED_TAGS = [
  "og:title",
  "og:description",
  "og:image",
  "og:url",
  "og:type",
] as const;

function isAbsoluteHttpUrl(value: string): boolean {
  try {
    const url = new URL(value);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

async function main() {
  const targetUrl = process.argv[2];

  if (!targetUrl) {
    console.error("Usage: npx tsx check-open-graph.ts <url>");
    process.exit(2);
  }

  const response = await fetch(targetUrl, {
    redirect: "follow",
    headers: {
      "User-Agent": "OpenGraphChecker/1.0",
    },
  });

  if (!response.ok) {
    console.error(`HTTP ${response.status} ${response.statusText}`);
    process.exit(1);
  }

  const html = await response.text();
  const $ = cheerio.load(html);

  let hasErrors = false;

  console.log(`HTTP: ${response.status}\n`);

  const values = Object.fromEntries(
    REQUIRED_TAGS.map((property) => {
      const value =
        $(`meta[property="${property}"]`).first().attr("content")?.trim() ?? "";

      if (!value) {
        console.log(`βœ— ${property}: missing`);
        hasErrors = true;
      } else {
        console.log(`βœ“ ${property}: ${value}`);
      }

      return [property, value];
    }),
  );

  const image = values["og:image"];
  const ogUrl = values["og:url"];

  if (image && !isAbsoluteHttpUrl(image)) {
    console.log(`βœ— og:image is not absolute: ${image}`);
    hasErrors = true;
  }

  if (ogUrl && !isAbsoluteHttpUrl(ogUrl)) {
    console.log(`βœ— og:url is not absolute: ${ogUrl}`);
    hasErrors = true;
  }

  console.log(
    hasErrors
      ? "\nRESULT: invalid or incomplete"
      : "\nRESULT: OK",
  );

  process.exit(hasErrors ? 1 : 0);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

State Before Implementation

The first run yielded the expected negative result:

HTTP: 200

βœ— og:title: missing
βœ— og:description: missing
βœ— og:image: missing
βœ— og:url: missing
βœ— og:type: missing

RESULT: invalid or incomplete

After that, the task was passed to the AI agent not as an abstract formulation like "add Open Graph", but along with a specific verification script and a reproducible way to check the result.

Agent Report

The agent reported the following changes:

Done. Changes:

index.tsx β€” added props image and ogType, rendering og:title, og:description, og:image, og:url, og:type
index.tsx β€” computed imageUrl and passed it to SeoHeaders with ogType="product"
You can check with the script:

npx tsx scripts/check-page-image/index.ts "http://localhost:3000/catalog/product.html"

Re-Verification

After the changes, the same verification approach yielded:

HTTP: 200

βœ“ og:title: Small-sized Playground Korabl
βœ“ og:description: Size 3.48 x 1.78 x 2.32 m.
βœ“ og:image: http://localhost:3000/images/resized/middle/images/img.jpg
βœ“ og:url: http://localhost:3000/catalog/product.html
βœ“ og:type: product

RESULT: OK

Practical Conclusion

The approach proved useful: first, a small executable check is created that captures the current state and readiness criteria, and then this same check is passed to the executor along with the task. This reduces ambiguity in the task assignment and makes the result verifiable not by the agent's report, but by the actual behavior of the system.

The next step is to strengthen this pattern: if the agent is given a verification script, the assignment should immediately require not only making changes, but also independently running this script after implementation and attaching the actual result. Then the cycle becomes closed-loop: reproduce β†’ implement β†’ verify, and the human is left with control verification rather than primary verification.