Widgets

Pure build functions and thin mount helpers for the browser.

@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-agent

What 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.