Features
Page analysis
Pick an objective, enter a URL (a keyword is optional — blank means Sjá picks the page's main term), and RUN. The engine fetches the page, parses the HTML, and produces a set of weighted checks across six categories, plus an overall score (0–100) and grade (A–F).
The objective decides what RUN does, not only how the score is weighted: Audit a page, Fix & optimize and Get indexed run an analysis; Crawl a site starts a site crawl; AI visibility runs the analysis and opens the report at the citation probe; Track changes runs the analysis and creates a monitor for the page (daily by default), so a page you want watched is watched from the first run. Each objective carries one or more recipes, chosen inline; language, market, crawl size and monitor interval sit behind Options. The launcher's Continue strip lists the pages you have analyzed with their score trend and one-click Re-run, Probe and Monitor; a project with nothing in it shows three ways to start instead.
| Category | Examples |
|---|---|
| Meta | title length, meta description, lang, keyword in title/description |
| Structure | single H1, subheadings, content depth, image alt text |
| Social | OpenGraph (title/description/image), Twitter Card |
| GEO / AI | AI crawler access (robots.txt per crawler, and the page fetched as an AI crawler to catch CDN blocks and bot challenges), content readable without JavaScript, snippet directives, JSON-LD validity and completeness (required and recommended properties per type, FAQ questions checked one by one), a resolvable publisher entity (Organization with url, logo, sameAs), dates that agree across schema, meta, <time> and Last-Modified, llms.txt quality and whether it lists the page, quotable passages and answer position (see What the model sees), answer-style headings, citable schema (Article/FAQ/HowTo) |
| Indexing | noindex, canonical, robots.txt, internal linking |
| International | hreflang syntax, self-reference, x-default, canonical agreement |
A category that measured nothing is not reported as a score. A page with no
hreflang gets a neutral note rather than a fake 100 for International, and the
same applies to Performance when PageSpeed can't be reached.
Recipes bias the weighting (e.g. GEO readiness emphasizes GEO). The origin's
robots.txt and llms.txt are also probed.
Performance / Core Web Vitals
Enable PageSpeed in Settings to add a Performance category via Google PageSpeed Insights: performance score, LCP, CLS, TBT (an INP proxy), and FCP. It runs on single-page analysis only (crawls stay fast). If the API is unavailable it degrades to a neutral note without hurting the score.
What the model sees
Answer engines do not read pages. They fetch the HTML, extract the main content, split it into chunks, keep what fits a reading budget, and quote passages. A page can pass every whole-page check and still have its one good answer buried after 3 000 tokens of navigation, or spread across paragraphs that each begin "This means…". So every report carries the page as the engine receives it:
- The extracted text, in reading order, taken from
<main>/<article>/role=main(or the body when a page has no landmark), with navigation, footers, cookie banners, sidebars and forms removed the way a readability extractor removes them. The report states how many visible words were discarded as boilerplate. - A token budget drawn across it. A dashed line marks where roughly 2 000 tokens end — the order of magnitude engines keep of one page. Blocks below it are greyed: what an engine never reads, it never quotes. Counts are estimates (≈4 characters per token) and say so.
- Question headings are tagged, so you can see where the page's Q&A actually lands.
From that text, each paragraph is judged as a quote, and the five best are shown with a 0–100 score and the reasons on both sides: the length engines quote whole (40–90 words), whether it stands on its own or opens with a "This"/"It"/"However" that leans on the previous paragraph, whether a heading — ideally a question — sits directly above it, whether it carries a concrete figure or date, whether it is a complete sentence, and whether it starts inside the budget. The gaps are the actionable half: "opens with «this»" or "no number, date or measurable fact" tells a writer exactly what to change.
Three GEO checks are derived from it: Quotable passages (is there at least one
passage scoring 70+), Answer within the reading budget (does the first question
heading or quotable passage start before the budget runs out), and Signal to
noise (what share of the visible text survives extraction). The PDF export lists
the top three passages; the MCP server and the CLI's --json report them too, so an
assistant editing the page works on the exact text the engine will see.
Cited by answer engines? (citation probe)
Everything else in the GEO category predicts. This measures the outcome. From a page's report, type the question a user would ask and probe: Sjá asks ChatGPT, Claude, Perplexity and Gemini (with Google Search grounding, the closest API path to Google's own answer surface) through their public APIs with web search on, using your own keys, and reports per engine:
- whether the answer cites the page's site (apex and
www.count as one), and at which position among the cited sources; - whether it mentions the brand without linking — the project name, the Organization the page declares, or the domain's own label, matched as whole words. A mention is not a citation, but it is not nothing either, and it is shown separately;
- who was cited instead — the distinct competitor hosts, which is usually the most useful line in the whole report;
- the start of the answer, so you can see what was actually said.
Every probe is stored (including failures, so a gap in the history is visible as a gap), the report shows the history for the page, and the Trends view shows the latest verdict per page and engine with the earlier ones as dots.
Schedule a probe (daily or weekly) from the same panel and it repeats — from the open app, or from the background runner while the app is closed. Scheduled runs compare each engine's verdict with the previous one and raise an alert when a citation is gained or lost, naming the competitors that took its place — and, with a Jev key, when a citing engine starts misrepresenting the page (see below). Schedules are managed under Monitors.
Two honesty notes, both stated in the UI. First, the APIs are close to, not identical with, the consumer products: models, search backends and ranking differ by surface. Second, a probe is one sample of a non-deterministic system. Read the trend and the competitor list, not a single yes or no. Each probe costs a few cents per engine on your own account; the default models are the current flagships with web search, and can be changed in Settings.
Why not cited, and cited how (Jev)
A yes/no per engine says whether; it does not say why. With a TypeSafe API key in Settings, every probe is also judged by Jev, TypeSafe's System One model — a model that answers typed questions about text it is given rather than writing text — from the page's own main text and the engines' answers:
- Coverage. How completely does the page answer the question, on a five-level scale from does not address it to answers it completely? Combined with the engine's verdict this splits "not cited" into two different problems, and the card says which: the page answers and the engine chose other sources (an authority and distribution problem — build links, mentions, freshness), or the page does not answer (a content problem — make it answer, or probe a question it does answer). Coverage is judged once per probe; it is about the page and the question, not the engine.
- Fidelity. When an engine cites the page — or names the brand without a
link — each sentence of its answer that is about the site is checked against
the page: does the page support it, contradict it, or say nothing about
it? The card shows the verdict (faithful, misrepresented, unsupported) and the
claims themselves, so a citation that gets your price, dates or terms wrong is
visible as what it is: a citation that misrepresents you. A scheduled probe
raises a
citation_misrepresentedalert the first time this happens, with the claim, and again only after it has gone away and come back. - Stance. How the whole answer positions the brand: recommended, listed as one option among several, a neutral reference as a source, or advised against. Judged for every engine answer, cited or not, from the answer alone, so a brand that is named without a link is no longer just "mentioned" — the card says whether the mention helps or hurts. A stance Jev is less than even sure of is stored but not stated.
The judgment is stored with the probe, so the history and its verdicts read the same later. Three limits, all stated in the UI. Jev's answers are shown, never acted on — the page is the text it reads, and a page can carry injected instructions. Its training language is English, so Norwegian pages are judged with lower accuracy; treat thresholds as a starting point and read the claims. And a spread-out probability is flagged uncertain rather than rounded to a verdict. Cost is negligible next to the engines: well under a cent per probe.
Why them, not you
"Not cited" usually comes with a list of who was cited instead. With a TypeSafe key, each competitor on a probe card has a why them? button that compares your page with the page the engine cited, for the question you asked.
Code fetches both pages and extracts the competitor's main text the way an engine would, then picks up to fourteen of its passages — the ones that use the question's words, then the most quotable. Jev answers, in one request, whether each passage helps answer the question and whether your page already gives the same information (covered, partial, missing), and scores how completely each page answers it. The card shows both coverage scores, a one-sentence verdict — a content gap when they answer more completely, an authority gap when your page answers as well and the engine chose them anyway — and the points they make that you do not, in their own words. Nothing is generated; every point is a passage from their page.
The competitor URL comes from an engine's answer, so it is treated as untrusted: private and local addresses are refused before anything is fetched. Comparisons are stored with the page and cost a small fraction of a cent.
AI share of voice
A single probe answers one question once. The Trends view turns every stored probe of a project into the number a marketing lead asks for: of the engines' answers to the questions you track, in the last 28 days, what share cites your site — against the 28 days before, week by week, per engine, and next to the sites that share the answers with you. Answers are counted, not URLs: an answer citing two of your pages counts once. Answers that name the brand without a link are shown separately. No new measurement is made; this is the probes you already ran, aggregated.
The number is only as good as the question set, so a crawl can propose one. Questions to track in the crawl view collects the site's question headings and FAQ questions, and Jev keeps those a real person would ask an AI assistant — in the live test "Hvor mye kan jeg tjene uten å betale skatt?" scored 0.93, "Hvorfor velge oss?" 0.13. Tick the ones you want and they become weekly probe schedules on every engine with a key, with the weekly call count shown before you commit.
Structured data, entity and dates
Counting @type values says a page has structured data. It does not say whether
an engine can use it, so the report now reads the JSON-LD for what the engine needs:
- Validity. A
<script type="application/ld+json">block that is not valid JSON is reported as such, not as "no structured data" — the outcome for the engine is the same, but the fix is different. - Completeness per type. Article and its kin need a
headline(and are weaker withoutimage,author,datePublished,dateModified); HowTo needsnameandstep; Product needsnameand one of offers, rating or review; FAQPage and QAPage are checked question by question — a Question without anameor without an answertextis called out. Required gaps fail the check, recommended gaps warn. - The publisher entity. Engines resolve who is behind the page from a
stable Organization name, a canonical
url, alogoandsameAslinks to real profiles. The report says which of those the page's Organization carries, or that only a WebSite (or nothing) is declared. Across a site crawl, every distinct Organization name the pages use is listed with its page count: three spellings of the company read to an engine as three weak entities. - Dates that agree.
datePublished/dateModifiedin schema,article:published_time/article:modified_timemeta tags, the first<time datetime>element and theLast-Modifiedheader are collected. A page that declares no dates gets a note, not a penalty — not every page type has one. A page that does declare them is held to them: a modified date before the published date fails, two modified dates that disagree warn, a future date or a date more than two years old warns, dates that are not ISO 8601 warn.
llms.txt is likewise graded by content, not presence: it needs a # Title and at
least one link to be usable, the report says how many entries carry a description,
and a separate check says whether the page being analyzed is one of them.
Fix-prompt builder
Every warn/fail check is clickable. Clicking appends a context-aware, copy-paste-ready fix prompt (using the page's URL, keyword, and actual values) to the Fix prompt panel, with an expert preamble. Stack several, then Copy or Run with AI.
AI fixes
Run with AI sends the prompt to your configured model (OpenAI / Anthropic)
via elyra::ai. If the current project has a local code path:
- The agent gets sandboxed file tools (
list_dir,read_file,write_file) scoped to that folder — it cannot escape it. - It explores, reads, and edits the real files, streaming its activity live.
- With Ask before the AI writes files on (default), each write pauses for a preview + Approve/Decline.
Without a local path, it returns copy-paste-ready corrections instead.
Character encodings
Pages are decoded by their declared charset — Content-Type, else a
<meta charset> sniff, else UTF-8 — so ISO-8859-1, Shift_JIS, GBK and
Windows-1252 pages produce correct titles, word counts and keyword matches. This
matters for non-ASCII keywords in particular: against a mis-decoded page they can
never match, and the keyword checks would fail for a reason that isn't real.
Headless mode (CI)
seo.rs has always been GUI-free; there is now a command line in front of it:
elyra-sja check https://example.com/page --keyword widgets --budget 80 --budget-geo 70
Exit codes are the interface — and 1 and 2 are deliberately different, because "the score dropped" and "the site is down" call for different reactions in a pipeline:
| code | meaning |
|---|---|
| 0 | every budget met |
| 1 | analysis ran, a budget was missed |
| 2 | could not run: usage error, fetch failure, missing licence |
Budgets can be per category, not just overall (--budget-geo,
--budget-indexing, …): a GEO regression otherwise hides behind a healthy total.
A budget on a category that was never measured fails rather than passes — a gate
that did not run must never report success.
--json gives machine-readable output whose field names are pinned by a test,
since a pipeline parses them.
No window, no database, nothing written to disk. Headless mode exits before the
app data directory is created, which is asserted by a test that points HOME at an
empty directory and checks nothing appears in it. That also means no Settings are
read: PageSpeed is off and everything comes from flags.
Licence: headless mode needs a valid signed code. CI has no Keychain and no
browser, so pass it in ELYRA_SJA_LICENSE; on your own Mac the activated code in
the Keychain is used automatically.
MCP server (use Sjá from your own AI assistant)
elyra-sja mcp
Speaks JSON-RPC 2.0 over stdio, so Claude Desktop, Zed or Cursor can call the engine
while someone edits content — a model driving Sjá instead of the other way round.
Two read-only tools: analyze_page (score plus every failing check with its
evidence) and robots_check (whether a named AI crawler may fetch a URL, and what
that crawler is for).
Nothing that writes files or spends an AI key is exposed, calls are rate limited to 30/minute, and nothing is written to disk.
→ Full guide: client configuration, tool reference, worked examples and troubleshooting
Market locale
The launcher's Language and Market pickers set a BCP 47 locale that is
sent as Accept-Language when the page is fetched. On a site that negotiates
content by language this is what makes "audit the German variant" mean
something: the analysis runs on the page that market actually receives, and the
locale is recorded on the report (and in the exported PDF) so a stored result says
which market it describes.
- As served (the default) sends no
Accept-Languageat all — the page is analyzed exactly as the site returns it, with no negotiation. - A language alone is a valid locale (
ja); the market is an optional country variant (pt+ Brazil →pt-BR). - The lists are built from the platform's own ICU data, so coverage is every
language and territory it knows, with names in your UI language — not a
hand-picked shortlist of markets. Codes search engines don't honour (
UK,EU) are excluded, matching the hreflang validator. - An invalid locale never becomes a request header.
Monitors have no locale of their own: a scheduled re-scan uses the default from Settings.
International targeting (hreflang)
Misconfigured hreflang is among the most expensive mistakes on a multi-market
site, because it fails silently — search engines discard the whole annotation
set without reporting an error anywhere. Sjá checks, from the page itself:
- Syntax of every language tag (BCP 47: ISO 639 language, optional script,
ISO 3166-1 alpha-2 or UN M.49 region), catching
en_US,english,en-GBRand the like.en-UKanden-EUget specific advice — they look right but are not codes search engines honour (the UK isGB; the EU is not a country). - Self-reference — every page in a set must list itself, or the set is dropped.
- Absolute URLs —
hreflangrequires fully-qualified hrefs. - Conflicts — one language tag pointing at two different URLs invalidates the set; identical duplicates only warn.
x-default— expected once two or more language variants exist, so visitors matching none of them have a declared fallback.- Canonical agreement —
hreflangpointing at a URL that is canonicalized elsewhere is ignored without warning. <html lang>vshreflang— when the two disagree, one of them is wrong.
A single-market page is never penalized for having no hreflang: it gets a
neutral note, weight zero.
Not checked yet: whether each alternate links back (return-link
reciprocity). That requires fetching the other pages, so it belongs to the site
crawl — the annotations are stored per page so a crawl-level check can be added
without re-analyzing anything. hreflang in HTTP headers or XML sitemaps is also
out of scope; only the HTML <link> form is read.
AI crawlers (server-log analysis)
Every other GEO signal in Sjá is a prediction about how an answer engine will read a page. This one is evidence: point it at a server access log and it reports which AI crawlers actually came.
Drop in an nginx/Apache access log (combined or common format) and you get, per
crawler: hits, distinct pages, status-code split, first and last seen, the most
requested paths — and the robots.txt verdict for that specific crawler.
Why the last part matters: a site that allows * and disallows GPTBot is the
most common way to be invisible to AI answer engines, and reading only the *
group would report it as "not blocked". Sjá parses every user-agent group and
resolves the most specific match, exactly as a crawler would.
The report distinguishes three kinds of crawler, because blocking them has very different costs:
| Purpose | Example | Cost of blocking |
|---|---|---|
| answers | OAI-SearchBot, Claude-User, Perplexity-User | Absent from AI answers today |
| training | GPTBot, ClaudeBot, CCBot, Bytespider | No visibility cost now; a licensing/values choice |
| search | Googlebot, Bingbot, PerplexityBot | Absent from classic search |
Two things it deliberately says out loud:
- Robots-only tokens.
Google-ExtendedandApplebot-Extendednever appear as a visiting user-agent — they only control training use of what Googlebot and Applebot already fetched. Reporting them as "never seen" would be meaningless, so they are marked as controls rather than absentees. - Blocked but visiting. When a crawler
robots.txtblocks shows up in the log anyway, that is flagged: either the rule is newer than the log, or the crawler ignores it. Neither is visible fromrobots.txtalone.
Unrecognised bot-like agents are listed separately, so a new crawler can be spotted before the registry knows about it.
Visits over time
The analysis keeps visits per crawler per day, shown as a sparkline per crawler for the days the log covers, and — when a project is selected — stored, so the Trends view shows crawler visits across every log analyzed for that project. Rows are keyed per log file: two servers' logs add up, and re-analyzing the same file replaces the days it covers instead of double counting them. Quiet days are zero, not missing. This answers the question the rest of GEO cannot: did the crawlers come back after we changed something?
Verified addresses
A User-Agent is a string anyone can send; scrapers borrow GPTBot's to get past
the rules people write for it. Verify addresses compares the log's client
addresses with the ranges each operator publishes (OpenAI, Google, Bing,
Perplexity, Apple) and reports, per crawler, whether every visit came from those
ranges, some did, or none did — with a few of the offending addresses, enough to
look up or block. Operators that publish no list, Anthropic among them, are
reported as unverifiable, never as verified. The published lists are
downloaded and cached locally for a week; your log's addresses never leave the
machine.
Privacy: the log is read where it lies. It is never copied into the app's data directory and never uploaded — access logs contain visitor IP addresses. Parsing streams line by line, so a multi-gigabyte file never lands in memory, and the report states what share of lines it could actually parse rather than presenting a partially-read file as a quiet site.
Limitation: user-agents are trivially spoofed. This reports what the log claims; confirming a visitor really was GPTBot needs a reverse-DNS check, which Sjá does not do.
Generating llms.txt and structured data
Sjá flags a missing llms.txt or missing JSON-LD. It can also write them — from
crawl data, not from a model's imagination.
llms.txtfrom a crawl (on the site-report): an H1 site name, a summary blockquote taken from the home page's own description, then sections grouped by the first path segment, each entry a real URL with its real title and description. Pages that returned an error, have no title, or are marked noindex are left out — advertising a page the site tells search engines to ignore would contradict the site itself. Output is deterministic, so re-running produces a diff of content changes rather than reshuffled sections.- FAQPage schema from a page (on the page report): built from the page's own question-style headings and the prose beneath them. A heading with no answer under it is skipped, and a page with no Q&A produces nothing — with an explanation — rather than invented questions.
- Article schema from a page: title, description, language and (optionally) the
agency name from Settings. No
datePublishedis emitted, because the crawl never saw one; claiming a date the page does not state would be a fabrication.
Everything can be copied, or saved into the project's checkout — through the same sandbox and the same confirmation dialog the AI agent uses. There is one write path, not a quieter second one.
Why deterministic and not AI-written: a generated llms.txt listing real URLs
is verifiable; a hallucinated one is a liability on a client's site. Structured data
invented for a page contradicts its content, which is a manual-action risk rather
than a quality issue. The Run with AI prompts now point at these generators and
tell the model not to invent fields.
Site crawl
Crawl a whole site from a seed URL:
- Discovery from
robots.txtSitemap:directives +/sitemap.xml(including sitemap indexes), then internal-link BFS. - Respects
robots.txtdisallow rules, with a politeness delay. - Aggregates per-page scores into a site score, category averages, and an issue inventory (how many pages each issue affects).
- Flags broken pages (4xx/5xx) and duplicate titles/descriptions. Broken pages are reported but not scored — a 404 scores near zero in every category, so averaging them in would lower the site score for a reason the report attributes elsewhere. When the counts differ, the header reads "N crawled · M scored". Duplicate detection skips them too, so a site full of "404 Not Found" doesn't produce a phantom duplicate-title finding.
- Honours
Crawl-delayas an actual rate limit: when a host declares one, the crawler drops to a single request at a time instead of spacing the starts of four parallel ones. - Stoppable: hit Stop while a crawl runs. No new pages are started, the requests already in flight are dropped, and the pages finished so far are aggregated and saved as a normal crawl — marked stopped early in the report. Stopping before the first page finishes saves nothing and reports an error.
Click any page in the results to open its full report (and fix it with AI).
Internal links (Jev)
Internal linking is a classification problem dressed up as writing: for each page, which other pages on the site does it talk about without linking to, and is there already a phrase in the copy that could carry the link? From a stored crawl, Plan internal links answers it for every page — with a TypeSafe key, since Jev does the judging.
Code does the lookups. For each crawled page it excludes error pages, noindex
pages and every page the source already links to (the crawl stores each page's
outgoing links), then ranks the rest by how many of the target's title words the
source text uses, ignoring words most titles share (the brand, a section name).
Up to twelve candidates per page go to Jev in one request, each with two
yes/no questions: does the source discuss the candidate's subject at a point
where a link serves the reader, and does the longest title phrase found in the
source copy read as link text for it. A link is proposed from 60 % probability;
an anchor phrase from 60 %, otherwise the proposal says add a sentence.
The plan is stored with the crawl and shown grouped by source page, with the phrase to link, the probabilities, and the pages where nothing honestly fits listed as such — a plan that proposes a link for every page is a plan you cannot trust. Copy as Markdown hands the list to whoever edits the site. Nothing is written anywhere. The cost line shows the tokens used and what they cost at TypeSafe's published price: a hundred-page crawl is a few tenths of a cent.
Two limits. A crawl made before this feature does not carry the link graph, so the plan asks for a fresh crawl rather than propose links that exist. And Jev reads English best; the candidate and anchor logic is language-agnostic, the judgments are Jev's.
Trends & comparison
For each URL in a project, Trends shows score-over-time with a sparkline and the delta vs the first run. Pick two runs to compare — which checks were fixed and which regressed, plus the score change.
Monitors
Scheduled re-scans:
- Add a monitor (URL, keyword, objective, interval: hourly/6h/daily/weekly).
- Each due run analyzes, saves to history/trends, and raises an alert on regression (score drop ≥ 5) or fetch error — with a native notification.
- The bell in the toolbar and the Monitors sidebar badge show unread alerts.
While the app is open, due monitors are checked every minute. To make a daily
monitor daily even when the app is closed, turn on Run in the background in
the Monitors view (or in Settings). That installs a launchd agent for your user
that runs elyra-sja tick every fifteen minutes; tick opens the database, runs
whatever is due, records that it ran, and exits — no window. The Monitors view
shows when the last background run happened. Turning it off removes the agent.
The app and the agent can both be running; a monitor is claimed before it runs,
so it runs once. tick never creates or migrates the database: on a machine where
the app has not been opened, or after an update the app has not yet applied, it
says so in its log (~/Library/Logs/Elyra Sja/tick.log) and does nothing.
The run log
Scheduled work fails quietly by nature, so every attempted run — a monitor, a
scheduled probe, a background pass — is a row in the Run log under Monitors:
when it started, which process ran it (the app or the background agent), whether
it ended ok, failed or skipped, why, and when it will be tried again.
The retry policy is explicit. A run whose fetch failed (a network error, or an
HTTP 4xx/5xx) is retried after 15 minutes, then 30, then 60; after three failures
in a row the job waits for its normal interval, and the row says which of those
applies. A run that produced a page resets the count. A run the process never
finished — the machine slept, the app quit, the agent was killed mid-run — is
marked failed with that reason the next time any runner looks, and the job's
next attempt is its normal interval. A monitor that keeps failing shows N failed
in a row on its row, and the background banner warns when the agent has not
reported in for two intervals.
Reports
Client-ready, print-optimized PDF exports, branded with your agency name (from Settings):
- Page report — score, category scores, and all findings.
- Site report — average score, issue inventory, broken pages, duplicates, and the full page table.
Export opens the report in your browser; use ⌘P → Save as PDF. JSON export is also available for a page report.