1
0
mirror of https://github.com/alexandrev/xslt-lab.git synced 2026-09-13 08:43:16 +00:00

169 Commits

Author SHA1 Message Date
alexandrev-tibco e6d0725268 Footer: enlace a Buy Me a Coffee
BMC pide verificar la cuenta con pruebas publicas de donde se comparte el
enlace. Un enlace estatico en el footer — visible y capturable — en vez del
widget flotante que se retiro en cc23601 por intrusivo (0 conversiones).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MuqqUBQZmX5dHaFgpqtAPv
2026-09-10 06:09:58 +02:00
claude-code c69934f5f2 Stop sending the two documents that cannot compile, and explain the third
Four days of the new gate in production, checked against the logs. It holds:
replaying a browser against the live site, a half-typed select is not sent and a
complete one is, and the errors it catches fell from 13.8% of a day's transforms
to 0% as cached bundles aged out. What the logs also showed is that the error
mix has moved on, and the top two are things it never looked at.

**92 times in 48 hours** — the single most common error in production — the
document in the stylesheet pane was not a stylesheet at all: well-formed XML
whose root element never declares the XSLT namespace, someone's input document
pasted into the wrong pane. It parses, so the well-formedness gate waves it
through, and it has no XSLT elements, so the expression gate finds nothing to
object to. It cannot compile under any reading — not xsl:stylesheet, not
xsl:transform, not a simplified stylesheet, which needs the namespace for its
xsl:version — so it is now decided in the browser, and needsStylesheetReset
counts it too, which puts "Restore the starter stylesheet" one click away for
someone who pasted into the wrong box.

**29 times** the same person hit `namespace="{if (…) then $ns else ''}"` running
as XSLT 1.0, and all Xalan would say was "Error parsing XPath expression
'null'". That one is not a mid-edit and should be sent — it is a genuine attempt
that a 2.0 processor would run. But the existing version hint only searches the
error text for function names, and there is no function here, so it stayed
silent. It now also reads the stylesheet: `if…then…else`, `for $x in`,
`some/every … satisfies`, `instance of`, `cast/castable/treat as` and `=>` are
looked for in expression attributes and in the {…} of attribute value templates
— parsed, so comments and literal output cannot trigger it. The error box offers
"Switch to XSLT 2.0", which is the answer those 29 attempts were looking for.

And a close bracket with nothing open is now treated like one that is never
closed: Saxon's "Unexpected token \"]\" beyond end of expression" showed up seven
times in one sitting, which is someone deleting the opening bracket, not a
considered mistake. It was deliberately left through before; that was the wrong
call.

On 28 failures pulled from the last 48 hours of logs: 8 no longer leave the
browser and 14 now carry the version hint, against 1 and 0 before. The 30
older ones still stop at 24. The module is renamed autoRunGate.js, which is
what it has grown into — the checks that decide whether the automatic run is
worth a round-trip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HnUwBeXv6PCTEzRpFoMSU
2026-08-23 08:35:41 +00:00
claude-code d34c969274 Test: assert on what was sent, not on the call count
The new gate's test failed on CI and passed everywhere locally. A run debounced
by an earlier test fires 2s later, lands on the fresh fetch stub that beforeEach
installed for this one, and "fetch was never called" stops being true through
no fault of the code under test. Checking that no request carried this
stylesheet says what the test actually means and does not depend on timing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HnUwBeXv6PCTEzRpFoMSU
2026-08-19 19:42:33 +00:00
claude-code e1e3b81610 Also stop the expression that was closed before it was finished
Replaying a day of real production failures through the gate showed the shape
it was missing: the predicate gets typed around an expression that is not
written yet, so the brackets balance and nothing looks wrong at the end of the
string — `[@CategoryId = ]`, `[@CategoryId = current()/]`, `[@Id = current()/@]`.
Xalan reports each of them as "Syntax error in '<expr>'", and they were still
being sent.

An operator, a step separator or an @ sitting immediately before a closing
bracket is the same unfinished edit as one at the end of the expression, so it
is treated the same way. Empty parentheses stay legal — count(), true(),
position() — because that is a function call; an empty predicate does not exist
and is flagged.

On the same 30 real failures the gate now holds back 24. The six that go
through are genuine mistakes with genuine messages: an undefined variable, a
function called with the wrong arity, an unsupported element.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HnUwBeXv6PCTEzRpFoMSU
2026-08-19 19:36:01 +00:00
claude-code 4e1f3ffdac Fix: give the backend room to run, and stop transforming half-typed expressions
The backend container runs three JVMs — Saxon 12, Saxon 9.6 and Xalan — whose
-Xmx values add up to 512 MB, inside a container limited to 256Mi. The limit was
half the heap the JVMs believed they had, so the kernel was killing the
container: OOMKilled twice a day on the 16th and 17th of August, five times a
day on the 18th and 19th, and 81 users in 24 hours got "transform service
unavailable" instead of a result. Before each kill the serial collector spends
hours in continuous full GC, which is why three of the five pods sat pinned at
exactly 0.5 CPU — their limit — around the clock, and why XSLT 2.0's p95 was
5.5s against 0.7s for the other two engines.

Those limits were never in the chart to begin with: the backend deployment has
never rendered a resources block, values.resources.backend was declared and
unused, and the live numbers came from a manual kubectl patch that an ArgoCD
sync would have silently dropped. So the chart now owns them, with room for
what actually runs in there. The CPU request moves 10m -> 250m as well: against
real usage of 500m the HPA read utilisation in the thousands of percent and the
deployment sat permanently at maxReplicas, unable to signal anything. And the
backend gets minReplicas 2, for the reason the frontend already learnt.

The other half is the mid-keystroke problem again, one level down. Gating the
automatic run on well-formedness took the error rate from 54% to 24%, but a
stylesheet whose XPath is half-typed is perfectly good XML, so it still went to
the backend and still came back as a compile error. A day of logs is mostly
that, keystroke by keystroke: 'current()/..[@N' then '[@Na' then '[@Name', and
"Required attribute 'select' is missing" 65 times on a single line. So the gate
now also declines to send an expression that is provably unfinished — a
required attribute that is absent, an empty value, a quote or bracket still
open, a trailing token that cannot end an expression. Facts about the text, not
guesses at intent: there are twice as many tests for what it must leave alone
as for what it catches, including every template the app itself ships. It is
skipped above 64KB, where a document was pasted rather than typed, and as
before it only holds back the automatic run — "Run it anyway" is always there.

Finally, the error classifier learns the rest of what was sitting in "other":
Xalan's "Syntax error in '<expr>'" is the stylesheet's XPath, not the input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HnUwBeXv6PCTEzRpFoMSU
2026-08-19 19:30:02 +00:00
claude-code 40b81728d6 Competitive batch: Saxon 12.9, 2.0 tracing, saved fiddles, multi-engine story
From the August competitive analysis, the items that close real gaps:

- Saxon HE 12.5 -> 12.9 (the closest competitor already ships 12.9). The
  trace/hotspot instrumentation is reflection-based, so it was verified
  against the new jar: variables and execution counts come back intact.

- Tracing now works for XSLT 2.0. The trace infrastructure needs Saxon 10+
  APIs and cannot load inside the Saxon 9 daemon, so traced 2.0 requests run
  on Saxon 12 in backwards-compatible mode instead of silently returning an
  empty trace — a real trace with a small, labeled semantic difference.
  Untraced 2.0 runs still use Saxon 9. XSLT 1.0 now says outright that XSLTC
  has no tracing hook rather than pretending.

- Saved fiddles: POST /fiddle stores the workspace under a 7-char id with an
  append-only revision history; ?f=ID loads it (optionally &r=N). Available
  whenever DATABASE_URL is configured — unlike /history this is
  unauthenticated, because a fiddle is a thing you share with someone who has
  no account. An unreachable database degrades to "fiddles off" instead of
  crashlooping: the chart has shipped a placeholder databaseUrl for a long
  time, and opening it eagerly would have taken production down.

- The home page now tells the truth about engines: 1.0 on XSLTC, 2.0 on
  Saxon 9, 3.0 on Saxon 12.9 — three real processors was already the
  architecture, it just was never marketed.

- Four posts aimed at where the demand data points: the FreeFormatter
  shutdown audience, Chrome's XSLT removal (v158, Nov 2026), Peppol/EN-16931
  Schematron validation and ISO 20022 camt transformations. Every example was
  executed against the real backend and the published output matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-14 11:39:00 +00:00
claude-code 864a5da6a4 Analytics: measure the landing pages, which were invisible
The app has Clarity, but the four landing pages loaded no script at all, so
their traffic never appeared anywhere except Search Console — and those are the
pages built to rank.

Adds a self-hosted Umami (K8s namespace umami, the shared Postgres, TLS
terminated at NPM). Cookieless, so no consent banner, and served from our own
domain, which matters for an audience of developers: this homelab's own Pihole
resolves clarity.ms to 0.0.0.0, and the people using the site block trackers at
the same rate, so Clarity is very likely undercounting.

Note this does not replace Search Console: neither Umami nor Clarity can see
search queries, and the keyword data was already there unused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-05 18:42:36 +00:00
claude-code 3682d35cb4 Ads: monetise the four landing pages, which carry the most content and none
Only the home page was earning anything, while the pages with the most text —
and therefore the ones that rank — carried no inventory at all: 1,620 words on
/xpath-tester/ and 1,651 on /xml-to-json/ against 915 on the home.

One unit per page, which is also the most EthicalAds allows ("we currently
only allow a single EthicalAds ad per page"). Placed at the top of <main>,
after the nav and before the first section, so it sits above the fold on both
desktop and mobile and above the main content rather than inside it, as their
placement policy requires. The slot reserves its height, so the ad appearing
doesn't shift the page.

These pages are plain static HTML and never load the React bundle, so they use
the standard EthicalAds embed rather than the app's ad logic.

Verified in a real browser: one unit per page, in the layout, above the fold at
1280x900 and at 390x844, and visible once content is injected. The units render
empty here because EthicalAds filters datacenter IPs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-05 10:33:38 +00:00
claude-code b44677ad98 Perf: don't transform a document that is only half-written
The editor re-runs on a 2s debounce, so it was sending the backend whatever
was in the buffer at that moment — including documents caught mid-keystroke.
Cross-checking the metrics with Clarity made the scale clear: ~45 human
sessions a day producing ~2,500 transforms, of which 54% "failed". That is ~55
transforms and ~30 errors per session, which nobody types on purpose.

The error text says the same thing: 190 truncated documents, unterminated
xsl:text / xsl:template / xsl:stylesheet, and "Open quote is expected for
attribute select" — someone partway through typing select=".

So the run is now gated on the document parsing at all, using the DOMParser
check the editor's linter already relies on. A parse error is reported locally
instead, which is both the same information and sooner: no round-trip, no
error flashing at someone who is still typing, no backend capacity spent on a
document that cannot compile — and it stops feeding the rate limiter that was
banning real users.

Only the automatic run is gated. The error offers "Run it anyway", which
records the exact text it forced so the gate steps aside for that document and
returns on the next edit: if DOMParser and Saxon ever disagree, the user is
never stuck.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-04 20:23:58 +00:00
claude-code 053a7d95e5 Fix: stop rejecting valid enterprise stylesheets, and classify the rest of "other"
Three things the error review turned up.

Secure processing caps an XPath expression at 100 operators, so the JDK was
refusing perfectly valid stylesheets with JAXP0801002 — a limit this service
imposes, not a mistake by the author. The XSLT 1.0 daemon now raises the op,
group and total limits instead of switching secure processing off, which also
guards external entity access; a runaway expression is still bounded by the
10s transform timeout, and any -D on the command line still wins.

That error is now classified as "backend" rather than "stylesheet": it belongs
with the bug candidates, not with user error, or the dashboard hides it.

The remaining unclassified errors were mostly classifiable all along, and the
patterns come straight from the production logs: multiply-defined and
undefined variables, format-number picture strings and missing Java extension
methods are the author's stylesheet; a stray <?xml ...?> declaration is the
input document.

Finally, calls to Java extension methods now say so: the playground runs stock
Saxon HE, so a class from the author's own project or their ESB runtime will
never resolve, and the raw "Cannot find external method" gives no clue why.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-03 09:43:31 +00:00
claude-code 3925ed9e77 Feat: report how often each construct actually ran, alongside the trace
Static rules can only guess at what is slow; the trace knows. The listener now
also counts template, apply-templates, for-each, for-each-group, call-template
and function executions, and reports the top 25 with the trace, so "this
template ran 4,312 times" is a fact rather than a heuristic.

The counting is kept strictly apart from the variable reporting it sits next
to. That deque pairs enter/leave and stays balanced only because both handlers
filter on isVariable() identically: pushing non-variable frames onto it would
make a variable's leave pop someone else's frame and silently attribute the
wrong value in the UI. countHotspot touches its own map and nothing else, and
handleEnter/handleLeave are unchanged.

Two things this needed along the way:

- Saxon never calls TraceListener.close() before the daemon reads the trace
  buffer, so relying on it dropped every hot spot. attachTraceListener now
  returns a flush the daemon runs once the transform is done.
- getConstructType() is null on these objects in Saxon 12 — which is why
  isVariable() already falls back to class names — so constructs are
  recognised the same way.

Verified end to end against a real transform: the variables still come back
intact (6 entries with their values) and a template applied to five elements
is reported as five executions.

Note for later: only SaxonDaemon attaches the listener, so tracing — and now
profiling — is XSLT 3.0 only. That predates this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-28 16:01:38 +00:00
claude-code 6642181bb3 Feat: review findings proven against the input, and a way back from an empty editor
Two checks that can be stated as fact rather than guessed, because the app
holds both the stylesheet and the input document:

- The input is in a default namespace while the stylesheet matches unprefixed
  names, so nothing matches and the transform silently yields empty output
  instead of an error. The suggested fix is version-aware: 1.0 has no
  xpath-default-namespace, so it gets told to bind a prefix instead.
- The stylesheet declares one version but runs as another. Saxon does not
  reject this — with version="1.0" it keeps 1.0 semantics, and asking for a
  newer version enables forwards-compatible mode, deferring the failure to
  run time.

Both bail out the moment they cannot prove the case: the guards (no namespace,
prefix already bound, xpath-default-namespace present, root-only patterns,
unparseable input) carry more tests than the positive paths, because a
confident wrong hint costs more than a missing one with this audience.

Also: wiping the editor leaves "Premature end of file" — the single most
frequent error in production — and no obvious way back. The error box now
offers to restore the starter skeleton, keeping the selected version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-28 15:46:26 +00:00
claude-code 9d2afd3bf8 Perf: build only the focused editor up front (TBT 80ms -> 16ms)
Clarity reported a 1.6s INP. The per-keystroke path turned out not to be the
cause — benchmarking stripParamBlock/addParams/injectParamBlock plus the
localStorage write at 100KB x 3 workspaces totals ~2ms. Lighthouse against
production instead showed 520ms of total blocking time during load, so an
early interaction lands on a main thread that is still busy.

A large part of that is this component: the page mounts up to five editors and
each one synchronously builds a CodeMirror EditorView. The 'eager' prop already
existed for exactly this distinction but was explicitly ignored. It now means
what it says — the stylesheet editor is built immediately, the rest when the
browser goes idle, or straight away if the user reaches them first.

Until then they render their text in a <pre> styled like the editor, so nothing
moves when the real view takes over and the content is never missing.

Measured A/B on the same local preview: TBT 80ms -> 16ms, CLS unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 18:59:07 +00:00
claude-code bc313f6c5c Feat: XPath tester and XML-to-JSON landing pages
Two adjacent-keyword tools built on the existing /transform endpoint — the
XML-to-JSON one reuses the XSLT 3.0 support the backend already has, so
neither needs any server change. Both link into the app through
?template=<id>, so the page and the tool stay in sync.

Every code sample was executed against Saxon HE 12.5 and matches the output
published on the page byte for byte.

Also links both tools from the home page, adds them to the sitemap (3 -> 5
URLs) and repoints the last blog.xsltplayground.com links left in the app
(footer, news URL and the autocomplete documentation links) at
xsltplayground.com/blog, which until now were only reachable via the 301.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 15:42:09 +00:00
claude-code a4b1535bc0 Feat: compact share links
Share URLs carried the stylesheet and input as plain base64, which for a real
stylesheet produced links too long to paste into a chat or an issue. They are
now gzipped into a ?c= parameter — several times shorter for the repetitive
XML a stylesheet is made of.

Compression is only available asynchronously, so the compact form gets its own
parameter: existing ?xslt= links keep decoding synchronously during startup and
only the new ones take the async path. If CompressionStream is unavailable, or
the compressed link would not actually be shorter, the plain link is used.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 15:37:06 +00:00
claude-code 1580417a4e Feat: compare output against an expected result
Paste the output a stylesheet should produce and the result pane reports a
match or a line-level diff — the manual half of the regression workflow the
testing guide describes, without leaving the editor.

The expected output travels with the workspace, so an exported JSON doubles as
a test case: import it later and you immediately see whether the transform
still produces the same output.

Whitespace and trailing blank lines are ignored by default, since serialisation
noise is not a real difference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 15:34:28 +00:00
claude-code ebce292fa7 Feat: runnable examples on the reference pages via an embedded mode
The 230 function pages linked out to the playground; now they can run their
own example in place. ?embed=1 renders the app without ads or workspace
chrome, plus a link out to the full editor.

The iframe is only created on click: embedding it eagerly on every reference
page would pull the editor bundle into 230 page loads and undo the LCP work.

Also adds ?template=<id> so a URL can open a starter workspace directly, and
an XPath tester template.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 15:31:18 +00:00
claude-code a70b4f05ca Feat: link Saxon errors to their reference page + starter template gallery
Two gaps the error triage exposed:

- The 17 error reference pages on the blog were unreachable from the app: the
  error box printed the raw Saxon message and nothing else. It now links the
  documented codes (and the prose well-formedness errors) to their page.
- 'No source document' was among the most frequent failures — people arrive at
  an empty editor and run it. A Templates button opens six starter workspaces
  (HTML table, grouping, CSV, JSON, identity transform, parameters), each with
  input XML that already runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 15:27:47 +00:00
claude-code 85f38532e1 UX: drop the floating usage survey so feedback is asked once, in context
The new inline satisfaction prompt and the old floating UsageSurvey had no
mutual exclusion: a user who ignored the inline one was shown a second,
fixed-position prompt at transform #3 — the intrusive pattern the contextual
rework set out to remove.

- Remove UsageSurvey (component, test, styles, lazy import, surveyDone state).
- Gate the inline prompt on transformCount >= 2 so it never fires on the very
  first run — ask once the tool has actually delivered value.
- Replace the survey tests with contextual-feedback ones, including a
  regression test asserting a single prompt is ever on screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 07:17:03 +00:00
claude-code 62b3761908 Add error metrics and contextual feedback 2026-07-24 15:35:56 +00:00
claude-code 4724a44a7d Backend: classify XSLT 1.0 (JAXP/Xalan) errors + frontend version-upgrade hint
Triage of transform_error logs (Loki) showed ~500 failures piling into
class="other" — all of them user errors from the XSLT 1.0 processor, which
classifyTransformError didn't recognise (it only knew Saxon phrasing).

Backend:
- classifyTransformError now recognises JAXP/Xalan (XSLT 1.0) messages:
  XML well-formedness failures -> input_xml; undefined-function / misplaced-
  element / illegal-attribute / type errors -> stylesheet. Adds containsAny
  helper and a table-driven test covering Saxon and Xalan phrasings.

Frontend:
- detectVersionUpgradeHint(error, version): when a transform fails because a
  newer-version function is called (e.g. current-date()/tokenize() in 1.0, or
  map:/array: in 2.0), the error box shows "<fn>() is an XSLT 2.0 feature —
  switch to XSLT 2.0" with a one-click button that bumps the tab version.
- Requires the call shape to fire, so plain words don't false-positive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-24 14:30:54 +00:00
claude-code b2737ec715 SEO: fix internal linking, de-cannibalize, move blog to /blog/ subpath
Phase 1 — internal linking:
- Real [related] index (tags/keywords/category) in hugo.toml; rewrite
  related-posts.html to use .Related instead of "first 4 posts", which
  had left 12/20 posts with zero inbound links.
- Add tags to 6 previously untagged posts.

Phase 2 — de-cannibalization of the transactional cluster:
- Soft-301 mechanism (redirectTo -> meta refresh + noindex,follow) in baseof.
- xslt-online-editor-guide redirects to the app (pure duplicate of home).
- xslt-validator-online retargeted informational; drop the "common errors"
  section that cannibalized the errors hub, link to it instead.
- Migrate why-xslt-playground-online-editor into /about/ with an alias.
- Remove the duplicate <h1> on the app landing page.

Phase 3 — consolidate authority on the money domain:
- baseURL -> https://xsltplayground.com/blog/ (served via NPM reverse
  proxy to GitHub Pages; subdomain will 301 to the subpath).
- Rewrite internal blog links to /blog/... and cross-domain app links
  to xsltplayground.com/blog/...

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-24 07:31:29 +00:00
claude-code c9078842d4 UX: fix the top dead-click zones Clarity flagged (38% of sessions)
Clarity showed dead clicks on 38% of sessions, all on desktop. Reproduced
the hotspots in a headless browser: the footer logo/brand, the "Input"
panel header text, and the "Input XML" label — all looked clickable but
did nothing.

- Footer logo + brand: wrap in <a href="/"> so it navigates home, the
  universal expectation for a site logo.
- "Input" header title: clicking it now toggles the panel collapse, the
  same action as the adjacent chevron button (keyboard-accessible too).
- "Input XML" label: clicking focuses the XML editor below it, like a
  real <label>.

Verified: none of the three register as dead clicks anymore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDk5guu51FjFxQMjgrrekW
2026-07-22 19:47:47 +00:00
claude-code 9c6f81a043 Perf: hand-rolled CodeMirror to slim the critical editor chunk (-29KB gz)
@uiw/react-codemirror's basicSetup statically imports @codemirror/lint,
/autocomplete and /search, so they landed in the critical codemirror
chunk regardless of the runtime basicSetup flags. Since lint and
autocomplete are already loaded on demand via useEditorExtras, they were
being shipped twice — once eager, once deferred.

Replace the @uiw wrapper with a thin EditorView binding (controlled
value with an ExternalChange annotation to avoid onChange echo, plus
compartments for theme / base options / deferred extras) and hand-pick
only the primitives the critical path needs: state, view (lineNumbers,
highlightActiveLine, drawSelection, keymap), commands (history), and
language (syntaxHighlighting, indentOnInput, bracketMatching). lint,
autocomplete and the completions table stay in useEditorExtras.

Critical codemirror chunk 448KB/147KB gz -> 361KB/118KB gz. The
autocomplete + lint code moves to deferred chunks, off first paint.

Behaviour verified in a headless browser: typing/onChange, undo/redo,
auto-close tags, deferred autocomplete (65 options), XML lint markers,
and light/dark theme switch (oneDark applied) all work; no page errors.

LCP/FCP/CLS unchanged (already gated by the static skeleton since the
prior commit). Time-to-interactive-editor on throttled mobile improves
from 2111ms to 1821ms median, with far tighter run-to-run variance.

Drops the @uiw/react-codemirror dependency; promotes the four
@codemirror/* primitives to direct dependencies. @monaco-editor/react
stays (only referenced by a test mock).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDk5guu51FjFxQMjgrrekW
2026-07-21 15:49:37 +00:00
claude-code f916372572 Perf: paint the LCP element at FCP (2.0s -> 0.6s)
After seeding the result pane, LCP was still gated on React mounting:
the largest element stayed a CodeMirror line, which cannot exist until
the bundle has downloaded, parsed and rendered.

Fill the two skeleton panes with the welcome example's actual XSLT and
XML as plain <pre> text. A CSS gradient is not an LCP candidate but text
is, so the largest element on the page now paints with the static HTML
and nothing rendered later exceeds it.

The panes keep their existing box model (border-box holds them at the
same width; only intrinsic height grows with the content), so the
skeleton-to-app transition is unchanged. Measured CLS is identical.
The <pre> blocks are aria-hidden so the shell's aria-live region does
not announce a wall of markup.

Measured over 3 runs, mobile emulation, 4x CPU throttle, 1.6Mbps:
LCP 1988ms -> 624ms median, now equal to FCP. CLS 0.036 unchanged.
Cumulative over both commits: LCP 4064ms -> 624ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDk5guu51FjFxQMjgrrekW
2026-07-20 16:09:37 +00:00
claude-code 01a4715efc Perf: halve LCP (4.1s -> 2.0s) + surface Saxon vs round-trip timing
LCP was gated on the backend: the largest element on the page is a
CodeMirror line in the *result* pane, which could not exist until the
2s debounce plus the POST /transform round-trip had completed.

- Seed the welcome example's known Saxon output into the result pane on
  first visit so it paints on mount. The real transform still runs and
  overwrites it; duration/serverMs stay null so no timing is claimed
  until the real run lands.
- Drop the static xml-formatter import that was silently defeating the
  existing dynamic import (Vite warned about this).
- Load lint, autocompletion, hover docs and the ~50KB completions table
  after mount via requestIdleCallback; the editor reconfigures when they
  arrive and degrades gracefully if they fail.

Critical index chunk 100KB -> 56KB (28KB -> 17.7KB gzipped);
xsltCompletions split into its own deferred 39.7KB chunk.

Measured over 3 runs, mobile emulation, 4x CPU throttle, 1.6Mbps:
LCP 4064ms -> 2032ms median, CLS 0.033 -> 0.036, FCP unchanged.

Also includes the in-progress timing work: the success box now shows the
client round-trip alongside server-side Saxon time, and Saxon
compilation failures without a code prefix classify as "stylesheet".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDk5guu51FjFxQMjgrrekW
2026-07-20 13:55:51 +00:00
claude-code e24ab01956 SEO: richer titles for 230 reference pages, categorized hub, cross-linking
Function pages ranked at pos 7-9 with 1.2% CTR (6.2k impressions/90d) and
the version pages sat at pos 22-24 with almost no internal links.

- Function/element pages: SERP title now "name — XPath function: syntax &
  examples" (one layout change, applies to all 230 pages)
- Reference hub: own title, intro copy, category sections with anchor nav
  (was a flat 230-item list ranking at pos 29.7)
- Cross-linking: version badge on every reference page links to its
  /xslt-2-0/ / /xslt-3-0/ page (~160 new internal links); version pages
  link back to the hub + 6 popular entries each; homepage links the hub

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgybX3QbAWVzb9ZnroCA79
2026-07-02 21:06:01 +00:00
claude-code d42d22e168 UX: reduce dead clicks on result pane and workspace title
Clarity heatmaps showed heavy dead clicks on the (blank) result pane and on
the "edit me!" workspace title.

- Result: add a one-click Copy button and an empty state ("Run a transform —
  the output appears here.") instead of a blank read-only editor
- Title: clicking the active workspace name now starts renaming (matches the
  "edit me!" hint); double-click also works

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgybX3QbAWVzb9ZnroCA79
2026-07-01 12:01:01 +00:00
claude-code 71a018b5ac Fix duplicate xsl:param for primary input (regression from input redesign)
syncParams re-derived param names from the injected stylesheet but only
deduped against params[1..], so the primary input's injected name was added
again as a named param — producing two <xsl:param name="input"> and a Saxon
"Variable is multiply defined" error on the default workspace.

- Skip the primary input's name when syncing named parameters
- Filter out named params colliding with the primary (self-heals state
  persisted by the previous build)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgybX3QbAWVzb9ZnroCA79
2026-06-30 14:38:12 +00:00
claude-code ed4208c2fa Redesign input UX: first-class "Input XML" editor, named params secondary
User feedback asked to paste/edit input XML directly. The input was modeled
as named "parameters" in a "Data Pipeline" — each collapsed and requiring a
name, burying the common single-input case.

- Primary "Input XML" (params[0]) is now an always-visible, name-free editor
- Multiple named parameters move to a collapsible secondary "Named parameters"
  section; backend pickSourceXML already auto-detects the source document
- Add-param now opens + auto-names; removeParam guards the primary input
- Maintain params[0] invariant across defaults, clear and import
- Update DataPipelineHeader (title "Input") and its test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgybX3QbAWVzb9ZnroCA79
2026-06-30 14:06:36 +00:00
claude-code 1071d8de2c Add XSLT 3.0 tester comparison post, enrich README, ease ad refresh to 60s
- blog: "Best free online XSLT 3.0 testers compared (2026)" — answer-first
  comparison post to improve LLM/AI-engine and search visibility
- README: keyword-rich header with live tool link, Saxon HE 3.0
  differentiator, feature list and suggested GitHub topics
- frontend: raise EthicalAds auto-refresh throttle from 45s to 60s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgybX3QbAWVzb9ZnroCA79
2026-06-30 10:59:14 +00:00
alexandrev-tibco 2b2a16ed43 Metrics: add Prometheus instrumentation + Grafana dashboard
Instrument the Go backend with Prometheus metrics (HTTP request rate/
latency/in-flight and per-version transformation outcomes, Saxon duration,
payload size, trace usage), exposed on a dedicated internal port 9100 so
they are never routed through the public ingress. Add an nginx exporter
sidecar to the frontend. Ship ServiceMonitors (release=kube-prom-stack)
and a Grafana dashboard ConfigMap via the Helm chart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 15:28:46 +02:00
alexandrev-tibco 085f210f08 Ads: stop refreshing EthicalAds on backgrounded tabs
maybeRefreshAd only checked a stale adVisibleRef that stays true when the
tab is hidden, so the 30s timer kept firing reload() on background tabs —
generating ad decisions that never count as views and tanking the view
rate of the xsltplayground-main placement. Gate on document.visibilityState
and relax the interval to 45s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:23:50 +02:00
alexandrev-tibco f58cb158e0 SEO: target striking-distance keywords + strengthen landing pages and blog
Phase 1 (home): add explicit "viewer", "validate xslt online" and
"xslt transformation online" coverage in copy, meta keywords, a new
section and a FAQ entry (+ JSON-LD) to lift queries stuck at position 8-10.

Phase 2 (landing pages): add unique runnable examples with input/output
(for-each-group on /xslt-2-0/, maps + fold-left on /xslt-3-0/) plus
internal links to related blog posts and pre/code styling.

Phase 3 (blog): xsl-online-tester gets links to both version landing pages,
a FAQ block and related guides; xslt-string-functions gets a snippet-bait
summary table and related links.

Adds scripts/seo_report.py to pull GSC + GA4 metrics for tracking progress.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 17:19:37 +02:00
alexandrev-tibco 25bb1dce07 Add xsl:result-document support and blog article
- SaxonDaemon (Saxon 12): capture secondary outputs via setResultDocumentHandler
- Saxon2Daemon (Saxon 9.6): capture secondary outputs via OutputURIResolver
- Go backend: propagate secondaryResults field in TransformResponse
- Frontend: secondary results panel with collapsible items and copy button
- Blog: xslt-result-document.md covering basic syntax, split-by-category,
  multi-format output, serialization, XSLT 3.0 streaming, and limitations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:23:42 +02:00
alexandrev-tibco cc2360118c Remove BuyMeACoffee widget — 0 conversions, wrong audience
XSLT is a professional/enterprise tool. Donation models don't
convert for this audience. Removes the external CDN request and
floating button clutter. EthicalAds remains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 07:47:30 +02:00
alexandrev-tibco cb2dd59fac CTR optimization, sitemap lastmod, internal links to landing pages
1. Title: "XSLT Tester & Validator Online — Free, XSLT 1.0/2.0/3.0"
   Description: leads with "Run, test and validate" action verbs.
   Targets low-CTR queries: xslt tester, xsl validator (pos 6-8, CTR <1%).

2. Sitemap: add lastmod 2026-05-26 to all entries to signal freshness
   and accelerate Googlebot crawl of /xslt-2-0/ and /xslt-3-0/
   (both 0 impressions — never crawled despite being live for 3 weeks).

3. Blog internal links: update 3 posts to link directly to
   /xslt-2-0/ and /xslt-3-0/ landing pages instead of root,
   giving Googlebot anchor text and link equity for those URLs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:22:18 +02:00
alexandrev-tibco e1effdef1b Reduce bounce rate: blog CTA + welcome example with auto-run
A) Blog: add prominent playground-cta card at the end of each post.
   Styled with brand gradient border, eyebrow + headline + CTA button.
   Remove the buried related-cta line from related-posts.html.

B) App: load a meaningful XSLT welcome example for first-time visitors
   (no localStorage) and auto-run it immediately (autoRunReady=true),
   so new organic users see live output instead of a blank editor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:53:24 +02:00
alexandrev-tibco 6501d44580 Add preconnect hints for Clarity and EthicalAds origins
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 12:32:21 +02:00
alexandrev-tibco 2bd3e94bb9 Support xsl:initial-template when no source XML is provided; drop "only" from copy
- Backend no longer defaults to <root/> as source document; when no XML
  parameter is detected, source is empty and setInitialContextNode is
  skipped, allowing Saxon to invoke xsl:initial-template automatically
- Remove "The only free online XSLT" wording from meta descriptions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 20:21:34 +02:00
alexandrev-tibco 1921079c8a Fix Clarity script: missing 'script' argument in IIFE call
The snippet was called with 4 args instead of 5, causing r=clarityId
(createElement with wrong tag) and i=undefined (tag src broken).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 00:10:54 +02:00
alexandrev-tibco b56564f6f4 Add /xslt-2-0/ and /xslt-3-0/ internal links in footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:49:22 +02:00
alexandrev-tibco 5581258bd0 Handle ?v= URL param to pre-select XSLT version from landing pages
Reading /?v=2.0 or /?v=3.0 now sets the version on the active workspace
and updates the stylesheet version declaration. URL param is cleaned after
reading. Enables CTAs from /xslt-2-0/ and /xslt-3-0/ landing pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:17:24 +02:00
alexandrev-tibco 77a2d2c123 Restore dual-placement: fixedheader explicit load + stickybox via MutationObserver
Revert to pre-5d81b58 pattern that had both working:
- xsltplayground-main: fixedheader, always in DOM, loaded with load(element)
- xsltplayground-params: stickybox, inside params panel, only rendered when
  ethicalAdsReady=true so EthicalAds MutationObserver auto-discovers it
No createPortal, no conflict between explicit and auto-discovery loads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 18:05:00 +02:00
alexandrev-tibco b9a9ba2a93 Re-add fixedheader: load stickybox explicitly, header with delay
Load stickybox via load(element) first (proven to work), then load
fixedheader separately 300ms later to avoid conflict between the two calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 16:48:00 +02:00
alexandrev-tibco 406f55865c Restore stickybox to exact b5736c5 implementation
Remove fixedheader slot, restore single xsltplayground-params stickybox
via createPortal with load(element) + innerHTML clear, exactly as at b5736c5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 10:35:41 +02:00
alexandrev-tibco ace77fee33 SEO: rewrite homepage, add XSLT 2.0/3.0 landing pages, update sitemap
- Rewrite index.html title, meta description, H1 targeting "xslt online"
- Add static 600-word SEO section (crawlable by Googlebot, outside #root)
- Add FAQPage + BreadcrumbList + SoftwareApplication schema.org markup
- New /xslt-2-0/ landing page (~800 words, feature grid, FAQ)
- New /xslt-3-0/ landing page (~900 words, "only free tool" badge, FAQ)
- Update sitemap.xml to include all three URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:42:08 +02:00
alexandrev-tibco 986a1e05ee Add robots.txt — was returning SPA index.html (404 for crawlers)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 15:58:17 +02:00
alexandrev-tibco 23a7f75e56 Restore stickybox: createPortal to body + original placement ID
Original working implementation used createPortal to document.body and
id=xsltplayground-params. Restore both to match the known-working state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 15:39:27 +02:00
alexandrev-tibco c4f78816bf Use ethicalads.load() no-arg to discover both slots simultaneously
Calling load(element) twice in sequence was silently failing on the second.
No-arg load() auto-discovers all EA elements in the DOM at once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 15:25:12 +02:00