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.
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.
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 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.
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.
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.
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.
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.
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.
<details> elements, and schema.org FAQ markup.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",
});
}
}
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.
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.
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.”
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 };
}
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);
}
<h1> elements (single-page apps with sections). The analyzer should allow per-page overrides or category-specific scoring rules.