← Oleksii Turovskyi

The Invisible Tags Shaping Your AI Citations

· 11 min read · Updated

The search results page of 2026 is no longer "ten blue links." It's a citation card in Perplexity, an inline link in ChatGPT with browsing, a source list in Google AI Overviews. And when an AI decides whether to surface your brand among the sources, it doesn't just read the article body. It reads metadata.

A title without an og:image. A description that got truncated mid-sentence. Two URLs pointing at the same content. Each of these mistakes costs you a citation — and the AI traffic that's currently being redistributed in favor of teams that bothered to clean up their <head>.

Let's break down which tags actually matter, why, and how to assemble them into a single metadata bundle.

How do LLM engines build a citation card?

Perplexity, ChatGPT with search, Claude with the web tool, Google AI Overviews — they all work as RAG systems. First retrieval (search), then synthesis, then a rendered answer with links. According to architectural notes on Perplexity, its orchestration engine embeds citation markers, source metadata (URL, publication date), and ranked fragments directly into a structured prompt before generation. Citations are not added post-hoc; they are architecturally bound to specific documents at the context-assembly stage.

Note where that publication date comes from. These four tags describe the page; datePublished and author in your JSON-LD describe the document, and a card assembled from one while the other says something else is a page arguing with itself.

When the model assembles the source card itself, it pulls three things: the page's <title>, the text from meta description (often reused as a snippet), and og:image for the preview.

An earlier version of this article credited that idea to Analyze AI's study of 83,670 citations. That was a misattribution and I have removed it. The study is real and worth reading, but it measures which domains and content types get cited — Wikipedia is 12.1% of ChatGPT's citations and 0.1% of Claude's; about 83% of citations point somewhere other than the brand's own site — and it does not examine metadata at all. Nothing in it supports a claim about og:title or og:description.

What is left, once the borrowed authority is stripped out, is a mechanism you can check yourself: whatever the card shows, it read from your tags. If those tags disagree with the visible page, the card misrepresents you. That is an argument for keeping them accurate, not evidence that accurate tags win citations.

The conclusion is simple. If you don't have og:image, your source in Perplexity will look like a grey rectangle with a favicon. Next to a competitor with a branded preview. Guess what the user clicks.

How long should a title and description be?

  • <title> — 10–60 characters

    The most visible element of the card. Too short (under 10 characters) is too sparse for semantic query matching. Too long (over 60) gets truncated in SERPs, in Slack previews, in Perplexity's citation card.

    Working formula: Primary intent | Brand context. Example: Metadata Bundle for AI Citations | DocsHub. Forty-three characters. Fits everywhere.

  • <meta name="description"> — 120–165 characters

    This tag doesn't directly affect Google rankings. But it's the primary candidate for the snippet an LLM returns in your card. When an AI system decides whether to cite your page, meta tags often shape first impressions, and a well-phrased meta description can be the deciding factor — your source gets quoted or skipped.

    The floor matters as much as the ceiling. Come in under about 120 characters and Google tends to pad the snippet out of your body text, which throws away the sentence you wrote on purpose. Go past 165 and the SERP truncates you mid-clause. This site enforces exactly that window at build time — see src/lib/snippet-limits.ts — and the same numbers are what AEO Checker flags on the page you are looking at.

  • og:image — 1200×630, absolute HTTPS, under 5 MB

    The universal size that works on nine of ten platforms — 1200×630 pixels. Less than 600×315 and the engine either ignores the image or shows a thumbnail instead of a full card. Keep key details inside the central 80% of the canvas. Format: JPG for photos, PNG for graphics with text.

    The most common mistake: a relative URL in og:image. Crawler bots cannot resolve it relative to your domain. Always write https://example.com/og/article.png, not /og/article.png.

What does canonical actually protect against?

Here's where SEO careers go to die and AI citations get lost.

LLM engines aggressively deduplicate content. They see the same article on ?utm_source=newsletter, on /blog/article/, on the m.example.com mirror — and have to decide which version is canonical. If you didn't tell them explicitly, they decide for you. For example, ChatGPT uses Bing's index for most real-time queries, and the URL Bing considers canonical is the one that ends up in ChatGPT's citation — regardless of what your <link rel="canonical"> says, if Bing already indexed a parameterized variant.

This is foundational. The canonical tag is not cosmetics. It's a signal-consolidation tool.

When the canonical tag points to one URL, the sitemap lists another, and internal links lead to a third — you're sending three contradictory signals. AI systems may split the citation potential between the three versions. You need three-way alignment: canonical = sitemap entry = internal links. One URL. One source of truth.

Separately, syndication. If an external portal republishes your article with a cross-domain canonical pointing back to your domain — that's still not a guarantee. From the model's perspective, the partner's domain may have higher brand authority, and the citation decision can still go to the partner. Defense is: structured author data, an explicit author attribution in the article body itself, and consistent entity signals.

The full metadata bundle — production code

Two implementations below. The first for Next.js 16 (App Router). The second is plain HTML as a fallback or reference for static sites.

Next.js App Router — server-rendered via generateMetadata

app/blog/[slug]/page.tsx
import type { Metadata } from "next";
 
interface PageProps {
  params: Promise<{ slug: string }>;
}
 
interface Article {
  title: string;
  excerpt: string;
  contentHtml: string;
  ogImageUrl: string;
  publishedAt: string;
  modifiedAt: string;
  authorName: string;
  slug: string;
}
 
const SITE_URL = "https://example.com";
 
async function fetchArticle(slug: string): Promise<Article> {
  const response = await fetch(`${SITE_URL}/api/articles/${slug}`, {
    next: { revalidate: 3600 },
  });
  if (!response.ok) {
    throw new Error(`Article not found: ${slug}`);
  }
  return response.json();
}
 
export async function generateMetadata(
  { params }: PageProps
): Promise<Metadata> {
  const { slug } = await params;
  const article = await fetchArticle(slug);
 
  const canonicalUrl = `${SITE_URL}/blog/${article.slug}`;
 
  return {
    title: article.title.slice(0, 60),
    description: article.excerpt.slice(0, 160),
    metadataBase: new URL(SITE_URL),
    alternates: {
      canonical: canonicalUrl,
    },
    openGraph: {
      type: "article",
      url: canonicalUrl,
      title: article.title.slice(0, 60),
      description: article.excerpt.slice(0, 160),
      siteName: "DocsHub",
      locale: "en_US",
      publishedTime: article.publishedAt,
      modifiedTime: article.modifiedAt,
      authors: [article.authorName],
      images: [
        {
          url: article.ogImageUrl,
          width: 1200,
          height: 630,
          alt: article.title,
          type: "image/png",
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
      title: article.title.slice(0, 60),
      description: article.excerpt.slice(0, 160),
      images: [article.ogImageUrl],
    },
    robots: {
      index: true,
      follow: true,
      googleBot: {
        index: true,
        follow: true,
        "max-image-preview": "large",
        "max-snippet": -1,
      },
    },
    other: {
      "article:author": article.authorName,
      "article:published_time": article.publishedAt,
      "article:modified_time": article.modifiedAt,
    },
  };
}
 
export default async function ArticlePage({ params }: PageProps) {
  const { slug } = await params;
  const article = await fetchArticle(slug);
 
  return (
    <article className="max-w-3xl mx-auto py-10 px-4">
      <header className="mb-8 border-b pb-4">
        <h1 className="text-4xl font-bold text-gray-900">{article.title}</h1>
        <div className="mt-4 flex items-center text-sm text-gray-500">
          <time dateTime={article.publishedAt}>
            {new Date(article.publishedAt).toLocaleDateString("en-US")}
          </time>
          <span className="mx-2">•</span>
          <span>{article.authorName}</span>
        </div>
      </header>
      <div
        className="prose prose-blue lg:prose-lg"
        dangerouslySetInnerHTML={{ __html: article.contentHtml }}
      />
    </article>
  );
}

Plain HTML — reference implementation for static sites

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 
    <title>Metadata Bundle for AI Citations | DocsHub</title>
    <meta
      name="description"
      content="How to assemble title, meta description, og:image, and canonical so your pages land on the citation cards of Perplexity and ChatGPT."
    />
 
    <link rel="canonical" href="https://example.com/blog/metadata-bundle" />
 
    <meta property="og:type" content="article" />
    <meta property="og:url" content="https://example.com/blog/metadata-bundle" />
    <meta property="og:title" content="Metadata Bundle for AI Citations" />
    <meta
      property="og:description"
      content="Title, meta description, og:image, and canonical — how to configure tags for Perplexity and ChatGPT."
    />
    <meta property="og:site_name" content="DocsHub" />
    <meta property="og:locale" content="en_US" />
    <meta property="og:image" content="https://example.com/og/metadata-bundle.png" />
    <meta property="og:image:width" content="1200" />
    <meta property="og:image:height" content="630" />
    <meta property="og:image:type" content="image/png" />
    <meta property="og:image:alt" content="Metadata bundle diagram for AI citations" />
 
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="Metadata Bundle for AI Citations" />
    <meta
      name="twitter:description"
      content="Title, meta description, og:image, and canonical — how to configure tags for Perplexity and ChatGPT."
    />
    <meta name="twitter:image" content="https://example.com/og/metadata-bundle.png" />
 
    <meta property="article:published_time" content="2026-05-01T09:00:00+00:00" />
    <meta property="article:modified_time" content="2026-05-01T09:00:00+00:00" />
    <meta property="article:author" content="Olena Kovalenko" />
 
    <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1" />
  </head>
  <body>
    <article>
      <h1>Metadata Bundle for AI Citations</h1>
      <p>
        Properly configured metadata ensures that AI can correctly identify
        and parse your source.
      </p>
    </article>
  </body>
</html>

What should you check before deploying?

Check Tool Red flag
canonical = sitemap = internal links Screaming Frog (impersonating GPTBot) Three different URLs for the same page.
og:image returns 200 OK, absolute HTTPS curl + browser debugger Redirect chain or 403 for the bot.
<title> ≤ 60, description 120–165 SERP simulator Truncation in Slack/Discord previews, or a snippet Google padded from body text.
Edge renders don't strip <head> Logs + emulate user-agent Edge workers that serve a simplified HTML to AI crawlers may inadvertently strip canonical tags during rendering. If the HTML GPTBot receives doesn't contain your canonical tag, it processes the content with no canonicalization signal.
AI bots see the same version humans do Logs of GPTBot, OAI-SearchBot, ClaudeBot Cloaking or JS-only render.

Closing

Title, description, image, canonical. Four tags, and a lot of AI visibility breaks on them — I am not going to put a percentage on that, because I have no way to measure it and neither does anyone quoting one at you. What I can show you is why the failure is so ordinary: between "the tag exists technically" and "the model sees the same canonical the human does" lies a gulf filled with parameterized URLs, edge CDN behavior, JS rendering, and syndication. Every one of those is invisible from the template you wrote.

Four tags is also the floor, not the finish. Once the card is right, the next thing a retriever wants is a typed object rather than prose behind it.

Fix this bundle. It's the cheapest AEO optimization with the best ROI.

FAQ

How long should a meta description be?

Between roughly 120 and 165 characters. Under 120 and the engine pads the snippet with body text it picked itself, which wastes the copy you wrote; over 165 and the desktop SERP truncates mid-sentence. This site enforces the window in a Zod schema at build time, because it is exactly the kind of rule that decays into "we usually remember".

Does the canonical tag matter for AI crawlers?

Less than for Google, and that asymmetry is the trap. Google consolidates signals onto the canonical URL; several AI crawlers just read whatever URL they fetched. So a parameterised or syndicated copy that Google correctly folds away can still be the version a model quotes — with the wrong URL attached.

What happens if og:title and the page title disagree?

You get two different citation cards for one page depending on who renders it, and neither is wrong enough to fix itself. Social previews use Open Graph, search uses the title element, and AI answers use whichever they parsed. If they must differ, make it deliberate; if they differ by accident, it is drift.

Do I need Twitter card tags separately from Open Graph?

Only for the cases Open Graph cannot express, such as summary_large_image vs summary. X falls back to Open Graph for title, description and image, so a full duplicate set is maintenance for no gain — declare the card type and let the rest inherit.

How do I see all four tags at once?

The AEO Checker extension reads title, description, canonical and the robots directives from the current tab and reports each with a pass, warn or fail, which is faster than reading <head> by eye and catches the header-level canonical that View Source misses.

Sources

Part of the Answer Engine Optimization cluster — the full reading order in dependency sequence, plus a glossary of every term used across these articles.


Verify your project's metadata right now. If your extension showed red status on basic tags — don't put it off. Follow me on LinkedIn for more practical breakdowns of AEO algorithms. Need help with a corporate portal audit or Next.js architecture optimization? Get in touch for deep technical consulting.

Get new posts

One article every couple of weeks, on AI search and the code behind it. Confirm by email; unsubscribe in one click.