WebGraph

Types

Document and Block field by field, the enums, ResolvedPage, PageMetadata and read_metadata - the data every function returns.

All of these are importable from webgraph. Document, Block, Rect and StructuredPayload are frozen Pydantic models: model_dump(), model_validate(), model_copy(update=...) all work, and they cannot be mutated in place.

Document

One page, read. Returned inside a ResolvedPage by resolve_page, and directly by build_document.

FieldTypeMeaning
urlstrThe page's final address, after redirects. Links and images in the blocks are absolute against it
htmlstrThe markup the blocks were parsed from — the merged page for a union. Usually the largest thing here; model_dump(exclude={"html"}) to leave it out
blockstuple[Block, ...]The page's content units in reading order. The whole point
titlestrThe <title>
descriptionstrThe <meta name="description">
reading_order_methodReadingOrderMethodHow blocks were ordered — measured, partly measured, or source order. See below
profileStackProfileWhat the page is built with: technologies (each a dict with name, category, version, confidence, evidence), frameworks, requires_render and the signals behind it, has_json_ld, has_next_data, …
structured_datatuple[StructuredPayload, ...]Machine-readable data the page handed over before any parsing: each with source (json-ld, microdata, open-graph, next-data, rsc-flight, nuxt, initial-state), data and the xpath it came from
markupMarkupStatsCounts over the markup for the page-type router: elements, tag_counts, class-name hits per vocabulary (docs, product, forum, …), rel_next, itemprop_count, generator, body_classes
content_hashstrSHA-256 of text. Computed over the extracted text, never the HTML, so two fetches of an unchanged page hash the same despite build hashes and CSRF tokens
gatedstr | NoneSet on a union document when the browser was shown a gate that hid the page (a country picker, a consent dialog with the body hidden behind it) and the plain fetch's page was kept whole. The sentence says how much was behind it
fetched_atdatetimeWhen, UTC
textstr (property)The blocks' text joined by blank lines
doc = page.document
doc.title                       # '3. An Informal Introduction to Python — Python 3.14.7 documentation'
doc.reading_order_method        # geometric-anchored
len(doc.blocks)                 # 164
doc.profile.technologies[:2]    # [{'name': 'nginx', 'category': 'Web servers', ...}, {'name': 'Fastly', 'category': 'CDN', ...}]
[p.source for p in doc.structured_data]   # [open-graph]
doc.content_hash[:12]           # 'ec1a08f6a2fd'

Block

One unit of content — a paragraph, a heading, a list item, a table, an image — with where it came from and, on a rendered page, where it was drawn.

FieldTypeMeaning
textstrThe plain text. For a table, rows joined with | and newlines; for an image, its alt; for a media placeholder, the engine's note
rich_textstr | NoneThe same with inline Markdown — links, emphasis, inline code — when the block has any. Kept apart from text so that hashes and deduplication key on words alone
kindBlockKindWhat it is; below
levelintHeading level 1–6, or a list item's nesting depth (1 for a top-level item). 0 when not applicable
tagstrThe source element's tag, lowercased: p, h2, li, td, img, pre
xpathstrAbsolute XPath of the source element: the provenance anchor, and the key a selector cache is built on
dom_indexintPosition in source order, kept so that DOM order stays recoverable after geometric reordering
rectRect | NoneThe measured box in CSS pixels, page-relative (x, y, width, height, plus right and bottom). None on a static parse, and for a block the browser laid out without a box
depthintNesting depth of the source element
hrefstr | NoneAn image's source, or a standalone link's target, absolute
altstr | NoneAn image's alt text
linkstr | NoneFor an image that is the whole content of a link, the link's target
orderedboolA numbered list's item
rowstuple[tuple[str, ...], ...]A table's cells, first row the header
rich_rowstuple[tuple[str, ...], ...]The same cells as inline Markdown, when any cell has some; empty for a plain grid
table_htmlstr | NoneA table's own cleaned markup, only when it merges cells or nests a table — the cases a pipe table cannot express
languagestr | NoneA code block's declared language
regionstr | NoneThe innermost landmark the block sits in, by tag or ARIA role: main, nav, header, footer, aside. None outside any
in_mainboolWhether any ancestor is a main landmark — a <nav> inside <main> is both, and the two questions differ
widgetstr | NoneThe interactive panel the block sits in, when the markup names one: filter, consent, rail, post-furniture, comments; select for a dropdown's choices, one block per <select> with the choices joined by ·, on the whole page and never in the content
quotedintHow many <blockquote>s the block is inside
body_ofstr | NoneXPath of the outermost ancestor that declares itself the article body (itemprop="articleBody", entry-content, …)
float_ofstr | NoneXPath of the outermost floated ancestor the renderer measured, so a float's pieces are read together
templatedstr | NoneThe client-template directive the block sits under (v-if, x-show, ng-if, …). A branch the browser did not build is never put back by the union
for b in page.document.blocks[:3]:
    print(b.kind, b.region, b.in_main, b.rect is not None, repr(b.text[:40]))
# image nav False True 'Python logo'
# list-item nav False True 'Python »'
# list-item nav False True '3.14.7 Documentation »'

Block.with_text(text) returns a copy with new text and everything else kept. Because blocks are frozen, that is how a transformation is written.

BlockKind

A string enum: paragraph, heading, list-item, table, image, code, quote, figure-caption, media (an <iframe>, <video> or <audio>, as a placeholder that says where it was and that it was not transcribed), rule (an <hr>).

ReadingOrderMethod

ValueMeaning
geometric-xy-cutEvery block was measured by the browser, and the order is the recursive XY-cut over their boxes: columns read down, rows across, cards one at a time
geometric-anchoredMost blocks were measured; the unmeasured ones (collapsed, hidden until hover, or only in the static page) were slotted after their nearest measured neighbour. A weaker claim than the first, and named so
dom-fallbackSource order: no geometry (a static fetch, supplied HTML), or too little to lead with (under 30% of blocks measured)
single-blockOne block; there is nothing to order

Strategy

static-only, rendered-only, union, supplied — see Pages.

PageType and Routing

PageType: article, documentation, service, forum, collection, listing, product, unknown. Routing is what the router returns: page_type, confidence, probabilities (every type), runner_up, and reasons when asked with explain=True. See Content selection.

PageMetadata

What a page declares about itself in <head>, read once, every address made absolute, every text value folded to one line and capped at 300 characters.

from webgraph import read_metadata

meta = read_metadata(page.document.html, page.url, structured_data=page.document.structured_data)
meta.title          # '3. An Informal Introduction to Python — Python 3.14.7 documentation'
meta.canonical      # 'https://docs.python.org/3/tutorial/introduction.html'
meta.language       # 'en'
meta.theme_color    # '#3776ab'
meta.open_graph     # {'og:title': ..., 'og:type': ..., 'og:url': ..., 'og:site_name': ..., 'og:description': ...}
FieldTypeMeaning
urlstrThe page
title, descriptionstr<title>, <meta name="description">
canonicalstr<link rel="canonical">, absolute
language, charsetstr<html lang>, the declared charset
robotsstr<meta name="robots">
generator, author, keywordsstrThe <meta> of those names
theme_color, viewportstrLikewise
iconstuple[str, ...]Every <link rel="icon">-family address
manifeststrThe web-app manifest address
open_graphdict[str, str]Every og:* and article:* property
twitterdict[str, str]Every twitter:*
alternatesdict[str, str]hreflang → href, capped at 24; alternate_count is the whole number
feedstuple[str, ...]RSS and Atom addresses
schema_typestuple[str, ...]The @types of the page's JSON-LD and microdata, when structured_data was passed
declared_elsewheretuple[str, ...]Declarations that name a different site from the one that served the page, as "canonical -> https://old-host.example". Empty is normal; a site that moved domains and kept its old metadata base fills it

read_metadata(html, url, structured_data=()) — the arguments are the resolved page's document.html, its url, and its document.structured_data for schema_types.