Website Layout Analysis API — Map Page Structure, Headings, CTAs & Navigation

Analyze any website layout programmatically. Map headings, buttons, forms, images, and navigation with pixel coordinates. Detect layout patterns, track redesigns, audit SEO. Try free.

Vitalii Holben Updated

You can learn a lot about a web page without reading a word of its content. Where does the navigation sit? How many headings break up the text? Are the CTAs above the fold or buried below three screens of copy? Which images dominate the layout, and how much space does the footer eat? These are layout questions, and the analyze endpoint answers them with structured JSON — six categories of elements, each tagged with its type and pixel position on the fully rendered page.

curl -X POST https://api.screenshotrun.com/v1/analyze \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "include": ["headings", "interactive", "images", "navigation", "text"]
  }'

Request all six categories and you get a structural blueprint of the page. Headings reveal the content hierarchy. Interactive elements show where the buttons and links live. Forms pinpoint data collection points. Images, text blocks, and navigation sections fill in the rest. Every element carries a bounding rectangle, so you know not just what's on the page but exactly where it sits.

Reading a page's structure from JSON

A typical analysis response for a SaaS landing page might return 4 headings, 22 interactive elements, 1 form, 8 images, 3 text blocks, and 2 navigation sections. That alone tells you something: this page is CTA-heavy (22 clickables), light on body copy (3 text blocks), and has a dual-nav structure (probably header + footer).

{
  "result": {
    "stats": {
      "headings": 4,
      "interactive": 22,
      "forms": 1,
      "images": 8,
      "text": 3,
      "navigation": 2
    },
    "headings": [
      { "level": 1, "text": "Ship faster with fewer bugs", "rect": [200, 120, 880, 56] },
      { "level": 2, "text": "Trusted by 500+ teams", "rect": [200, 800, 880, 40] },
      { "level": 2, "text": "Pricing", "rect": [200, 2400, 880, 40] },
      { "level": 2, "text": "FAQ", "rect": [200, 3200, 880, 40] }
    ],
    "navigation": [
      { "label": "Main navigation", "links": 7, "rect": [0, 0, 1280, 64] },
      { "label": "", "links": 12, "rect": [0, 4200, 1280, 200] }
    ]
  }
}

The heading positions tell you how the page is zoned. H1 at y=120 (hero section), first H2 at y=800 (social proof, roughly one viewport down), pricing at y=2400, FAQ at y=3200. You can reconstruct the page flow from four numbers without looking at a screenshot.

Comparing layouts across competitors

Analyze five competitor landing pages and you start seeing patterns. Do they all put their primary CTA above y=600? Does anyone lead with a video instead of a hero image? How deep do you have to scroll before hitting the pricing section?

import requests

competitors = [
    "https://competitor-a.com",
    "https://competitor-b.com",
    "https://competitor-c.com",
]

for url in competitors:
    data = requests.post(
        "https://api.screenshotrun.com/v1/analyze",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"url": url, "include": ["headings", "interactive", "images", "navigation"]},
        timeout=60,
    ).json()

    stats = data["result"]["stats"]
    nav = data["result"]["navigation"]
    headings = data["result"]["headings"]

    # Find the first CTA (button or role="button")
    cta = next(
        (el for el in data["result"]["interactive"]
         if el["tag"] == "button" or el.get("role") == "button"),
        None,
    )

    print(f"\n{url}")
    print(f"  Headings: {stats['headings']}, Images: {stats['images']}, Nav links: {sum(n['links'] for n in nav)}")
    print(f"  H1: '{headings[0]['text'][:50]}' at y={headings[0]['rect'][1]}")
    if cta:
        print(f"  First CTA: '{cta.get('text', '')}' at y={cta['rect'][1]}")

This kind of competitive layout analysis runs in seconds per page. A human doing the same work would open each site, scroll through it, take notes, and spend half an hour per competitor. The API compresses that into a JSON diff you can process programmatically.

Detecting layout patterns automatically

Once you have structural data, pattern matching becomes straightforward. A pricing page has a heading with "pricing" in it, multiple interactive elements clustered horizontally (the plan cards), and probably a FAQ section lower down. A blog post has one H1, multiple H2s spaced evenly, lots of text blocks, and few interactive elements beyond the navigation.

def classify_page(data):
    stats = data["result"]["stats"]
    headings = data["result"]["headings"]

    h1_count = sum(1 for h in headings if h["level"] == 1)
    h2_count = sum(1 for h in headings if h["level"] == 2)

    if stats["forms"] >= 2 and stats["interactive"] > 15:
        return "checkout/signup"
    if h2_count >= 4 and stats["text"] >= 6 and stats["interactive"] < 10:
        return "blog-post"
    if any("pricing" in h["text"].lower() for h in headings):
        return "pricing"
    if stats["images"] > 5 and stats["interactive"] > 10:
        return "landing-page"
    return "unknown"

Page classification from structured layout data is useful for large-scale crawls. Crawl 500 URLs on a competitor's site, classify each by layout pattern, and you know their site architecture without reading a single page.

Tracking layout changes over time

Store analysis results in a database and compare them weekly. When a competitor redesigns their homepage, you'll see it in the data: navigation link count changes, heading positions shift, new interactive elements appear. You don't need to watch their site manually — a cron job and a diff script handle it.

import json
from datetime import date

# Assume analyze() wraps the API call
today = analyze("https://competitor.com", ["headings", "interactive", "navigation"])
yesterday = json.load(open(f"snapshots/competitor-{date.today() - timedelta(days=7)}.json"))

# Compare heading structure
old_headings = [(h["level"], h["text"]) for h in yesterday["result"]["headings"]]
new_headings = [(h["level"], h["text"]) for h in today["result"]["headings"]]

added = set(new_headings) - set(old_headings)
removed = set(old_headings) - set(new_headings)

if added or removed:
    print("HEADING CHANGES DETECTED:")
    for h in added: print(f"  + H{h[0]}: {h[1]}")
    for h in removed: print(f"  - H{h[0]}: {h[1]}")

# Compare nav link counts
for old_nav, new_nav in zip(yesterday["result"]["navigation"], today["result"]["navigation"]):
    if old_nav["links"] != new_nav["links"]:
        print(f"NAV CHANGE: '{new_nav.get('label', 'unlabeled')}' — {old_nav['links']} → {new_nav['links']} links")

Pair this with brand monitoring for a complete picture: structural changes (layout analysis) plus visual changes (screenshot comparison).

Above-the-fold content audit

The first 800 pixels of a page (a standard desktop viewport) determine whether visitors scroll or bounce. Layout analysis tells you exactly what lives in that critical zone.

const fold = 800;
const data = await analyze("https://yoursite.com/landing", [
  "headings", "interactive", "images", "text"
]);

const aboveFold = {
  headings: data.result.headings.filter(h => h.rect[1] < fold),
  buttons: data.result.interactive.filter(
    el => (el.tag === "button" || el.role === "button") && el.rect[1] + el.rect[3] <= fold
  ),
  images: data.result.images.filter(i => i.rect[1] < fold),
  text: data.result.text.filter(t => t.rect[1] < fold),
};

console.log("Above the fold:");
console.log(`  ${aboveFold.headings.length} headings`);
console.log(`  ${aboveFold.buttons.length} buttons/CTAs`);
console.log(`  ${aboveFold.images.length} images`);
console.log(`  ${aboveFold.text.length} text blocks`);

if (aboveFold.buttons.length === 0) {
  console.warn("WARNING: No CTA above the fold!");
}

Run this across all your landing pages and flag any that lack an above-fold CTA, or have no heading in the first viewport, or show three images but no text. These are layout problems that affect conversion rates, and they're easy to catch programmatically once you have the data.

Layout analysis for SEO audits

Search engines care about heading hierarchy, link structure, and content-to-chrome ratio. Layout analysis provides all three.

Heading hierarchy: Does every page have exactly one H1? Do H2s follow H1s, or are there gaps (H1 → H3 with no H2)? The heading array answers this instantly — iterate through levels and flag any that skip a step.

Navigation weight: How many links sit in navigation sections versus the page body? A page where 80% of links are in the nav and only 20% in the content might have thin body copy. The navigation category separates nav links from content links automatically.

Image-to-text ratio: Count the bounding box areas of images versus text blocks. A page that's mostly images with little text might struggle to rank for anything, while a wall of text with no visual breaks hurts engagement.

Design system compliance

If your design system says "every landing page must have an H1 in the first 200px, at least one CTA button above the fold, and no more than two navigation bars," you can enforce those rules programmatically across your entire site.

rules_violations = []

# Rule 1: H1 in first 200px
h1s = [h for h in data["result"]["headings"] if h["level"] == 1]
if not h1s or h1s[0]["rect"][1] > 200:
    rules_violations.append("H1 missing or below y=200")

# Rule 2: CTA above the fold (800px)
ctas = [
    el for el in data["result"]["interactive"]
    if el["tag"] == "button" or el.get("role") == "button"
]
above_fold_ctas = [c for c in ctas if c["rect"][1] + c["rect"][3] <= 800]
if not above_fold_ctas:
    rules_violations.append("No CTA button above the fold")

# Rule 3: Max 2 navigation sections
if data["result"]["stats"]["navigation"] > 2:
    rules_violations.append(f"Too many nav sections: {data['result']['stats']['navigation']}")

if rules_violations:
    print(f"VIOLATIONS on {url}:")
    for v in rules_violations:
        print(f"  ✗ {v}")
else:
    print(f"✓ {url} passes all layout rules")

Scale this to hundreds of pages, run it nightly, and layout violations get caught before customers notice them.

How it works

The analyze endpoint loads your URL in a real Chromium instance. After JavaScript finishes executing and the layout settles, it scans the DOM for six categories of elements, measures each one's bounding rectangle via getBoundingClientRect(), and returns everything as a flat JSON structure. The whole process takes 2–4 seconds per page. Full rendering parameters (viewport width, device emulation, stealth mode, cookie blocking) carry over from the screenshot API, so you can analyze pages exactly as your users see them.

Try it in the Playground — paste any URL, pick your categories, and see the layout map. When you're ready to build, the API docs have the full parameter reference and examples in Python, Node.js, Go, and PHP.

FAQ#

A structured JSON object with six element categories: headings (h1-h6 with level and text), interactive elements (buttons, links, inputs), forms (action URL, method, field count), images (src, alt text), navigation sections (link count), and text blocks. Every element includes a bounding rectangle [x, y, width, height] in pixels. A stats object gives element counts per category.

Yes. Store analysis results as JSON snapshots and diff them programmatically. Compare heading structures, element counts, navigation link counts, and element positions between staging and production, or between weekly snapshots of a competitor page.

Use the structural data for pattern matching. A pricing page typically has a heading containing "pricing" and many horizontally clustered interactive elements. A blog post has one H1, multiple evenly spaced H2s, many text blocks, and few interactive elements. You can build a classifier from these signals.

Yes. Set your viewport height (e.g. 800px) and check if the button bounding rectangle falls within that range. Any interactive element where y + height is less than or equal to the viewport height is above the fold.

Yes. The endpoint loads pages in a real Chromium browser and waits for JavaScript to execute before analyzing. React, Vue, Angular, and dynamically loaded content are all captured. Enable stealth mode for sites that block headless browsers.

Written by

Vitalii Holben

Co-founder and CEO of ScreenshotRun. Full-stack developer building screenshot API tools that help developers capture, automate, and monitor web pages at scale. Based in Europe, shipping code since 201...

Screenshot API in a few clicks

Free plan includes 200 screenshots every month. No credit card required.

Sign up → copy your key → make your first call