diff --git a/example/aframe/xrsh/assets b/example/aframe/xrsh/assets
new file mode 120000
index 0000000..41aef43
--- /dev/null
+++ b/example/aframe/xrsh/assets
@@ -0,0 +1 @@
+../../assets
\ No newline at end of file
diff --git a/example/aframe/xrsh/dist b/example/aframe/xrsh/dist
new file mode 120000
index 0000000..35f47bf
--- /dev/null
+++ b/example/aframe/xrsh/dist
@@ -0,0 +1 @@
+../../../dist
\ No newline at end of file
diff --git a/example/aframe/xrsh/index.glb b/example/aframe/xrsh/index.glb
new file mode 120000
index 0000000..fbab693
--- /dev/null
+++ b/example/aframe/xrsh/index.glb
@@ -0,0 +1 @@
+./../../assets/index.glb
\ No newline at end of file
diff --git a/example/aframe/xrsh/index.html b/example/aframe/xrsh/index.html
new file mode 100644
index 0000000..7138be6
--- /dev/null
+++ b/example/aframe/xrsh/index.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>XR Fragments aframe viewer</title>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+
+    <!-- AFRAME v1.5.0 + extra THREE.js extra loaders --> 
+    <script src="./../../../dist/aframe.min.js"></script> 
+    <script src="./../../../dist/xrfragment.aframe.js"></script>
+    <script src="https://xrsh.isvery.ninja/xrsh.js"></script>
+
+    <!-- important: allow touchevents in AR -->
+    <style type="text/css">
+      canvas.a-dom-overlay:not(.a-no-style) { padding: 0; pointer-events: auto; }
+    </style>
+
+  </head>
+  <body>
+    <a-scene xr-mode-ui="XRMode: xr"  
+             renderer="colorManagement: false; stencil: true; antialias:true; highRefreshRate:true; foveationLevel: 0.5; toneMapping: ACESFilmic; exposure: 3.0" 
+             device-orientation-permission-ui xrf-gaze-always joystick
+             light="defaultLightsEnabled: false">
+      <a-entity id="player" movement-controls touch-controls="axis:y" wasd-controls="fly:false" look-controls="magicWindowTrackingEnabled:true">
+        <a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity>
+        <a-entity id="left-hand" hand-tracking-grab-controls="hand:left;modelColor:#cccccc" raycaster="objects:.ray" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: .floor">
+          <a-entity rotation="-35 0 0" position="0 0.1 0" id="navigator"> 
+            <a-entity id="back" xrf-button="label: <; width:0.05; action: history.back()"    position="-0.025 0 0" class="ray"></a-entity>
+            <a-entity id="next" xrf-button="label: >; width:0.05; action: history.forward()" position=" 0.025 0 0" class="ray"></a-entity>
+          </a-entity>
+        </a-entity>
+        <a-entity id="right-hand" hand-tracking-grab-controls="hand:right;modelColor:#cccccc" laser-controls="hand: right" raycaster="objects:.ray" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: .floor" xrf-pinchmove="rig: #player"></a-entity>
+      </a-entity>
+
+      <a-entity id="home" xrf="./../../assets/elearning.glb"></a-entity>
+      <a-entity isoterminal position="0 1.6 -0.3"></a-entity>
+    </a-scene>
+
+    <!-- initialize XRSH -->
+    <script>
+      document.querySelector('a-scene').addEventListener('isoterminal_init', function(e){
+
+        const isoterminal = document.querySelector('[isoterminal]').components.isoterminal
+
+        isoterminal.el.addEventListener('init', () => {
+          // override/extend bootmenu-array with your own REPL(s)
+          // see com/isoterminal/feat/boot.REPL.*.js for examples
+          ISOTerminal.prototype.boot.menu = [] // reset options 
+          window.term = isoterminal.term
+
+          const sanitizeTranscript = (str) => {
+            return str
+                   .replaceAll("<[^>]*>", "") // strip html
+                   .split('\n')
+                   .map( (l) => String(l+'.').replace(/(^|:|;|!|\?|\.)\.$/g,'\$1') ) // add dot if needed
+                   .join('. ')
+          }
+
+          xrf.addEventListener('href', (e) => {
+            if( typeof e.selected == 'undefined' || !e.mesh ) return; // only process mouse-overs
+            let name = "object"
+            let info = ""
+            if( e.mesh.userData ){
+              if( e.mesh.userData.href && e.mesh.userData.href.match("pos=")  ){
+                name = "portal: "
+                info = "to "+ xrfragment.URI.parse( e.mesh.userData.href ).XRF.pos.string 
+              }
+              if( e.mesh.userData['aria-description'] ){
+                info = sanitizeTranscript(e.mesh.userData['aria-description'])
+              }
+              if( !info && e.mesh.name ) name = e.mesh.name
+              // traverse ancestors to nearest aria-description
+              // info: scene description
+            }
+            term.send(`\r\n${name} ${info}\n\r`)
+          })
+
+        })
+
+      })
+    </script>
+
+
+    <!-- everything below is completely optional and not part of the spec -->
+
+    <script src="./../../../dist/aframe-blink-controls.min.js"></script>      <!-- teleporting using controllers -->
+
+  </body>
+</html>
diff --git a/example/aframe/xrsh/other.glb b/example/aframe/xrsh/other.glb
new file mode 120000
index 0000000..f068d54
--- /dev/null
+++ b/example/aframe/xrsh/other.glb
@@ -0,0 +1 @@
+../../assets/other.glb
\ No newline at end of file