GEO HTML/Markdown Structural Analyzer

Data Engineering & Processing intermediate Building with AI ~18 min read

Overview

Generative Engine Optimization (GEO) is the practice of structuring web content so that AI-powered search engines and language models can effectively parse, understand, and cite it. A GEO analysis tool implements structural analyzers that dissect HTML and Markdown documents to evaluate how well they conform to GEO best practices—assessing heading hierarchy, FAQ patterns, structured data, content density, and overall readability.

The analyzers are built on Cheerio, a fast, server-side jQuery-like library for parsing and traversing HTML. For Markdown input, the content is first rendered to HTML via a CommonMark parser, then passed through the same Cheerio-based analysis pipeline. This unified approach means the scoring logic is written once and works for both content formats.

Each analyzer produces a structured report with severity-scored issues (error, warning, info), along with aggregate statistics: heading counts by level, FAQ pattern matches, structured data block counts, list and link counts, image counts, and word counts. These reports feed into a GEO scoring system that produces an overall optimization score and prioritized recommendations.

Core Concepts

Heading Hierarchy Analysis

Well-structured content uses a logical heading hierarchy: a single <h1>, followed by <h2> sections, with <h3> subsections beneath them. The analyzer checks for common violations: multiple <h1> tags, skipped heading levels (jumping from <h2> to <h4>), and sections without any headings. Each violation is scored by severity—a missing <h1> is an error, while skipping a heading level is a warning.

Key Concept: Heading Hierarchy as Semantic Outline

Search engines and AI models use heading hierarchy to understand document structure. A page with a clear h1 > h2 > h3 progression is easier to parse into a topic outline than one with flat or inconsistent headings. The heading hierarchy analysis enforces the same structural discipline that makes content machine-readable.

FAQ Pattern Detection

FAQ sections are high-value content for AI search engines because they provide direct question-answer pairs. The analyzer detects FAQ patterns by looking for headings or list items that end with question marks, <details>/<summary> elements, and schema.org FAQPage markup. Content with explicit FAQ structures scores higher in GEO because it maps directly to the question-answering format that generative engines prefer.

Key Concept: Structured Q&A for AI Citability

When an AI search engine cites a source, it looks for content that directly answers a question. A well-structured FAQ section with <details> elements or schema.org FAQ markup gives the engine a clean extraction target. Pages with FAQ patterns are cited 3–5x more often than pages with equivalent content in unstructured prose.

Structured Data Blocks

The analyzer identifies structured data in the document: JSON-LD scripts, Microdata attributes, tables with header rows, definition lists, and code blocks with language annotations. Each type is counted and categorized. Pages with rich structured data help AI models extract facts with higher confidence, leading to better citation rates.

Content Density Metrics

Beyond structure, the analyzer computes content density metrics: word count, average sentence length, list-to-paragraph ratio, link density, and image-to-text ratio. These metrics identify thin content (too few words), link farms (excessive link density), and image-heavy pages that lack textual context for AI models to extract meaning from.

Key Concept: Link Density as Quality Signal

Pages where more than 30% of the text is anchor text are typically navigation pages, link directories, or blogrolls—not substantive content. The analyzer flags high link density as a warning because AI engines deprioritize pages that appear to be link aggregators rather than original content.

How It Works

The analysis pipeline loads HTML content into a Cheerio DOM, then runs a series of analyzer functions that each examine specific structural aspects. Results are collected into a unified report.

  1. Parse: HTML is loaded into Cheerio; Markdown is first converted to HTML via a CommonMark renderer.
  2. Heading Analysis: All heading elements are collected, their levels checked for hierarchy violations, and counts accumulated per level.
  3. FAQ Detection: The DOM is searched for question patterns in headings, <details> elements, and schema.org FAQ markup.
  4. Structured Data Scan: JSON-LD scripts, Microdata, tables, definition lists, and annotated code blocks are identified and counted.
  5. Content Metrics: Text nodes are extracted, word count computed, links counted, images tallied, and density ratios calculated.
  6. Scoring: Each issue is assigned a severity and point deduction. The aggregate score starts at 100 and decreases for each issue found.
import * as cheerio from "cheerio";

interface GEOReport {
  score: number;
  issues: GEOIssue[];
  stats: ContentStats;
}

interface GEOIssue {
  severity: "error" | "warning" | "info";
  code: string;
  message: string;
  element?: string;
}

interface ContentStats {
  wordCount: number;
  headingCounts: Record<string, number>;
  faqPatterns: number;
  structuredDataBlocks: number;
  listCount: number;
  linkCount: number;
  imageCount: number;
}

function analyzeGEO(html: string): GEOReport {
  const $ = cheerio.load(html);
  const issues: GEOIssue[] = [];
  const stats: ContentStats = {
    wordCount: 0,
    headingCounts: {},
    faqPatterns: 0,
    structuredDataBlocks: 0,
    listCount: 0,
    linkCount: 0,
    imageCount: 0,
  };

  // Heading analysis.
  analyzeHeadings($, issues, stats);

  // FAQ pattern detection.
  detectFAQPatterns($, issues, stats);

  // Structured data scan.
  scanStructuredData($, issues, stats);

  // Content density metrics.
  computeContentMetrics($, issues, stats);

  // Score calculation: start at 100, deduct per issue.
  let score = 100;
  for (const issue of issues) {
    if (issue.severity === "error") score -= 10;
    else if (issue.severity === "warning") score -= 5;
    else score -= 1;
  }

  return { score: Math.max(0, score), issues, stats };
}
function analyzeHeadings(
  $: cheerio.CheerioAPI,
  issues: GEOIssue[],
  stats: ContentStats
): void {
  const headings = $("h1, h2, h3, h4, h5, h6");
  let prevLevel = 0;

  headings.each((_, el) => {
    const tag = (el as cheerio.TagElement).tagName;
    const level = parseInt(tag[1], 10);
    stats.headingCounts[tag] = (stats.headingCounts[tag] || 0) + 1;

    // Check for skipped levels.
    if (prevLevel > 0 && level > prevLevel + 1) {
      issues.push({
        severity: "warning",
        code: "HEADING_SKIP",
        message: `Heading level skipped: ${tag} after h${prevLevel}`,
        element: $(el).text().slice(0, 60),
      });
    }
    prevLevel = level;
  });

  // Check for missing or multiple h1.
  const h1Count = stats.headingCounts["h1"] || 0;
  if (h1Count === 0) {
    issues.push({
      severity: "error",
      code: "NO_H1",
      message: "Page is missing an h1 element",
    });
  } else if (h1Count > 1) {
    issues.push({
      severity: "warning",
      code: "MULTIPLE_H1",
      message: `Page has ${h1Count} h1 elements; should have exactly one`,
    });
  }
}

function detectFAQPatterns(
  $: cheerio.CheerioAPI,
  issues: GEOIssue[],
  stats: ContentStats
): void {
  // Check for details/summary elements (accordion FAQ).
  stats.faqPatterns += $("details > summary").length;

  // Check for headings ending in "?".
  $("h2, h3, h4").each((_, el) => {
    if ($(el).text().trim().endsWith("?")) {
      stats.faqPatterns++;
    }
  });

  // Check for schema.org FAQPage.
  $('script[type="application/ld+json"]').each((_, el) => {
    try {
      const data = JSON.parse($(el).html() || "");
      if (data["@type"] === "FAQPage") {
        stats.faqPatterns += (data.mainEntity || []).length;
      }
    } catch { /* ignore parse errors */ }
  });

  if (stats.faqPatterns === 0) {
    issues.push({
      severity: "info",
      code: "NO_FAQ",
      message: "No FAQ patterns detected; consider adding a FAQ section",
    });
  }
}

Practical Applications

Content Audit Pipelines

The GEO analyzer integrates into CI/CD pipelines to audit content before publication. When a documentation pull request is opened, the analyzer runs against all changed pages and posts a report as a PR comment. Pages scoring below a threshold are flagged for revision, ensuring that published content consistently meets GEO standards.

SEO/GEO Scoring Dashboards

The structured reports feed into dashboards that track GEO scores across an entire documentation site. Content teams can identify their lowest-scoring pages and prioritize improvements based on severity. Trend lines show whether structural quality is improving or regressing over time.

Automated Content Recommendations

By categorizing issues with specific codes (HEADING_SKIP, NO_FAQ, HIGH_LINK_DENSITY), the system can generate actionable recommendations: “Add a FAQ section with 3–5 questions,” “Fix heading hierarchy in the Setup section,” or “Reduce link density by converting the link list to a resource table.”

Implementation Patterns

Composable Analyzer Functions

Each analysis concern (headings, FAQ, structured data, content metrics) is a standalone function that receives the Cheerio DOM and appends to the shared issues and stats objects. This composable design makes it easy to add new analyzers without modifying existing ones, and to run a subset of analyzers for targeted audits.

type AnalyzerFn = (
  $: cheerio.CheerioAPI,
  issues: GEOIssue[],
  stats: ContentStats
) => void;

const defaultAnalyzers: AnalyzerFn[] = [
  analyzeHeadings,
  detectFAQPatterns,
  scanStructuredData,
  computeContentMetrics,
];

function runAnalysis(html: string, analyzers = defaultAnalyzers): GEOReport {
  const $ = cheerio.load(html);
  const issues: GEOIssue[] = [];
  const stats: ContentStats = { /* ...defaults */ } as ContentStats;

  for (const analyze of analyzers) {
    analyze($, issues, stats);
  }

  const score = computeScore(issues);
  return { score, issues, stats };
}

Markdown-to-HTML Normalization

Rather than building separate analysis logic for Markdown, the pipeline converts Markdown to HTML first. This normalization step uses a CommonMark-compliant renderer so that structural elements (headings, lists, code blocks, links) map to their HTML equivalents. The analyzer then works on a single representation regardless of the original format.

import { marked } from "marked";

function analyzeMarkdown(markdown: string): GEOReport {
  // Convert Markdown to HTML, then analyze.
  const html = marked.parse(markdown, { gfm: true });
  return analyzeGEO(html);
}

Common Pitfalls

Further Reading