diff --git a/src/Web/Rep/Examples.hs b/src/Web/Rep/Examples.hs
--- a/src/Web/Rep/Examples.hs
+++ b/src/Web/Rep/Examples.hs
@@ -1,7 +1,6 @@
 {-# LANGUAGE ApplicativeDo #-}
 {-# LANGUAGE OverloadedLabels #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
 {-# OPTIONS_GHC -Wno-name-shadowing #-}
 
 -- | Some simple usage examples to get started with the library.
@@ -20,7 +19,6 @@
 where
 
 import Data.ByteString (ByteString)
-import Data.String.Interpolate
 import FlatParse.Basic (takeRest)
 import GHC.Generics
 import MarkupParse
@@ -72,34 +70,30 @@
 css1 :: Css
 css1 =
   Css
-    [i|
-{
-  font-size   : 10px;
-  font-family : "Arial","Helvetica", sans-serif;
-}
-
-\#btnGo
-{
-  margin-top    : 20px;
-  margin-bottom : 20px;
-}
-
-\#btnGo.on
-{
-  color : \#008000;
-}
-|]
+    "{\n\
+    \  font-size   : 10px;\n\
+    \  font-family : \"Arial\",\"Helvetica\", sans-serif;\n\
+    \}\n\
+    \\n\
+    \#btnGo\n\
+    \{\n\
+    \  margin-top    : 20px;\n\
+    \  margin-bottom : 20px;\n\
+    \}\n\
+    \\n\
+    \#btnGo.on\n\
+    \{\n\
+    \  color : #008000;\n\
+    \}"
 
 -- js
 click :: Js
 click =
   Js
-    [i|
-$('\#btnGo').click( function() {
-  $('\#btnGo').toggleClass('on');
-  alert('bada bing!');
-});
-|]
+    "$('#btnGo').click( function() {\n\
+    \  $('#btnGo').toggleClass('on');\n\
+    \  alert('bada bing!');\n\
+    \});"
 
 button1 :: Markup
 button1 =
diff --git a/src/Web/Rep/Page.hs b/src/Web/Rep/Page.hs
--- a/src/Web/Rep/Page.hs
+++ b/src/Web/Rep/Page.hs
@@ -1,6 +1,5 @@
 {-# LANGUAGE OverloadedLabels #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
 {-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
 
 -- | Representations of a web page, covering Html, CSS & JS artifacts.
@@ -23,7 +22,6 @@
 
 import Data.ByteString (ByteString)
 import Data.ByteString.Char8 qualified as C
-import Data.String.Interpolate
 import GHC.Generics
 import MarkupParse
 import Optics.Core
@@ -137,26 +135,11 @@
 cssColorScheme :: Css
 cssColorScheme =
   Css
-    [i|
-{
-  color-scheme: light dark;
-}
-{
-  body {
-    background-color: rgb(92%, 92%, 92%);
-    color: rgb(5%, 5%, 5%);
-  }
-}
-@media (prefers-color-scheme:dark) {
-  body {
-    background-color: rgb(5%, 5%, 5%);
-    color: rgb(92%, 92%, 92%);
-  }
-}|]
+    "{\n  color-scheme: light dark;\n}\n{\n  body {\n    background-color: rgb(92%, 92%, 92%);\n    color: rgb(5%, 5%, 5%);\n  }\n}\n@media (prefers-color-scheme:dark) {\n  body {\n    background-color: rgb(5%, 5%, 5%);\n    color: rgb(92%, 92%, 92%);\n  }\n}"
 
 -- | Javascript as string
 newtype Js = Js {jsByteString :: ByteString} deriving (Eq, Show, Generic, Semigroup, Monoid)
 
 -- | Add the windows.onload assignment
 onLoad :: Js -> Js
-onLoad (Js t) = Js [i| window.onload=function(){#{t}};|]
+onLoad (Js t) = Js (" window.onload=function(){" <> t <> "};")
diff --git a/src/Web/Rep/SharedReps.hs b/src/Web/Rep/SharedReps.hs
--- a/src/Web/Rep/SharedReps.hs
+++ b/src/Web/Rep/SharedReps.hs
@@ -1,7 +1,6 @@
 {-# LANGUAGE ApplicativeDo #-}
 {-# LANGUAGE OverloadedLabels #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
 {-# OPTIONS_GHC -Wno-name-shadowing #-}
 {-# OPTIONS_GHC -Wno-x-partial #-}
 
@@ -48,7 +47,6 @@
 import Data.HashMap.Strict qualified as HashMap
 import Data.List qualified as List
 import Data.Maybe
-import Data.String.Interpolate
 import FlatParse.Basic hiding (take)
 import MarkupParse
 import Optics.Core hiding (element)
@@ -366,12 +364,15 @@
   elementc
     "script"
     []
-    [i|
-$('\##{checkName}').on('change', (function(){
-  var vis = this.checked ? "block" : "none";
-  document.getElementById("#{toggleId}").style.display = vis;
-}));
-|]
+    ( "$('#"
+        <> checkName
+        <> "').on('change', (function(){"
+        <> "  var vis = this.checked ? \"block\" : \"none\";"
+        <> "  document.getElementById(\""
+        <> toggleId
+        <> "\").style.display = vis;"
+        <> "}));"
+    )
 
 -- | A (fixed-size) list represented in html as an accordion card
 -- A major restriction of the library is that a 'SharedRep' does not have a Monad instance. In practice, this means that the external representation of lists cannot have a dynamic size.
diff --git a/src/Web/Rep/Socket.hs b/src/Web/Rep/Socket.hs
--- a/src/Web/Rep/Socket.hs
+++ b/src/Web/Rep/Socket.hs
@@ -1,6 +1,5 @@
 {-# LANGUAGE OverloadedLabels #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE TemplateHaskell #-}
 {-# OPTIONS_GHC -Wno-name-shadowing #-}
 
@@ -54,7 +53,6 @@
 import Data.Functor.Contravariant
 import Data.HashMap.Strict as HashMap
 import Data.Profunctor
-import Data.String.Interpolate
 import Data.Text (Text)
 import Data.Text.Encoding
 import FlatParse.Basic
@@ -278,11 +276,11 @@
 -- | {"event":{"element":"textid","value":"abcdees"}}
 parserJ :: Parser e (ByteString, ByteString)
 parserJ = do
-  _ <- $(string [i|{"event":{"element":"|])
+  _ <- $(string "{\"event\":{\"element\":\"")
   e <- byteStringOf $ some (satisfy (/= '"'))
-  _ <- $(string [i|","value":"|])
+  _ <- $(string "\",\"value\":\"")
   v <- byteStringOf $ some (satisfy (/= '"'))
-  _ <- $(string [i|"}}|])
+  _ <- $(string "\"}}")
   pure (e, v)
 
 -- * code hooks
@@ -308,31 +306,21 @@
 
 -- | write to the console
 console :: ByteString -> ByteString
-console t = [i| console.log(#{t}) |]
+console t = " console.log(" <> t <> ") "
 
 -- | send arbitrary byestrings.
 val :: ByteString -> ByteString
-val t = [i| jsb.ws.send(#{t}) |]
+val t = " jsb.ws.send(" <> t <> ") "
 
 -- | replace a container and run any embedded scripts
 replace :: ByteString -> ByteString -> ByteString
 replace d t =
-  [i|
-     var $container = document.getElementById('#{d}');
-     $container.innerHTML = '#{clean t}';
-     runScripts($container);
-     refreshJsb();
-     |]
+  "\n     var $container = document.getElementById('" <> d <> "');\n     $container.innerHTML = '" <> clean t <> "';\n     runScripts($container);\n     refreshJsb();\n     "
 
 -- | append to a container and run any embedded scripts
 append :: ByteString -> ByteString -> ByteString
 append d t =
-  [i|
-     var $container = document.getElementById('#{d}');
-     $container.innerHTML += '#{clean t}';
-     runScripts($container);
-     refreshJsb();
-     |]
+  "\n     var $container = document.getElementById('" <> d <> "');\n     $container.innerHTML += '" <> clean t <> "';\n     runScripts($container);\n     refreshJsb();\n     "
 
 -- | Double backslash newline and single quotes.
 clean :: ByteString -> ByteString
@@ -348,15 +336,7 @@
 webSocket :: Js
 webSocket =
   Js
-    [i|
-window.jsb = {ws: new WebSocket('ws://' + location.host + '/')};
-jsb.event = function(ev) {
-    jsb.ws.send(JSON.stringify({event: ev}));
-};
-jsb.ws.onmessage = function(evt){ 
-    eval('(function(){' + evt.data + '})()');
-};
-|]
+    "\nwindow.jsb = {ws: new WebSocket('ws://' + location.host + '/')};\njsb.event = function(ev) {\n    jsb.ws.send(JSON.stringify({event: ev}));\n};\njsb.ws.onmessage = function(evt){ \n    eval('(function(){' + evt.data + '})()');  \n};\n"
 
 -- * scripts
 
@@ -364,68 +344,13 @@
 refreshJsbJs :: Js
 refreshJsbJs =
   Js
-    [i|
-function refreshJsb () {
-  $('.jsbClassEventInput').off('input');
-  $('.jsbClassEventInput').on('input', (function(){
-    jsb.event({ 'element': this.id, 'value': this.value});
-  }));
-  $('.jsbClassEventChange').off('change');
-  $('.jsbClassEventChange').on('change', (function(){
-    jsb.event({ 'element': this.id, 'value': this.value});
-  }));
-  $('.jsbClassEventFocusout').off('focusout');
-  $('.jsbClassEventFocusout').on('focusout', (function(){
-    jsb.event({ 'element': this.id, 'value': this.value});
-  }));
-  $('.jsbClassEventButton').off('click');
-  $('.jsbClassEventButton').on('click', (function(){
-    jsb.event({ 'element': this.id, 'value': this.value});
-  }));
-  $('.jsbClassEventToggle').off('click');
-  $('.jsbClassEventToggle').on('click', (function(){
-    jsb.event({ 'element': this.id, 'value': ('true' !== this.getAttribute('aria-pressed')).toString()});
-  }));
-  $('.jsbClassEventCheckbox').off('click');
-  $('.jsbClassEventCheckbox').on('click', (function(){
-    jsb.event({ 'element': this.id, 'value': this.checked.toString()});
-  }));
-  $('.jsbClassEventChooseFile').off('input');
-  $('.jsbClassEventChooseFile').on('input', (function(){
-    jsb.event({ 'element': this.id, 'value': this.files[0].name});
-  }));
-  $('.jsbClassEventShowSum').off('change');
-  $('.jsbClassEventShowSum').on('change', (function(){
-    var v = this.value;
-    $(this).parent('.sumtype-group').siblings('.subtype').each(function(i) {
-      if (this.dataset.sumtype === v) {
-        this.style.display = 'block';
-        } else {
-        this.style.display = 'none';
-      }
-    })
-  }));
-  $('.jsbClassEventChangeMultiple').off('change');
-  $('.jsbClassEventChangeMultiple').on('change', (function(){
-    jsb.event({ 'element': this.id, 'value': [...this.options].filter(option => option.selected).map(option => option.value).join(',')});
-  }));
-};
-|]
+    "\nfunction refreshJsb () {\n  $('.jsbClassEventInput').off('input');\n  $('.jsbClassEventInput').on('input', (function(){\n    jsb.event({ 'element': this.id, 'value': this.value});\n  }));\n  $('.jsbClassEventChange').off('change');\n  $('.jsbClassEventChange').on('change', (function(){\n    jsb.event({ 'element': this.id, 'value': this.value});\n  }));\n  $('.jsbClassEventFocusout').off('focusout');\n  $('.jsbClassEventFocusout').on('focusout', (function(){\n    jsb.event({ 'element': this.id, 'value': this.value});\n  }));\n  $('.jsbClassEventButton').off('click');\n  $('.jsbClassEventButton').on('click', (function(){\n    jsb.event({ 'element': this.id, 'value': this.value});\n  }));\n  $('.jsbClassEventToggle').off('click');\n  $('.jsbClassEventToggle').on('click', (function(){\n    jsb.event({ 'element': this.id, 'value': ('true' !== this.getAttribute('aria-pressed')).toString()});\n  }));\n  $('.jsbClassEventCheckbox').off('click');\n  $('.jsbClassEventCheckbox').on('click', (function(){\n    jsb.event({ 'element': this.id, 'value': this.checked.toString()});\n  }));\n  $('.jsbClassEventChooseFile').off('input');\n  $('.jsbClassEventChooseFile').on('input', (function(){\n    jsb.event({ 'element': this.id, 'value': this.files[0].name});\n  }));\n  $('.jsbClassEventShowSum').off('change');\n  $('.jsbClassEventShowSum').on('change', (function(){\n    var v = this.value;\n    $(this).parent('.sumtype-group').siblings('.subtype').each(function(i) {\n      if (this.dataset.sumtype === v) {\n        this.style.display = 'block';\n        } else {\n        this.style.display = 'none';\n      }\n    })\n  }));\n  $('.jsbClassEventChangeMultiple').off('change');\n  $('.jsbClassEventChangeMultiple').on('change', (function(){\n    jsb.event({ 'element': this.id, 'value': [...this.options].filter(option => option.selected).map(option => option.value).join(',')});\n  }));\n};\n"
 
 -- | prevent the Enter key from triggering an event
 preventEnter :: Js
 preventEnter =
   Js
-    [i|
-window.addEventListener('keydown',function(e) {
-  if(e.keyIdentifier=='U+000A' || e.keyIdentifier=='Enter' || e.keyCode==13) {
-    if(e.target.nodeName=='INPUT' && e.target.type !== 'textarea') {
-      e.preventDefault();
-      return false;
-    }
-  }
-}, true);
-|]
+    "\nwindow.addEventListener('keydown',function(e) {\n  if(e.keyIdentifier=='U+000A' || e.keyIdentifier=='Enter' || e.keyCode==13) {\n    if(e.target.nodeName=='INPUT' && e.target.type !== 'textarea') {\n      e.preventDefault();\n      return false;\n    }\n  }\n}, true);\n"
 
 -- | script injection js.
 --
@@ -433,33 +358,7 @@
 runScriptJs :: Js
 runScriptJs =
   Js
-    [i|
-function insertScript ($script) {
-  var s = document.createElement('script')
-  s.type = 'text/javascript'
-  if ($script.src) {
-    s.onload = callback
-    s.onerror = callback
-    s.src = $script.src
-  } else {
-    s.textContent = $script.innerText
-  }
-
-  // re-insert the script tag so it executes.
-  document.head.appendChild(s)
-
-  // clean-up
-  $script.parentNode.removeChild($script)
-}
-
-function runScripts ($container) {
-  // get scripts tags from a node
-  var $scripts = $container.querySelectorAll('script')
-  $scripts.forEach(function ($script) {
-    insertScript($script)
-  })
-}
-|]
+    "\nfunction insertScript ($script) {\n  var s = document.createElement('script')\n  s.type = 'text/javascript'\n  if ($script.src) {\n    s.onload = callback\n    s.onerror = callback\n    s.src = $script.src\n  } else {\n    s.textContent = $script.innerText\n  }\n\n  // re-insert the script tag so it executes.\n  document.head.appendChild(s)\n\n  // clean-up\n  $script.parentNode.removeChild($script)\n}\n\nfunction runScripts ($container) {\n  // get scripts tags from a node\n  var $scripts = $container.querySelectorAll('script')\n  $scripts.forEach(function ($script) {\n    insertScript($script)\n  })\n}\n"
 
 -- | Run a Parser, throwing away leftovers. Returns Left on 'Fail' or 'Err'.
 runParserEither :: Parser ByteString a -> ByteString -> Either ByteString a
diff --git a/test/doctests.hs b/test/doctests.hs
deleted file mode 100644
--- a/test/doctests.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Main where
-
-import System.Environment (getArgs)
-import Test.DocTest (mainFromCabal)
-import Prelude (IO, (=<<))
-
-main :: IO ()
-main = mainFromCabal "web-rep" =<< getArgs
diff --git a/web-rep.cabal b/web-rep.cabal
--- a/web-rep.cabal
+++ b/web-rep.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: web-rep
-version: 0.14.0.0
+version: 0.14.0.1
 license: BSD-3-Clause
 license-file: LICENSE
 copyright: Tony Day (c) 2015
@@ -19,6 +19,7 @@
   ghc ==9.8.4
   ghc ==9.10.2
   ghc ==9.12.2
+  ghc ==9.14.1
 
 extra-doc-files:
   ChangeLog.md
@@ -84,7 +85,6 @@
     optics-extra >=0.4 && <0.5,
     profunctors >=5.6.2 && <5.7,
     scotty >=0.11.5 && <0.23,
-    string-interpolate >=0.3 && <0.4,
     text >=1.2 && <2.2,
     transformers >=0.5.6 && <0.6.2,
     unordered-containers >=0.2 && <0.3,
@@ -120,13 +120,3 @@
     optparse-applicative >=0.17 && <0.20,
     web-rep,
 
-test-suite doctests
-  import: ghc2024-stanza
-  main-is: doctests.hs
-  hs-source-dirs: test
-  build-depends:
-    base >=4.14 && <5,
-    doctest-parallel >=0.3 && <0.5,
-
-  ghc-options: -threaded
-  type: exitcode-stdio-1.0
