See what
they see
.
Search engines and AI answer-engines read your page very differently from a human. Sjá shows you exactly what each of them sees — then turns every finding into a fix, and lets an AI agent apply it in your actual files. Pages are fetched and analyzed on your machine; your projects never leave it.
Apple Silicon macOS · 3-day trial, then a license code · offline activation
Old Norse
sjá
/sjaː/ · verb
“to see” — to look, to perceive, to understand.
The Old Norse sjá is the ancestor of the Norwegian se and the English see. It carries both meanings the word still has today: to look at something, and to understand it. In the sagas it is also the verb of the seer — the one who perceives what others cannot.
That is the job. A crawler and a language model both “read” your page, but neither reads it the way you wrote it: one sees markup, headings and link graphs; the other looks for claims it can quote and attribute. Sjá makes both views visible — and actionable.
It joins the rest of the Norse-named Elyra family: Askr, the world-tree that carries the load, and Ratatosk, the squirrel that runs messages up and down it.
We were tired of
five apps and a spreadsheet.
Auditing one page used to mean opening a browser extension for the meta tags, a web tool for the structured data, a crawler for the site, PageSpeed in another tab, a spreadsheet to remember last month's score — and then a chat window to ask a model how to fix any of it. Six tools, four subscriptions, no memory, and every finding copy-pasted by hand.
So we built the app we wanted: everything in one place. Analysis, crawling, structured data, Core Web Vitals, history, trends, monitoring, AI fixes and client-ready reports — one native binary on your own machine, with your projects in a local database.
One app, not six
Audit, crawl, structured data, Core Web Vitals, history, monitoring and reporting share one workspace — and one mental model. No exporting between tools.
Local-first, not a SaaS
Pages are fetched and analyzed on your machine; projects and history live in a local SQLite database. No crawl budget, no seat-based dashboard, no data leaving your laptop.
Findings become fixes
Every warning is one click from a context-aware fix prompt — and, with your code folder connected, from an AI agent that edits the real files behind a confirmation step.
Ranking is not the
only game any more.
More and more of your traffic never sees a results page. Someone asks an assistant, and the assistant answers — citing whichever pages were legible and quotable enough to use. That is Generative Engine Optimization, and it is scored differently from classic SEO.
Sjá grades it as a first-class category: JSON-LD structured data, llms.txt, answer-style headings that map to real questions, and citable schema types — Article, FAQPage, HowTo — that let a model quote you with attribution.
And since 0.5.0 it stops predicting and starts measuring: point it at your access log and it reports which AI crawlers actually came — and whether your own robots.txt is quietly turning them away. Then it writes the missing pieces from pages it really crawled.
Scored categories
Weighted checks across every category, an overall score (0–100) and a grade (A–F). Pick an objective — audit a page, crawl a site, AI visibility, track changes, fix & optimize, get indexed — and it decides both what RUN does and which signals the weighting favours. A category that measured nothing is left out rather than scored a flattering 100.
From one page to
the whole site.
Page analysis
An objective, a URL, RUN — in that order since 0.8.2, which is the order you think in; the keyword is optional and blank means Sjá takes the page's main term, and language, market, crawl size and interval wait behind Options. The objective decides what RUN does rather than only how the score is weighted: Crawl a site crawls, AI visibility opens the report at the citation probe, Track changes runs and creates the daily monitor, so a page you want watched is watched from the first run. Below the launcher, Continue lists the pages you have already analyzed with their trend and Re-run, Probe or Monitor on the row. Sjá fetches the page, parses the HTML and returns weighted checks across six categories — seven once PageSpeed adds Core Web Vitals — with an overall score and grade, every finding backed by the actual value it found. Since 0.6.0 the GEO category is built along the path a page actually travels — access (may the answer crawlers fetch it, and does the server let them when they ask), content (what the engine extracts, where its reading budget ends, which passages it could quote), and outcome (is the page cited, did the crawlers come and keep coming) — fifteen checks that decide roughly half the score under Improve AI visibility. Scores moved between 0.5.1 and 0.6.0: up for pages the crawlers can reach and read, down for pages behind a bot-blocking CDN or a JavaScript shell. Which is the point.
hreflang, validated
Misconfigured hreflang fails silently — search engines discard the whole annotation set without reporting an error. Sjá checks BCP 47 syntax (catching en_US, en-UK, en-EU), self-reference, absolute URLs, conflicting targets, x-default and canonical agreement. A single-market page is never penalized. Analyze as a market with Language + Market, sent as Accept-Language.
Search Console, your own client
Connect Google Search Console with a credential from your own Google Cloud project — PKCE, a loopback redirect open only for the duration of the flow, refresh token in the Keychain. List the properties an account can read, one account per app or per project, so an agency keeps each client's Google access isolated.
Core Web Vitals
Enable PageSpeed to add a sixth category: performance score, LCP, CLS, TBT (an INP proxy) and FCP. If the API is unavailable it degrades to a neutral note instead of hurting your score.
Fix-prompt builder
Click any warning to append a context-aware, copy-paste-ready fix prompt — with the page URL, keyword and real values — to a stack. Copy the lot, or hand it to the agent.
AI fixes in your files
Connect a project to a local code folder and the agent gets sandboxed file tools scoped to it. It explores, reads and edits the real files, streaming its work — pausing for a preview and your approval before each write.
Site crawl
Seed a URL and Sjá discovers pages from robots.txt sitemaps and internal links, respects disallow rules with a politeness delay, then aggregates a site score, an issue inventory, broken pages and duplicate titles/descriptions. Stop it mid-flight and everything crawled so far is saved, badged as stopped early so a partial crawl is never mistaken for a complete one. And since 0.8.1 a crawl can plan the site's internal links. Internal linking is a classification problem that has been priced as writing: for every page the questions are small and many — does this page have a real reason to link to that one, and is there already a phrase in its copy to hang the link on? Code finds the candidates first (excluding error pages, noindex pages and everything already linked, ranking by the title words the source text uses minus site-wide words) and the anchor phrase; Jev answers the two yes/no questions per pair in one request per page. The plan lists, per page, the pages it discusses but does not link to, with the phrase and the probabilities — and the pages where nothing honestly fits are listed as such, because a plan that finds a link for every page is not one you can trust. It needs the TypeSafe key from 0.8.0, sends each page's main text (bounded) and a dozen sibling titles, costs a few tenths of a cent for a hundred pages, copies as Markdown, and writes nothing to the site. A crawl made before 0.8.1 has no link graph, and the planner says so rather than propose links that already exist.
Trends & comparison
Every run is kept per project. Watch score-over-time with a sparkline and the delta since the first run, then diff two runs to see exactly which checks were fixed and which regressed.
AI share of voice
One probe is an anecdote; share of voice is the number that answers how are we doing overall. Since 0.9.0 Trends opens with the share of engine answers over the last 28 days that cite your site, the change against the 28 days before, a weekly line, a split per engine, and the sites you share the answers with — yours always among them, even at zero, because a list that quietly leaves you off is a list that flatters. It is computed from probes you already ran and needs no key: an answer counts once however many of your URLs it cites, and a brand named without a link is shown on its own line rather than rounded into either side. The questions it is built on can come from your own site — the crawl proposes your question headings and FAQ entries, keeps the ones a person would actually ask an assistant, and turns the ones you tick into weekly probes. That costs money every week, and the weekly call count is shown before anything is scheduled.
Monitors
Re-scan URLs on a schedule — hourly, 6-hourly, daily, weekly. A score drop of 5 or more (or a fetch error) raises an alert with a native notification, and the bell counts what you have not read. Until 0.7.0 that schedule meant daily if the window happened to be open, which is not what the word says. Run in the background installs a launchd agent for your user that runs elyra-sja tick every fifteen minutes — off until you turn it on, removed when you turn it off. The app and the agent may be running at once and a monitor still runs exactly once, because a claim writes last_run_at only if it still holds the value it read. The runner never creates or migrates the database: the app is the only migrator, and a pending migration means open the app once, logged and skipped rather than guessed at. And the app re-registers the agent when the binary has moved, so an update cannot leave a schedule pointing at nothing. What 0.7.1 adds is the half of scheduled work nobody sees: the failures. A monitor that could not fetch its page, a probe whose engines all errored, a pass that never finished because the machine went to sleep — each of those used to leave no trace but a missing data point, which is indistinguishable from a quiet week. Every attempted run is a row now, with what ran it, how it ended, why, and when it will be tried again: after 15, 30 and 60 minutes, then back to the normal interval, rather than whenever the interval next comes round. A run the process never finished is marked failed with that reason the next time any runner looks, instead of passing for a run that never came due. The banner says when the agent has not reported in for two intervals, and a monitor says N failed in a row.
Did the AI crawlers actually come?
Point Sjá at an nginx or Apache access log and it reports which AI crawlers really visited: hits, distinct pages, status split, first and last seen, top paths — and each crawler's own robots.txt verdict. Answer engines are separated from training and search crawlers, because blocking the first means absence from answers today. Logs are read where they lie, never copied and never uploaded: an access log is full of visitor IP addresses. 0.6.0 keeps visits per crawler per day, so Did ClaudeBot come back after we changed robots.txt? is a chart in Trends rather than a guess. And because a User-Agent is a string anyone can send, Verify addresses compares the log's client addresses with the ranges each operator publishes and says per crawler whether every visit came from them, some did, or none did. Operators that publish no list — Anthropic among them — are unverifiable, never verified. The lists are cached locally; no address from the log leaves the machine.
The robots.txt trap it found
Sjá used to read only the User-agent: * group — so a site that allows * and disallows GPTBot, which is the most common way to be invisible to answer engines, was reported as not blocked. The parser now reads every group and resolves the most specific match, the crawler obeys rules written for its own token, and Crawl-delay is read from the group that actually governs it. But robots.txt is advice. Since 0.6.0 the page is fetched a second time with OAI-SearchBot's real User-Agent, because that is the only way to see a CDN's block-AI-bots switch, a WAF rule or a bot challenge — Cloudflare's Just a moment is recognised even on a 200. Blocking an answer crawler is a fail: they fetch pages to answer questions now. Blocking a training crawler is a weight-zero note: a licensing choice, not a visibility problem. And a page with under fifty words of HTML behind an empty app root fails too — AI crawlers execute nothing.
Cited by answer engines? Ask them
Everything else in GEO predicts; the citation probe asks. From a page's report, type the question a user would ask and Sjá puts it to ChatGPT, Claude, Perplexity and — since 0.7.0 — Gemini, through Google Search grounding, because Google's answer surface was the one this did not cover, with your own keys, concurrently, and judges each answer: does it cite the page's site, at which position among the sources, and who was cited instead. Every probe is a row — failures included, so a gap in the history is visible as a gap — and Trends shows the latest verdict per page and engine with the earlier ones as dots. The interface says what a probe is: one sample of a non-deterministic system, through APIs close to but not identical with the consumer products, at a few cents per engine on your own account. What to read is the trend and the competitor list, not one answer. 0.7.0 makes that trend something you do not have to remember to collect: Schedule repeats the question daily or weekly, and an engine that starts or stops citing you raises an alert naming who took your place. It costs what it costs, and the interface says so — a daily probe on four engines is four API calls a day on your own account until you pause it. And an answer that names you without linking is recorded as what it is: mentioned, no link, kept out of both the cited and the not-cited counts, because a mention is neither. And since 0.8.0 the probe can say why. Until now a no was a dead end: was the page not good enough, or good and simply not chosen? With a TypeSafe key, every probe is also judged by Jev — a model that answers typed questions about text rather than writing it — from the page's own text and the engines' answers. Coverage rates how completely the page answers the question, which splits not cited into an authority problem (the page answers; the engine chose other sources) and a content problem (it does not) — two different jobs the card now names. Fidelity checks each thing a citing engine says about the site against the page, so a citation that gets your price or your terms wrong shows as misrepresented, with the claim, and a scheduled probe raises an alert the first time it happens. Stance says whether each answer recommends you, lists you, references you or advises against you — for every engine, cited or not, so mentioned now says whether the mention helps. It is opt-in and off until you add the key, and the boundary is stated where you add it: with a key, the probed page's main text and the engines' answers leave the machine for TypeSafe; without one, probes work exactly as before. Jev's answers are shown and stored, never acted on, and a stance is only stated from 0.5 confidence. Jev reads English best; the interface says so. And since 0.9.0 each competitor on a probe card has a why them? button: Sjá fetches the page the engine cited instead of yours, picks the passages that touch the question, and asks whether each one helps answer it and whether your page already says the same. The answer names which of two problems you have — a content gap, with the points they make that you do not, in their words; or an authority gap, where you answer as well and the engine chose them for another reason, which no rewrite will fix. The competitor's URL came from an engine's answer, so it is treated as untrusted and private addresses are refused.
What the model sees
Engines do not read pages. They extract the main content, keep what fits a reading budget, and quote passages. Since 0.6.0 every report carries that view: the extracted text in reading order with navigation, footers, cookie banners and sidebars removed the way a readability extractor removes them, a marker at roughly 2 000 tokens with every block knowing whether it starts inside it, and the five passages most likely to be quoted, each scored 0–100 with the reasons on both sides — 40 to 90 words, stands alone, sits directly under a heading, carries a figure or a date, begins inside the budget. The gaps are what a writer changes. The MCP server and the CLI return the same extraction, so an assistant edits the exact text the engine will see.
Generates llms.txt and JSON-LD
Sjá already told you these were missing; now it writes them from pages it actually crawled. llms.txt lists real URLs with their real titles, excluding error pages, untitled pages and anything marked noindex — advertising a page the site tells search engines to ignore would contradict the site itself. FAQPage schema is built from the page's own question headings and the prose beneath them; a page with no Q&A generates nothing, with an explanation, rather than invented questions. Output is stable across runs, so a diff shows content changes rather than reordering. And since 0.6.0 structured data is read for what an engine needs rather than counted: a block that is not valid JSON is a fail that says so instead of reading as no structured data — same outcome for the engine, different fix; required properties per type fail when missing and recommended ones warn; the publisher entity behind the page is named or found absent, and a crawl lists every Organization the site claims to be; and the dates are held to agree — modified before published fails, two modified dates that disagree warn, no dates at all is a note, not a penalty.
A budget your CI can fail on
elyra-sja check https://example.com --budget 80 runs headless: no window, no database, not even the app data directory. Exit codes are the interface — 0 met, 1 a budget missed, 2 could not run — deliberately distinct so a pipeline can tell a score regression from a broken site. Budget per category too (--budget-geo 70), because a GEO regression otherwise hides behind a healthy overall score, and a budget on a category that was never measured fails rather than passes.
An MCP server, so a model can ask
elyra-sja mcp speaks JSON-RPC over stdio, so Claude Desktop, Zed or Cursor can call the analysis while someone edits content — a model driving Sjá rather than the other way round. Two read-only tools: analyze_page, and robots_check for whether a named crawler may fetch a URL. Nothing that writes files or spends an AI key is exposed, because an external agent must not be able to edit a checkout through this door. And since 0.5.1 it refuses local and private targets by default: its URLs come from an assistant that may have been steered by a page it just read, so analyze_page and robots_check reject a local target before any fetch, naming the opt-in — ELYRA_SJA_ALLOW_PRIVATE=1 in the client's env block audits a dev server through an assistant. A valid licence does not lift this. Client-by-client setup, the tool contracts and real output are in the MCP guide in the docs.
Client-ready reports
Print-optimized page and site reports branded with your agency name: scores, category breakdowns, every finding, the issue inventory and the full page table. Export to PDF, or JSON for a page.
Secrets in the Keychain
Your AI API key and license code live in the macOS Keychain — never in the SQLite file, never in a config you might commit.
A sandbox that holds
The agent's file tools are scoped to your project folder and the walk is symlink-safe — including dangling symlinks, the case that quietly defeats an exists() check. And because Sjá fetches URLs it did not choose — redirect targets, the sitemaps a robots.txt names, whatever an assistant hands the MCP server — those requests are vetted by where a host resolves, not only by what the URL looks like. That sentence is true since 0.5.1. Before it the guard ran only inside the redirect policy, so it never saw the first request or the sitemap URLs, and as a check on address literals it was bypassed by any hostname that resolves to a private address. A filtering resolver now drops loopback, private, link-local, CGNAT and IPv6 unique-local answers; the one host you typed yourself is exempt, so auditing localhost:3000 still works, and a redirect from it to a different private name is refused. The same review found the fix agent could not overwrite an existing file at all — a path that already existed gained a trailing separator and every write failed with Not a directory; creating files worked, so the sandbox tests never noticed. Writes are atomic now, temp file beside the target and a rename. Each fix ships with a test that fails against the version before it.
One binary. No Electron.
Sjá is built on the Elyra Framework — Rust on the inside, Svelte 5 on the outside, compiled to a single native binary with no bundled Node runtime and no browser engine to ship. It starts instantly, idles at nothing, and the whole app is one file you can drag to Applications.
- Rust + Svelte 5
- Native binary
- SQLite
- Local projects & history
- Keychain
- Secrets at rest
- Ed25519
- Offline license
Download Sjá
Signed and notarized for Apple Silicon macOS. Every install runs a 3-day trial with full functionality; after that, paste a license code in Settings → License.
Requirements
macOS on Apple Silicon (M1 or newer).
Install
Open the DMG and drag Elyra Sjá to Applications.
Activate
Settings → License → paste your code. Verified offline.
Buy it once a year.
Or once, forever.
One license covers the whole app — every category, the crawler, monitors, AI fixes and branded reports. No per-crawl credits, no feature gates.
Pro
For the person who owns the site.
up to 3 devices
- Every check & category
- Site crawl & issue inventory
- Trends, comparison & monitors
- AI fixes in your own files
- Branded PDF reports
- A year of updates
Studio
For an agency or an in-house team.
up to 10 devices
- Everything in Pro
- Up to 10 devices
- Shared team database Soon
- Client-ready branded reports
- Unlimited projects & monitors
- Priority support
- A year of updates
Lifetime
Pay once. Keep it.
up to 3 devices
- Everything in Pro
- Never expires
- All future updates
- Up to 3 devices
- One-time payment
- Launch offer
One database for the team
Coming soonToday every seat keeps its own local database. On a Studio licence that becomes a choice: point the whole team at one shared database, so projects, history, trends and monitors are the team's — not each laptop's. Shipping in a future update; tracked in the changelog.
Offline activation, no phoning home
Your code is an Ed25519-signed token. Sjá verifies it against a public key baked into the app, so activation works on a plane — and we never see your usage. Details in the licensing guide.
Try before you buy
Every install includes a 3-day trial with full functionality — no card, no account. Already bought? Manage your license and re-download any time from your account.
Stop guessing what
the crawler saw.
One app. Your machine. Every finding traced back to the markup that caused it.