@ascent/seo-agent/widgets is a small set of zero-dependency, pure-ESM browser widgets that render Ascent data honestly: what's measured (from Google, the internet) is visually and textually distinct from what's estimated (an Ascent projection).
Install
npm i @ascent/seo-agentWhat ships
Four widgets — a representative set, not the full catalog. Long-tail widgets are deferred.
import {
buildRankBadge, mountRankBadge,
buildHealthGauge, mountHealthGauge,
buildResultCard, mountResultCard,
buildHeadTags, injectHead,
makeAscentClient,
} from '@ascent/seo-agent/widgets'Load the theme once, wherever your page's CSS lives:
<link rel="stylesheet" href="node_modules/@ascent/seo-agent/widgets/theme.css">theme.css defines the honesty contract in color: teal (--ascent-measured) for anything that came from the internet, amber (--ascent-estimate) for anything Ascent projected. It respects prefers-color-scheme for light/dark automatically — no JS toggle needed.
Rank badge — measured
A keyword's measured Google position. Never claims a rank Ascent hasn't observed.
mountRankBadge(el, { keyword: 'best hiking boots', position: 4, checkedAt: '2026-07-10' })If position is omitted, it renders "not yet measured" instead of guessing.
Health gauge — potential, not results
An Ascent estimate of on-page health — a potential score, never framed as a result.
mountHealthGauge(el, { score: 72 })Labeled "Potential (on-page health) · Ascent estimate" — deliberately not "health score" or "results," to avoid implying Google confirmed it.
Result card — the honest 3-point journey
Shows now (measured), what you've gained (measured re-ranks, if any), and could reach (an Ascent estimate) — each row tagged with its source so measured and projected numbers are never visually conflated.
mountResultCard(el, {
now: { label: 'best hiking boots', value: '#4', source: 'measured' },
gained: [{ keyword: 'trail boots', before: 12, after: 6, source: 'measured' }],
aspire: { label: 'best hiking boots', value: 'top 3', source: 'estimate' },
})If nothing has re-ranked yet, the card says so plainly rather than inventing a gain.
Meta injector
Builds <title>/meta/JSON-LD tags from an entity + profile (wraps core/metaTags.mjs and core/jsonld.mjs) and injects them into a document's <head>.
const html = buildHeadTags({ entity, profile, pageMeta, siteUrl: 'https://example.com' })
injectHead(document, html)Talking to the backend: makeAscentClient
A browser-safe client for the deployed SP1 service. It never holds a secret key — only a bearer token — and fails soft ({ configured: false }) instead of throwing on a bad response or network error.
const client = makeAscentClient({ serviceUrl: 'https://your-deploy.example.com', token })
const rankings = await client.rankings({ domain: 'acme.com' })Actions: analyze, generate, rankings, competitors, content, status — one per SP1 endpoint.
Shared render helpers
esc, attrs, mount, formFactor, isTouch (from widgets/render.mjs) back every builder/mount pair above and are exported for building your own widgets on the same escaping + mount conventions.
Not shipped
No Web Components, no framework wrappers, no build step — these are plain functions that return an HTML string (build*) or write it into an element (mount*). No widgets beyond the four above exist yet.