diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -130,8 +130,8 @@
 
 ```nix
 with (import (builtins.fetchTarball {
-  url = "https://github.com/dmjio/miso/archive/561ffad.tar.gz";
-  sha256 = "1wwzckz2qxb873wdkwqmx9gmh0wshcdxi7gjwkba0q51jnkfdi41";
+  url = "https://github.com/dmjio/miso/archive/ea25964565074e73d4052b56b60b6e101fa08bc5.tar.gz";
+  sha256 = "1yb9yvc0ln4yn1jk2k5kwwa1s32310abawz40yd8cqqkm1z7w6wg";
 }) {});
 pkgs.haskell.packages.ghcjs.callCabal2nix "app" ./. {}
 ```
@@ -179,10 +179,10 @@
 ## Examples
 
 ### TodoMVC
-  - [Link](https://todo-mvc.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/todo-mvc/Main.hs)
+  - [Link](http://todo-mvc.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/todo-mvc/Main.hs)
 
 ### Flatris
-  - [Link](https://flatris.haskell-miso.org/) / [Source](https://github.com/ptigwe/hs-flatris/)
+  - [Link](http://flatris.haskell-miso.org/) / [Source](https://github.com/ptigwe/hs-flatris/)
 
 ### 2048
   - [Link](http://2048.haskell-miso.org/) / [Source](https://github.com/ptigwe/hs2048/)
@@ -191,25 +191,25 @@
   - [Link](http://snake.haskell-miso.org/) / [Source](https://github.com/lbonn/miso-snake)
 
 ### Mario
-  - [Link](https://mario.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/mario/Main.hs)
+  - [Link](http://mario.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/mario/Main.hs)
 
 ### Miso Plane (Flappy Birds)
-  - [Link](https://miso-plane.lermex.net/) / [Source](https://github.com/Lermex/miso-plane)
+  - [Link](http://miso-plane.lermex.net/) / [Source](https://github.com/Lermex/miso-plane)
 
 ### Websocket
-  - [Link](https://websocket.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/websocket/Main.hs)
+  - [Link](http://websocket.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/websocket/Main.hs)
 
 ### SSE
-  - [Link](https://sse.haskell-miso.org/) / [Client](https://github.com/dmjio/miso/blob/master/examples/sse/client/Main.hs) / [Server](https://github.com/dmjio/miso/blob/master/examples/sse/server/Main.hs)
+  - [Link](http://sse.haskell-miso.org/) / [Client](https://github.com/dmjio/miso/blob/master/examples/sse/client/Main.hs) / [Server](https://github.com/dmjio/miso/blob/master/examples/sse/server/Main.hs)
 
 ### XHR
-  - [Link](https://xhr.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/xhr/Main.hs)
+  - [Link](http://xhr.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/xhr/Main.hs)
 
 ### Router
-  - [Link](https://router.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/router/Main.hs)
+  - [Link](http://router.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/router/Main.hs)
 
 ### SVG
-  - [Link](https://svg.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/svg/Main.hs)
+  - [Link](http://svg.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/svg/Main.hs)
 
 ### Canvas 2D
   - [Link](http://canvas.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/canvas2d/Main.hs)
@@ -218,13 +218,13 @@
   - [Link](http://threejs.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/three/Main.hs)
 
 ### Simple
-  - [Link](https://simple.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/exe/Main.hs)
+  - [Link](http://simple.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/exe/Main.hs)
 
 ### File Reader
-  - [Link](https://file-reader.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/file-reader/Main.hs)
+  - [Link](http://file-reader.haskell-miso.org/) / [Source](https://github.com/dmjio/miso/blob/master/examples/file-reader/Main.hs)
 
 ### WebVR
-  - [Link](https://fizruk.github.io/fpconf-2017-talk/miso-aframe-demo/dist/demo.jsexe/index.html) / [Source](https://github.com/fizruk/miso-aframe)
+  - [Link](http://fizruk.github.io/fpconf-2017-talk/miso-aframe-demo/dist/demo.jsexe/index.html) / [Source](https://github.com/fizruk/miso-aframe)
 
 ## Haddocks
 
@@ -269,6 +269,7 @@
     events = defaultEvents        -- default delegated events
     subs   = []                   -- empty subscription list
     mountPoint = Nothing          -- mount point for application (Nothing defaults to 'body')
+    logLevel = Off                -- used during prerendering to see if the VDOM and DOM are in sync (only applies to `miso` function)
 
 -- | Updates model, optionally introduces side effects
 updateModel :: Action -> Model -> Effect Action Model
@@ -342,6 +343,7 @@
     events = defaultEvents        -- default delegated events
     subs   = []                   -- empty subscription list
     mountPoint = Nothing          -- mount point for application (Nothing defaults to 'body')
+    logLevel = Off                -- used during prerendering to see if the VDOM and DOM are in sync (only applies to `miso` function)
 
 -- | Updates model, optionally introduces side effects
 updateModel :: Action -> Transition Action Model ()
diff --git a/exe/Main.hs b/exe/Main.hs
--- a/exe/Main.hs
+++ b/exe/Main.hs
@@ -37,7 +37,7 @@
 
 -- | Entry point for a miso application
 main :: IO ()
-main = runApp $ startApp App {..}
+main = runApp $ miso $ \_ -> App {..}
   where
     initialAction = SayHelloWorld -- initial action to be executed on application load
     model  = 0                    -- initial model
@@ -46,6 +46,7 @@
     events = defaultEvents        -- default delegated events
     subs   = []                   -- empty subscription list
     mountPoint = Nothing          -- mount point for application (Nothing defaults to 'body')
+    logLevel = Off                -- Used to copy DOM into VDOM, applies only to `miso` function
 
 -- | Updates model, optionally introduces side effects
 updateModel :: Action -> Model -> Effect Action Model
diff --git a/frontend-src/Miso.hs b/frontend-src/Miso.hs
--- a/frontend-src/Miso.hs
+++ b/frontend-src/Miso.hs
@@ -139,7 +139,7 @@
     VTree (OI.Object iv) <- flip runView writeEvent initialView
     mountEl <- mountElement mountPoint
     -- Initial diff can be bypassed, just copy DOM into VTree
-    copyDOMIntoVTree mountEl iv
+    copyDOMIntoVTree (logLevel == DebugPrerender) mountEl iv
     let initialVTree = VTree (OI.Object iv)
     -- Create virtual dom, perform initial diff
     liftIO (newIORef initialVTree)
diff --git a/frontend-src/Miso/Html/Internal.hs b/frontend-src/Miso/Html/Internal.hs
--- a/frontend-src/Miso/Html/Internal.hs
+++ b/frontend-src/Miso/Html/Internal.hs
@@ -34,6 +34,7 @@
   -- * Smart `View` constructors
   , node
   , text
+  , textRaw
   -- * Key patch internals
   , Key    (..)
   , ToKey  (..)
@@ -130,7 +131,9 @@
   set "tag" tag vnode
   set "key" key vnode
   setAttrs vnode sink
-  flip (set "children") vnode =<< ghcjsPure . jsval =<< setKids sink
+  flip (set "children") vnode
+    =<< ghcjsPure . jsval
+    =<< setKids sink
   pure $ VTree vnode
     where
       setAttrs vnode sink =
@@ -162,6 +165,10 @@
   set "type" ("vtext" :: JSString) vtree
   set "text" t vtree
   pure $ VTree vtree
+
+-- | For parity with server-side rendering. Don't use directly.
+textRaw :: MisoString -> View m
+textRaw = text
 
 -- | `IsString` instance
 instance IsString (View a) where
diff --git a/frontend-src/Miso/Types.hs b/frontend-src/Miso/Types.hs
--- a/frontend-src/Miso/Types.hs
+++ b/frontend-src/Miso/Types.hs
@@ -9,6 +9,7 @@
 ----------------------------------------------------------------------------
 module Miso.Types
   ( App (..)
+  , LogLevel (..)
   , Effect
   , Sub
 
@@ -53,7 +54,15 @@
   -- ^ Initial action that is run after the application has loaded
   , mountPoint :: Maybe MisoString
   -- ^ Id of the root element for DOM diff. If 'Nothing' is provided, the entire document body is used as a mount point.
+  , logLevel :: LogLevel
+  -- ^ Display warning messages when prerendering if the DOM and VDOM are not in sync.
   }
+
+-- | Optional Logging for debugging miso internals (useful to see if prerendering is successful)
+data LogLevel
+  = Off
+  | DebugPrerender
+  deriving (Show, Eq)
 
 -- | A monad for succinctly expressing model transitions in the 'update' function.
 --
diff --git a/ghc-src/Miso/Html/Internal.hs b/ghc-src/Miso/Html/Internal.hs
--- a/ghc-src/Miso/Html/Internal.hs
+++ b/ghc-src/Miso/Html/Internal.hs
@@ -30,6 +30,7 @@
   -- * Smart `View` constructors
   , node
   , text
+  , textRaw
   -- * Key patch internals
   , Key    (..)
   , ToKey  (..)
@@ -74,6 +75,14 @@
            } -> VTree action
   VText :: { vText :: Text -- ^ TextNode content
            } -> VTree action
+  -- Invariant: To avoid complexity with collapsing mixed VText and
+  -- VTextRaw nodes, VTextRaw node is always the only child.
+  -- That's not a big limitation, since the intended purpose is to be able
+  -- to use <style> and <script> tags. That means we can't support HTML
+  -- entities like &nbsp; with VTextRaw, but the same can be achieved with
+  -- Unicode.
+  VTextRaw :: { vText :: Text -- ^ Raw TextNode content
+              } -> VTree action
   deriving Functor
 
 instance Show (VTree action) where
@@ -84,6 +93,8 @@
   toHtmlRaw = L.toHtml
   toHtml (VText x) | T.null x = L.toHtml (" " :: MisoString)
                    | otherwise = L.toHtml x
+  toHtml (VTextRaw x) | T.null x = L.toHtml (" " :: MisoString)
+                      | otherwise = L.toHtmlRaw x
   toHtml VNode{..} =
     let
       noEnd = ["img", "input", "br", "hr", "meta"]
@@ -114,8 +125,21 @@
                      , "autocomplete"
                      ]
         toTag = T.toLower
-        kids = foldMap L.toHtml vChildren
+        kids
+          = foldMap L.toHtml
+          . V.fromList
+          . collapseSiblingTextNodes
+          . V.toList
+          $ vChildren
 
+collapseSiblingTextNodes :: [VTree a] -> [VTree a]
+collapseSiblingTextNodes [] = []
+collapseSiblingTextNodes (VText x : VText y : xs) =
+  collapseSiblingTextNodes (VText (x <> y) : xs)
+-- VTextRaw is the only child, so no need to collapse.
+collapseSiblingTextNodes (x:xs) =
+  x : collapseSiblingTextNodes xs
+
 -- | Helper for turning JSON into Text
 -- Object, Array and Null are kind of non-sensical here
 toHtmlFromJSON :: Value -> Text
@@ -170,6 +194,10 @@
 -- | `VText` creation
 text :: MisoString -> View action
 text = View . VText
+
+-- | `VTextRaw` creation. Don't use directly
+textRaw :: MisoString -> View action
+textRaw = View . VTextRaw
 
 -- | `IsString` instance
 instance IsString (View a) where
diff --git a/ghcjs-ffi/Miso/FFI.hs b/ghcjs-ffi/Miso/FFI.hs
--- a/ghcjs-ffi/Miso/FFI.hs
+++ b/ghcjs-ffi/Miso/FFI.hs
@@ -121,7 +121,6 @@
 foreign import javascript unsafe "$1.preventDefault();"
     eventPreventDefault :: JSVal -> IO ()
 
-
 -- | Window object
 foreign import javascript unsafe "$r = window;"
   getWindow :: IO JSVal
@@ -242,9 +241,10 @@
 
 -- | Copies DOM pointers into virtual dom
 -- entry point into isomorphic javascript
-foreign import javascript unsafe "window['copyDOMIntoVTree']($1, $2);"
+foreign import javascript unsafe "window['copyDOMIntoVTree']($1, $2, $3);"
   copyDOMIntoVTree
-    :: JSVal -- ^ mountPoint element of the isomorphic app
+    :: Bool  -- ^ Display debugging information when pre-rendering
+    -> JSVal -- ^ mountPoint element of the isomorphic app
     -> JSVal -- ^ VDom object
     -> IO ()
 
diff --git a/jsaddle-ffi/Miso/FFI.hs b/jsaddle-ffi/Miso/FFI.hs
--- a/jsaddle-ffi/Miso/FFI.hs
+++ b/jsaddle-ffi/Miso/FFI.hs
@@ -18,16 +18,12 @@
    , objectToJSVal
    , ghcjsPure
    , syncPoint
-
    , addEventListener
    , windowAddEventListener
-
    , windowInnerHeight
    , windowInnerWidth
-
    , eventPreventDefault
    , eventStopPropagation
-
    , now
    , consoleLog
    , consoleLogJSVal
@@ -40,19 +36,14 @@
    , getDoc
    , getElementById
    , diff'
-
    , integralToJSString
    , realFloatToJSString
    , jsStringToDouble
-
    , delegateEvent
-
    , copyDOMIntoVTree
-
    , swapCallbacks
    , releaseCallbacks
    , registerCallback
-
    , focus
    , blur
    , scrollIntoView
@@ -220,7 +211,7 @@
 
 delegateEvent :: JSVal -> JSVal -> JSM JSVal -> JSM ()
 delegateEvent mountPoint events getVTree = do
-  cb' <- function $ \_ _ [continuation] -> do
+  cb' <- asyncFunction $ \_ _ [continuation] -> do
     res <- getVTree
     _ <- call continuation global res
     pure ()
@@ -231,8 +222,8 @@
 
 -- | Copies DOM pointers into virtual dom
 -- entry point into isomorphic javascript
-copyDOMIntoVTree :: JSVal -> JSVal -> JSM ()
-copyDOMIntoVTree mountPoint a = () <$ jsg2 "copyDOMIntoVTree" mountPoint a
+copyDOMIntoVTree :: Bool -> JSVal -> JSVal -> JSM ()
+copyDOMIntoVTree logLevel mountPoint a = () <$ jsg3 "copyDOMIntoVTree" logLevel mountPoint a
 
 -- TODO For now, we do not free callbacks when compiling with JSaddle
 
diff --git a/jsbits/isomorphic.js b/jsbits/isomorphic.js
--- a/jsbits/isomorphic.js
+++ b/jsbits/isomorphic.js
@@ -1,19 +1,57 @@
 window = typeof window === 'undefined' ? {} : window;
-window['copyDOMIntoVTree'] = function copyDOMIntoVTree(mountPoint, vtree, doc) {
+window['collapseSiblingTextNodes'] = function collapseSiblingTextNodes(vs) {
+  if (!vs) { return []; }
+  var ax = 0, adjusted = vs.length > 0 ? [vs[0]] : [];
+  for (var ix = 1; ix < vs.length; ix++) {
+    if (adjusted[ax]['type'] === 'vtext' && vs[ix]['type'] === 'vtext') {
+	adjusted[ax]['text'] += vs[ix]['text'];
+	continue;
+    }
+    adjusted[++ax] = vs[ix];
+  }
+  return adjusted;
+}
+
+window['copyDOMIntoVTree'] = function copyDOMIntoVTree(logLevel,mountPoint, vtree, doc) {
   if (!doc) { doc = window.document; }
-  var node = mountPoint ? mountPoint.firstChild : doc.body.firstChild;
-  if (!window['walk'](vtree, node, doc)) {
-    console.warn('Could not copy DOM into virtual DOM, falling back to diff');
+    var mountChildIdx = 0, node;
+    // If script tags are rendered first in body, skip them.
+    if (!mountPoint) {
+	if (doc.body.childNodes.length > 0) {
+	    node = doc.body.firstChild;
+	} else {
+	    node = doc.body.appendChild (doc.createElement('div'));
+	}
+    } else if (mountPoint.childNodes.length === 0) {
+	node = mountPoint.appendChild (doc.createElement('div'));
+    } else {
+	while (mountPoint.childNodes[mountChildIdx].localName === 'script' || mountPoint.childNodes[mountChildIdx].nodeType === Node.TEXT_NODE){
+	  mountChildIdx++;
+	}
+	node = mountPoint.childNodes[mountChildIdx];
+    }
+
+    if (!window['walk'](logLevel,vtree, node, doc)) {
+    if (logLevel) {
+      console.warn('Could not copy DOM into virtual DOM, falling back to diff');
+    }
     // Remove all children before rebuilding DOM
-    while (node.firstChild)
-      node.removeChild(node.lastChild);
-    window['diff'](null, vtree, node, doc);
+    while (node.firstChild) node.removeChild(node.lastChild);
+    vtree['domRef'] = node;
+    window['populate'](null, vtree, doc);
     return false;
   }
+  if (logLevel) {
+    console.info ('Successfully prendered page');
+  }
   return true;
 }
 
-window['walk'] = function walk(vtree, node, doc) {
+window['diagnoseError'] = function diagnoseError(logLevel, vtree, node) {
+    if (logLevel) console.warn('VTree differed from node', vtree, node);
+}
+
+window['walk'] = function walk(logLevel, vtree, node, doc) {
   // This is slightly more complicated than one might expect since
   // browsers will collapse consecutive text nodes into a single text node.
   // There can thus be fewer DOM nodes than VDOM nodes.
@@ -25,35 +63,31 @@
   // Fire onCreated events as though the elements had just been created.
   window['callCreated'](vtree);
 
+  vtree.children = window['collapseSiblingTextNodes'](vtree.children);
   for (var i = 0; i < vtree.children.length; i++) {
     vdomChild = vtree['children'][i];
     domChild = node.childNodes[i];
+      if (!domChild) {
+	  window['diagnoseError'](logLevel,vdomChild, domChild);
+	  return false;
+      }
     if (vdomChild.type === 'vtext') {
-        if (domChild.nodeType !== Node.TEXT_NODE) return false;
+        if (domChild.nodeType !== Node.TEXT_NODE) {
+  	    window['diagnoseError'](logLevel, vdomChild, domChild);
+	    return false;
+	}
 
         if (vdomChild['text'] === domChild.textContent) {
           vdomChild['domRef'] = domChild;
         } else {
-          var len = vdomChild.text.length,
-              domNodeText = domChild.textContent.substring(0, len);
-          if (domNodeText !== vdomChild.text) return false;
-
-          // There are more VDOM nodes than DOM nodes
-          // Create new DOM node to ensure synchrony between VDom and DOM
-          var partialTxt = doc.createTextNode(domNodeText);
-          node.insertBefore(partialTxt, domChild);
-          vdomChild['domRef'] = partialTxt;
-          domChild.textContent = domChild.textContent.substring(len);
-        }
+          window['diagnoseError'](logLevel, vdomChild, domChild);
+          return false;
+	}
     } else {
       if (domChild.nodeType !== Node.ELEMENT_NODE) return false;
-      window['walk'](vdomChild, domChild, doc);
+      vdomChild['domRef'] = domChild;
+      if(!window['walk'](logLevel, vdomChild, domChild, doc)) return false;
     }
-
   }
-  // After walking the sizes of VDom and DOM should be equal
-  // Otherwise there are DOM nodes unaccounted for
-  if (vtree.children.length !== node.childNodes.length) return false;
-
   return true;
 }
diff --git a/miso.cabal b/miso.cabal
--- a/miso.cabal
+++ b/miso.cabal
@@ -1,5 +1,5 @@
 name:                miso
-version:             1.6.0.0
+version:             1.7.0.0
 category:            Web, Miso, Data Structures
 license:             BSD3
 license-file:        LICENSE
diff --git a/src/Miso/Html.hs b/src/Miso/Html.hs
--- a/src/Miso/Html.hs
+++ b/src/Miso/Html.hs
@@ -34,7 +34,7 @@
    , module Miso.Html.Property
    ) where
 
-import Miso.Html.Element
+import Miso.Html.Element hiding (style_)
 import Miso.Html.Event
-import Miso.Html.Internal
+import Miso.Html.Internal hiding (textRaw)
 import Miso.Html.Property hiding (form_)
diff --git a/src/Miso/Html/Element.hs b/src/Miso/Html/Element.hs
--- a/src/Miso/Html/Element.hs
+++ b/src/Miso/Html/Element.hs
@@ -52,6 +52,7 @@
     , math_
     , script_
     , link_
+    , Miso.Html.Element.style_
     -- * Inputs
     , select_
     , option_
@@ -484,9 +485,28 @@
 -- | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q
 q_ :: [Attribute action] -> [View action] -> View action
 q_ = nodeHtml "q"
--- | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
-script_ :: [Attribute action] -> [View action] -> View action
-script_ = nodeHtml "script"
 -- | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
 link_ :: [Attribute action] -> View action
 link_ = flip (nodeHtml "link") []
+-- | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
+-- This takes the raw text to be put in the style tag.
+-- That means that if any part of the text is not trusted there's
+-- a potential CSS injection. Read more at
+-- https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client_Side_Testing/05-Testing_for_CSS_Injection
+--
+-- You can also easily shoot yourself in the foot with something like:
+--
+--   style_ [] "</style>"
+style_ :: [Attribute action] -> MisoString -> View action
+style_ attrs rawText = node HTML "style" Nothing attrs [textRaw rawText]
+-- | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
+-- This takes the raw text to be put in the script tag.
+-- That means that if any part of the text is not trusted there's
+-- a potential JavaScript injection. Read more at
+-- https://owasp.org/www-community/attacks/xss/
+--
+-- You can also easily shoot yourself in the foot with something like:
+--
+--   script_ [] "</script>"
+script_ :: [Attribute action] -> MisoString -> View action
+script_ attrs rawText = node HTML "script" Nothing attrs [textRaw rawText]
