- Article pages (single.html): two-column grid layout — article + 160px
sidebar with sticky EA ad (id="xsltplayground-blog")
- Non-article pages (home, list): keep existing sticky header bar
(id="xsltplayground-blog-header")
- Move EA script to <head> in baseof.html so it loads on all pages
regardless of placement; remove script tag from ethicalads partial
- has-ethicalads body class now only applied on non-article pages
(no 64px header-offset shift on article pages)
- Sidebar hidden on viewports <= 860px (mobile/tablet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace fixedheader/stickybox A/B with a single inline placement inside
the result pane (before the editor, no data-ea-style) to improve view rate
- Remove adPlacement random split and derived variables (isCompactEthicalAd,
ethicalAdStyle, ethicalAdHeight, ethicalAdVariant)
- Drop createPortal import (no longer needed)
- SEO: swap title order to "Online XSLT Editor, Tester & Validator — XSLT Playground"
and rewrite meta description leading with "Test XSLT online" for higher CTR
on queries: xslt online, xslt tester, xslt validator
- Bump chart to 0.1.4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After formatting XSLT or result XML, an undo button appears next to
the sparkles icon. Clicking it restores the pre-format content.
Undo state resets on new transform or tab switch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents overwhelming the backend with requests on every keystroke.
Users reported slowdown when typing — 2s gap feels natural and still
gives near-instant feedback after finishing edits.
Update test timeouts accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 50/50 random per session: header (xsltplayground-main fixedheader)
vs sidebar (xsltplayground-params stickybox via portal)
- BuyMeACoffee moved to Left position to avoid collision with stickybox
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runs npm test (vitest) as a required job before build-and-push.
Catches runtime import errors like the createPortal/react-dom mistake.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove A/B test and xsltplayground-main fixedheader. Use only
xsltplayground-params (stickybox) rendered via createPortal to document.body,
always active. Higher eCPM ($1.66 vs $0.82) observed vs fixedheader.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The stickybox div was being crushed to 0 height inside app-container
(height:100% flex column). Use createPortal to mount it at document.body
so EthicalAds can position it freely as a floating widget.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sidebar ad was inside the paramsCollapsed conditional block, so
when params were collapsed ethicalSlotRef was null and the ad never loaded.
Move both ad variants to the root level of the component so they're
always in the DOM regardless of panel state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each page load randomly assigns adPlacement='header' or 'sidebar'.
- header: xsltplayground-main fixedheader (current behavior)
- sidebar: xsltplayground-params stickybox in the params panel
Never shows both simultaneously, complying with EthicalAds single-ad policy.
GA4 event 'ad_placement_variant' tracks which variant each user sees.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single ad per page policy violation: remove xsltplayground-params placement
from the params panel, keeping only xsltplayground-main.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Always show injectParamBlock() in the editor — no more switching value
on focus/blur. onChange already calls stripParamBlock() before saving,
so editing is safe. Removes editorFocused state entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Button blends into green/red bar (no separate border/background)
- Text changed to "🔗 Copy link" — unambiguous, not "uploading"
- Also shown in error panel header (red context styling)
- Ad retry only fires if slot is still empty after 1.5s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Share button only appears after user's own transform (not auto-run)
- Clipboard text includes motivating message: "Check out my XSLT transformation! ✨"
- Success bar still spans full width; share button floats right aligned
- EthicalAds main slot retries load() after 1s to fix intermittent race condition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OG image (1200x630) generated from brand assets for Slack/Teams unfurls
- Twitter card + og:image:width/height meta tags in index.html
- "Share transform" pill button appears next to "Success" after each run:
copies shareable ?xslt=...&xml=... URL to clipboard
- Fires gtag share_transform event with xslt_version dimension
- CSS: gradient pill, hover lift, green "copied" state, dark mode support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove transform-count gate — ad now refreshes every 30s if visible,
regardless of user activity. Improves view count for users who do few
transforms (e.g. blog tryUrl arrivals).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Colors data embedded in the XML input document, avoiding the local
file reference that fails in the sandbox.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- autoRunReady initializes to true when page loads from ?xslt= URL,
so the transform fires immediately without requiring a click
- Add doc(uri) to completions list (XSLT 2.0+) with note that only
HTTP/HTTPS URLs are supported in the playground
- Show a yellow hint in the error panel when FODC0002/I-O error
detected, explaining that local file paths are not supported
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 6 xsl:merge-related articles had tryUrls that failed at runtime because
doc() tried to load external XML files that don't exist in the backend sandbox.
Replaced with self-contained XSLTs using inline variable data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert foldGutter: false — was causing comments to appear hidden
when editor was focused (fold markers + codeFolding() interference)
- FeedbackWidget: default position now top-right at editor height
(STORAGE_KEY bumped to feedbackPos2 to reset saved position)
- FeedbackWidget: add optional email input mapped to 'mail' in webhook
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace passive anchor click with explicit pointerdown handler that
opens the URL via window.open. This fires before any tooltip-close
logic can intercept the interaction, and stopPropagation prevents
CodeMirror from treating it as an editor event.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds id="xsltplayground-params" so this unit is tracked separately
from the main "xsltplayground-main" placement in EthicalAds analytics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add mousedown stopPropagation on tooltip wrapper so CodeMirror doesn't
intercept clicks and block "Full docs →" link navigation
- Enable foldGutter in editor basicSetup so expand/collapse controls
appear in the line number gutter area
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix EthicalAds second unit: switch text→image type, add overflow:hidden
and flex centering to prevent ad from bleeding outside params column
- Copyright: 2025 → 2026
- Author name now links to alexandre-vazquez.com
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a text-type ad at the bottom of the data pipeline (params)
column, visible when the panel is open. Only renders when EthicalAds
is enabled and loaded, so no impact on localhost or ad-blocked users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changed reload trigger from every-transform+30s to:
- Requires ≥3 transforms since last reload
- Minimum 2 minutes between reloads (was 30s)
- Ticker interval also updated to 2 minutes
Active users were generating decisions on every transform without
proportional views, deflating the view rate metric.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a second ad unit (text type) just before the "Open live example"
CTA on all /xslt/functions/ pages. Increases ad visibility on long
reference pages where the sticky header unit may scroll out of view.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Add JVM startup flags (-XX:TieredStopAtLevel=1, -XX:+UseSerialGC,
-Xms32m, -Xmx256m) to reduce per-request JVM startup time by ~30-50%
- Replace deprecated io/ioutil.TempDir with os.MkdirTemp (Go 1.17+)
Frontend:
- Track isRunning per workspace in workspaceStatus
- Show animated "Running…" indicator while transform is in-flight,
replacing the stale "Success in X ms" badge
- Blur and disable pointer events on the result editor while running
so users know old output is stale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hugo < 0.154 doesn't resolve layouts/xslt/functions/single.html for
content under content/xslt/functions/ — it falls back to _default.
Aligning CI with the local version fixes the reference page layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- App: parse ?xslt=, ?xml=, ?version=, ?title= query params on load
(base64url-encoded) to pre-populate the editor from external links
- App: Share button in XSLT toolbar generates a preload URL for the
current tab and copies it to clipboard (icon flips to ✓ for 2s)
- Icon: add share and check icons
- Blog: 225 reference pages now include a tryUrl frontmatter field
with the first example pre-encoded; layout renders "Open live
example in XSLT Playground →" CTA pointing directly to that URL
- Script: scripts/add_try_urls.py to regenerate tryUrl fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full coverage of XSLT 1.0, 2.0 and 3.0 elements and XPath functions:
- 59 XSLT elements (xsl:stylesheet → xsl:use-accumulators)
- 170 XPath functions (1.0 node/string/numeric/boolean, 2.0 sequence/
date/QName/string, 3.0 HOF/map/array/JSON/streaming)
Each page: description, parameters table, return value, 2 runnable
Saxon examples with input XML + stylesheet + output, notes, cross-links.
xsltCompletions.js: all 229 entries now have blogSlug for hover links.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- hoverTooltip: hovering any xsl:element or xpath function shows
description, version badge, argument signature, and "Full docs" link
(300ms delay, above cursor)
- Attribute completion: pass full ElementSpec[] schema to xml() via
completeFromSchema(); covers all major xsl:* attributes with enum
values where applicable (yes/no, validation modes, output methods…)
- Context-aware sources: custom source skips AttributeName nodes so
attribute completion from the schema takes over cleanly
- ELEMENT_ATTRS map added to xsltCompletions.js: ~40 elements with
their full attribute sets, filtered by active XSLT version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @codemirror/autocomplete with a custom completion source
- New xsltCompletions.js: ~80 xsl:* elements + ~120 XPath functions,
each tagged with minVersion (1.0/2.0/3.0) and optional blogSlug
- Completions filter to the active XSLT version of each workspace
- Tooltip info() renders argument signature + "Full docs →" link
pointing to blog.xsltplayground.com/xslt/functions/<slug>
(placeholders until Phase 2 blog pages are written)
- Auto-close tags, XML linting and Format button also wired up
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds VITE_FEEDBACK_WEBHOOK_URL env var passed to frontend container,
wired to frontend.feedbackWebhookUrl in values.yaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Simple form with type selector (idea/bug/other) + textarea
- POSTs to VITE_FEEDBACK_WEBHOOK_URL (runtime env, set in Helm/entrypoint)
- States: idle → sending → success/error
- Removes email/GitHub links — no context switch for user
- Adds VITE_FEEDBACK_WEBHOOK_URL to entrypoint.sh env
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Main XSLT editor uses eager=true to skip requestIdleCallback delay
(it's the LCP element — must render ASAP)
- Secondary editors (XML inputs, result) keep deferred loading
- Preload Monaco CDN loader.js and editor.main.js in <head> so
they start downloading before JS requests them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The v0.2.0 label was meaningless. Now shows the short commit SHA
that links directly to the commit on GitHub. Injected at build time
via Vite (execSync git rev-parse --short HEAD).
Also removes CHANGELOG.md (unmaintained) and the changelogAnchor helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All editors were mounting simultaneously on initial render, causing
Monaco to block the main thread (TBT 1520ms). Now uses requestIdleCallback
(timeout 1.5s) to defer Monaco initialization until after first paint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gzip level 6 for JS, CSS, JSON, SVG, fonts
- immutable cache (1yr) for hashed JS/CSS assets
- 7-day cache for static assets (SVG, fonts, images)
- no-store for env.js (runtime config must be fresh)
- no-cache for HTML (SPA index)
Reduces JS transfer from ~460KB to ~115KB gzip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The file-input CSS class (position:absolute 1px clip) was preventing the
label click from triggering the file picker in some browsers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users were rage-clicking the "Drop your input XML files here" area
expecting a file picker. Converted to a label with hidden file input
supporting multiple XML files, same logic as drag-and-drop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Refreshing every 30s regardless of visibility was causing low view rate
(23-38%) because the ad was often scrolled out of view. Now uses
IntersectionObserver (threshold 0.5) to gate both the interval refresh
and the post-transform refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ad div was first in DOM causing it to be the LCP element (10.2s).
Moved to end of app-container and repositioned visually with order:-1
so the ad still appears at the top without blocking LCP measurement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only the arrow button toggled the panel. Now the full header
acts as the toggle, with a guard to ignore clicks that were actually drags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add id="xsltplayground-main" to EthicalAds div for Placements report
- Refresh ad every 30s via setInterval
- Also refresh on each successful transform (throttled to 30s minimum)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>