diff --git a/IHP/HSX/ConvertibleStrings.hs b/IHP/HSX/ConvertibleStrings.hs
new file mode 100644
--- /dev/null
+++ b/IHP/HSX/ConvertibleStrings.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE UndecidableInstances  #-}
+
+module IHP.HSX.ConvertibleStrings where
+
+import Prelude
+import Data.String.Conversions (ConvertibleStrings (convertString), cs)
+import Text.Blaze.Html5
+import Data.Text
+import Data.ByteString
+import qualified Text.Blaze.Html5        as Html5
+import qualified Data.ByteString.Lazy as LBS
+
+instance ConvertibleStrings String Html5.AttributeValue where
+    {-# INLINE convertString #-}
+    convertString = stringValue
+
+instance ConvertibleStrings Text Html5.AttributeValue where
+    {-# INLINE convertString #-}
+    convertString = Html5.textValue
+
+instance ConvertibleStrings String Html5.Html where
+    {-# INLINE convertString #-}
+    convertString = Html5.string
+
+instance ConvertibleStrings ByteString Html5.AttributeValue where
+    {-# INLINE convertString #-}
+    convertString value = convertString (cs value :: Text)
+
+instance ConvertibleStrings LBS.ByteString Html5.AttributeValue where
+    {-# INLINE convertString #-}
+    convertString value = convertString (cs value :: Text)
+
+instance ConvertibleStrings Text Html5.Html where
+    {-# INLINE convertString #-}
+    convertString = Html5.text
diff --git a/IHP/HSX/Parser.hs b/IHP/HSX/Parser.hs
new file mode 100644
--- /dev/null
+++ b/IHP/HSX/Parser.hs
@@ -0,0 +1,632 @@
+{-# LANGUAGE PackageImports  #-}
+{-# LANGUAGE ScopedTypeVariables  #-}
+{-# LANGUAGE NamedFieldPuns  #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE TypeFamilies  #-}
+{-|
+Module: IHP.HSX.Parser
+Description: Parser for HSX code
+Copyright: (c) digitally induced GmbH, 2022
+-}
+module IHP.HSX.Parser
+( parseHsx
+, Node (..)
+, Attribute (..)
+, AttributeValue (..)
+, collapseSpace
+) where
+
+import Prelude
+import Data.Text
+import Data.Set
+import Text.Megaparsec
+import Text.Megaparsec.Char
+import Data.Void
+import qualified Data.Char as Char
+import qualified Data.Text as Text
+import Control.Monad.Fail
+import Data.String.Conversions
+import qualified Data.List as List
+import Control.Monad (unless)
+import Prelude (show)
+import qualified Language.Haskell.Meta as Haskell
+import qualified Language.Haskell.TH.Syntax as Haskell
+import qualified "template-haskell" Language.Haskell.TH as TH
+import qualified Data.Set as Set
+import qualified Data.Containers.ListUtils as List
+
+data AttributeValue = TextValue !Text | ExpressionValue !Haskell.Exp deriving (Eq, Show)
+
+data Attribute = StaticAttribute !Text !AttributeValue | SpreadAttributes !Haskell.Exp deriving (Eq, Show)
+
+data Node = Node !Text ![Attribute] ![Node] !Bool
+    | TextNode !Text
+    | PreEscapedTextNode !Text -- ^ Used in @script@ or @style@ bodies
+    | SplicedNode !Haskell.Exp -- ^ Inline haskell expressions like @{myVar}@ or @{f "hello"}@
+    | Children ![Node]
+    | CommentNode !Text
+    deriving (Eq, Show)
+
+-- | Parses a HSX text and returns a 'Node'
+--
+-- __Example:__
+--
+-- > let filePath = "my-template"
+-- > let line = 0
+-- > let col = 0
+-- > let position = Megaparsec.SourcePos filePath (Megaparsec.mkPos line) (Megaparsec.mkPos col)
+-- > let hsxText = "<strong>Hello</strong>"
+-- >
+-- > let (Right node) = parseHsx position hsxText
+parseHsx :: SourcePos -> Text -> Either (ParseErrorBundle Text Void) Node
+parseHsx position code = runParser (setPosition position *> parser) "" code
+
+type Parser = Parsec Void Text
+
+setPosition pstateSourcePos = updateParserState (\state -> state {
+        statePosState = (statePosState state) { pstateSourcePos }
+    })
+
+parser :: Parser Node
+parser = do
+    space
+    node <- manyHsxElement <|> hsxElement
+    space
+    eof
+    pure node
+
+hsxElement = try hsxComment <|> try hsxSelfClosingElement <|> hsxNormalElement
+
+manyHsxElement = do
+    children <- many hsxChild
+    pure (Children (stripTextNodeWhitespaces children))
+
+hsxSelfClosingElement = do
+    _ <- char '<'
+    name <- hsxElementName
+    let isLeaf = name `Set.member` leafs
+    attributes <-
+      if isLeaf
+        then hsxNodeAttributes (string ">" <|> string "/>")
+        else hsxNodeAttributes (string "/>")
+    space
+    pure (Node name attributes [] isLeaf)
+
+hsxNormalElement = do
+    (name, attributes) <- hsxOpeningElement
+    let parsePreEscapedTextChildren transformText = do
+                    let closingElement = "</" <> name <> ">"
+                    text <- cs <$> manyTill anySingle (string closingElement)
+                    pure [PreEscapedTextNode (transformText text)]
+    let parseNormalHSXChildren = stripTextNodeWhitespaces <$> (space >> (manyTill (try hsxChild) (try (space >> hsxClosingElement name))))
+
+    -- script and style tags have special handling for their children. Inside those tags
+    -- we allow any kind of content. Using a haskell expression like @<script>{myHaskellExpr}</script>@
+    -- will just literally output the string @{myHaskellExpr}@ without evaluating the haskell expression itself.
+    --
+    -- Here is an example HSX code explaining the problem:
+    -- [hsx|<style>h1 { color: red; }</style>|]
+    -- The @{ color: red; }@ would be parsed as a inline haskell expression without the special handling
+    --
+    -- Additionally we don't do the usual escaping for style and script bodies, as this will make e.g. the
+    -- javascript unusuable.
+    children <- case name of
+            "script" -> parsePreEscapedTextChildren Text.strip
+            "style" -> parsePreEscapedTextChildren (collapseSpace . Text.strip)
+            otherwise -> parseNormalHSXChildren
+    pure (Node name attributes children False)
+
+hsxOpeningElement = do
+    char '<'
+    name <- hsxElementName
+    space
+    attributes <- hsxNodeAttributes (char '>')
+    pure (name, attributes)
+
+hsxComment :: Parser Node
+hsxComment = do
+    string "<!--"
+    body :: String <- manyTill (satisfy (const True)) (string "-->")
+    space
+    pure (CommentNode (cs body))
+
+
+hsxNodeAttributes :: Parser a -> Parser [Attribute]
+hsxNodeAttributes end = staticAttributes
+    where
+        staticAttributes = do
+            attributes <- manyTill (hsxNodeAttribute <|> hsxSplicedAttributes) end
+            let staticAttributes = List.filter isStaticAttribute attributes
+            let keys = List.map (\(StaticAttribute name _) -> name) staticAttributes
+            let uniqueKeys = List.nubOrd keys
+            unless (keys == uniqueKeys) (fail $ "Duplicate attribute found in tag: " <> show (keys List.\\ uniqueKeys))
+            pure attributes
+
+isStaticAttribute (StaticAttribute _ _) = True
+isStaticAttribute _ = False
+
+hsxSplicedAttributes :: Parser Attribute
+hsxSplicedAttributes = do
+    name <- between (string "{...") (string "}") (takeWhile1P Nothing (\c -> c /= '}'))
+    space
+    haskellExpression <- case Haskell.parseExp (cs name) of
+            Right expression -> pure (patchExpr expression)
+            Left error -> fail (show error)
+    pure (SpreadAttributes haskellExpression)
+
+hsxNodeAttribute = do
+    key <- hsxAttributeName
+    space
+
+    -- Boolean attributes like <input disabled/> will be represented as <input disabled="disabled"/>
+    -- as there is currently no other way to represent them with blaze-html.
+    --
+    -- There's a special case for data attributes: Data attributes like <form data-disable-javascript-submission/> will be represented as <form data-disable-javascript-submission="true"/>
+    --
+    -- See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
+    let attributeWithoutValue = do
+            let value = if "data-" `Text.isPrefixOf` key
+                    then "true"
+                    else key
+            pure (StaticAttribute key (TextValue value))
+
+    -- Parsing normal attributes like <input value="Hello"/>
+    let attributeWithValue = do
+            _ <- char '='
+            space
+            value <- hsxQuotedValue <|> hsxSplicedValue
+            space
+            pure (StaticAttribute key value)
+
+    attributeWithValue <|> attributeWithoutValue
+
+
+hsxAttributeName :: Parser Text
+hsxAttributeName = do
+        name <- rawAttribute
+        unless (isValidAttributeName name) (fail $ "Invalid attribute name: " <> cs name)
+        pure name
+    where
+        isValidAttributeName name =
+            "data-" `Text.isPrefixOf` name
+            || "aria-" `Text.isPrefixOf` name
+            || "hx-" `Text.isPrefixOf` name
+            || "hx-" `Text.isPrefixOf` name
+            || name `Set.member` attributes
+
+        rawAttribute = takeWhile1P Nothing (\c -> Char.isAlphaNum c || c == '-')
+
+
+hsxQuotedValue :: Parser AttributeValue
+hsxQuotedValue = do
+    value <- between (char '"') (char '"') (takeWhileP Nothing (\c -> c /= '\"'))
+    pure (TextValue value)
+
+hsxSplicedValue :: Parser AttributeValue
+hsxSplicedValue = do
+    value <- between (char '{') (char '}') (takeWhile1P Nothing (\c -> c /= '}'))
+    haskellExpression <- case Haskell.parseExp (cs value) of
+            Right expression -> pure (patchExpr expression)
+            Left error -> fail (show error)
+    pure (ExpressionValue haskellExpression)
+
+hsxClosingElement name = (hsxClosingElement' name) <?> friendlyErrorMessage
+    where
+        friendlyErrorMessage = show (Text.unpack ("</" <> name <> ">"))
+        hsxClosingElement' name = do
+            _ <- string ("</" <> name)
+            space
+            char ('>')
+            pure ()
+
+hsxChild = hsxElement <|> hsxSplicedNode <|> try (space >> hsxElement) <|> hsxText
+
+-- | Parses a hsx text node
+--
+-- Stops parsing when hitting a variable, like `{myVar}`
+hsxText :: Parser Node
+hsxText = buildTextNode <$> takeWhile1P (Just "text") (\c -> c /= '{' && c /= '}' && c /= '<' && c /= '>')
+
+-- | Builds a TextNode and strips all surround whitespace from the input string
+buildTextNode :: Text -> Node
+buildTextNode value = TextNode (collapseSpace value)
+
+data TokenTree = TokenLeaf Text | TokenNode [TokenTree] deriving (Show)
+
+hsxSplicedNode :: Parser Node
+hsxSplicedNode = do
+        expression <- doParse
+        haskellExpression <- case Haskell.parseExp (cs expression) of
+                Right expression -> pure (patchExpr expression)
+                Left error -> fail (show error)
+        pure (SplicedNode haskellExpression)
+    where
+        doParse = do
+            tree <- node
+            let value = (treeToString "" tree)
+            pure $ Text.init $ Text.tail value
+
+        parseTree = node <|> leaf
+        node = TokenNode <$> between (char '{') (char '}') (many parseTree)
+        leaf = TokenLeaf <$> takeWhile1P Nothing (\c -> c /= '{' && c /= '}')
+        treeToString :: Text -> TokenTree -> Text
+        treeToString acc (TokenLeaf value)  = acc <> value
+        treeToString acc (TokenNode [])     = acc
+        treeToString acc (TokenNode (x:xs)) = ((treeToString (acc <> "{") x) <> (Text.concat $ fmap (treeToString "") xs)) <> "}"
+
+
+hsxElementName :: Parser Text
+hsxElementName = do
+    name <- takeWhile1P (Just "identifier") (\c -> Char.isAlphaNum c || c == '_' || c == '-')
+    let isValidParent = name `Set.member` parents
+    let isValidLeaf = name `Set.member` leafs
+    let isValidCustomWebComponent = "-" `Text.isInfixOf` name
+    unless (isValidParent || isValidLeaf || isValidCustomWebComponent) (fail $ "Invalid tag name: " <> cs name)
+    space
+    pure name
+
+hsxIdentifier :: Parser Text
+hsxIdentifier = do
+    name <- takeWhile1P (Just "identifier") (\c -> Char.isAlphaNum c || c == '_')
+    space
+    pure name
+
+
+attributes :: Set Text
+attributes = Set.fromList
+        [ "accept", "accept-charset", "accesskey", "action", "alt", "async"
+        , "autocomplete", "autofocus", "autoplay", "challenge", "charset"
+        , "checked", "cite", "class", "cols", "colspan", "content"
+        , "contenteditable", "contextmenu", "controls", "coords", "data"
+        , "datetime", "defer", "dir", "disabled", "draggable", "enctype"
+        , "form", "formaction", "formenctype", "formmethod", "formnovalidate"
+        , "for"
+        , "formtarget", "headers", "height", "hidden", "high", "href"
+        , "hreflang", "http-equiv", "icon", "id", "ismap", "item", "itemprop"
+        , "itemscope", "itemtype"
+        , "keytype", "label", "lang", "list", "loop", "low", "manifest", "max"
+        , "maxlength", "media", "method", "min", "multiple", "name"
+        , "novalidate", "onbeforeonload", "onbeforeprint", "onblur", "oncanplay"
+        , "oncanplaythrough", "onchange", "oncontextmenu", "onclick"
+        , "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragleave"
+        , "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied"
+        , "onended", "onerror", "onfocus", "onformchange", "onforminput"
+        , "onhaschange", "oninput", "oninvalid", "onkeydown", "onkeyup"
+        , "onload", "onloadeddata", "onloadedmetadata", "onloadstart"
+        , "onmessage", "onmousedown", "onmousemove", "onmouseout", "onmouseover"
+        , "onmouseup", "onmousewheel", "ononline", "onpagehide", "onpageshow"
+        , "onpause", "onplay", "onplaying", "onprogress", "onpropstate"
+        , "onratechange", "onreadystatechange", "onredo", "onresize", "onscroll"
+        , "onseeked", "onseeking", "onselect", "onstalled", "onstorage"
+        , "onsubmit", "onsuspend", "ontimeupdate", "onundo", "onunload"
+        , "onvolumechange", "onwaiting", "open", "optimum", "pattern", "ping"
+        , "placeholder", "preload", "pubdate", "radiogroup", "readonly", "rel"
+        , "required", "reversed", "rows", "rowspan", "sandbox", "scope"
+        , "scoped", "seamless", "selected", "shape", "size", "sizes", "span"
+        , "spellcheck", "src", "srcdoc", "srcset", "start", "step", "style", "subject"
+        , "summary", "tabindex", "target", "title", "type", "usemap", "value"
+        , "width", "wrap", "xmlns"
+        , "ontouchstart", "download"
+        , "allowtransparency", "minlength", "maxlength", "property"
+        , "role"
+        , "d", "viewBox", "cx", "cy", "r", "x", "y", "text-anchor", "alignment-baseline"
+        , "line-spacing", "letter-spacing"
+        , "integrity", "crossorigin", "poster"
+        , "accent-height", "accumulate", "additive", "alphabetic", "amplitude"
+        , "arabic-form", "ascent", "attributeName", "attributeType", "azimuth"
+        , "baseFrequency", "baseProfile", "bbox", "begin", "bias", "by", "calcMode"
+        , "cap-height", "class", "clipPathUnits", "contentScriptType"
+        , "contentStyleType", "cx", "cy", "d", "descent", "diffuseConstant", "divisor"
+        , "dur", "dx", "dy", "edgeMode", "elevation", "end", "exponent"
+        , "externalResourcesRequired", "filterRes", "filterUnits", "font-family"
+        , "font-size", "font-stretch", "font-style", "font-variant", "font-weight"
+        , "format", "from", "fx", "fy", "g1", "g2", "glyph-name", "glyphRef"
+        , "gradientTransform", "gradientUnits", "hanging", "height", "horiz-adv-x"
+        , "horiz-origin-x", "horiz-origin-y", "id", "ideographic", "in", "in2"
+        , "intercept", "k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength"
+        , "keyPoints", "keySplines", "keyTimes", "lang", "lengthAdjust"
+        , "limitingConeAngle", "local", "markerHeight", "markerUnits", "markerWidth"
+        , "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method"
+        , "min", "mode", "name", "numOctaves", "offset", "onabort", "onactivate"
+        , "onbegin", "onclick", "onend", "onerror", "onfocusin", "onfocusout", "onload"
+        , "onmousedown", "onmousemove", "onmouseout", "onmouseover", "onmouseup"
+        , "onrepeat", "onresize", "onscroll", "onunload", "onzoom", "operator", "order"
+        , "orient", "orientation", "origin", "overline-position", "overline-thickness"
+        , "panose-1", "path", "pathLength", "patternContentUnits", "patternTransform"
+        , "patternUnits", "points", "pointsAtX", "pointsAtY", "pointsAtZ"
+        , "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "r", "radius"
+        , "refX", "refY", "rendering-intent", "repeatCount", "repeatDur"
+        , "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx"
+        , "ry", "scale", "seed", "slope", "spacing", "specularConstant"
+        , "specularExponent", "spreadMethod", "startOffset", "stdDeviation", "stemh"
+        , "stemv", "stitchTiles", "strikethrough-position", "strikethrough-thickness"
+        , "string", "style", "surfaceScale", "systemLanguage", "tableValues", "target"
+        , "targetX", "targetY", "textLength", "title", "to", "transform", "type", "u1"
+        , "u2", "underline-position", "underline-thickness", "unicode", "unicode-range"
+        , "units-per-em", "v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical"
+        , "values", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox"
+        , "viewTarget", "width", "widths", "x", "x-height", "x1", "x2"
+        , "xChannelSelector", "xlink:actuate", "xlink:arcrole", "xlink:href"
+        , "xlink:role", "xlink:show", "xlink:title", "xlink:type", "xml:base"
+        , "xml:lang", "xml:space", "y", "y1", "y2", "yChannelSelector", "z", "zoomAndPan"
+        , "alignment-baseline", "baseline-shift", "clip-path", "clip-rule"
+        , "clip", "color-interpolation-filters", "color-interpolation"
+        , "color-profile", "color-rendering", "color", "cursor", "direction"
+        , "display", "dominant-baseline", "enable-background", "fill-opacity"
+        , "fill-rule", "fill", "filter", "flood-color", "flood-opacity"
+        , "font-size-adjust", "glyph-orientation-horizontal"
+        , "glyph-orientation-vertical", "image-rendering", "kerning", "letter-spacing"
+        , "lighting-color", "marker-end", "marker-mid", "marker-start", "mask"
+        , "opacity", "overflow", "pointer-events", "shape-rendering", "stop-color"
+        , "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap"
+        , "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width"
+        , "stroke", "text-anchor", "text-decoration", "text-rendering", "unicode-bidi"
+        , "visibility", "word-spacing", "writing-mode", "is"
+        , "cellspacing", "cellpadding", "bgcolor", "classes"
+        , "loading"
+        , "frameborder", "allow", "allowfullscreen", "nonce", "referrerpolicy"
+        ]
+
+parents :: Set Text
+parents = Set.fromList
+          [ "a"
+          , "abbr"
+          , "address"
+          , "animate"
+          , "animateMotion"
+          , "animateTransform"
+          , "article"
+          , "aside"
+          , "audio"
+          , "b"
+          , "bdi"
+          , "bdo"
+          , "blink"
+          , "blockquote"
+          , "body"
+          , "button"
+          , "canvas"
+          , "caption"
+          , "circle"
+          , "cite"
+          , "clipPath"
+          , "code"
+          , "colgroup"
+          , "command"
+          , "data"
+          , "datalist"
+          , "dd"
+          , "defs"
+          , "del"
+          , "desc"
+          , "details"
+          , "dfn"
+          , "dialog"
+          , "discard"
+          , "div"
+          , "dl"
+          , "dt"
+          , "ellipse"
+          , "em"
+          , "feBlend"
+          , "feColorMatrix"
+          , "feComponentTransfer"
+          , "feComposite"
+          , "feConvolveMatrix"
+          , "feDiffuseLighting"
+          , "feDisplacementMap"
+          , "feDistantLight"
+          , "feDropShadow"
+          , "feFlood"
+          , "feFuncA"
+          , "feFuncB"
+          , "feFuncG"
+          , "feFuncR"
+          , "feGaussianBlur"
+          , "feImage"
+          , "feMerge"
+          , "feMergeNode"
+          , "feMorphology"
+          , "feOffset"
+          , "fePointLight"
+          , "feSpecularLighting"
+          , "feSpotLight"
+          , "feTile"
+          , "feTurbulence"
+          , "fieldset"
+          , "figcaption"
+          , "figure"
+          , "filter"
+          , "footer"
+          , "foreignObject"
+          , "form"
+          , "g"
+          , "h1"
+          , "h2"
+          , "h3"
+          , "h4"
+          , "h5"
+          , "h6"
+          , "hatch"
+          , "hatchpath"
+          , "head"
+          , "header"
+          , "hgroup"
+          , "html"
+          , "i"
+          , "iframe"
+          , "ins"
+          , "ion-icon"
+          , "kbd"
+          , "label"
+          , "legend"
+          , "li"
+          , "line"
+          , "linearGradient"
+          , "loading"
+          , "main"
+          , "map"
+          , "mark"
+          , "marker"
+          , "marquee"
+          , "mask"
+          , "menu"
+          , "mesh"
+          , "meshgradient"
+          , "meshpatch"
+          , "meshrow"
+          , "metadata"
+          , "meter"
+          , "mpath"
+          , "nav"
+          , "noscript"
+          , "object"
+          , "ol"
+          , "optgroup"
+          , "option"
+          , "output"
+          , "p"
+          , "path"
+          , "pattern"
+          , "picture"
+          , "polygon"
+          , "polyline"
+          , "pre"
+          , "progress"
+          , "q"
+          , "radialGradient"
+          , "rect"
+          , "rp"
+          , "rt"
+          , "ruby"
+          , "s"
+          , "samp"
+          , "script"
+          , "section"
+          , "select"
+          , "set"
+          , "slot"
+          , "small"
+          , "source"
+          , "span"
+          , "stop"
+          , "strong"
+          , "style"
+          , "sub"
+          , "summary"
+          , "sup"
+          , "svg"
+          , "switch"
+          , "symbol"
+          , "table"
+          , "tbody"
+          , "td"
+          , "template"
+          , "text"
+          , "textPath"
+          , "textarea"
+          , "tfoot"
+          , "th"
+          , "thead"
+          , "time"
+          , "title"
+          , "tr"
+          , "track"
+          , "tspan"
+          , "u"
+          , "ul"
+          , "unknown"
+          , "use"
+          , "var"
+          , "video"
+          , "view"
+          , "wbr"
+          ]
+
+leafs :: Set Text
+leafs = Set.fromList
+        [ "area"
+        , "base"
+        , "br"
+        , "col"
+        , "embed"
+        , "hr"
+        , "img"
+        , "input"
+        , "link"
+        , "meta"
+        , "param"
+        ]
+
+stripTextNodeWhitespaces nodes = stripLastTextNodeWhitespaces (stripFirstTextNodeWhitespaces nodes)
+
+stripLastTextNodeWhitespaces nodes =
+    let strippedLastElement = if List.length nodes > 0
+            then case List.last nodes of
+                TextNode text -> Just $ TextNode (Text.stripEnd text)
+                otherwise -> Nothing
+            else Nothing
+    in case strippedLastElement of
+        Just last -> (fst $ List.splitAt ((List.length nodes) - 1) nodes) <> [last]
+        Nothing -> nodes
+
+stripFirstTextNodeWhitespaces nodes =
+    let strippedFirstElement = if List.length nodes > 0
+            then case List.head nodes of
+                TextNode text -> Just $ TextNode (Text.stripStart text)
+                otherwise -> Nothing
+            else Nothing
+    in case strippedFirstElement of
+        Just first -> first:(List.tail nodes)
+        Nothing -> nodes
+
+-- | Replaces multiple space characters with a single one
+collapseSpace :: Text -> Text
+collapseSpace text = cs $ filterDuplicateSpaces (cs text)
+    where
+        filterDuplicateSpaces :: String -> String
+        filterDuplicateSpaces string = filterDuplicateSpaces' string False
+
+        filterDuplicateSpaces' :: String -> Bool -> String
+        filterDuplicateSpaces' (char:rest) True | Char.isSpace char = filterDuplicateSpaces' rest True
+        filterDuplicateSpaces' (char:rest) False | Char.isSpace char = ' ':(filterDuplicateSpaces' rest True)
+        filterDuplicateSpaces' (char:rest) isRemovingSpaces = char:(filterDuplicateSpaces' rest False)
+        filterDuplicateSpaces' [] isRemovingSpaces = []
+
+
+patchExpr :: TH.Exp -> TH.Exp
+patchExpr (TH.UInfixE (TH.VarE varName) (TH.VarE hash) (TH.VarE labelValue)) | hash == TH.mkName "#" = TH.AppE (TH.VarE varName) fromLabel
+    where
+            fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))
+--- UInfixE (UInfixE a (VarE |>) (VarE get)) (VarE #) (VarE firstName)
+patchExpr input@(TH.UInfixE (TH.UInfixE a (TH.VarE arrow) (TH.VarE get)) (TH.VarE hash) (TH.VarE labelValue)) | (hash == TH.mkName "#") && (arrow == TH.mkName "|>") && (get == TH.mkName "get") =
+        (TH.UInfixE (patchExpr a) (TH.VarE arrow) (TH.AppE (TH.VarE get) fromLabel))
+    where
+            fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))
+-- UInfixE (UInfixE a (VarE $) (VarE get)) (VarE #) (AppE (VarE id) (VarE checklist))
+patchExpr (TH.UInfixE (TH.UInfixE a b get) (TH.VarE hash) (TH.AppE (TH.VarE labelValue) (TH.VarE d))) | (hash == TH.mkName "#") =
+        TH.UInfixE (patchExpr a) (patchExpr b) (TH.AppE (TH.AppE get fromLabel) (TH.VarE d))
+    where
+            fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))
+patchExpr (TH.UInfixE (TH.VarE varName) (TH.VarE hash) (TH.AppE (TH.VarE labelValue) arg)) | hash == TH.mkName "#" = TH.AppE (TH.AppE (TH.VarE varName) fromLabel) arg
+    where
+            fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))
+patchExpr (TH.UInfixE (TH.VarE a) (TH.VarE hash) (TH.AppE (TH.VarE labelValue) (TH.VarE b))) | hash == TH.mkName "#" =
+        TH.AppE (TH.AppE (TH.VarE a) fromLabel) (TH.VarE b)
+    where
+            fromLabel = TH.AppTypeE (TH.VarE (TH.mkName "fromLabel")) (TH.LitT (TH.StrTyLit (show labelValue)))
+
+patchExpr (TH.UInfixE a b c) = TH.UInfixE (patchExpr a) (patchExpr b) (patchExpr c)
+patchExpr (TH.ParensE e) = TH.ParensE (patchExpr e)
+patchExpr (TH.RecUpdE a b) = TH.RecUpdE (patchExpr a) b
+patchExpr (TH.AppE a b) = TH.AppE (patchExpr a) (patchExpr b)
+patchExpr (TH.LamE a b) = TH.LamE a (patchExpr b)
+patchExpr (TH.LetE a b) = TH.LetE a' (patchExpr b)
+    where
+        a' = List.map patchDec a
+        patchDec (TH.ValD a (TH.NormalB b) c) = (TH.ValD a (TH.NormalB (patchExpr b)) c)
+        patchDec a = a
+patchExpr (TH.CondE a b c) = TH.CondE (patchExpr a) (patchExpr b) (patchExpr c)
+patchExpr (TH.SigE a b) = TH.SigE (patchExpr a) b
+patchExpr e = e
diff --git a/IHP/HSX/QQ.hs b/IHP/HSX/QQ.hs
new file mode 100644
--- /dev/null
+++ b/IHP/HSX/QQ.hs
@@ -0,0 +1,132 @@
+{-# LANGUAGE TemplateHaskell, UndecidableInstances, BangPatterns, PackageImports, FlexibleInstances, OverloadedStrings #-}
+
+{-|
+Module: IHP.HSX.QQ
+Description: Defines the @[hsx||]@ syntax
+Copyright: (c) digitally induced GmbH, 2022
+-}
+module IHP.HSX.QQ (hsx) where
+
+import           Prelude
+import qualified Data.Text as Text
+import Data.Text (Text)
+import           IHP.HSX.Parser
+import qualified "template-haskell" Language.Haskell.TH           as TH
+import qualified "template-haskell" Language.Haskell.TH.Syntax           as TH
+import           Language.Haskell.TH.Quote
+import           Text.Blaze.Html5              ((!))
+import qualified Text.Blaze.Html5              as Html5
+import Text.Blaze.Html (Html)
+import Text.Blaze.Internal (attribute, MarkupM (Parent, Leaf), StaticString (..))
+import Data.String.Conversions
+import IHP.HSX.ToHtml
+import Control.Monad.Fail
+import qualified Text.Megaparsec as Megaparsec
+import qualified Text.Blaze.Html.Renderer.String as BlazeString
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import Data.List (foldl')
+
+hsx :: QuasiQuoter
+hsx = QuasiQuoter {
+        quoteExp = quoteHsxExpression,
+        quotePat = error "quotePat: not defined",
+        quoteDec = error "quoteDec: not defined",
+        quoteType = error "quoteType: not defined"
+    }
+
+quoteHsxExpression :: String -> TH.ExpQ
+quoteHsxExpression code = do
+        hsxPosition <- findHSXPosition
+        expression <- case parseHsx hsxPosition (cs code) of
+                Left error   -> fail (Megaparsec.errorBundlePretty error)
+                Right result -> pure result
+        compileToHaskell expression
+    where
+
+        findHSXPosition = do
+            loc <- TH.location
+            let (line, col) = TH.loc_start loc
+            pure $ Megaparsec.SourcePos (TH.loc_filename loc) (Megaparsec.mkPos line) (Megaparsec.mkPos col)
+
+compileToHaskell :: Node -> TH.ExpQ
+compileToHaskell (Node name attributes children isLeaf) =
+    let
+        renderedChildren = TH.listE $ map compileToHaskell children
+        stringAttributes = TH.listE $ map toStringAttribute attributes
+        openTag :: Text
+        openTag = "<" <> tag
+        tag :: Text
+        tag = cs name
+    in
+        if isLeaf
+            then
+                let
+                    closeTag :: Text
+                    closeTag = ">"
+                in [| (applyAttributes (Leaf (textToStaticString $(TH.lift tag)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) ()) $(stringAttributes)) |]
+            else
+                let
+                    closeTag :: Text
+                    closeTag = "</" <> tag <> ">"
+                in [| (applyAttributes (makeParent (textToStaticString $(TH.lift name)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) $renderedChildren) $(stringAttributes)) |]
+
+compileToHaskell (Children children) =
+    let
+        renderedChildren = TH.listE $ map compileToHaskell children
+    in [| mconcat $(renderedChildren) |]
+
+compileToHaskell (TextNode value) = [| Html5.preEscapedText value |]
+compileToHaskell (PreEscapedTextNode value) = [| Html5.preEscapedText value |]
+compileToHaskell (SplicedNode expression) = [| toHtml $(pure expression) |]
+compileToHaskell (CommentNode value) = [| Html5.textComment value |]
+
+
+toStringAttribute :: Attribute -> TH.ExpQ
+toStringAttribute (StaticAttribute name (TextValue value)) = do
+    let nameWithSuffix = " " <> name <> "=\""
+    if Text.null value
+        then [| \h -> h ! ((attribute (Html5.textTag name) (Html5.textTag nameWithSuffix)) mempty) |]
+        else [| \h -> h ! ((attribute (Html5.textTag name) (Html5.textTag nameWithSuffix)) (Html5.preEscapedTextValue value)) |]
+
+toStringAttribute (StaticAttribute name (ExpressionValue expression)) = let nameWithSuffix = " " <> name <> "=\"" in [| applyAttribute name nameWithSuffix $(pure expression) |]
+toStringAttribute (SpreadAttributes expression) = [| spreadAttributes $(pure expression) |]
+
+spreadAttributes :: ApplyAttribute value => [(Text, value)] -> Html5.Html -> Html5.Html
+spreadAttributes attributes html = applyAttributes html $ map (\(name, value) -> applyAttribute name (" " <> name <> "=\"") value) attributes
+
+applyAttributes :: Html5.Html -> [Html5.Html -> Html5.Html] -> Html5.Html
+applyAttributes element attributes = foldl' (\element attribute -> attribute element) element attributes
+{-# INLINE applyAttributes #-}
+
+makeParent :: StaticString -> StaticString -> StaticString -> [Html] -> Html
+makeParent tag openTag closeTag children = Parent tag openTag closeTag (mconcat children)
+{-# INLINE makeParent #-}
+
+textToStaticString :: Text -> StaticString
+textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 text) text
+{-# INLINE textToStaticString #-}
+
+class ApplyAttribute value where
+    applyAttribute :: Text -> Text -> value -> (Html5.Html -> Html5.Html)
+
+instance ApplyAttribute Bool where
+    applyAttribute attr attr' True h = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') (Html5.textValue value))
+        where
+            value = if "data-" `Text.isPrefixOf` attr
+                    then "true" -- "true" for data attributes
+                    else attr -- normal html boolean attriubtes, like <input disabled="disabled"/>, see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
+    applyAttribute attr attr' false h | "data-" `Text.isPrefixOf` attr = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') "false") -- data attribute set to "false"
+    applyAttribute attr attr' false h = h -- html boolean attribute, like <input disabled/> will be dropped as there is no other way to specify that it's set to false
+    {-# INLINE applyAttribute #-}
+
+instance ApplyAttribute attribute => ApplyAttribute (Maybe attribute) where
+    applyAttribute attr attr' (Just value) h = applyAttribute attr attr' value h
+    applyAttribute attr attr' Nothing h = h
+
+instance {-# OVERLAPPABLE #-} ConvertibleStrings value Html5.AttributeValue => ApplyAttribute value where
+    applyAttribute attr attr' value h = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') (cs value))
+    {-# INLINE applyAttribute #-}
+
+instance Show (MarkupM ()) where
+    show html = BlazeString.renderHtml html
diff --git a/IHP/HSX/ToHtml.hs b/IHP/HSX/ToHtml.hs
new file mode 100644
--- /dev/null
+++ b/IHP/HSX/ToHtml.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE  UndecidableInstances #-}
+{-# LANGUAGE  FlexibleInstances #-}
+
+{-|
+Module: IHP.HSX.ToHtml
+Description: Provides a few helper instances that convert data structures to HTML
+Copyright: (c) digitally induced GmbH, 2022
+-}
+module IHP.HSX.ToHtml where
+
+import Prelude
+import qualified Text.Blaze.Html5 as Html5
+import qualified Text.Blaze.Internal
+import Data.Text
+import Data.ByteString
+import Data.String.Conversions (cs)
+import IHP.HSX.ConvertibleStrings ()
+
+class ToHtml a where
+    toHtml :: a -> Html5.Html
+
+instance ToHtml (Text.Blaze.Internal.MarkupM ()) where
+    {-# INLINE toHtml #-}
+    toHtml a = a
+
+instance ToHtml Text where
+    {-# INLINE toHtml #-}
+    toHtml = Html5.text
+
+instance ToHtml String where
+    {-# INLINE toHtml #-}
+    toHtml = Html5.string
+
+instance ToHtml ByteString where
+    {-# INLINE toHtml #-}
+    toHtml value = toHtml (cs value :: Text)
+
+instance {-# OVERLAPPABLE #-} ToHtml a => ToHtml (Maybe a) where
+    {-# INLINE toHtml #-}
+    toHtml maybeValue = maybe mempty toHtml maybeValue
+
+instance {-# OVERLAPPABLE #-} Show a => ToHtml a where
+    {-# INLINE toHtml #-}
+    toHtml value = Html5.string (show value)
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 digitally induced GmbH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,406 @@
+# HSX
+
+This `ihp-hsx` package provides `[hsx|<h1>Hello World</h1>|]` syntax to IHP projects.
+
+To use it outside of IHP, add this package to your cabal file and use the modules like this:
+
+```haskell
+import IHP.HSX.QQ -- <- The main thing
+import IHP.HSX.ConvertibleStrings () -- <- Helper instancess
+import IHP.HSX.ToHtml () -- <- More helper instances
+
+import Text.Blaze.Html5 -- The blaze html library, IHP HSX is built on top of that
+
+view :: Html
+view = [hsx|
+    <h1>Hello World</h1>
+|]
+
+-- To render the above view:
+import Text.Blaze.Html.Renderer.Text (renderHtml)
+
+rendered :: Text
+rendered = renderHtml view
+```
+
+## Introduction
+
+HSX can be written pretty much like normal HTML. You can write an HSX expression inside your Haskell code by wrapping it with [`[hsx|YOUR HSX CODE|]`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:hsx). HSX expressions are just a syntax for [BlazeHtml](https://jaspervdj.be/blaze/) and thus are automatically escaped as described in the blaze documentation.
+
+Because the HSX is parsed, you will get a syntax error when you type in invalid HTML.
+
+### Inline Haskell
+
+HSX can access Haskell variables wrapped with `{}` like this:
+
+```haskell
+let
+    x :: Text = "World"
+in
+    [hsx|Hello {x}!|]
+```
+
+**If the variable is another HSX expression, a blaze HTML element, a text or string**: it is just included as you would expect.
+
+**If the variable is any other custom Haskell data structure**: it will first be converted to a string representation by calling [`show`](https://ihp.digitallyinduced.com/api-docs/IHP-Prelude.html#v:show) on it. You can add a custom [`ToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-HSX-ToHtml.html#t:ToHtml) (import it from `IHP.HSX.ToHtml`) instance, to customize rendering a data structure.
+
+You can also write more complex code like:
+
+```haskell
+let
+    items :: [Int] = [ 0, 1, 2 ]
+    renderItem n = [hsx|Hello {n}!|]
+in
+    [hsx|Complex demo: {forEach items renderItem}!|]
+```
+
+As the HSX expressions are compiled to Haskell code at compile-time, type errors inside these `{}` expressions will be reported to you by the compiler.
+
+### Dynamic Attributes
+
+The variable syntax can also be used in attribute values:
+
+```haskell
+let
+    inputValue = "Hello World" :: Text
+in
+    [hsx|<input type="text" value={inputValue}/>|]
+```
+
+#### Boolean Attribute Values
+
+HSX has special handling for Boolean values to make it easy to deal with HTML Boolean attributes like `disabled`, `readonly`, `checked`, etc.
+
+You can write
+
+```haskell
+<input disabled={True} />
+```
+
+as a short form for:
+
+```haskell
+<input disabled="disabled" />
+```
+
+Writing `False`:
+
+```haskell
+<input disabled={False} />
+```
+
+This will not render the attribute, [as specified in the HTML standard](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes):
+
+```haskell
+<input />
+```
+
+#### Boolean data attributes
+
+This behavior of omiting a attribute when it's set to `False` does not apply to `data-` attributes.
+
+You can write
+
+```haskell
+<form data-disable-javascript-submission={True}/>
+```
+
+and it will render like this:
+
+```html
+<form data-disable-javascript-submission="true"/>
+```
+
+When set to `False`, like this:
+
+```haskell
+<form data-disable-javascript-submission={False}/>
+```
+
+the output HTML will keep the attribute and set it to `"false"`:
+
+```html
+<form data-disable-javascript-submission="false"/>
+```
+
+#### Maybe Attribute Values / Optional Attributes
+
+HSX has special handling for Maybe values to make it easy to deal with optional attributes.
+
+You can write
+
+```haskell
+let
+    target :: Maybe Text
+    target = Just "_blank"
+in
+    [hsx|<a target={target} />|]
+```
+
+and it will render to:
+
+```haskell
+<a target="_blank" />
+```
+
+Using `Nothing` results in the `target` attribute not being in the output HTML:
+
+```haskell
+let
+    target :: Maybe Text
+    target = Nothing
+in
+    [hsx|<a target={target} />|]
+```
+
+This will render to:
+
+```haskell
+<a />
+```
+
+### Spread Values
+
+For dynamic use cases you can use `{...attributeList}`:
+
+```haskell
+<div { ...[ ("data-my-attribute" :: Text, "Hello World!" :: Text) ] } />
+<div { ...[ ("data-user-" <> tshow userId, tshow userFirstname) ] } />
+<div { ...someVariable } />
+```
+
+Note the `<>` concatenation operator.
+
+### Special Elements: `<script>` and `<style>`
+
+For `<script>` and `<style>` tags HSX applies some special handling. This only applies to tags with inline scripts or styles:
+
+```html
+<!-- No Special Handling: -->
+
+<script src="/hello.js"></script>
+<link rel="stylesheet" href="layout.css" />
+
+<!-- Special Handling applies: -->
+
+<script>
+    alert("Hello");
+</script>
+<style>
+    h1 {
+        color: blue;
+    }
+</style>
+```
+
+Inside those tags using a Haskell expression will not work:
+
+```haskell
+<script>{myHaskellExpr}</script>
+```
+
+This will just literally output the string `{myHaskellExpr}` without evaluating the Haskell expression itself. This is because JavaScript usually uses `{}` for object expressions like `{ a: "hello" }`. The same applies to inline CSS inside `<style>` elements.
+
+So using `{haskellVariables}` inside your JavaScript like this will not work:
+
+```html
+<script>
+    var apiKey = "{apiKey}";
+</script>
+```
+
+Instead use a `data-` attribute to solve this:
+
+```html
+<script data-api-key={apiKey}>
+    var apiKey = document.currentScript.dataset.apiKey;
+</script>
+```
+
+Additionally, HSX will not do the usual escaping for style and script bodies, as this will make e.g. the
+JavaScript unusable.
+
+### Syntax Rules
+
+While most HTML is also valid HSX, there are some difference you need to be aware of:
+
+#### Closing Tags
+
+Tags always need to have a closing tag or be self-closing: so instead of `<br>` you need to write `<br/>`
+
+#### JSX Differences
+
+In JSX you have the restriction that you always have to have a single root tag. In HSX this restriction does not apply, so this is valid HSX (but not valid JSX):
+
+```haskell
+[hsx|
+<div>A</div>
+<div>B</div>
+|]
+```
+
+#### Whitespace
+
+Spaces and newline characters are removed where possible at HSX parse time.
+
+#### Comments
+
+HTML Comments are supported and can be used like this:
+
+```html
+<div>
+    <!-- Begin of Main Section -->
+    <h1>Hello</h1>
+</div>
+```
+
+#### Empty Attributes
+
+HSX allows you to write empty attributes like these:
+
+```haskell
+[hsx|
+    <input disabled/>
+|]
+```
+
+The underlying HTML library blaze currently does not support an empty HTML attribute. Therefore empty attributes are implemented by setting the attribute value to the attribute name. [This is valid HTML supported by all browsers.](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes). Therefore the generated HTML looks like this:
+
+```html
+<input disabled="disabled" />
+```
+
+#### Unescaped Strings
+
+If you use HTML entities, such as `&nbsp;` for a non-breaking space, you will notice they appear exactly like that. To output directly (i.e. unescaped) use the method `preEscapedToMarkup` from `Text.Blaze.Html5`.
+
+
+## Common HSX Patterns
+
+### Dealing with Maybe Values
+
+When dealing with [`Maybe`](https://ihp.digitallyinduced.com/api-docs/IHP-Prelude.html#t:Maybe) values you sometimes want to conditionally render something. E.g. in react to render a tag with JSX you would do it like this:
+
+```html
+<p>Hi {user.name}</p>
+{user.country && <p><small>from {user.country}</small></p>}
+<p>Welcome!</p>
+```
+
+In HSX you usually write it like this:
+
+
+```haskell
+render user = [hsx|
+    <p>Hi {get #name user}</p>
+    {renderCountry}
+|]
+    where
+        renderCountry = case get #country user of
+            Just country -> [hsx|<p><small>{country}</small></p>|]
+            Nothing -> [hsx||]
+```
+
+What about if the country is a empty string? A simple solution could look like this:
+
+```haskell
+renderCountry = case get #country user of
+    Just "" -> [hsx||]
+    Just country -> [hsx|<p>from {country}!</p>|]
+    Nothing -> [hsx||]
+```
+
+That code doesn't feel right. It's better to deal with the problem at create time of the `User` record already. Use  [`emptyValueToNothing`](https://ihp.digitallyinduced.com/api-docs/IHP-Controller-Param.html#v:emptyValueToNothing) to transform an empty `Just ""` to `Nothing` before inserting it into the db:
+
+```haskell
+action CreateUserAction = do
+    newRecord @User
+        |> fill '["name", "country"]
+        |> emptyValueToNothing #country
+        |> createRecord
+```
+
+Now when the country input field is empty when creating the user, the `country` field will be set to `Nothing` instead of `Just ""`.
+
+**The `emptyValueToNothing` function is part of the IHP framework and not bundled in this ihp-hsx package, so you might need to implement it yourself if you're using HSX outside of an IHP app.**
+
+### Displaying HTML without Escaping
+
+In HSX all variables and expressions are automatically escaped to avoid [XSS](https://en.wikipedia.org/wiki/Cross-site_scripting).
+
+Let's say we have variable `myVariable = "<script>alert(1)</script>"`. This is how the HSX will behave:
+
+```html
+<div>{myVariable}</div>
+
+-- RENDERS to this HTML:
+<div>%3Cscript%3Ealert%281%29%3C/script%3E</div>
+```
+
+Sometimes you want to insert some actual HTML code to your HSX using a variable. E.g. when your app is rendering markdown, the generated HTML by the markdown library should not be escaped, the markdown library typically already takes care of that.
+
+In these cases you can use [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) to mark a text as already escaped:
+
+```html
+<div>{markdownHtml |> preEscapedToHtml}</div>
+
+-- Let's define `markdownHtml = "<p>hello</p>"`
+
+-- This will render the following HTML:
+<div><p>hello</p></div>
+```
+
+Be careful when using [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) as it can easily introduce security issues.
+
+The [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) function can also be used to output HTML code that is not supported by HSX:
+The [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelude.html#v:preEscapedToHtml) function can also be used to output HTML code that is not supported by HSX:
+
+```html
+{"<!--[if IE]> Internet Explorer Conditional Comments <![endif]-->" |> preEscapedToHtml}
+```
+
+## Example: HSX and the equivalent BlazeHtml
+
+The following code using HSX:
+
+```haskell
+instance View EditView where
+    html EditView { .. } = [hsx|
+        <nav>
+            <ol class="breadcrumb">
+                <li class="breadcrumb-item"><a href={PostsAction}>Posts</a></li>
+                <li class="breadcrumb-item active">Edit Post</li>
+            </ol>
+        </nav>
+        <h1>Edit Post</h1>
+        {renderForm post}
+    |]
+```
+
+is roughly equivalent to the following BlazeHTML code:
+
+```haskell
+instance View EditView where
+    html EditView { .. } = do
+        H.nav $ do
+            H.ol ! A.class_ "breadcrumb" $ do
+                H.li ! A.class_ "breadcrumb-item" $ do
+                    H.a ! A.href (fromString (P.show PostsAction)) $ do
+                        "Posts"
+                H.li ! A.class_ "breadcrumb-item active" $ do
+                    "Edit Post"
+        H.h1 "Edit Post"
+        renderForm post
+```
+
+given the following imports:
+
+```haskell
+module Web.View.Posts.Edit where
+import Web.View.Prelude
+
+import qualified Text.Blaze.Html5 as H
+import qualified Text.Blaze.Html5.Attributes as A
+
+import Prelude as P
+```
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,5 @@
+# Changelog for `ihp-hsx`
+
+## Versions < 0.18:
+
+[See the IHP main upgrade instructions](https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md)
diff --git a/ihp-hsx.cabal b/ihp-hsx.cabal
new file mode 100644
--- /dev/null
+++ b/ihp-hsx.cabal
@@ -0,0 +1,82 @@
+cabal-version:       2.2
+name:                ihp-hsx
+version:             0.18.0
+synopsis:            JSX-like but for Haskell
+description:         JSX-like templating syntax for Haskell
+license:             MIT
+license-file:        LICENSE
+author:              digitally induced GmbH
+maintainer:          support@digitallyinduced.com
+bug-reports:         https://github.com/digitallyinduced/ihp/issues
+category:            HTML
+build-type:          Simple
+extra-source-files: README.md, changelog.md
+
+source-repository head
+    type:     git
+    location: https://github.com/digitallyinduced/ihp.git
+
+library
+    default-language: Haskell2010
+    build-depends:
+          base               >= 4.1 && <= 4.16
+        , blaze-html         >= 0.9.1 && < 0.10
+        , bytestring         >= 0.10.12 && < 0.11
+        , text               >= 1.2.4 && < 1.3
+        , containers         >= 0.6.5 && < 0.7
+        , template-haskell   >= 2.16.0 && < 2.17
+        , blaze-markup       >= 0.8.2 && < 0.9
+        , haskell-src-meta   >= 0.8.7 && < 0.9
+        , megaparsec         >= 9.0.1 && < 9.1
+        , string-conversions >= 0.4.0 && < 0.5
+    default-extensions:
+        OverloadedStrings
+        , NoImplicitPrelude
+        , ImplicitParams
+        , Rank2Types
+        , NamedFieldPuns
+        , TypeSynonymInstances
+        , FlexibleInstances
+        , DisambiguateRecordFields
+        , DuplicateRecordFields
+        , OverloadedLabels
+        , FlexibleContexts
+        , DataKinds
+        , QuasiQuotes
+        , TypeFamilies
+        , PackageImports
+        , ScopedTypeVariables
+        , RecordWildCards
+        , TypeApplications
+        , DataKinds
+        , InstanceSigs
+        , DeriveGeneric
+        , MultiParamTypeClasses
+        , TypeOperators
+        , DeriveDataTypeable
+        , DefaultSignatures
+        , BangPatterns
+        , FunctionalDependencies
+        , PartialTypeSignatures
+        , BlockArguments
+        , LambdaCase
+        , StandaloneDeriving
+        , TemplateHaskell
+    ghc-options:
+        -fstatic-argument-transformation
+        -funbox-strict-fields
+        -haddock
+        -Wredundant-constraints
+        -Wunused-imports
+        -Wunused-foralls
+        -Wmissing-fields
+        -Winaccessible-code
+        -Wmissed-specialisations
+        -Wall-missed-specialisations
+        -fexpose-all-unfoldings
+    hs-source-dirs: .
+    exposed-modules:
+        IHP.HSX.Parser
+        , IHP.HSX.QQ
+        , IHP.HSX.ToHtml
+        , IHP.HSX.ConvertibleStrings
