Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra

Features

Page analysis

Enter a URL (+ optional keyword), pick an objective, 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).

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.

Objectives bias the weighting (e.g. Improve AI visibility 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 and Perplexity 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;
  • 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.

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.

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 without image, author, datePublished, dateModified); HowTo needs name and step; Product needs name and one of offers, rating or review; FAQPage and QAPage are checked question by question — a Question without a name or without an answer text is 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, a logo and sameAs links 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 / dateModified in schema, article:published_time / article:modified_time meta tags, the first <time datetime> element and the Last-Modified header 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 charsetContent-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-Language at 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-GBR and the like. en-UK and en-EU get specific advice — they look right but are not codes search engines honour (the UK is GB; the EU is not a country).
  • Self-reference — every page in a set must list itself, or the set is dropped.
  • Absolute URLshreflang requires 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 agreementhreflang pointing at a URL that is canonicalized elsewhere is ignored without warning.
  • <html lang> vs hreflang — 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-Extended and Applebot-Extended never 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.txt blocks 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 from robots.txt alone.

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.txt from 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 datePublished is 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.txt Sitemap: directives + /sitemap.xml (including sitemap indexes), then internal-link BFS.
  • Respects robots.txt disallow 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-delay as 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).

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 that run while the app is open (checked every minute):

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

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.