diff --git a/IHP/HSX/Attribute.hs b/IHP/HSX/Attribute.hs
deleted file mode 100644
--- a/IHP/HSX/Attribute.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-{-# LANGUAGE UndecidableInstances  #-}
-{-|
-Module: IHP.HSX.Attribute
-Copyright: (c) digitally induced GmbH, 2023
--}
-module IHP.HSX.Attribute
-( ApplyAttribute (..)
-) where
-
-import Prelude
-import Text.Blaze.Html5 ((!))
-import qualified Text.Blaze.Html5 as Html5
-import Text.Blaze.Internal (attribute, MarkupM (Parent, Leaf), StaticString (..))
-import Data.String.Conversions
-import IHP.HSX.ToHtml
-import qualified Data.Text as Text
-import Data.Text (Text)
-
-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
-    {-# INLINE applyAttribute #-}
-
-instance ApplyAttribute Html5.AttributeValue where
-    applyAttribute attr attr' value h = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') value)
-    {-# INLINE applyAttribute #-}
-
-instance {-# OVERLAPPABLE #-} ConvertibleStrings value Html5.AttributeValue => ApplyAttribute value where
-    applyAttribute attr attr' value h = applyAttribute attr attr' ((cs value) :: Html5.AttributeValue) h
-    {-# INLINE applyAttribute #-}
diff --git a/IHP/HSX/ConvertibleStrings.hs b/IHP/HSX/ConvertibleStrings.hs
deleted file mode 100644
--- a/IHP/HSX/ConvertibleStrings.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-{-# 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/HaskellParser.hs b/IHP/HSX/HaskellParser.hs
deleted file mode 100644
--- a/IHP/HSX/HaskellParser.hs
+++ /dev/null
@@ -1,89 +0,0 @@
-{-# LANGUAGE CPP #-}
-module IHP.HSX.HaskellParser (parseHaskellExpression) where
-
-import Prelude
-import GHC.Parser.Lexer (ParseResult (..), PState (..))
-import GHC.Types.SrcLoc
-import qualified GHC.Parser as Parser
-import qualified GHC.Parser.Lexer as Lexer
-import GHC.Data.FastString
-import GHC.Data.StringBuffer
-import GHC.Parser.PostProcess
-import Text.Megaparsec.Pos
-import qualified "template-haskell" Language.Haskell.TH as TH
-
-import qualified GHC.Data.EnumSet as EnumSet
-import GHC
-import IHP.HSX.HsExpToTH (toExp)
-
-import GHC.Types.Error
-import GHC.Utils.Outputable hiding ((<>))
-import GHC.Utils.Error
-import qualified GHC.Types.SrcLoc as SrcLoc
-#if __GLASGOW_HASKELL__ >= 908
-import GHC.Unit.Module.Warnings
-#endif
-
-parseHaskellExpression :: SourcePos -> [TH.Extension] -> String -> Either (Int, Int, String) TH.Exp
-parseHaskellExpression sourcePos extensions input =
-        case expr of
-            POk parserState result -> Right (toExp (unLoc result))
-            PFailed parserState ->
-                let
-                    error = renderWithContext defaultSDocContext
-                        $ vcat
-#if __GLASGOW_HASKELL__ >= 908
-                        $ map formatBulleted
-#else
-                        $ map (formatBulleted defaultSDocContext)
-#endif
-#if __GLASGOW_HASKELL__ >= 906
-                        $ map (diagnosticMessage NoDiagnosticOpts)
-#else
-                        $ map diagnosticMessage
-#endif
-                        $ map errMsgDiagnostic
-                        $ sortMsgBag Nothing
-                        $ getMessages parserState.errors
-                    line = SrcLoc.srcLocLine parserState.loc.psRealLoc
-                    col = SrcLoc.srcLocCol parserState.loc.psRealLoc
-                in
-                    Left (line, col, error)
-    where
-        expr :: ParseResult (LocatedA (HsExpr GhcPs))
-        expr = case Lexer.unP Parser.parseExpression parseState of
-                POk parserState result -> Lexer.unP (runPV (unECP result)) parserState
-                PFailed parserState -> PFailed parserState
-
-        location :: RealSrcLoc
-        location = mkRealSrcLoc filename line col
-        
-        filename :: FastString
-        filename = mkFastString sourcePos.sourceName
-
-        line :: Int
-        line = unPos sourcePos.sourceLine
-
-        col :: Int
-        col = unPos sourcePos.sourceColumn
-
-        buffer = stringToStringBuffer input
-        parseState = Lexer.initParserState parserOpts buffer location
-
-        parserOpts :: Lexer.ParserOpts
-        parserOpts = Lexer.mkParserOpts (EnumSet.fromList extensions) diagOpts [] False False False False
-
-diagOpts :: DiagOpts
-diagOpts =
-    DiagOpts
-    { diag_warning_flags = EnumSet.empty
-    , diag_fatal_warning_flags = EnumSet.empty
-    , diag_warn_is_error = False
-    , diag_reverse_errors = False
-    , diag_max_errors = Nothing
-    , diag_ppr_ctx = defaultSDocContext
-#if __GLASGOW_HASKELL__ >= 908
-    , diag_custom_warning_categories = emptyWarningCategorySet
-    , diag_fatal_custom_warning_categories = emptyWarningCategorySet
-#endif
-    }
diff --git a/IHP/HSX/HsExpToTH.hs b/IHP/HSX/HsExpToTH.hs
deleted file mode 100644
--- a/IHP/HSX/HsExpToTH.hs
+++ /dev/null
@@ -1,239 +0,0 @@
-{-# LANGUAGE ViewPatterns, CPP #-}
-{-|
-Module: IHP.HSX.HsExpToTH
-Copyright: (c) digitally induced GmbH, 2022
-Description: Converts Haskell AST to Template Haskell AST
-
-Based on https://github.com/guibou/PyF/blob/b3aaee12d34380e55aa3909690041eccb8fcf001/src/PyF/Internal/Meta.hs
--}
-module IHP.HSX.HsExpToTH (toExp) where
-
-import Prelude
-
-import GHC.Hs.Expr as Expr
-import GHC.Hs.Extension as Ext
-import GHC.Hs.Pat as Pat
-import GHC.Hs.Lit
-import qualified GHC.Hs.Utils as Utils
-import qualified Data.ByteString as B
-import qualified Language.Haskell.TH.Syntax as TH
-import GHC.Types.SrcLoc
-import GHC.Types.Name
-import GHC.Types.Name.Reader
-import GHC.Data.FastString
-import GHC.Utils.Outputable (Outputable, ppr, showSDocUnsafe)
-import GHC.Types.Basic (Boxity(..))
-import GHC.Types.SourceText (il_value, rationalFromFractionalLit)
-import qualified GHC.Unit.Module as Module
-import GHC.Stack
-import qualified Data.List.NonEmpty as NonEmpty
-import Language.Haskell.Syntax.Type
-#if __GLASGOW_HASKELL__ >= 906
-import Language.Haskell.Syntax.Basic
-#endif
-
-
-fl_value = rationalFromFractionalLit
-
-toLit :: HsLit GhcPs -> TH.Lit
-toLit (HsChar _ c) = TH.CharL c
-toLit (HsCharPrim _ c) = TH.CharPrimL c
-toLit (HsString _ s) = TH.StringL (unpackFS s)
-toLit (HsStringPrim _ s) = TH.StringPrimL (B.unpack s)
-toLit (HsInt _ i) = TH.IntegerL (il_value i)
-toLit (HsIntPrim _ i) = TH.IntPrimL i
-toLit (HsWordPrim _ i) = TH.WordPrimL i
-toLit (HsInt64Prim _ i) = TH.IntegerL i
-toLit (HsWord64Prim _ i) = TH.WordPrimL i
-toLit (HsInteger _ i _) = TH.IntegerL i
-toLit (HsRat _ f _) = TH.FloatPrimL (fl_value f)
-toLit (HsFloatPrim _ f) = TH.FloatPrimL (fl_value f)
-toLit (HsDoublePrim _ f) = TH.DoublePrimL (fl_value f)
-
-toLit' :: OverLitVal -> TH.Lit
-toLit' (HsIntegral i) = TH.IntegerL (il_value i)
-toLit' (HsFractional f) = TH.RationalL (fl_value f)
-toLit' (HsIsString _ fs) = TH.StringL (unpackFS fs)
-
-toType :: HsType GhcPs -> TH.Type
-toType (HsWildCardTy _) = TH.WildCardT
-toType (HsTyVar _ _ n) =
-  let n' = unLoc n
-   in if isRdrTyVar n'
-        then TH.VarT (toName n')
-        else TH.ConT (toName n')
-toType t = todo "toType" t
-
-toName :: RdrName -> TH.Name
-toName n = case n of
-  (Unqual o) -> TH.mkName (occNameString o)
-  (Qual m o) -> TH.mkName (Module.moduleNameString m <> "." <> occNameString o)
-  (Exact name) -> TH.mkName ((occNameString . rdrNameOcc . getRdrName) name) --error "exact"
-  (Orig _ _) -> error "orig"
-
-toFieldExp :: a
-toFieldExp = undefined
-
-toPat :: Pat.Pat GhcPs -> TH.Pat
-toPat (Pat.VarPat _ (unLoc -> name)) = TH.VarP (toName name)
-toPat (TuplePat _ p _) = TH.TupP (map (toPat . unLoc) p)
-toPat (ParPat xP _ lP _) = (toPat . unLoc) lP
-toPat (ConPat pat_con_ext ((unLoc -> name)) pat_args) = TH.ConP (toName name) (map toType []) (map (toPat . unLoc) (Pat.hsConPatArgs pat_args))
-toPat (ViewPat pat_con pat_args pat_con_ext) = error "TH.ViewPattern not implemented"
-toPat (SumPat _ _ _ _) = error "TH.SumPat not implemented"
-toPat (WildPat _ ) = error "TH.WildPat not implemented"
-toPat (NPat _ _ _ _ ) = error "TH.NPat not implemented"
-toPat p = todo "toPat" p
-
-toExp :: Expr.HsExpr GhcPs -> TH.Exp
-toExp (Expr.HsVar _ n) =
-  let n' = unLoc n
-   in if isRdrDataCon n'
-        then TH.ConE (toName n')
-        else TH.VarE (toName n')
-
-#if __GLASGOW_HASKELL__ >= 906
-toExp (Expr.HsUnboundVar _ n)              = TH.UnboundVarE (TH.mkName . occNameString $ occName n)
-#else
-toExp (Expr.HsUnboundVar _ n)              = TH.UnboundVarE (TH.mkName . occNameString $ n)
-#endif
-
-toExp Expr.HsIPVar {}
-  = noTH "toExp" "HsIPVar"
-
-toExp (Expr.HsLit _ l)
-  = TH.LitE (toLit l)
-
-toExp (Expr.HsOverLit _ OverLit {ol_val})
-  = TH.LitE (toLit' ol_val)
-
-toExp (Expr.HsApp _ e1 e2)
-  = TH.AppE (toExp . unLoc $ e1) (toExp . unLoc $ e2)
-
-#if __GLASGOW_HASKELL__ >= 906
-toExp (Expr.HsAppType _ e _ HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body)
-#else
-toExp (Expr.HsAppType _ e HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body)
-#endif
-toExp (Expr.ExprWithTySig _ e HsWC{hswc_body=unLoc -> HsSig{sig_body}}) = TH.SigE (toExp . unLoc $ e) (toType . unLoc $ sig_body)
-
-toExp (Expr.OpApp _ e1 o e2)
-  = TH.UInfixE (toExp . unLoc $ e1) (toExp . unLoc $ o) (toExp . unLoc $ e2)
-
-toExp (Expr.NegApp _ e _)
-  = TH.AppE (TH.VarE 'negate) (toExp . unLoc $ e)
-
--- NOTE: for lambda, there is only one match
-#if __GLASGOW_HASKELL__ >= 906
-toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)]))))
-#else
-toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)])) _))
-#endif
-  = TH.LamE (fmap toPat ps) (toExp e)
-
--- toExp (Expr.Let _ bs e)                       = TH.LetE (toDecs bs) (toExp e)
---
-toExp (Expr.HsIf _ a b c)                   = TH.CondE (toExp (unLoc a)) (toExp (unLoc b)) (toExp (unLoc c))
-
--- toExp (Expr.MultiIf _ ifs)                    = TH.MultiIfE (map toGuard ifs)
--- toExp (Expr.Case _ e alts)                    = TH.CaseE (toExp e) (map toMatch alts)
--- toExp (Expr.Do _ ss)                          = TH.DoE (map toStmt ss)
--- toExp e@Expr.MDo{}                            = noTH "toExp" e
---
-toExp (Expr.ExplicitTuple _ args boxity) = ctor tupArgs
-  where
-    toTupArg (Expr.Present _ e) = Just $ unLoc e
-    toTupArg (Expr.Missing _) = Nothing
-    toTupArg _ = error "impossible case"
-
-    ctor = case boxity of
-      Boxed -> TH.TupE
-      Unboxed -> TH.UnboxedTupE
-
-    tupArgs = fmap ((fmap toExp) . toTupArg) args
-
--- toExp (Expr.List _ xs)                        = TH.ListE (fmap toExp xs)
-toExp (Expr.HsPar _ _ e _)
-  = TH.ParensE (toExp . unLoc $ e)
-
-toExp (Expr.SectionL _ (unLoc -> a) (unLoc -> b))
-  = TH.InfixE (Just . toExp $ a) (toExp b) Nothing
-
-toExp (Expr.SectionR _ (unLoc -> a) (unLoc -> b))
-  = TH.InfixE Nothing (toExp a) (Just . toExp $ b)
-
-toExp (Expr.RecordCon _ name HsRecFields {rec_flds})
-  = TH.RecConE (toName . unLoc $ name) (fmap toFieldExp rec_flds)
-
-toExp (Expr.RecordUpd _ (unLoc -> e) xs)                 = TH.RecUpdE (toExp e) $ case xs of
-#if __GLASGOW_HASKELL__ >= 908
-    RegularRecUpdFields { recUpdFields = fields } ->
-#else
-    Left fields ->
-#endif
-        let
-            f (unLoc -> x) = (name, value)
-                where
-                    value = toExp $ unLoc $ hfbRHS x
-                    name =
-                        case unLoc (hfbLHS x) of
-                            Unambiguous _ (unLoc -> name) -> toName name
-                            Ambiguous _ (unLoc -> name) -> toName name
-        in
-            map f fields
-    otherwise -> error "todo"
--- toExp (Expr.ListComp _ e ss)                  = TH.CompE $ map convert ss ++ [TH.NoBindS (toExp e)]
---  where
---   convert (Expr.QualStmt _ st)                = toStmt st
---   convert s                                   = noTH "toExp ListComp" s
--- toExp (Expr.ExpTypeSig _ e t)                 = TH.SigE (toExp e) (toType t)
---
-toExp (Expr.ExplicitList _ (map unLoc -> args)) = TH.ListE (map toExp args)
-
-toExp (Expr.ArithSeq _ _ e)
-  = TH.ArithSeqE $ case e of
-    (From a) -> TH.FromR (toExp $ unLoc a)
-    (FromThen a b) -> TH.FromThenR (toExp $ unLoc a) (toExp $ unLoc b)
-    (FromTo a b) -> TH.FromToR (toExp $ unLoc a) (toExp $ unLoc b)
-    (FromThenTo a b c) -> TH.FromThenToR (toExp $ unLoc a) (toExp $ unLoc b) (toExp $ unLoc c)
-
-
-toExp (Expr.HsProjection _ locatedFields) =
-  let
-    extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr
-    extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..."
-  in
-#if __GLASGOW_HASKELL__ >= 906
-    TH.ProjectionE (NonEmpty.map (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc) locatedFields)
-#else
-    TH.ProjectionE (NonEmpty.map (unpackFS . unLoc . extractFieldLabel . unLoc) locatedFields)
-#endif
-
-toExp (Expr.HsGetField _ expr locatedField) =
-  let
-    extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr
-    extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..."
-  in
-#if __GLASGOW_HASKELL__ >= 906
-    TH.GetFieldE (toExp (unLoc expr)) (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc $ locatedField)
-#else
-    TH.GetFieldE (toExp (unLoc expr)) (unpackFS . unLoc . extractFieldLabel . unLoc $ locatedField)
-#endif
-
-#if __GLASGOW_HASKELL__ >= 906
-toExp (Expr.HsOverLabel _ _ fastString) = TH.LabelE (unpackFS fastString)
-#else
-toExp (Expr.HsOverLabel _ fastString) = TH.LabelE (unpackFS fastString)
-#endif
-
-toExp e = todo "toExp" e
-
-
-todo :: Outputable e => String -> e -> a
-todo fun thing = error . concat $ [moduleName, ".", fun, ": not implemented: ", (showSDocUnsafe $ ppr thing)]
-
-noTH :: (HasCallStack, Show e) => String -> e -> a
-noTH fun thing = error . concat $ [moduleName, ".", fun, ": no TemplateHaskell for: ", show thing]
-
-moduleName :: String
-moduleName = "IHP.HSX.HsExpToTH"
diff --git a/IHP/HSX/Parser.hs b/IHP/HSX/Parser.hs
deleted file mode 100644
--- a/IHP/HSX/Parser.hs
+++ /dev/null
@@ -1,642 +0,0 @@
-{-# 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
-, HsxSettings (..)
-) 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 Data.String.Conversions
-import qualified Data.List as List
-import Control.Monad (unless)
-import qualified "template-haskell" 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
-import qualified IHP.HSX.HaskellParser as HaskellParser
-
-data HsxSettings = HsxSettings
-    { checkMarkup :: Bool
-    , additionalTagNames :: Set Text
-    , additionalAttributeNames :: Set Text
-    }
-
-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 -- ^ A Comment that is rendered in the final HTML
-    | NoRenderCommentNode -- ^ A comment that is not rendered in the final HTML
-    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 settings position [] hsxText
-parseHsx :: HsxSettings -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node
-parseHsx settings position extensions code =
-    let
-        ?extensions = extensions
-        ?settings = settings
-    in
-        runParser (setPosition position *> parser) "" code
-
-type Parser a = (?extensions :: [TH.Extension], ?settings :: HsxSettings) => Parsec Void Text a
-
-setPosition pstateSourcePos = updateParserState (\state -> state {
-        statePosState = (statePosState state) { pstateSourcePos }
-    })
-
-parser :: Parser Node
-parser = do
-    space
-    node <- manyHsxElement <|> hsxElement
-    space
-    eof
-    pure node
-
-hsxElement :: Parser Node
-hsxElement = try hsxNoRenderComment <|> try hsxComment <|> try hsxSelfClosingElement <|> hsxNormalElement
-
-manyHsxElement :: Parser Node
-manyHsxElement = do
-    children <- many hsxChild
-    pure (Children (stripTextNodeWhitespaces children))
-
-hsxSelfClosingElement :: Parser Node
-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 :: Parser Node
-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 :: Parser (Text, [Attribute])
-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))
-
-hsxNoRenderComment :: Parser Node
-hsxNoRenderComment = do
-    string "{-"
-    manyTill (satisfy (const True)) (string "-}")
-    space
-    pure NoRenderCommentNode
-
-
-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
-    (pos, name) <- between (do char '{'; optional space; string "...") (string "}") do
-            pos <- getSourcePos
-            code <- takeWhile1P Nothing (\c -> c /= '}')
-            pure (pos, code)
-    space
-    haskellExpression <- parseHaskellExpression pos (cs name)
-    pure (SpreadAttributes haskellExpression)
-
-parseHaskellExpression :: SourcePos -> Text -> Parser Haskell.Exp
-parseHaskellExpression sourcePos input = do
-    case HaskellParser.parseHaskellExpression sourcePos ?extensions (cs input) of
-        Right expression -> pure expression
-        Left (line, col, error) -> do
-            pos <- getSourcePos
-            setPosition pos { sourceLine = mkPos line, sourceColumn = mkPos col }
-            fail (show error)
-
-hsxNodeAttribute :: Parser Attribute
-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
-        let checkingMarkup = ?settings.checkMarkup
-        unless (isValidAttributeName name || not checkingMarkup) (fail $ "Invalid attribute name: " <> cs name)
-        pure name
-    where
-        isValidAttributeName name =
-            "data-" `Text.isPrefixOf` name
-            || "aria-" `Text.isPrefixOf` name
-            || "hx-" `Text.isPrefixOf` name
-            || name `Set.member` attributes
-            || name `Set.member` ?settings.additionalAttributeNames
-
-        rawAttribute = takeWhile1P Nothing (\c -> Char.isAlphaNum c || c == '-' || c == '_')
-
-
-hsxQuotedValue :: Parser AttributeValue
-hsxQuotedValue = do
-    value <- between (char '"') (char '"') (takeWhileP Nothing (\c -> c /= '\"'))
-    pure (TextValue value)
-
-hsxSplicedValue :: Parser AttributeValue
-hsxSplicedValue = do
-    (pos, value) <- between (char '{') (char '}') do
-        pos <- getSourcePos
-        code <- takeWhile1P Nothing (\c -> c /= '}')
-        pure (pos, code)
-    haskellExpression <- parseHaskellExpression pos (cs value)
-    pure (ExpressionValue haskellExpression)
-
-hsxClosingElement name = (hsxClosingElement' name) <?> friendlyErrorMessage
-    where
-        friendlyErrorMessage = show (Text.unpack ("</" <> name <> ">"))
-        hsxClosingElement' name = do
-            _ <- string ("</" <> name)
-            space
-            char ('>')
-            pure ()
-
-hsxChild :: Parser Node
-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
-        (pos, expression) <- doParse
-        haskellExpression <- parseHaskellExpression pos (cs expression)
-        pure (SplicedNode haskellExpression)
-    where
-        doParse = do
-            (pos, tree) <- node
-            let value = (treeToString "" tree)
-            pure (pos, Text.init $ Text.tail value)
-
-        parseTree = (snd <$> node) <|> leaf
-        node = between (char '{') (char '}') do
-                pos <- getSourcePos
-                tree <- many parseTree
-                pure (pos, TokenNode tree)
-        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 == '-' || c == '!')
-    let isValidParent = name `Set.member` parents
-    let isValidLeaf = name `Set.member` leafs
-    let isValidCustomWebComponent = "-" `Text.isInfixOf` name 
-                                  && not (Text.isPrefixOf "-" name)
-                                  && not (Char.isNumber (Text.head name))
-    let isValidAdditionalTag = name `Set.member` ?settings.additionalTagNames
-    let checkingMarkup = ?settings.checkMarkup
-    unless (isValidParent || isValidLeaf || isValidCustomWebComponent || isValidAdditionalTag || not checkingMarkup) (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", "slot"
-        , "kind"
-        , "html"
-        , "sse-connect", "sse-swap"
-        ]
-
-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"
-        , "!DOCTYPE"
-        ]
-
-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 = []
diff --git a/IHP/HSX/QQ.hs b/IHP/HSX/QQ.hs
deleted file mode 100644
--- a/IHP/HSX/QQ.hs
+++ /dev/null
@@ -1,481 +0,0 @@
-{-# 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, uncheckedHsx, customHsx) where
-
-import           Prelude
-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 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')
-import IHP.HSX.Attribute
-import qualified Text.Blaze.Html5.Attributes as Attributes
-import qualified Data.HashMap.Strict as HashMap
-import qualified Data.Set as Set
-
-hsx :: QuasiQuoter
-hsx = customHsx 
-        (HsxSettings 
-            { checkMarkup = True
-            , additionalTagNames = Set.empty
-            , additionalAttributeNames = Set.empty
-            }
-        )
-
-uncheckedHsx :: QuasiQuoter
-uncheckedHsx = customHsx
-        (HsxSettings 
-            { checkMarkup = False
-            , additionalTagNames = Set.empty
-            , additionalAttributeNames = Set.empty
-            }
-        )
-
-customHsx :: HsxSettings -> QuasiQuoter
-customHsx settings = 
-    QuasiQuoter 
-        { quoteExp = quoteHsxExpression settings
-        , quotePat = error "quotePat: not defined"
-        , quoteDec = error "quoteDec: not defined"
-        , quoteType = error "quoteType: not defined"
-        }
-
-quoteHsxExpression :: HsxSettings -> String -> TH.ExpQ
-quoteHsxExpression settings code = do
-        hsxPosition <- findHSXPosition
-        extensions <- TH.extsEnabled
-        expression <- case parseHsx settings hsxPosition extensions (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 "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True) = [| Html5.docType |]
-compileToHaskell (Node name attributes children isLeaf) =
-    let
-        renderedChildren = TH.listE $ map compileToHaskell children
-        stringAttributes = TH.listE $ map toStringAttribute attributes
-    in
-        if isLeaf
-            then
-                let
-                    element = nodeToBlazeLeaf name
-                in
-                    [| applyAttributes $element $stringAttributes |]
-            else
-                let
-                    element = nodeToBlazeElement name
-                in [| applyAttributes ($element (mconcat $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 |]
-compileToHaskell (NoRenderCommentNode) = [| mempty |]
-
-nodeToBlazeElement :: Text -> TH.Q TH.Exp
-nodeToBlazeElement name =
-    HashMap.findWithDefault (nodeToBlazeElementGeneric name) name knownElements
-
-knownElements :: HashMap.HashMap Text TH.ExpQ
-knownElements =
-    HashMap.fromList
-        [ ("a", [| Html5.a |])
-        , ("abbr", [| Html5.abbr |])
-        , ("address", [| Html5.address |])
-        , ("article", [| Html5.article |])
-        , ("aside", [| Html5.aside |])
-        , ("audio", [| Html5.audio |])
-        , ("b", [| Html5.b |])
-        , ("blockquote", [| Html5.blockquote |])
-        , ("body", [| Html5.body |])
-        , ("button", [| Html5.button |])
-        , ("canvas", [| Html5.canvas |])
-        , ("caption", [| Html5.caption |])
-        , ("cite", [| Html5.cite |])
-        , ("code", [| Html5.code |])
-        , ("colgroup", [| Html5.colgroup |])
-        , ("datalist", [| Html5.datalist |])
-        , ("dd", [| Html5.dd |])
-        , ("del", [| Html5.del |])
-        , ("details", [| Html5.details |])
-        , ("dfn", [| Html5.dfn |])
-        , ("div", [| Html5.div |])
-        , ("dl", [| Html5.dl |])
-        , ("dt", [| Html5.dt |])
-        , ("em", [| Html5.em |])
-        , ("fieldset", [| Html5.fieldset |])
-        , ("figcaption", [| Html5.figcaption |])
-        , ("figure", [| Html5.figure |])
-        , ("footer", [| Html5.footer |])
-        , ("form", [| Html5.form |])
-        , ("h1", [| Html5.h1 |])
-        , ("h2", [| Html5.h2 |])
-        , ("h3", [| Html5.h3 |])
-        , ("h4", [| Html5.h4 |])
-        , ("h5", [| Html5.h5 |])
-        , ("h6", [| Html5.h6 |])
-        , ("head", [| Html5.head |])
-        , ("header", [| Html5.header |])
-        , ("hgroup", [| Html5.hgroup |])
-        , ("html", [| Html5.html |])
-        , ("i", [| Html5.i |])
-        , ("iframe", [| Html5.iframe |])
-        , ("ins", [| Html5.ins |])
-        , ("kbd", [| Html5.kbd |])
-        , ("label", [| Html5.label |])
-        , ("legend", [| Html5.legend |])
-        , ("li", [| Html5.li |])
-        , ("main", [| Html5.main |])
-        , ("map", [| Html5.map |])
-        , ("mark", [| Html5.mark |])
-        , ("menu", [| Html5.menu |])
-        , ("menuitem", [| Html5.menuitem |])
-        , ("meter", [| Html5.meter |])
-        , ("nav", [| Html5.nav |])
-        , ("noscript", [| Html5.noscript |])
-        , ("object", [| Html5.object |])
-        , ("ol", [| Html5.ol |])
-        , ("optgroup", [| Html5.optgroup |])
-        , ("option", [| Html5.option |])
-        , ("output", [| Html5.output |])
-        , ("p", [| Html5.p |])
-        , ("pre", [| Html5.pre |])
-        , ("progress", [| Html5.progress |])
-        , ("q", [| Html5.q |])
-        , ("rp", [| Html5.rp |])
-        , ("rt", [| Html5.rt |])
-        , ("ruby", [| Html5.ruby |])
-        , ("s", [| Html5.s |])
-        , ("samp", [| Html5.samp |])
-        , ("script", [| Html5.script |])
-        , ("section", [| Html5.section |])
-        , ("select", [| Html5.select |])
-        , ("small", [| Html5.small |])
-        , ("span", [| Html5.span |])
-        , ("strong", [| Html5.strong |])
-        , ("style", [| Html5.style |])
-        , ("sub", [| Html5.sub |])
-        , ("summary", [| Html5.summary |])
-        , ("sup", [| Html5.sup |])
-        , ("table", [| Html5.table |])
-        , ("tbody", [| Html5.tbody |])
-        , ("td", [| Html5.td |])
-        , ("textarea", [| Html5.textarea |])
-        , ("tfoot", [| Html5.tfoot |])
-        , ("th", [| Html5.th |])
-        , ("thead", [| Html5.thead |])
-        , ("time", [| Html5.time |])
-        , ("title", [| Html5.title |])
-        , ("tr", [| Html5.tr |])
-        , ("u", [| Html5.u |])
-        , ("ul", [| Html5.ul |])
-        , ("var", [| Html5.var |])
-        , ("video", [| Html5.video |])
-        ]
-
-nodeToBlazeLeaf :: Text -> TH.Q TH.Exp
-nodeToBlazeLeaf name =
-    HashMap.findWithDefault (nodeToBlazeLeafGeneric name) name knownLeafs
-
-knownLeafs :: HashMap.HashMap Text TH.ExpQ
-knownLeafs =
-    HashMap.fromList
-        [ ("area", [| Html5.area |])
-        , ("base", [| Html5.base |])
-        , ("br", [| Html5.br |])
-        , ("col", [| Html5.col |])
-        , ("embed", [| Html5.embed |])
-        , ("hr", [| Html5.hr |])
-        , ("img", [| Html5.img |])
-        , ("input", [| Html5.input |])
-        , ("keygen", [| Html5.keygen |])
-        , ("link", [| Html5.link |])
-        , ("meta", [| Html5.meta |])
-        , ("param", [| Html5.param |])
-        , ("source", [| Html5.source |])
-        , ("track", [| Html5.track |])
-        , ("wbr", [| Html5.wbr |])
-        ]
-
-nodeToBlazeElementGeneric :: Text -> TH.Q TH.Exp
-nodeToBlazeElementGeneric name =
-    let
-        openTag :: Text
-        openTag = "<" <> tag
-        
-        tag :: Text
-        tag = cs name
-
-        closeTag :: Text
-        closeTag = "</" <> tag <> ">"
-    in
-        [| makeParent (textToStaticString $(TH.lift name)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) |]
-
-nodeToBlazeLeafGeneric :: Text -> TH.Q TH.Exp
-nodeToBlazeLeafGeneric name =
-    let
-        openTag :: Text
-        openTag = "<" <> tag
-
-        closeTag :: Text
-        closeTag = ">"
-        
-        tag :: Text
-        tag = cs name
-    in
-        [| (Leaf (textToStaticString $(TH.lift tag)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) ()) |]
-
-toStringAttribute :: Attribute -> TH.ExpQ
-toStringAttribute (StaticAttribute name (TextValue value)) =
-    attributeFromName name value
-
-toStringAttribute (StaticAttribute name (ExpressionValue expression)) = let nameWithSuffix = " " <> name <> "=\"" in [| applyAttribute name nameWithSuffix $(pure expression) |]
-toStringAttribute (SpreadAttributes expression) = [| spreadAttributes $(pure expression) |]
-
-attributeFromName :: Text -> Text -> TH.ExpQ
-attributeFromName name value =
-    let
-        value' :: TH.ExpQ
-        value' = if Text.null value then [| mempty |] else [| Html5.preEscapedTextValue value |] 
-
-        attr = attributeFromName' name
-    in
-        [| (! $attr $value') |]
-
-attributeFromName' :: Text -> TH.ExpQ
-attributeFromName' name =
-    HashMap.findWithDefault (attributeFromNameGeneric name) name knownAttributes
-
-knownAttributes :: HashMap.HashMap Text TH.ExpQ
-knownAttributes =
-    HashMap.fromList
-        [ ("accept", [| Attributes.accept |])
-        , ( "accept-charset", [| Attributes.acceptCharset |])
-        , ( "accesskey", [| Attributes.accesskey |])
-        , ( "action", [| Attributes.action |])
-        , ( "alt", [| Attributes.alt |])
-        , ( "async", [| Attributes.async |])
-        , ( "autocomplete", [| Attributes.autocomplete |])
-        , ( "autofocus", [| Attributes.autofocus |])
-        , ( "autoplay", [| Attributes.autoplay |])
-        , ( "challenge", [| Attributes.challenge |])
-        , ( "charset", [| Attributes.charset |])
-        , ( "checked", [| Attributes.checked |])
-        , ( "cite", [| Attributes.cite |])
-        , ( "class", [| Attributes.class_ |])
-        , ( "cols", [| Attributes.cols |])
-        , ( "colspan", [| Attributes.colspan |])
-        , ( "content", [| Attributes.content |])
-        , ( "contenteditable", [| Attributes.contenteditable |])
-        , ( "contextmenu", [| Attributes.contextmenu |])
-        , ( "controls", [| Attributes.controls |])
-        , ( "coords", [| Attributes.coords |])
-        , ( "data", [| Attributes.data_ |])
-        , ( "datetime", [| Attributes.datetime |])
-        , ( "defer", [| Attributes.defer |])
-        , ( "dir", [| Attributes.dir |])
-        , ( "disabled", [| Attributes.disabled |])
-        , ( "download", [| Attributes.download |])
-        , ( "draggable", [| Attributes.draggable |])
-        , ( "enctype", [| Attributes.enctype |])
-        , ( "for", [| Attributes.for |])
-        , ( "form", [| Attributes.form |])
-        , ( "formaction", [| Attributes.formaction |])
-        , ( "formenctype", [| Attributes.formenctype |])
-        , ( "formmethod", [| Attributes.formmethod |])
-        , ( "formnovalidate", [| Attributes.formnovalidate |])
-        , ( "formtarget", [| Attributes.formtarget |])
-        , ( "headers", [| Attributes.headers |])
-        , ( "height", [| Attributes.height |])
-        , ( "hidden", [| Attributes.hidden |])
-        , ( "high", [| Attributes.high |])
-        , ( "href", [| Attributes.href |])
-        , ( "hreflang", [| Attributes.hreflang |])
-        , ( "http-equiv", [| Attributes.httpEquiv |])
-        , ( "icon", [| Attributes.icon |])
-        , ( "id", [| Attributes.id |])
-        , ( "ismap", [| Attributes.ismap |])
-        , ( "item", [| Attributes.item |])
-        , ( "itemprop", [| Attributes.itemprop |])
-        , ( "itemscope", [| Attributes.itemscope |])
-        , ( "itemtype", [| Attributes.itemtype |])
-        , ( "keytype", [| Attributes.keytype |])
-        , ( "label", [| Attributes.label |])
-        , ( "lang", [| Attributes.lang |])
-        , ( "list", [| Attributes.list |])
-        , ( "loop", [| Attributes.loop |])
-        , ( "low", [| Attributes.low |])
-        , ( "manifest", [| Attributes.manifest |])
-        , ( "max", [| Attributes.max |])
-        , ( "maxlength", [| Attributes.maxlength |])
-        , ( "media", [| Attributes.media |])
-        , ( "method", [| Attributes.method |])
-        , ( "min", [| Attributes.min |])
-        , ( "minlength", [| Attributes.minlength |])
-        , ( "multiple", [| Attributes.multiple |])
-        , ( "muted", [| Attributes.muted |])
-        , ( "name", [| Attributes.name |])
-        , ( "novalidate", [| Attributes.novalidate |])
-        , ( "onbeforeonload", [| Attributes.onbeforeonload |])
-        , ( "onbeforeprint", [| Attributes.onbeforeprint |])
-        , ( "onblur", [| Attributes.onblur |])
-        , ( "oncanplay", [| Attributes.oncanplay |])
-        , ( "oncanplaythrough", [| Attributes.oncanplaythrough |])
-        , ( "onchange", [| Attributes.onchange |])
-        , ( "onclick", [| Attributes.onclick |])
-        , ( "oncontextmenu", [| Attributes.oncontextmenu |])
-        , ( "ondblclick", [| Attributes.ondblclick |])
-        , ( "ondrag", [| Attributes.ondrag |])
-        , ( "ondragend", [| Attributes.ondragend |])
-        , ( "ondragenter", [| Attributes.ondragenter |])
-        , ( "ondragleave", [| Attributes.ondragleave |])
-        , ( "ondragover", [| Attributes.ondragover |])
-        , ( "ondragstart", [| Attributes.ondragstart |])
-        , ( "ondrop", [| Attributes.ondrop |])
-        , ( "ondurationchange", [| Attributes.ondurationchange |])
-        , ( "onemptied", [| Attributes.onemptied |])
-        , ( "onended", [| Attributes.onended |])
-        , ( "onerror", [| Attributes.onerror |])
-        , ( "onfocus", [| Attributes.onfocus |])
-        , ( "onformchange", [| Attributes.onformchange |])
-        , ( "onforminput", [| Attributes.onforminput |])
-        , ( "onhaschange", [| Attributes.onhaschange |])
-        , ( "oninput", [| Attributes.oninput |])
-        , ( "oninvalid", [| Attributes.oninvalid |])
-        , ( "onkeydown", [| Attributes.onkeydown |])
-        , ( "onkeypress", [| Attributes.onkeypress |])
-        , ( "onkeyup", [| Attributes.onkeyup |])
-        , ( "onload", [| Attributes.onload |])
-        , ( "onloadeddata", [| Attributes.onloadeddata |])
-        , ( "onloadedmetadata", [| Attributes.onloadedmetadata |])
-        , ( "onloadstart", [| Attributes.onloadstart |])
-        , ( "onmessage", [| Attributes.onmessage |])
-        , ( "onmousedown", [| Attributes.onmousedown |])
-        , ( "onmousemove", [| Attributes.onmousemove |])
-        , ( "onmouseout", [| Attributes.onmouseout |])
-        , ( "onmouseover", [| Attributes.onmouseover |])
-        , ( "onmouseup", [| Attributes.onmouseup |])
-        , ( "onmousewheel", [| Attributes.onmousewheel |])
-        , ( "ononline", [| Attributes.ononline |])
-        , ( "onpagehide", [| Attributes.onpagehide |])
-        , ( "onpageshow", [| Attributes.onpageshow |])
-        , ( "onpause", [| Attributes.onpause |])
-        , ( "onplay", [| Attributes.onplay |])
-        , ( "onplaying", [| Attributes.onplaying |])
-        , ( "onprogress", [| Attributes.onprogress |])
-        , ( "onpropstate", [| Attributes.onpropstate |])
-        , ( "onratechange", [| Attributes.onratechange |])
-        , ( "onreadystatechange", [| Attributes.onreadystatechange |])
-        , ( "onredo", [| Attributes.onredo |])
-        , ( "onresize", [| Attributes.onresize |])
-        , ( "onscroll", [| Attributes.onscroll |])
-        , ( "onseeked", [| Attributes.onseeked |])
-        , ( "onseeking", [| Attributes.onseeking |])
-        , ( "onselect", [| Attributes.onselect |])
-        , ( "onstalled", [| Attributes.onstalled |])
-        , ( "onstorage", [| Attributes.onstorage |])
-        , ( "onsubmit", [| Attributes.onsubmit |])
-        , ( "onsuspend", [| Attributes.onsuspend |])
-        , ( "ontimeupdate", [| Attributes.ontimeupdate |])
-        , ( "onundo", [| Attributes.onundo |])
-        , ( "onunload", [| Attributes.onunload |])
-        , ( "onvolumechange", [| Attributes.onvolumechange |])
-        , ( "onwaiting", [| Attributes.onwaiting |])
-        , ( "open", [| Attributes.open |])
-        , ( "optimum", [| Attributes.optimum |])
-        , ( "pattern", [| Attributes.pattern |])
-        , ( "ping", [| Attributes.ping |])
-        , ( "placeholder", [| Attributes.placeholder |])
-        , ( "poster", [| Attributes.poster |])
-        , ( "preload", [| Attributes.preload |])
-        , ( "property", [| Attributes.property |])
-        , ( "pubdate", [| Attributes.pubdate |])
-        , ( "radiogroup", [| Attributes.radiogroup |])
-        , ( "readonly", [| Attributes.readonly |])
-        , ( "rel", [| Attributes.rel |])
-        , ( "required", [| Attributes.required |])
-        , ( "reversed", [| Attributes.reversed |])
-        , ( "role", [| Attributes.role |])
-        , ( "rows", [| Attributes.rows |])
-        , ( "rowspan", [| Attributes.rowspan |])
-        , ( "sandbox", [| Attributes.sandbox |])
-        , ( "scope", [| Attributes.scope |])
-        , ( "scoped", [| Attributes.scoped |])
-        , ( "seamless", [| Attributes.seamless |])
-        , ( "selected", [| Attributes.selected |])
-        , ( "shape", [| Attributes.shape |])
-        , ( "size", [| Attributes.size |])
-        , ( "sizes", [| Attributes.sizes |])
-        , ( "span", [| Attributes.span |])
-        , ( "spellcheck", [| Attributes.spellcheck |])
-        , ( "src", [| Attributes.src |])
-        , ( "srcdoc", [| Attributes.srcdoc |])
-        , ( "start", [| Attributes.start |])
-        , ( "step", [| Attributes.step |])
-        , ( "style", [| Attributes.style |])
-        , ( "subject", [| Attributes.subject |])
-        , ( "summary", [| Attributes.summary |])
-        , ( "tabindex", [| Attributes.tabindex |])
-        , ( "target", [| Attributes.target |])
-        , ( "title", [| Attributes.title |])
-        , ( "type", [| Attributes.type_ |])
-        , ( "usemap", [| Attributes.usemap |])
-        , ( "value", [| Attributes.value |])
-        , ( "width", [| Attributes.width |])
-        , ( "wrap", [| Attributes.wrap |])
-        , ( "xmlns", [| Attributes.xmlns |])
-        ]
-
-attributeFromNameGeneric :: Text -> TH.ExpQ
-attributeFromNameGeneric name =
-    let
-        nameWithSuffix = " " <> name <> "=\""
-    in
-        [| attribute (Html5.textTag name) (Html5.textTag nameWithSuffix) |]
-
-spreadAttributes :: ApplyAttribute value => [(Text, value)] -> Html5.Html -> Html5.Html
-spreadAttributes attributes html = applyAttributes html $ map (\(name, value) -> applyAttribute name (" " <> name <> "=\"") value) attributes
-{-# INLINE spreadAttributes #-}
-
-applyAttributes :: Html5.Html -> [Html5.Html -> Html5.Html] -> Html5.Html
-applyAttributes element (attribute:rest) = applyAttributes (attribute element) rest
-applyAttributes element [] = element
-{-# INLINE applyAttributes #-}
-
-makeParent :: StaticString -> StaticString -> StaticString -> Html -> Html
-makeParent tag openTag closeTag children = Parent tag openTag closeTag children
-{-# INLINE makeParent #-}
-
-textToStaticString :: Text -> StaticString
-textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 text) text
-{-# INLINE textToStaticString #-}
-
-instance Show (MarkupM ()) where
-    show html = BlazeString.renderHtml html
diff --git a/IHP/HSX/ToHtml.hs b/IHP/HSX/ToHtml.hs
deleted file mode 100644
--- a/IHP/HSX/ToHtml.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-{-# 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/Test/IHP/HSX/CustomHsxCases.hs b/Test/IHP/HSX/CustomHsxCases.hs
--- a/Test/IHP/HSX/CustomHsxCases.hs
+++ b/Test/IHP/HSX/CustomHsxCases.hs
@@ -4,15 +4,42 @@
 -}
 module IHP.HSX.CustomHsxCases where
 
-import Test.Hspec
 import Prelude
-import IHP.HSX.QQ
-import qualified Text.Blaze.Renderer.Text as Blaze
-import Data.Text
+import qualified IHP.HSX.QQ as Blaze
+import qualified IHP.HSX.ToHtml as Blaze
+import qualified IHP.HSX.Lucid2.QQ as Lucid2
+import qualified IHP.HSX.Lucid2.ToHtml as IHPLucid2
+import qualified Text.Blaze as Blaze
 import Language.Haskell.TH.Quote
+import qualified "template-haskell" Language.Haskell.TH           as TH
 import IHP.HSX.Parser
 import qualified Data.Set as Set
+import qualified "lucid2" Lucid.Base as Lucid2 (Html, HtmlT, ToHtml(..))
+import Lucid.Base (generalizeHtmlT)
 
+{- This allows us to share test cases between the blaze and lucid backends for
+ - HSX. We build a QuasiQuoter that takes the same string splice as input, and
+ - runs it through both backends. It also converts the results into lazy text
+ - values for testing purposes.
+ -}
+customHsx :: HsxSettings -> QuasiQuoter
+customHsx settings =
+    QuasiQuoter
+        { quoteExp = quoteHsxExpressionShared settings
+        , quotePat = error "quotePat: not defined"
+        , quoteDec = error "quoteDec: not defined"
+        , quoteType = error "quoteType: not defined"
+        }
+
+hsx :: QuasiQuoter
+hsx = customHsx
+        (HsxSettings
+            { checkMarkup = True
+            , additionalTagNames = Set.empty
+            , additionalAttributeNames = Set.empty
+            }
+        )
+
 myCustomHsx :: QuasiQuoter
 myCustomHsx = customHsx 
     (HsxSettings { checkMarkup = True
@@ -36,3 +63,31 @@
                  , additionalAttributeNames = Set.fromList ["my-custom-attr", "anothercustomattr"]
                  }
     )
+
+data AllBackends = MkAllBackends
+  { blazeMarkup :: !Blaze.Markup
+  , lucid2Html :: !(Lucid2.Html ())
+  , lucid2HtmlM :: !(Lucid2.Html ())
+  } deriving Show
+
+instance Lucid2.ToHtml AllBackends where
+  toHtml = generalizeHtmlT . lucid2Html
+  toHtmlRaw = generalizeHtmlT . lucid2Html
+
+instance (Monad m) => IHPLucid2.EmbedAsHtml (Lucid2.HtmlT m) AllBackends where
+    toHtml = IHPLucid2.Lucid2Html . generalizeHtmlT . lucid2HtmlM
+    toHtmlRaw = IHPLucid2.Lucid2Html . generalizeHtmlT . lucid2HtmlM
+
+instance Blaze.ToHtml AllBackends where
+  toHtml = blazeMarkup
+
+quoteHsxExpressionShared :: HsxSettings -> String -> TH.ExpQ
+quoteHsxExpressionShared settings spliceStr =
+  let blazeExp = Blaze.quoteHsxExpression settings spliceStr
+      lucidExp = Lucid2.quoteHsxExpression settings spliceStr
+      lucidExpM = Lucid2.quoteHsxExpressionM settings spliceStr
+   in [| MkAllBackends
+       { blazeMarkup = $blazeExp
+       , lucid2Html = $lucidExp
+       , lucid2HtmlM = $lucidExpM
+       } |]
diff --git a/Test/IHP/HSX/QQSpec.hs b/Test/IHP/HSX/QQSpec.hs
--- a/Test/IHP/HSX/QQSpec.hs
+++ b/Test/IHP/HSX/QQSpec.hs
@@ -6,111 +6,118 @@
 
 import Test.Hspec
 import Prelude
-import IHP.HSX.QQ
+import Control.Monad.State.Strict (State, get, put, evalState)
+import qualified IHP.HSX.QQ as Blaze
+import qualified IHP.HSX.Lucid2.QQ as Lucid2
+import qualified IHP.HSX.Lucid2.Attribute as Lucid2
 import qualified Text.Blaze.Renderer.Text as Blaze
+import qualified Text.Blaze as Blaze
 import Text.Blaze (preEscapedTextValue)
-import Data.Text
+import Data.Text (Text)
+import qualified Data.Text.Lazy as TL
 import IHP.HSX.CustomHsxCases
+import qualified "lucid2" Lucid.Base as Lucid2 (Html, HtmlT, renderText, renderTextT)
 
 tests :: SpecWith ()
 tests = do
     describe "HSX" do
         it "should work with static html" do
-            [hsx|<strong>hello world</strong>|] `shouldBeHtml` "<strong>hello world</strong>"
-            [hsx|<h1>hello world</h1>|] `shouldBeHtml` "<h1>hello world</h1>"
-            [hsx|<blink>web scale</blink>|] `shouldBeHtml` "<blink>web scale</blink>"
+            [hsx|<strong>hello world</strong>|] `shouldBeSameHtml` "<strong>hello world</strong>"
+            [hsx|<h1>hello world</h1>|] `shouldBeSameHtml` "<h1>hello world</h1>"
+            [hsx|<blink>web scale</blink>|] `shouldBeSameHtml` "<blink>web scale</blink>"
 
         it "should work with an empty input" do
-            [hsx||] `shouldBeHtml` ""
+            [hsx||] `shouldBeSameHtml` ""
 
         it "should support multiple root nodes" do
-            [hsx|<u>underlined</u><i>italic</i>|] `shouldBeHtml` "<u>underlined</u><i>italic</i>"
+            [hsx|<u>underlined</u><i>italic</i>|] `shouldBeSameHtml` "<u>underlined</u><i>italic</i>"
 
         it "should work with text variables" do
             let myString :: Text = "World!"
-            [hsx|Hello {myString}|] `shouldBeHtml` "Hello World!"
+            [hsx|Hello {myString}|] `shouldBeSameHtml` "Hello World!"
 
         it "should work with complex haskell expressions" do
             let project = Project { name = "Testproject" }
-            [hsx|<h1>Project: {project.name}</h1>|] `shouldBeHtml` "<h1>Project: Testproject</h1>"
+            [hsx|<h1>Project: {project.name}</h1>|] `shouldBeSameHtml` "<h1>Project: Testproject</h1>"
 
         it "should support lambdas and pattern matching on constructors" do
             let placeData = PlaceId "Punches Cross"
-            [hsx|<h1>{(\(PlaceId x) -> x)(placeData)}</h1>|] `shouldBeHtml` "<h1>Punches Cross</h1>"
+            [hsx|<h1>{(\(PlaceId x) -> x)(placeData)}</h1>|] `shouldBeSameHtml` "<h1>Punches Cross</h1>"
 
         it "should support infix notation for standard constructors e.g. (:):" do
-            [hsx| <h1>{show $ (:) 1  [2,3,42]}</h1> |] `shouldBeHtml` "<h1>[1,2,3,42]</h1>"
+            [hsx| <h1>{show $ (:) 1  [2,3,42]}</h1> |] `shouldBeSameHtml` "<h1>[1,2,3,42]</h1>"
 
         it "should support infix notation for standard constructors e.g. (,):" do
-            [hsx|<h1>{((,) 1 2)}</h1>|] `shouldBeHtml` "<h1>(1,2)</h1>"
+            [hsx|<h1>{show $ ((,) 1 2)}</h1>|] `shouldBeSameHtml` "<h1>(1,2)</h1>"
 
         it "should support self closing tags" do
-            [hsx|<input>|] `shouldBeHtml` "<input>"
-            [hsx|<br><br/>|] `shouldBeHtml` "<br><br>"
+            [hsx|<input>|] `shouldBeSameHtml` "<input>"
+            [hsx|<br><br/>|] `shouldBeSameHtml` "<br><br>"
 
         it "should support boolean attributes" do
-            [hsx|<input disabled>|] `shouldBeHtml` "<input disabled=\"disabled\">"
-            [hsx|<input disabled={True}>|] `shouldBeHtml` "<input disabled=\"disabled\">"
-            [hsx|<input disabled={False}>|] `shouldBeHtml` "<input>"
+            [hsx|<input disabled>|] `shouldBeSameHtml` "<input disabled=\"disabled\">"
+            [hsx|<input disabled={True}>|] `shouldBeSameHtml` "<input disabled=\"disabled\">"
+            [hsx|<input disabled={False}>|] `shouldBeSameHtml` "<input>"
 
         it "should correctly output True and False values in data attributes" do
-            [hsx|<form data-disabled-javascript-submission></form>|] `shouldBeHtml` "<form data-disabled-javascript-submission=\"true\"></form>"
-            [hsx|<form data-disabled-javascript-submission={True}></form>|] `shouldBeHtml` "<form data-disabled-javascript-submission=\"true\"></form>"
-            [hsx|<form data-disabled-javascript-submission={False}></form>|] `shouldBeHtml` "<form data-disabled-javascript-submission=\"false\"></form>"
+            [hsx|<form data-disabled-javascript-submission></form>|] `shouldBeSameHtml` "<form data-disabled-javascript-submission=\"true\"></form>"
+            [hsx|<form data-disabled-javascript-submission={True}></form>|] `shouldBeSameHtml` "<form data-disabled-javascript-submission=\"true\"></form>"
+            [hsx|<form data-disabled-javascript-submission={False}></form>|] `shouldBeSameHtml` "<form data-disabled-javascript-submission=\"false\"></form>"
 
         it "should work with inline JS" do
-            [hsx|<script>var a = { hello: true }; alert('hello world');</script>|] `shouldBeHtml` "<script>var a = { hello: true }; alert('hello world');</script>"
-            [hsx|<script>{haskellCodeNotWorkingHere}</script>|] `shouldBeHtml` "<script>{haskellCodeNotWorkingHere}</script>"
+            [hsx|<script>var a = { hello: true }; alert('hello world');</script>|] `shouldBeSameHtml` "<script>var a = { hello: true }; alert('hello world');</script>"
+            [hsx|<script>{haskellCodeNotWorkingHere}</script>|] `shouldBeSameHtml` "<script>{haskellCodeNotWorkingHere}</script>"
 
         it "should work with inline CSS" do
-            [hsx|<style>.blue { background: blue; }</style>|] `shouldBeHtml` "<style>.blue { background: blue; }</style>"
-            [hsx|<style>{haskellCodeNotWorkingHere}</style>|] `shouldBeHtml` "<style>{haskellCodeNotWorkingHere}</style>"
+            [hsx|<style>.blue { background: blue; }</style>|] `shouldBeSameHtml` "<style>.blue { background: blue; }</style>"
+            [hsx|<style>{haskellCodeNotWorkingHere}</style>|] `shouldBeSameHtml` "<style>{haskellCodeNotWorkingHere}</style>"
 
         it "should strip whitespace around script tags" do
             [hsx|
                 <script>
                     var apiKey = document.currentScript.dataset.apiKey;
                 </script>
-            |] `shouldBeHtml` "<script>var apiKey = document.currentScript.dataset.apiKey;</script>"
+            |] `shouldBeSameHtml` "<script>var apiKey = document.currentScript.dataset.apiKey;</script>"
 
         it "should strip whitespace around text nodes" do
-            [hsx| <strong> Hello World </strong> |] `shouldBeHtml` "<strong>Hello World</strong>"
-            [hsx|<i> a</i>|] `shouldBeHtml` "<i>a</i>"
-            [hsx|<i> a b</i>|] `shouldBeHtml` "<i>a b</i>"
-            [hsx|<i> a b c </i>|] `shouldBeHtml` "<i>a b c</i>"
+            [hsx| <strong> Hello World </strong> |] `shouldBeSameHtml` "<strong>Hello World</strong>"
+            [hsx|<i> a</i>|] `shouldBeSameHtml` "<i>a</i>"
+            [hsx|<i> a b</i>|] `shouldBeSameHtml` "<i>a b</i>"
+            [hsx|<i> a b c </i>|] `shouldBeSameHtml` "<i>a b c</i>"
 
         it "should collapse spaces" do
             [hsx|
                 Hello
                 World
                 !
-            |] `shouldBeHtml` "Hello World !"
+            |] `shouldBeSameHtml` "Hello World !"
 
         it "should not strip whitespace around variables near to text nodes" do
             let name :: Text = "Tester"
-            [hsx| <strong> Hello {name} ! </strong> |] `shouldBeHtml` "<strong>Hello Tester !</strong>"
-            [hsx| <strong> Hello {name}{name} ! </strong> |] `shouldBeHtml` "<strong>Hello TesterTester !</strong>"
-            [hsx| <strong> Hello {name} {name} ! </strong> |] `shouldBeHtml` "<strong>Hello Tester Tester !</strong>"
-            [hsx|{name}|] `shouldBeHtml` "Tester"
+            [hsx| <strong> Hello {name} ! </strong> |] `shouldBeSameHtml` "<strong>Hello Tester !</strong>"
+            [hsx| <strong> Hello {name}{name} ! </strong> |] `shouldBeSameHtml` "<strong>Hello TesterTester !</strong>"
+            [hsx| <strong> Hello {name} {name} ! </strong> |] `shouldBeSameHtml` "<strong>Hello Tester Tester !</strong>"
+            [hsx|{name}|] `shouldBeSameHtml` "Tester"
 
             let question :: Text = "Q"
             let answer :: Text = "A"
-            [hsx|<td>{question} → {answer}</td>|] `shouldBeHtml` "<td>Q → A</td>"
+            [hsx|<td>{question} → {answer}</td>|] `shouldBeSameHtml` "<td>Q → A</td>"
 
         it "should work with html comments" do
-            [hsx|<div><!--my comment--></div>|] `shouldBeHtml` "<div><!-- my comment --></div>"
+            [Blaze.hsx|<div><!--my comment--></div>|] `shouldBeBlazeHtml` "<div><!-- my comment --></div>"
+            [Lucid2.hsx|<div><!--my comment--></div>|] `shouldBeLucid2Html` "<div><!--my comment--></div>"
 
         it "should work with no render comments" do
-            [hsx|<div>{- my comment -}</div>|] `shouldBeHtml` "<div></div>"
+            [hsx|<div>{- my comment -}</div>|] `shouldBeSameHtml` "<div></div>"
 
         it "should escape variables to avoid XSS" do
             let variableContent :: Text = "<script>alert(1);</script>"
-            [hsx|{variableContent}|] `shouldBeHtml` "&lt;script&gt;alert(1);&lt;/script&gt;"
+            [hsx|{variableContent}|] `shouldBeSameHtml` "&lt;script&gt;alert(1);&lt;/script&gt;"
 
         it "should parse custom web component tags" do
-            [hsx|<confetti-effect></confetti-effect>|] `shouldBeHtml` "<confetti-effect></confetti-effect>"
-            [hsx|<confetti-effect/>|] `shouldBeHtml` "<confetti-effect></confetti-effect>" -- Currently we cannot deal with self closing tags as expected
-            [hsx|<div is="confetti-effect"></div>|] `shouldBeHtml` "<div is=\"confetti-effect\"></div>"
+            [hsx|<confetti-effect></confetti-effect>|] `shouldBeSameHtml` "<confetti-effect></confetti-effect>"
+            [hsx|<confetti-effect/>|] `shouldBeSameHtml` "<confetti-effect></confetti-effect>" -- Currently we cannot deal with self closing tags as expected
+            [hsx|<div is="confetti-effect"></div>|] `shouldBeSameHtml` "<div is=\"confetti-effect\"></div>"
 
         it "should parse a small hsx document" do
             let metaTags = [hsx|
@@ -135,13 +142,13 @@
                         <title>IHP Forum</title>
                     </head>
                 </html>
-            |] `shouldBeHtml` "<html><head><meta charset=\"utf-8\"> <link rel=\"stylesheet\" href=\"/vendor/bootstrap.min.css\"><link rel=\"stylesheet\" href=\"/vendor/flatpickr.min.css\"><link rel=\"stylesheet\" href=\"/app.css\"> <script src=\"/prod.js\"></script><title>IHP Forum</title></head></html>"
+            |] `shouldBeSameHtml` "<html><head><meta charset=\"utf-8\"> <link rel=\"stylesheet\" href=\"/vendor/bootstrap.min.css\"><link rel=\"stylesheet\" href=\"/vendor/flatpickr.min.css\"><link rel=\"stylesheet\" href=\"/app.css\"> <script src=\"/prod.js\"></script><title>IHP Forum</title></head></html>"
 
         it "should parse an example hsx document" do
             let metaTags = [hsx|
                 <meta charset="utf-8">
             |]
-            metaTags `shouldBeHtml` "<meta charset=\"utf-8\">"
+            metaTags `shouldBeSameHtml` "<meta charset=\"utf-8\">"
             let stylesheets = [hsx|
                 <link rel="stylesheet" href="/vendor/bootstrap.min.css"/>
                 <link rel="stylesheet" href="/vendor/flatpickr.min.css"/>
@@ -168,44 +175,67 @@
                         </div>
                     </body>
                 </html>
-            |] `shouldBeHtml` "<html><head><meta charset=\"utf-8\"> <link rel=\"stylesheet\" href=\"/vendor/bootstrap.min.css\"><link rel=\"stylesheet\" href=\"/vendor/flatpickr.min.css\"><link rel=\"stylesheet\" href=\"/app.css\"> <script src=\"/prod.js\"></script><title>IHP Forum</title></head><body><div class=\"container mt-4\"><nav class=\"navbar navbar-expand-lg navbar-light mb-4\"><a class=\"navbar-brand\" href=\"/\">\955 IHP Forum</a></nav></div></body></html>"
+            |] `shouldBeSameHtml` "<html><head><meta charset=\"utf-8\"> <link rel=\"stylesheet\" href=\"/vendor/bootstrap.min.css\"><link rel=\"stylesheet\" href=\"/vendor/flatpickr.min.css\"><link rel=\"stylesheet\" href=\"/app.css\"> <script src=\"/prod.js\"></script><title>IHP Forum</title></head><body><div class=\"container mt-4\"><nav class=\"navbar navbar-expand-lg navbar-light mb-4\"><a class=\"navbar-brand\" href=\"/\">\955 IHP Forum</a></nav></div></body></html>"
 
         it "should handle spread attributes with a variable" do
             let customAttributes :: [(Text, Text)] = [
                     ("hello", "world")
                     ]
-            [hsx|<div {...customAttributes}></div>|] `shouldBeHtml` "<div hello=\"world\"></div>"
+            [hsx|<div {...customAttributes}></div>|] `shouldBeSameHtml` "<div hello=\"world\"></div>"
 
         it "should handle spread attributes with a list" do
             -- See https://github.com/digitallyinduced/ihp/issues/1226
 
-            [hsx|<div {...[ ("data-hoge" :: Text, "Hello World!" :: Text) ]}></div>|] `shouldBeHtml` "<div data-hoge=\"Hello World!\"></div>"
+            [hsx|<div {...[ ("data-hoge" :: Text, "Hello World!" :: Text) ]}></div>|] `shouldBeSameHtml` "<div data-hoge=\"Hello World!\"></div>"
 
-        it "should support pre escaped class names" do
+        it "should support pre escaped class names for Blaze" do
             -- See https://github.com/digitallyinduced/ihp/issues/1527
 
             let className = preEscapedTextValue "a&"
-            [hsx|<div class={className}></div>|] `shouldBeHtml` "<div class=\"a&\"></div>"
+            [Blaze.hsx|<div class={className}></div>|] `shouldBeBlazeHtml` "<div class=\"a&\"></div>"
 
+        it "should support pre escaped class names for Lucid2" do
+            -- See https://github.com/digitallyinduced/ihp/issues/1527
+
+            let className = Lucid2.MkLucidAttributeRaw "a&"
+            [Lucid2.hsx|<div class={className}></div>|] `shouldBeLucid2Html` "<div class=\"a&\"></div>"
+
         it "should support support doctype" do
             -- See https://github.com/digitallyinduced/ihp/issues/1717
 
-            [hsx|<!DOCTYPE html><html lang="en"><body>hello</body></html>|] `shouldBeHtml` "<!DOCTYPE HTML>\n<html lang=\"en\"><body>hello</body></html>"
+            [Blaze.hsx|<!DOCTYPE html><html lang="en"><body>hello</body></html>|] `shouldBeBlazeHtml` "<!DOCTYPE HTML>\n<html lang=\"en\"><body>hello</body></html>"
+            [Lucid2.hsx|<!DOCTYPE html><html lang="en"><body>hello</body></html>|] `shouldBeLucid2Html` "<!DOCTYPE HTML><html lang=\"en\"><body>hello</body></html>"
 
+        it "should support non-Identity effects for Lucid2" do
+            let increment :: State Int String = do
+                  x <- get
+                  put $! (x + 1)
+                  pure $! show x
+                monadFragment :: Lucid2.HtmlT (State Int) ()
+                monadFragment = [Lucid2.hsxM|<div>{increment}</div><div>{increment}</div><div>{increment}</div>|]
+                textFragment :: TL.Text
+                textFragment = evalState (Lucid2.renderTextT monadFragment) 1
+                insertMonadFragment :: Lucid2.HtmlT (State Int) ()
+                insertMonadFragment = [Lucid2.hsxM|<div>{monadFragment}</div><div>{monadFragment}</div>|]
+                doubleTextFragment :: TL.Text
+                doubleTextFragment = evalState (Lucid2.renderTextT insertMonadFragment) 1
+            textFragment `shouldBe` "<div>1</div><div>2</div><div>3</div>"
+            doubleTextFragment `shouldBe` "<div><div>1</div><div>2</div><div>3</div></div><div><div>4</div><div>5</div><div>6</div></div>"
+
     describe "customHsx" do
         it "should allow specified custom tags" do
-            [myTagsOnlyHsx|<mycustomtag>hello</mycustomtag>|] `shouldBeHtml` "<mycustomtag>hello</mycustomtag>"
-            [myTagsOnlyHsx|<anothercustomtag>world</anothercustomtag>|] `shouldBeHtml` "<anothercustomtag>world</anothercustomtag>"
+            [myTagsOnlyHsx|<mycustomtag>hello</mycustomtag>|] `shouldBeSameHtml` "<mycustomtag>hello</mycustomtag>"
+            [myTagsOnlyHsx|<anothercustomtag>world</anothercustomtag>|] `shouldBeSameHtml` "<anothercustomtag>world</anothercustomtag>"
 
         it "should allow specified custom attributes" do
-            [myAttrsOnlyHsx|<div my-custom-attr="hello">test</div>|] `shouldBeHtml` "<div my-custom-attr=\"hello\">test</div>"
-            [myAttrsOnlyHsx|<div anothercustomattr="world">test</div>|] `shouldBeHtml` "<div anothercustomattr=\"world\">test</div>"
+            [myAttrsOnlyHsx|<div my-custom-attr="hello">test</div>|] `shouldBeSameHtml` "<div my-custom-attr=\"hello\">test</div>"
+            [myAttrsOnlyHsx|<div anothercustomattr="world">test</div>|] `shouldBeSameHtml` "<div anothercustomattr=\"world\">test</div>"
 
         it "should allow combining custom tags and attributes" do
-            [myCustomHsx|<mycustomtag my-custom-attr="hello">test</mycustomtag>|] `shouldBeHtml` "<mycustomtag my-custom-attr=\"hello\">test</mycustomtag>"
+            [myCustomHsx|<mycustomtag my-custom-attr="hello">test</mycustomtag>|] `shouldBeSameHtml` "<mycustomtag my-custom-attr=\"hello\">test</mycustomtag>"
 
         it "should work with regular HTML tags and attributes too" do
-           [myCustomHsx|<div class="hello" my-custom-attr="test">world</div>|] `shouldBeHtml` "<div class=\"hello\" my-custom-attr=\"test\">world</div>"
+           [myCustomHsx|<div class="hello" my-custom-attr="test">world</div>|] `shouldBeSameHtml` "<div class=\"hello\" my-custom-attr=\"test\">world</div>"
 
 data Project = Project { name :: Text }
 
@@ -216,6 +246,16 @@
 newPlaceData = NewPlaceId "New Punches Cross"
 locationId = LocationId 17 (PlaceId "Punches Cross")
 
+shouldBeSameHtml :: HasCallStack => AllBackends -> TL.Text -> Expectation
+shouldBeSameHtml MkAllBackends {..} expectedHtml = do
+  Blaze.renderMarkup blazeMarkup `shouldBe` expectedHtml
+  Lucid2.renderText lucid2Html `shouldBe` expectedHtml
+  Lucid2.renderText lucid2HtmlM `shouldBe` expectedHtml
 
+shouldBeBlazeHtml :: HasCallStack => Blaze.Markup -> TL.Text -> Expectation
+shouldBeBlazeHtml blazeMarkup expectedHtml =
+  Blaze.renderMarkup blazeMarkup `shouldBe` expectedHtml
 
-shouldBeHtml hsx expectedHtml = (Blaze.renderMarkup hsx) `shouldBe` expectedHtml
+shouldBeLucid2Html :: HasCallStack => Lucid2.Html () -> TL.Text -> Expectation
+shouldBeLucid2Html lucid2Html expectedHtml =
+  Lucid2.renderText lucid2Html `shouldBe` expectedHtml
diff --git a/Test/Main.hs b/Test/Main.hs
--- a/Test/Main.hs
+++ b/Test/Main.hs
diff --git a/blaze/IHP/HSX/Attribute.hs b/blaze/IHP/HSX/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/blaze/IHP/HSX/Attribute.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE UndecidableInstances  #-}
+{-|
+Module: IHP.HSX.Attribute
+Copyright: (c) digitally induced GmbH, 2023
+-}
+module IHP.HSX.Attribute
+( ApplyAttribute (..)
+) where
+
+import Prelude
+import Text.Blaze.Html5 ((!))
+import qualified Text.Blaze.Html5 as Html5
+import Text.Blaze.Internal (attribute, MarkupM (Parent, Leaf), StaticString (..))
+import Data.String.Conversions
+import IHP.HSX.ToHtml
+import qualified Data.Text as Text
+import Data.Text (Text)
+
+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
+    {-# INLINE applyAttribute #-}
+
+instance ApplyAttribute Html5.AttributeValue where
+    applyAttribute attr attr' value h = h ! (attribute (Html5.textTag attr) (Html5.textTag attr') value)
+    {-# INLINE applyAttribute #-}
+
+instance {-# OVERLAPPABLE #-} ConvertibleStrings value Html5.AttributeValue => ApplyAttribute value where
+    applyAttribute attr attr' value h = applyAttribute attr attr' ((cs value) :: Html5.AttributeValue) h
+    {-# INLINE applyAttribute #-}
diff --git a/blaze/IHP/HSX/ConvertibleStrings.hs b/blaze/IHP/HSX/ConvertibleStrings.hs
new file mode 100644
--- /dev/null
+++ b/blaze/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/blaze/IHP/HSX/QQ.hs b/blaze/IHP/HSX/QQ.hs
new file mode 100644
--- /dev/null
+++ b/blaze/IHP/HSX/QQ.hs
@@ -0,0 +1,486 @@
+{-# 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
+  , uncheckedHsx
+  , customHsx
+  , quoteHsxExpression
+  ) where
+
+import           Prelude
+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 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')
+import IHP.HSX.Attribute
+import qualified Text.Blaze.Html5.Attributes as Attributes
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Set as Set
+
+hsx :: QuasiQuoter
+hsx = customHsx 
+        (HsxSettings 
+            { checkMarkup = True
+            , additionalTagNames = Set.empty
+            , additionalAttributeNames = Set.empty
+            }
+        )
+
+uncheckedHsx :: QuasiQuoter
+uncheckedHsx = customHsx
+        (HsxSettings 
+            { checkMarkup = False
+            , additionalTagNames = Set.empty
+            , additionalAttributeNames = Set.empty
+            }
+        )
+
+customHsx :: HsxSettings -> QuasiQuoter
+customHsx settings = 
+    QuasiQuoter 
+        { quoteExp = quoteHsxExpression settings
+        , quotePat = error "quotePat: not defined"
+        , quoteDec = error "quoteDec: not defined"
+        , quoteType = error "quoteType: not defined"
+        }
+
+quoteHsxExpression :: HsxSettings -> String -> TH.ExpQ
+quoteHsxExpression settings code = do
+        hsxPosition <- findHSXPosition
+        extensions <- TH.extsEnabled
+        expression <- case parseHsx settings hsxPosition extensions (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 "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True) = [| Html5.docType |]
+compileToHaskell (Node name attributes children isLeaf) =
+    let
+        renderedChildren = TH.listE $ map compileToHaskell children
+        stringAttributes = TH.listE $ map toStringAttribute attributes
+    in
+        if isLeaf
+            then
+                let
+                    element = nodeToBlazeLeaf name
+                in
+                    [| applyAttributes $element $stringAttributes |]
+            else
+                let
+                    element = nodeToBlazeElement name
+                in [| applyAttributes ($element (mconcat $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 |]
+compileToHaskell (NoRenderCommentNode) = [| mempty |]
+
+nodeToBlazeElement :: Text -> TH.Q TH.Exp
+nodeToBlazeElement name =
+    HashMap.findWithDefault (nodeToBlazeElementGeneric name) name knownElements
+
+knownElements :: HashMap.HashMap Text TH.ExpQ
+knownElements =
+    HashMap.fromList
+        [ ("a", [| Html5.a |])
+        , ("abbr", [| Html5.abbr |])
+        , ("address", [| Html5.address |])
+        , ("article", [| Html5.article |])
+        , ("aside", [| Html5.aside |])
+        , ("audio", [| Html5.audio |])
+        , ("b", [| Html5.b |])
+        , ("blockquote", [| Html5.blockquote |])
+        , ("body", [| Html5.body |])
+        , ("button", [| Html5.button |])
+        , ("canvas", [| Html5.canvas |])
+        , ("caption", [| Html5.caption |])
+        , ("cite", [| Html5.cite |])
+        , ("code", [| Html5.code |])
+        , ("colgroup", [| Html5.colgroup |])
+        , ("datalist", [| Html5.datalist |])
+        , ("dd", [| Html5.dd |])
+        , ("del", [| Html5.del |])
+        , ("details", [| Html5.details |])
+        , ("dfn", [| Html5.dfn |])
+        , ("div", [| Html5.div |])
+        , ("dl", [| Html5.dl |])
+        , ("dt", [| Html5.dt |])
+        , ("em", [| Html5.em |])
+        , ("fieldset", [| Html5.fieldset |])
+        , ("figcaption", [| Html5.figcaption |])
+        , ("figure", [| Html5.figure |])
+        , ("footer", [| Html5.footer |])
+        , ("form", [| Html5.form |])
+        , ("h1", [| Html5.h1 |])
+        , ("h2", [| Html5.h2 |])
+        , ("h3", [| Html5.h3 |])
+        , ("h4", [| Html5.h4 |])
+        , ("h5", [| Html5.h5 |])
+        , ("h6", [| Html5.h6 |])
+        , ("head", [| Html5.head |])
+        , ("header", [| Html5.header |])
+        , ("hgroup", [| Html5.hgroup |])
+        , ("html", [| Html5.html |])
+        , ("i", [| Html5.i |])
+        , ("iframe", [| Html5.iframe |])
+        , ("ins", [| Html5.ins |])
+        , ("kbd", [| Html5.kbd |])
+        , ("label", [| Html5.label |])
+        , ("legend", [| Html5.legend |])
+        , ("li", [| Html5.li |])
+        , ("main", [| Html5.main |])
+        , ("map", [| Html5.map |])
+        , ("mark", [| Html5.mark |])
+        , ("menu", [| Html5.menu |])
+        , ("menuitem", [| Html5.menuitem |])
+        , ("meter", [| Html5.meter |])
+        , ("nav", [| Html5.nav |])
+        , ("noscript", [| Html5.noscript |])
+        , ("object", [| Html5.object |])
+        , ("ol", [| Html5.ol |])
+        , ("optgroup", [| Html5.optgroup |])
+        , ("option", [| Html5.option |])
+        , ("output", [| Html5.output |])
+        , ("p", [| Html5.p |])
+        , ("pre", [| Html5.pre |])
+        , ("progress", [| Html5.progress |])
+        , ("q", [| Html5.q |])
+        , ("rp", [| Html5.rp |])
+        , ("rt", [| Html5.rt |])
+        , ("ruby", [| Html5.ruby |])
+        , ("s", [| Html5.s |])
+        , ("samp", [| Html5.samp |])
+        , ("script", [| Html5.script |])
+        , ("section", [| Html5.section |])
+        , ("select", [| Html5.select |])
+        , ("small", [| Html5.small |])
+        , ("span", [| Html5.span |])
+        , ("strong", [| Html5.strong |])
+        , ("style", [| Html5.style |])
+        , ("sub", [| Html5.sub |])
+        , ("summary", [| Html5.summary |])
+        , ("sup", [| Html5.sup |])
+        , ("table", [| Html5.table |])
+        , ("tbody", [| Html5.tbody |])
+        , ("td", [| Html5.td |])
+        , ("textarea", [| Html5.textarea |])
+        , ("tfoot", [| Html5.tfoot |])
+        , ("th", [| Html5.th |])
+        , ("thead", [| Html5.thead |])
+        , ("time", [| Html5.time |])
+        , ("title", [| Html5.title |])
+        , ("tr", [| Html5.tr |])
+        , ("u", [| Html5.u |])
+        , ("ul", [| Html5.ul |])
+        , ("var", [| Html5.var |])
+        , ("video", [| Html5.video |])
+        ]
+
+nodeToBlazeLeaf :: Text -> TH.Q TH.Exp
+nodeToBlazeLeaf name =
+    HashMap.findWithDefault (nodeToBlazeLeafGeneric name) name knownLeafs
+
+knownLeafs :: HashMap.HashMap Text TH.ExpQ
+knownLeafs =
+    HashMap.fromList
+        [ ("area", [| Html5.area |])
+        , ("base", [| Html5.base |])
+        , ("br", [| Html5.br |])
+        , ("col", [| Html5.col |])
+        , ("embed", [| Html5.embed |])
+        , ("hr", [| Html5.hr |])
+        , ("img", [| Html5.img |])
+        , ("input", [| Html5.input |])
+        , ("keygen", [| Html5.keygen |])
+        , ("link", [| Html5.link |])
+        , ("meta", [| Html5.meta |])
+        , ("param", [| Html5.param |])
+        , ("source", [| Html5.source |])
+        , ("track", [| Html5.track |])
+        , ("wbr", [| Html5.wbr |])
+        ]
+
+nodeToBlazeElementGeneric :: Text -> TH.Q TH.Exp
+nodeToBlazeElementGeneric name =
+    let
+        openTag :: Text
+        openTag = "<" <> tag
+        
+        tag :: Text
+        tag = cs name
+
+        closeTag :: Text
+        closeTag = "</" <> tag <> ">"
+    in
+        [| makeParent (textToStaticString $(TH.lift name)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) |]
+
+nodeToBlazeLeafGeneric :: Text -> TH.Q TH.Exp
+nodeToBlazeLeafGeneric name =
+    let
+        openTag :: Text
+        openTag = "<" <> tag
+
+        closeTag :: Text
+        closeTag = ">"
+        
+        tag :: Text
+        tag = cs name
+    in
+        [| (Leaf (textToStaticString $(TH.lift tag)) (textToStaticString $(TH.lift openTag)) (textToStaticString $(TH.lift closeTag)) ()) |]
+
+toStringAttribute :: Attribute -> TH.ExpQ
+toStringAttribute (StaticAttribute name (TextValue value)) =
+    attributeFromName name value
+
+toStringAttribute (StaticAttribute name (ExpressionValue expression)) = let nameWithSuffix = " " <> name <> "=\"" in [| applyAttribute name nameWithSuffix $(pure expression) |]
+toStringAttribute (SpreadAttributes expression) = [| spreadAttributes $(pure expression) |]
+
+attributeFromName :: Text -> Text -> TH.ExpQ
+attributeFromName name value =
+    let
+        value' :: TH.ExpQ
+        value' = if Text.null value then [| mempty |] else [| Html5.preEscapedTextValue value |] 
+
+        attr = attributeFromName' name
+    in
+        [| (! $attr $value') |]
+
+attributeFromName' :: Text -> TH.ExpQ
+attributeFromName' name =
+    HashMap.findWithDefault (attributeFromNameGeneric name) name knownAttributes
+
+knownAttributes :: HashMap.HashMap Text TH.ExpQ
+knownAttributes =
+    HashMap.fromList
+        [ ("accept", [| Attributes.accept |])
+        , ( "accept-charset", [| Attributes.acceptCharset |])
+        , ( "accesskey", [| Attributes.accesskey |])
+        , ( "action", [| Attributes.action |])
+        , ( "alt", [| Attributes.alt |])
+        , ( "async", [| Attributes.async |])
+        , ( "autocomplete", [| Attributes.autocomplete |])
+        , ( "autofocus", [| Attributes.autofocus |])
+        , ( "autoplay", [| Attributes.autoplay |])
+        , ( "challenge", [| Attributes.challenge |])
+        , ( "charset", [| Attributes.charset |])
+        , ( "checked", [| Attributes.checked |])
+        , ( "cite", [| Attributes.cite |])
+        , ( "class", [| Attributes.class_ |])
+        , ( "cols", [| Attributes.cols |])
+        , ( "colspan", [| Attributes.colspan |])
+        , ( "content", [| Attributes.content |])
+        , ( "contenteditable", [| Attributes.contenteditable |])
+        , ( "contextmenu", [| Attributes.contextmenu |])
+        , ( "controls", [| Attributes.controls |])
+        , ( "coords", [| Attributes.coords |])
+        , ( "data", [| Attributes.data_ |])
+        , ( "datetime", [| Attributes.datetime |])
+        , ( "defer", [| Attributes.defer |])
+        , ( "dir", [| Attributes.dir |])
+        , ( "disabled", [| Attributes.disabled |])
+        , ( "download", [| Attributes.download |])
+        , ( "draggable", [| Attributes.draggable |])
+        , ( "enctype", [| Attributes.enctype |])
+        , ( "for", [| Attributes.for |])
+        , ( "form", [| Attributes.form |])
+        , ( "formaction", [| Attributes.formaction |])
+        , ( "formenctype", [| Attributes.formenctype |])
+        , ( "formmethod", [| Attributes.formmethod |])
+        , ( "formnovalidate", [| Attributes.formnovalidate |])
+        , ( "formtarget", [| Attributes.formtarget |])
+        , ( "headers", [| Attributes.headers |])
+        , ( "height", [| Attributes.height |])
+        , ( "hidden", [| Attributes.hidden |])
+        , ( "high", [| Attributes.high |])
+        , ( "href", [| Attributes.href |])
+        , ( "hreflang", [| Attributes.hreflang |])
+        , ( "http-equiv", [| Attributes.httpEquiv |])
+        , ( "icon", [| Attributes.icon |])
+        , ( "id", [| Attributes.id |])
+        , ( "ismap", [| Attributes.ismap |])
+        , ( "item", [| Attributes.item |])
+        , ( "itemprop", [| Attributes.itemprop |])
+        , ( "itemscope", [| Attributes.itemscope |])
+        , ( "itemtype", [| Attributes.itemtype |])
+        , ( "keytype", [| Attributes.keytype |])
+        , ( "label", [| Attributes.label |])
+        , ( "lang", [| Attributes.lang |])
+        , ( "list", [| Attributes.list |])
+        , ( "loop", [| Attributes.loop |])
+        , ( "low", [| Attributes.low |])
+        , ( "manifest", [| Attributes.manifest |])
+        , ( "max", [| Attributes.max |])
+        , ( "maxlength", [| Attributes.maxlength |])
+        , ( "media", [| Attributes.media |])
+        , ( "method", [| Attributes.method |])
+        , ( "min", [| Attributes.min |])
+        , ( "minlength", [| Attributes.minlength |])
+        , ( "multiple", [| Attributes.multiple |])
+        , ( "muted", [| Attributes.muted |])
+        , ( "name", [| Attributes.name |])
+        , ( "novalidate", [| Attributes.novalidate |])
+        , ( "onbeforeonload", [| Attributes.onbeforeonload |])
+        , ( "onbeforeprint", [| Attributes.onbeforeprint |])
+        , ( "onblur", [| Attributes.onblur |])
+        , ( "oncanplay", [| Attributes.oncanplay |])
+        , ( "oncanplaythrough", [| Attributes.oncanplaythrough |])
+        , ( "onchange", [| Attributes.onchange |])
+        , ( "onclick", [| Attributes.onclick |])
+        , ( "oncontextmenu", [| Attributes.oncontextmenu |])
+        , ( "ondblclick", [| Attributes.ondblclick |])
+        , ( "ondrag", [| Attributes.ondrag |])
+        , ( "ondragend", [| Attributes.ondragend |])
+        , ( "ondragenter", [| Attributes.ondragenter |])
+        , ( "ondragleave", [| Attributes.ondragleave |])
+        , ( "ondragover", [| Attributes.ondragover |])
+        , ( "ondragstart", [| Attributes.ondragstart |])
+        , ( "ondrop", [| Attributes.ondrop |])
+        , ( "ondurationchange", [| Attributes.ondurationchange |])
+        , ( "onemptied", [| Attributes.onemptied |])
+        , ( "onended", [| Attributes.onended |])
+        , ( "onerror", [| Attributes.onerror |])
+        , ( "onfocus", [| Attributes.onfocus |])
+        , ( "onformchange", [| Attributes.onformchange |])
+        , ( "onforminput", [| Attributes.onforminput |])
+        , ( "onhaschange", [| Attributes.onhaschange |])
+        , ( "oninput", [| Attributes.oninput |])
+        , ( "oninvalid", [| Attributes.oninvalid |])
+        , ( "onkeydown", [| Attributes.onkeydown |])
+        , ( "onkeypress", [| Attributes.onkeypress |])
+        , ( "onkeyup", [| Attributes.onkeyup |])
+        , ( "onload", [| Attributes.onload |])
+        , ( "onloadeddata", [| Attributes.onloadeddata |])
+        , ( "onloadedmetadata", [| Attributes.onloadedmetadata |])
+        , ( "onloadstart", [| Attributes.onloadstart |])
+        , ( "onmessage", [| Attributes.onmessage |])
+        , ( "onmousedown", [| Attributes.onmousedown |])
+        , ( "onmousemove", [| Attributes.onmousemove |])
+        , ( "onmouseout", [| Attributes.onmouseout |])
+        , ( "onmouseover", [| Attributes.onmouseover |])
+        , ( "onmouseup", [| Attributes.onmouseup |])
+        , ( "onmousewheel", [| Attributes.onmousewheel |])
+        , ( "ononline", [| Attributes.ononline |])
+        , ( "onpagehide", [| Attributes.onpagehide |])
+        , ( "onpageshow", [| Attributes.onpageshow |])
+        , ( "onpause", [| Attributes.onpause |])
+        , ( "onplay", [| Attributes.onplay |])
+        , ( "onplaying", [| Attributes.onplaying |])
+        , ( "onprogress", [| Attributes.onprogress |])
+        , ( "onpropstate", [| Attributes.onpropstate |])
+        , ( "onratechange", [| Attributes.onratechange |])
+        , ( "onreadystatechange", [| Attributes.onreadystatechange |])
+        , ( "onredo", [| Attributes.onredo |])
+        , ( "onresize", [| Attributes.onresize |])
+        , ( "onscroll", [| Attributes.onscroll |])
+        , ( "onseeked", [| Attributes.onseeked |])
+        , ( "onseeking", [| Attributes.onseeking |])
+        , ( "onselect", [| Attributes.onselect |])
+        , ( "onstalled", [| Attributes.onstalled |])
+        , ( "onstorage", [| Attributes.onstorage |])
+        , ( "onsubmit", [| Attributes.onsubmit |])
+        , ( "onsuspend", [| Attributes.onsuspend |])
+        , ( "ontimeupdate", [| Attributes.ontimeupdate |])
+        , ( "onundo", [| Attributes.onundo |])
+        , ( "onunload", [| Attributes.onunload |])
+        , ( "onvolumechange", [| Attributes.onvolumechange |])
+        , ( "onwaiting", [| Attributes.onwaiting |])
+        , ( "open", [| Attributes.open |])
+        , ( "optimum", [| Attributes.optimum |])
+        , ( "pattern", [| Attributes.pattern |])
+        , ( "ping", [| Attributes.ping |])
+        , ( "placeholder", [| Attributes.placeholder |])
+        , ( "poster", [| Attributes.poster |])
+        , ( "preload", [| Attributes.preload |])
+        , ( "property", [| Attributes.property |])
+        , ( "pubdate", [| Attributes.pubdate |])
+        , ( "radiogroup", [| Attributes.radiogroup |])
+        , ( "readonly", [| Attributes.readonly |])
+        , ( "rel", [| Attributes.rel |])
+        , ( "required", [| Attributes.required |])
+        , ( "reversed", [| Attributes.reversed |])
+        , ( "role", [| Attributes.role |])
+        , ( "rows", [| Attributes.rows |])
+        , ( "rowspan", [| Attributes.rowspan |])
+        , ( "sandbox", [| Attributes.sandbox |])
+        , ( "scope", [| Attributes.scope |])
+        , ( "scoped", [| Attributes.scoped |])
+        , ( "seamless", [| Attributes.seamless |])
+        , ( "selected", [| Attributes.selected |])
+        , ( "shape", [| Attributes.shape |])
+        , ( "size", [| Attributes.size |])
+        , ( "sizes", [| Attributes.sizes |])
+        , ( "span", [| Attributes.span |])
+        , ( "spellcheck", [| Attributes.spellcheck |])
+        , ( "src", [| Attributes.src |])
+        , ( "srcdoc", [| Attributes.srcdoc |])
+        , ( "start", [| Attributes.start |])
+        , ( "step", [| Attributes.step |])
+        , ( "style", [| Attributes.style |])
+        , ( "subject", [| Attributes.subject |])
+        , ( "summary", [| Attributes.summary |])
+        , ( "tabindex", [| Attributes.tabindex |])
+        , ( "target", [| Attributes.target |])
+        , ( "title", [| Attributes.title |])
+        , ( "type", [| Attributes.type_ |])
+        , ( "usemap", [| Attributes.usemap |])
+        , ( "value", [| Attributes.value |])
+        , ( "width", [| Attributes.width |])
+        , ( "wrap", [| Attributes.wrap |])
+        , ( "xmlns", [| Attributes.xmlns |])
+        ]
+
+attributeFromNameGeneric :: Text -> TH.ExpQ
+attributeFromNameGeneric name =
+    let
+        nameWithSuffix = " " <> name <> "=\""
+    in
+        [| attribute (Html5.textTag name) (Html5.textTag nameWithSuffix) |]
+
+spreadAttributes :: ApplyAttribute value => [(Text, value)] -> Html5.Html -> Html5.Html
+spreadAttributes attributes html = applyAttributes html $ map (\(name, value) -> applyAttribute name (" " <> name <> "=\"") value) attributes
+{-# INLINE spreadAttributes #-}
+
+applyAttributes :: Html5.Html -> [Html5.Html -> Html5.Html] -> Html5.Html
+applyAttributes element (attribute:rest) = applyAttributes (attribute element) rest
+applyAttributes element [] = element
+{-# INLINE applyAttributes #-}
+
+makeParent :: StaticString -> StaticString -> StaticString -> Html -> Html
+makeParent tag openTag closeTag children = Parent tag openTag closeTag children
+{-# INLINE makeParent #-}
+
+textToStaticString :: Text -> StaticString
+textToStaticString text = StaticString (Text.unpack text ++) (Text.encodeUtf8 text) text
+{-# INLINE textToStaticString #-}
+
+instance Show (MarkupM ()) where
+    show html = BlazeString.renderHtml html
diff --git a/blaze/IHP/HSX/ToHtml.hs b/blaze/IHP/HSX/ToHtml.hs
new file mode 100644
--- /dev/null
+++ b/blaze/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 (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/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
 # Changelog for `ihp-hsx`
 
+## Version 1.5.0
+
+- Suport for GHC 9.10.x and 9.12.x
+- Add backend for lucid2 via dedidcated ihp-hsx-lucid2 library
+- Add missing attributes muted, onkeypress
+
 ## 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
--- a/ihp-hsx.cabal
+++ b/ihp-hsx.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                ihp-hsx
-version:             1.4.1
+version:             1.5.0
 synopsis:            JSX-like but for Haskell
 description:         JSX-like templating syntax for Haskell
 license:             MIT
@@ -16,20 +16,7 @@
     type:     git
     location: https://github.com/digitallyinduced/ihp.git
 
-library
-    default-language: Haskell2010
-    build-depends:
-          base >= 4.17.0 && < 4.20
-        , blaze-html
-        , bytestring
-        , template-haskell >= 2.19.0 && < 2.22
-        , text
-        , containers
-        , blaze-markup
-        , ghc >= 9.4.4 && < 9.9
-        , megaparsec
-        , string-conversions
-        , unordered-containers
+common common-extensions
     default-extensions:
         OverloadedStrings
         , NoImplicitPrelude
@@ -64,6 +51,8 @@
         , StandaloneDeriving
         , TemplateHaskell
         , OverloadedRecordDot
+
+common common-flags
     ghc-options:
         -fstatic-argument-transformation
         -funbox-strict-fields
@@ -80,17 +69,66 @@
         -fdicts-strict
         -fexpose-all-unfoldings
         -optc-O3
-    hs-source-dirs: .
+
+common common-depends
+    build-depends:
+          base >= 4.17.0 && < 4.22
+        , bytestring
+        , template-haskell >= 2.19.0 && < 2.24
+        , text
+        , containers
+        , megaparsec
+        , string-conversions
+        , unordered-containers
+
+library ihp-hsx-parser
+    import: common-extensions, common-flags, common-depends
+    default-language: Haskell2010
+    build-depends:
+        ghc >= 9.4.4 && < 9.13
+    hs-source-dirs: parser
     exposed-modules:
         IHP.HSX.Parser
-        , IHP.HSX.QQ
+        , IHP.HSX.HaskellParser
+        , IHP.HSX.HsExpToTH
+
+library
+    import: common-extensions, common-flags, common-depends
+    build-depends:
+        ihp-hsx-parser
+        , blaze-html
+        , blaze-markup
+    default-language: Haskell2010
+    hs-source-dirs: blaze
+    exposed-modules:
+        IHP.HSX.QQ
         , IHP.HSX.ToHtml
         , IHP.HSX.ConvertibleStrings
+        , IHP.HSX.Attribute
+    reexported-modules:
+        IHP.HSX.Parser
         , IHP.HSX.HaskellParser
         , IHP.HSX.HsExpToTH
-        , IHP.HSX.Attribute
 
+library ihp-hsx-lucid2
+    import: common-extensions, common-flags, common-depends
+    build-depends:
+        ihp-hsx-parser
+        , lucid2
+        , transformers
+    default-language: Haskell2010
+    hs-source-dirs: lucid2
+    exposed-modules:
+        IHP.HSX.Lucid2.QQ
+        , IHP.HSX.Lucid2.Attribute
+        , IHP.HSX.Lucid2.ToHtml
+    reexported-modules:
+        IHP.HSX.Parser
+        , IHP.HSX.HaskellParser
+        , IHP.HSX.HsExpToTH
+
 test-suite ihp-hsx-tests
+    import: common-extensions, common-flags, common-depends
     type:                exitcode-stdio-1.0
     hs-source-dirs:      Test
     main-is:             Main.hs
@@ -100,46 +138,11 @@
         IHP.HSX.CustomHsxCases
     default-language:    Haskell2010
     build-depends:
-          base >= 4.17.0 && < 4.20
         , ihp-hsx
+        , ihp-hsx-lucid2
         , hspec
-        , text
-        , bytestring
-        , containers
         , blaze-markup
         , megaparsec
         , template-haskell
-    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
-        , OverloadedRecordDot
+        , lucid2
+        , mtl
diff --git a/lucid2/IHP/HSX/Lucid2/Attribute.hs b/lucid2/IHP/HSX/Lucid2/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/lucid2/IHP/HSX/Lucid2/Attribute.hs
@@ -0,0 +1,42 @@
+{-|
+Module: IHP.HSX.Lucid2.Attribute
+Copyright: (c) digitally induced GmbH, 2023
+-}
+module IHP.HSX.Lucid2.Attribute
+  ( LucidAttributeRaw (..)
+  , LucidAttributeValue (..)
+  ) where
+
+import Prelude
+import qualified Data.Text as Text
+import Data.Text (Text)
+import Lucid.Base (Attributes, makeAttributes, makeAttributesRaw)
+
+class LucidAttributeValue value where
+    buildAttribute :: Text -> value -> Attributes
+
+instance LucidAttributeValue Bool where
+    buildAttribute attr True = makeAttributes attr 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
+    buildAttribute attr False | "data-" `Text.isPrefixOf` attr = makeAttributesRaw attr "false" -- data attribute set to "false"
+    buildAttribute attr False = mempty -- html boolean attribute, like <input disabled/> will be dropped as there is no other way to specify that it's set to false
+    {-# INLINE buildAttribute #-}
+
+instance LucidAttributeValue lav => LucidAttributeValue (Maybe lav) where
+    buildAttribute attr (Just value) = buildAttribute attr value
+    buildAttribute attr Nothing = mempty
+    {-# INLINE buildAttribute #-}
+
+instance LucidAttributeValue Text where
+    buildAttribute = makeAttributes
+    {-# INLINE buildAttribute #-}
+
+newtype LucidAttributeRaw = MkLucidAttributeRaw { unLucidAttributeRaw :: Text }
+
+instance LucidAttributeValue LucidAttributeRaw where
+    buildAttribute attr MkLucidAttributeRaw { unLucidAttributeRaw = textVal } =
+      makeAttributesRaw attr textVal
+    {-# INLINE buildAttribute #-}
diff --git a/lucid2/IHP/HSX/Lucid2/QQ.hs b/lucid2/IHP/HSX/Lucid2/QQ.hs
new file mode 100644
--- /dev/null
+++ b/lucid2/IHP/HSX/Lucid2/QQ.hs
@@ -0,0 +1,209 @@
+{-# LANGUAGE TemplateHaskell, UndecidableInstances, BangPatterns, PackageImports, FlexibleInstances, OverloadedStrings #-}
+{-# LANGUAGE ExistentialQuantification #-}
+
+{-|
+Module: IHP.HSX.Lucid2.QQ
+Description: Defines the @[hsx||]@ and @[hsxM||]@ syntax
+Copyright: (c) digitally induced GmbH, 2025
+-}
+module IHP.HSX.Lucid2.QQ
+  ( hsx
+  , uncheckedHsx
+  , customHsx
+  , quoteHsxExpression
+  , hsxM
+  , uncheckedHsxM
+  , customHsxM
+  , quoteHsxExpressionM
+  ) where
+
+import           Prelude
+import Data.Foldable (Foldable(..))
+import Data.Text (Text)
+import           IHP.HSX.Parser
+import           IHP.HSX.Lucid2.Attribute
+import qualified IHP.HSX.Lucid2.ToHtml as M
+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 Data.String.Conversions
+import qualified Text.Megaparsec as Megaparsec
+import qualified Data.Set as Set
+import Lucid.Html5 (doctype_)
+import Lucid.Base
+  ( Attributes
+  , ToHtml (..)
+  , makeElement
+  , makeElementNoEnd
+  )
+
+hsx :: QuasiQuoter
+hsx = customHsx
+        (HsxSettings
+            { checkMarkup = True
+            , additionalTagNames = Set.empty
+            , additionalAttributeNames = Set.empty
+            }
+        )
+
+uncheckedHsx :: QuasiQuoter
+uncheckedHsx = customHsx
+        (HsxSettings
+            { checkMarkup = False
+            , additionalTagNames = Set.empty
+            , additionalAttributeNames = Set.empty
+            }
+        )
+
+customHsx :: HsxSettings -> QuasiQuoter
+customHsx settings =
+    QuasiQuoter
+        { quoteExp = quoteHsxExpression settings
+        , quotePat = error "quotePat: not defined"
+        , quoteDec = error "quoteDec: not defined"
+        , quoteType = error "quoteType: not defined"
+        }
+
+quoteHsxExpression :: HsxSettings -> String -> TH.ExpQ
+quoteHsxExpression settings code = do
+        hsxPosition <- findHSXPosition
+        extensions <- TH.extsEnabled
+        expression <- case parseHsx settings hsxPosition extensions (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 "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True) = [| doctype_ |]
+compileToHaskell (Node name attributes children isLeaf) =
+    let
+        renderedChildren = TH.listE $ map compileToHaskell children
+        listAttributes = TH.listE $ map toLucidAttributes attributes
+    in
+        if isLeaf
+            then
+                let
+                    element = nodeToLucidLeaf name
+                in
+                    [| $element $listAttributes |]
+            else
+                let
+                    element = nodeToLucidElement name
+                in [| $element $listAttributes (sequence_ @[] $renderedChildren) |]
+compileToHaskell (Children children) =
+    let
+        renderedChildren = TH.listE $ map compileToHaskell children
+    in [| (sequence_ @[] $(renderedChildren)) |]
+
+compileToHaskell (TextNode value) = [| toHtmlRaw value |]
+compileToHaskell (PreEscapedTextNode value) = [| toHtmlRaw value |]
+compileToHaskell (SplicedNode expression) = [| toHtml $(pure expression) |]
+compileToHaskell (CommentNode value) = [| toHtmlRaw @Text "<!--" >> toHtmlRaw value >> toHtmlRaw @Text "-->" |]
+compileToHaskell NoRenderCommentNode = [| pure () |]
+
+nodeToLucidElement :: Text -> TH.Q TH.Exp
+nodeToLucidElement name =
+    [| makeElement $(TH.lift name) |]
+
+nodeToLucidLeaf :: Text -> TH.Q TH.Exp
+nodeToLucidLeaf name =
+    [| makeElementNoEnd $(TH.lift name) |]
+
+toLucidAttributes :: Attribute -> TH.ExpQ
+toLucidAttributes (StaticAttribute name (TextValue value)) =
+    [| buildAttribute name value |]
+toLucidAttributes (StaticAttribute name (ExpressionValue expression)) =
+    [| buildAttribute name $(pure expression) |]
+toLucidAttributes (SpreadAttributes expression) =
+    [| spreadAttributes $(pure expression) |]
+
+spreadAttributes :: (LucidAttributeValue lav) => [(Text, lav)] -> Attributes
+spreadAttributes = foldMap' (uncurry buildAttribute)
+
+
+
+-- Monad Version
+hsxM :: QuasiQuoter
+hsxM = customHsxM
+        (HsxSettings
+            { checkMarkup = True
+            , additionalTagNames = Set.empty
+            , additionalAttributeNames = Set.empty
+            }
+        )
+
+uncheckedHsxM :: QuasiQuoter
+uncheckedHsxM = customHsxM
+        (HsxSettings
+            { checkMarkup = False
+            , additionalTagNames = Set.empty
+            , additionalAttributeNames = Set.empty
+            }
+        )
+
+customHsxM :: HsxSettings -> QuasiQuoter
+customHsxM settings =
+    QuasiQuoter
+        { quoteExp = quoteHsxExpressionM settings
+        , quotePat = error "quotePat: not defined"
+        , quoteDec = error "quoteDec: not defined"
+        , quoteType = error "quoteType: not defined"
+        }
+
+quoteHsxExpressionM :: HsxSettings -> String -> TH.ExpQ
+quoteHsxExpressionM settings code = do
+        hsxPosition <- findHSXPosition
+        extensions <- TH.extsEnabled
+        expression <- case parseHsx settings hsxPosition extensions (cs code) of
+                Left error   -> fail (Megaparsec.errorBundlePretty error)
+                Right result -> pure result
+        [| M.unHtmlType $(compileToHaskellM 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)
+
+compileToHaskellM :: Node -> TH.ExpQ
+compileToHaskellM (Node "!DOCTYPE" [StaticAttribute "html" (TextValue "html")] [] True) = [| M.Lucid2Html doctype_ |]
+compileToHaskellM (Node name attributes children isLeaf) =
+    let
+        renderedChildren = TH.listE $ map compileToHaskellM children
+        listAttributes = TH.listE $ map toLucidAttributes attributes
+    in
+        if isLeaf
+            then
+                let
+                    element = nodeToLucidLeafM name
+                in
+                    [| $element $listAttributes |]
+            else
+                let
+                    element = nodeToLucidElementM name
+                in [| $element $listAttributes (M.sequenceChildren $renderedChildren) |]
+compileToHaskellM (Children children) =
+    let
+        renderedChildren = TH.listE $ map compileToHaskellM children
+    in [| (M.sequenceChildren $(renderedChildren)) |]
+
+compileToHaskellM (TextNode value) = [| M.toHtmlRaw value |]
+compileToHaskellM (PreEscapedTextNode value) = [| M.toHtmlRaw value |]
+compileToHaskellM (SplicedNode expression) = [| M.toHtml $(pure expression) |]
+compileToHaskellM (CommentNode value) =
+  [| M.sequenceChildren [M.toHtmlRaw @_ @Text "<!--", M.toHtmlRaw value, M.toHtmlRaw @_ @Text "-->"] |]
+compileToHaskellM NoRenderCommentNode = [| M.Lucid2Html (pure ()) |]
+
+nodeToLucidElementM :: Text -> TH.Q TH.Exp
+nodeToLucidElementM name =
+    [| M.makeElement $(TH.lift name) |]
+
+nodeToLucidLeafM :: Text -> TH.Q TH.Exp
+nodeToLucidLeafM name =
+    [| M.makeElementNoEnd $(TH.lift name) |]
diff --git a/lucid2/IHP/HSX/Lucid2/ToHtml.hs b/lucid2/IHP/HSX/Lucid2/ToHtml.hs
new file mode 100644
--- /dev/null
+++ b/lucid2/IHP/HSX/Lucid2/ToHtml.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-|
+Module: IHP.HSX.Lucid2.ToHtml
+Description: Provides an alternative ToHtml class for Lucid2 that tracks the wrapped monad
+Copyright: (c) digitally induced GmbH, 2025
+-}
+
+module IHP.HSX.Lucid2.ToHtml where
+
+import Prelude (Applicative (..), Monad (..), String, ($), (.), id, mapM_)
+import Lucid.Base (HtmlT (..))
+import Control.Monad.Trans.Class (MonadTrans (lift))
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Lazy as BL (ByteString)
+import Data.Kind (Type)
+import Data.Text (Text)
+import qualified Data.Text.Lazy as TL (Text)
+import qualified Lucid.Base as Lucid2 (ToHtml (..), Attributes, makeElementNoEnd, makeElement)
+
+class (Monad m) => HtmlGen m where
+    data HtmlType m :: Type
+
+    unHtmlType :: HtmlType m -> m ()
+    makeElement :: Text -> [Lucid2.Attributes] -> HtmlType m -> HtmlType m
+    makeElementNoEnd :: Text -> [Lucid2.Attributes] -> HtmlType m
+    sequenceChildren :: [HtmlType m] -> HtmlType m
+
+class (HtmlGen m) => EmbedAsHtml m a where
+    toHtml :: a -> HtmlType m
+    toHtmlRaw :: a -> HtmlType m
+
+instance (Monad m) => HtmlGen (HtmlT m) where
+    newtype HtmlType (HtmlT m) = Lucid2Html { unLucid2Html :: HtmlT m () }
+    makeElement name attrs Lucid2Html { unLucid2Html = content } =
+      Lucid2Html (Lucid2.makeElement name attrs content)
+    makeElementNoEnd name attrs = Lucid2Html (Lucid2.makeElementNoEnd name attrs)
+    sequenceChildren = Lucid2Html . mapM_ unLucid2Html
+    unHtmlType = unLucid2Html
+
+instance (HtmlGen m) => EmbedAsHtml m (HtmlType m) where
+    toHtml = id
+    toHtmlRaw = id
+
+instance (Monad m) => EmbedAsHtml (HtmlT m) String where
+    toHtml = Lucid2Html . Lucid2.toHtml
+    toHtmlRaw = Lucid2Html . Lucid2.toHtmlRaw
+
+instance (Monad m) => EmbedAsHtml (HtmlT m) Text where
+    toHtml = Lucid2Html . Lucid2.toHtml
+    toHtmlRaw = Lucid2Html . Lucid2.toHtmlRaw
+
+instance (Monad m) => EmbedAsHtml (HtmlT m) TL.Text where
+    toHtml = Lucid2Html . Lucid2.toHtml
+    toHtmlRaw = Lucid2Html . Lucid2.toHtmlRaw
+
+instance (Monad m) => EmbedAsHtml (HtmlT m) ByteString where
+    toHtml = Lucid2Html . Lucid2.toHtml
+    toHtmlRaw = Lucid2Html . Lucid2.toHtmlRaw
+
+instance (Monad m) => EmbedAsHtml (HtmlT m) BL.ByteString where
+    toHtml = Lucid2Html . Lucid2.toHtml
+    toHtmlRaw = Lucid2Html . Lucid2.toHtmlRaw
+
+instance Lucid2.ToHtml () where
+    toHtml = pure
+    toHtmlRaw = pure
+
+instance (Monad m, Lucid2.ToHtml a) => EmbedAsHtml (HtmlT m) (HtmlT m a) where
+    toHtml mHtml = Lucid2Html $ mHtml >>= Lucid2.toHtml
+    toHtmlRaw mHtml = Lucid2Html $ mHtml >>= Lucid2.toHtmlRaw
+
+instance (Monad m, Lucid2.ToHtml a) => EmbedAsHtml (HtmlT m) (m a) where
+    toHtml mHtml = Lucid2Html $ lift mHtml >>= Lucid2.toHtml
+    toHtmlRaw mHtml = Lucid2Html $ lift mHtml >>= Lucid2.toHtmlRaw
diff --git a/parser/IHP/HSX/HaskellParser.hs b/parser/IHP/HSX/HaskellParser.hs
new file mode 100644
--- /dev/null
+++ b/parser/IHP/HSX/HaskellParser.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE CPP #-}
+module IHP.HSX.HaskellParser (parseHaskellExpression) where
+
+import Prelude
+import GHC.Parser.Lexer (ParseResult (..), PState (..))
+import GHC.Types.SrcLoc
+import qualified GHC.Parser as Parser
+import qualified GHC.Parser.Lexer as Lexer
+import GHC.Data.FastString
+import GHC.Data.StringBuffer
+import GHC.Parser.PostProcess
+import Text.Megaparsec.Pos
+import qualified "template-haskell" Language.Haskell.TH as TH
+
+import qualified GHC.Data.EnumSet as EnumSet
+import GHC
+import IHP.HSX.HsExpToTH (toExp)
+
+import GHC.Types.Error
+import GHC.Utils.Outputable hiding ((<>))
+import GHC.Utils.Error
+import qualified GHC.Types.SrcLoc as SrcLoc
+#if __GLASGOW_HASKELL__ >= 908
+import GHC.Unit.Module.Warnings
+#endif
+
+parseHaskellExpression :: SourcePos -> [TH.Extension] -> String -> Either (Int, Int, String) TH.Exp
+parseHaskellExpression sourcePos extensions input =
+        case expr of
+            POk parserState result -> Right (toExp (unLoc result))
+            PFailed parserState ->
+                let
+                    error = renderWithContext defaultSDocContext
+                        $ vcat
+#if __GLASGOW_HASKELL__ >= 908
+                        $ map formatBulleted
+#else
+                        $ map (formatBulleted defaultSDocContext)
+#endif
+#if __GLASGOW_HASKELL__ >= 906
+                        $ map (diagnosticMessage NoDiagnosticOpts)
+#else
+                        $ map diagnosticMessage
+#endif
+                        $ map errMsgDiagnostic
+                        $ sortMsgBag Nothing
+                        $ getMessages parserState.errors
+                    line = SrcLoc.srcLocLine parserState.loc.psRealLoc
+                    col = SrcLoc.srcLocCol parserState.loc.psRealLoc
+                in
+                    Left (line, col, error)
+    where
+        expr :: ParseResult (LocatedA (HsExpr GhcPs))
+        expr = case Lexer.unP Parser.parseExpression parseState of
+                POk parserState result -> Lexer.unP (runPV (unECP result)) parserState
+                PFailed parserState -> PFailed parserState
+
+        location :: RealSrcLoc
+        location = mkRealSrcLoc filename line col
+        
+        filename :: FastString
+        filename = mkFastString sourcePos.sourceName
+
+        line :: Int
+        line = unPos sourcePos.sourceLine
+
+        col :: Int
+        col = unPos sourcePos.sourceColumn
+
+        buffer = stringToStringBuffer input
+        parseState = Lexer.initParserState parserOpts buffer location
+
+        parserOpts :: Lexer.ParserOpts
+        parserOpts = Lexer.mkParserOpts (EnumSet.fromList extensions) diagOpts [] False False False False
+
+diagOpts :: DiagOpts
+diagOpts =
+    DiagOpts
+    { diag_warning_flags = EnumSet.empty
+    , diag_fatal_warning_flags = EnumSet.empty
+    , diag_warn_is_error = False
+    , diag_reverse_errors = False
+    , diag_max_errors = Nothing
+    , diag_ppr_ctx = defaultSDocContext
+#if __GLASGOW_HASKELL__ >= 908
+    , diag_custom_warning_categories = emptyWarningCategorySet
+    , diag_fatal_custom_warning_categories = emptyWarningCategorySet
+#endif
+    }
diff --git a/parser/IHP/HSX/HsExpToTH.hs b/parser/IHP/HSX/HsExpToTH.hs
new file mode 100644
--- /dev/null
+++ b/parser/IHP/HSX/HsExpToTH.hs
@@ -0,0 +1,262 @@
+{-# LANGUAGE ViewPatterns, CPP #-}
+{-|
+Module: IHP.HSX.HsExpToTH
+Copyright: (c) digitally induced GmbH, 2022
+Description: Converts Haskell AST to Template Haskell AST
+
+Based on https://github.com/guibou/PyF/blob/b3aaee12d34380e55aa3909690041eccb8fcf001/src/PyF/Internal/Meta.hs
+-}
+module IHP.HSX.HsExpToTH (toExp) where
+
+import Prelude
+
+import GHC.Hs.Expr as Expr
+import GHC.Hs.Extension as Ext
+import GHC.Hs.Pat as Pat
+import GHC.Hs.Lit
+import qualified GHC.Hs.Utils as Utils
+import qualified Data.ByteString as B
+import qualified Language.Haskell.TH.Syntax as TH
+import GHC.Types.SrcLoc
+import GHC.Types.Name
+import GHC.Types.Name.Reader
+import GHC.Data.FastString
+import GHC.Utils.Outputable (Outputable, ppr, showSDocUnsafe)
+import GHC.Types.Basic (Boxity(..))
+import GHC.Types.SourceText (il_value, rationalFromFractionalLit)
+import qualified GHC.Unit.Module as Module
+import GHC.Stack
+import qualified Data.List.NonEmpty as NonEmpty
+import Language.Haskell.Syntax.Type
+#if __GLASGOW_HASKELL__ >= 906
+import Language.Haskell.Syntax.Basic
+#endif
+
+
+fl_value = rationalFromFractionalLit
+
+toLit :: HsLit GhcPs -> TH.Lit
+toLit (HsChar _ c) = TH.CharL c
+toLit (HsCharPrim _ c) = TH.CharPrimL c
+toLit (HsString _ s) = TH.StringL (unpackFS s)
+toLit (HsStringPrim _ s) = TH.StringPrimL (B.unpack s)
+toLit (HsInt _ i) = TH.IntegerL (il_value i)
+toLit (HsIntPrim _ i) = TH.IntPrimL i
+toLit (HsWordPrim _ i) = TH.WordPrimL i
+toLit (HsInt64Prim _ i) = TH.IntegerL i
+toLit (HsWord64Prim _ i) = TH.WordPrimL i
+toLit (HsInteger _ i _) = TH.IntegerL i
+toLit (HsRat _ f _) = TH.FloatPrimL (fl_value f)
+toLit (HsFloatPrim _ f) = TH.FloatPrimL (fl_value f)
+toLit (HsDoublePrim _ f) = TH.DoublePrimL (fl_value f)
+
+toLit' :: OverLitVal -> TH.Lit
+toLit' (HsIntegral i) = TH.IntegerL (il_value i)
+toLit' (HsFractional f) = TH.RationalL (fl_value f)
+toLit' (HsIsString _ fs) = TH.StringL (unpackFS fs)
+
+toType :: HsType GhcPs -> TH.Type
+toType (HsWildCardTy _) = TH.WildCardT
+toType (HsTyVar _ _ n) =
+  let n' = unLoc n
+   in if isRdrTyVar n'
+        then TH.VarT (toName n')
+        else TH.ConT (toName n')
+toType t = todo "toType" t
+
+toName :: RdrName -> TH.Name
+toName n = case n of
+  (Unqual o) -> TH.mkName (occNameString o)
+  (Qual m o) -> TH.mkName (Module.moduleNameString m <> "." <> occNameString o)
+  (Exact name) -> TH.mkName ((occNameString . rdrNameOcc . getRdrName) name) --error "exact"
+  (Orig _ _) -> error "orig"
+
+toFieldExp :: a
+toFieldExp = undefined
+
+toPat :: Pat.Pat GhcPs -> TH.Pat
+toPat (Pat.VarPat _ (unLoc -> name)) = TH.VarP (toName name)
+toPat (TuplePat _ p _) = TH.TupP (map (toPat . unLoc) p)
+#if __GLASGOW_HASKELL__ >= 910
+toPat (ParPat xP lP)  = (toPat . unLoc) lP
+#else
+toPat (ParPat xP _ lP _) = (toPat . unLoc) lP
+#endif
+toPat (ConPat pat_con_ext ((unLoc -> name)) pat_args) = TH.ConP (toName name) (map toType []) (map (toPat . unLoc) (Pat.hsConPatArgs pat_args))
+toPat (ViewPat pat_con pat_args pat_con_ext) = error "TH.ViewPattern not implemented"
+toPat (SumPat _ _ _ _) = error "TH.SumPat not implemented"
+toPat (WildPat _ ) = error "TH.WildPat not implemented"
+toPat (NPat _ _ _ _ ) = error "TH.NPat not implemented"
+toPat p = todo "toPat" p
+
+toExp :: Expr.HsExpr GhcPs -> TH.Exp
+toExp (Expr.HsVar _ n) =
+  let n' = unLoc n
+   in if isRdrDataCon n'
+        then TH.ConE (toName n')
+        else TH.VarE (toName n')
+
+#if __GLASGOW_HASKELL__ >= 906
+toExp (Expr.HsUnboundVar _ n)              = TH.UnboundVarE (TH.mkName . occNameString $ occName n)
+#else
+toExp (Expr.HsUnboundVar _ n)              = TH.UnboundVarE (TH.mkName . occNameString $ n)
+#endif
+
+toExp Expr.HsIPVar {}
+  = noTH "toExp" "HsIPVar"
+
+toExp (Expr.HsLit _ l)
+  = TH.LitE (toLit l)
+
+toExp (Expr.HsOverLit _ OverLit {ol_val})
+  = TH.LitE (toLit' ol_val)
+
+toExp (Expr.HsApp _ e1 e2)
+  = TH.AppE (toExp . unLoc $ e1) (toExp . unLoc $ e2)
+
+#if __GLASGOW_HASKELL__ >= 910
+toExp (Expr.HsAppType _ e HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body)
+#elif __GLASGOW_HASKELL__ >= 906
+toExp (Expr.HsAppType _ e _ HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body)
+#else
+toExp (Expr.HsAppType _ e HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body)
+#endif
+toExp (Expr.ExprWithTySig _ e HsWC{hswc_body=unLoc -> HsSig{sig_body}}) = TH.SigE (toExp . unLoc $ e) (toType . unLoc $ sig_body)
+
+toExp (Expr.OpApp _ e1 o e2)
+  = TH.UInfixE (toExp . unLoc $ e1) (toExp . unLoc $ o) (toExp . unLoc $ e2)
+
+toExp (Expr.NegApp _ e _)
+  = TH.AppE (TH.VarE 'negate) (toExp . unLoc $ e)
+
+-- NOTE: for lambda, there is only one match
+#if __GLASGOW_HASKELL__ >= 912
+toExp (Expr.HsLam _ LamSingle (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc . unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)]))))
+#elif __GLASGOW_HASKELL__ >= 910
+toExp (Expr.HsLam _ LamSingle (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)]))))
+#elif __GLASGOW_HASKELL__ >= 906
+toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)]))))
+#else
+toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)])) _))
+#endif
+  = TH.LamE (fmap toPat ps) (toExp e)
+
+-- toExp (Expr.Let _ bs e)                       = TH.LetE (toDecs bs) (toExp e)
+--
+toExp (Expr.HsIf _ a b c)                   = TH.CondE (toExp (unLoc a)) (toExp (unLoc b)) (toExp (unLoc c))
+
+-- toExp (Expr.MultiIf _ ifs)                    = TH.MultiIfE (map toGuard ifs)
+-- toExp (Expr.Case _ e alts)                    = TH.CaseE (toExp e) (map toMatch alts)
+-- toExp (Expr.Do _ ss)                          = TH.DoE (map toStmt ss)
+-- toExp e@Expr.MDo{}                            = noTH "toExp" e
+--
+toExp (Expr.ExplicitTuple _ args boxity) = ctor tupArgs
+  where
+    toTupArg (Expr.Present _ e) = Just $ unLoc e
+    toTupArg (Expr.Missing _) = Nothing
+    toTupArg _ = error "impossible case"
+
+    ctor = case boxity of
+      Boxed -> TH.TupE
+      Unboxed -> TH.UnboxedTupE
+
+    tupArgs = fmap ((fmap toExp) . toTupArg) args
+
+-- toExp (Expr.List _ xs)                        = TH.ListE (fmap toExp xs)
+#if __GLASGOW_HASKELL__ >= 910
+toExp (Expr.HsPar _ e) =
+#else
+toExp (Expr.HsPar _ _ e _) =
+#endif
+  TH.ParensE (toExp . unLoc $ e)
+
+toExp (Expr.SectionL _ (unLoc -> a) (unLoc -> b))
+  = TH.InfixE (Just . toExp $ a) (toExp b) Nothing
+
+toExp (Expr.SectionR _ (unLoc -> a) (unLoc -> b))
+  = TH.InfixE Nothing (toExp a) (Just . toExp $ b)
+
+toExp (Expr.RecordCon _ name HsRecFields {rec_flds})
+  = TH.RecConE (toName . unLoc $ name) (fmap toFieldExp rec_flds)
+
+toExp (Expr.RecordUpd _ (unLoc -> e) xs)                 = TH.RecUpdE (toExp e) $ case xs of
+#if __GLASGOW_HASKELL__ >= 908
+    RegularRecUpdFields { recUpdFields = fields } ->
+#else
+    Left fields ->
+#endif
+        let
+            f (unLoc -> x) = (name, value)
+                where
+                    value = toExp $ unLoc $ hfbRHS x
+                    name =
+                        case unLoc (hfbLHS x) of
+#if __GLASGOW_HASKELL__ >= 912
+                            FieldOcc _ (unLoc -> name) -> toName name
+                            XFieldOcc _ -> error "todo"
+#else
+                            Unambiguous _ (unLoc -> name) -> toName name
+                            Ambiguous _ (unLoc -> name) -> toName name
+#endif
+        in
+            map f fields
+    otherwise -> error "todo"
+-- toExp (Expr.ListComp _ e ss)                  = TH.CompE $ map convert ss ++ [TH.NoBindS (toExp e)]
+--  where
+--   convert (Expr.QualStmt _ st)                = toStmt st
+--   convert s                                   = noTH "toExp ListComp" s
+-- toExp (Expr.ExpTypeSig _ e t)                 = TH.SigE (toExp e) (toType t)
+--
+toExp (Expr.ExplicitList _ (map unLoc -> args)) = TH.ListE (map toExp args)
+
+toExp (Expr.ArithSeq _ _ e)
+  = TH.ArithSeqE $ case e of
+    (From a) -> TH.FromR (toExp $ unLoc a)
+    (FromThen a b) -> TH.FromThenR (toExp $ unLoc a) (toExp $ unLoc b)
+    (FromTo a b) -> TH.FromToR (toExp $ unLoc a) (toExp $ unLoc b)
+    (FromThenTo a b c) -> TH.FromThenToR (toExp $ unLoc a) (toExp $ unLoc b) (toExp $ unLoc c)
+
+
+toExp (Expr.HsProjection _ locatedFields) =
+  let
+    extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr
+    extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..."
+  in
+#if __GLASGOW_HASKELL__ >= 912
+    TH.ProjectionE (NonEmpty.map (unpackFS . (.field_label) . unLoc . extractFieldLabel) locatedFields)
+#elif __GLASGOW_HASKELL__ >= 906
+    TH.ProjectionE (NonEmpty.map (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc) locatedFields)
+#else
+    TH.ProjectionE (NonEmpty.map (unpackFS . unLoc . extractFieldLabel . unLoc) locatedFields)
+#endif
+
+toExp (Expr.HsGetField _ expr locatedField) =
+  let
+    extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr
+    extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..."
+  in
+#if __GLASGOW_HASKELL__ >= 906
+    TH.GetFieldE (toExp (unLoc expr)) (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc $ locatedField)
+#else
+    TH.GetFieldE (toExp (unLoc expr)) (unpackFS . unLoc . extractFieldLabel . unLoc $ locatedField)
+#endif
+
+#if __GLASGOW_HASKELL__ >= 912
+toExp (Expr.HsOverLabel _ fastString) = TH.LabelE (unpackFS fastString)
+#elif __GLASGOW_HASKELL__ >= 906
+toExp (Expr.HsOverLabel _ _ fastString) = TH.LabelE (unpackFS fastString)
+#else
+toExp (Expr.HsOverLabel _ fastString) = TH.LabelE (unpackFS fastString)
+#endif
+
+toExp e = todo "toExp" e
+
+
+todo :: Outputable e => String -> e -> a
+todo fun thing = error . concat $ [moduleName, ".", fun, ": not implemented: ", (showSDocUnsafe $ ppr thing)]
+
+noTH :: (HasCallStack, Show e) => String -> e -> a
+noTH fun thing = error . concat $ [moduleName, ".", fun, ": no TemplateHaskell for: ", show thing]
+
+moduleName :: String
+moduleName = "IHP.HSX.HsExpToTH"
diff --git a/parser/IHP/HSX/Parser.hs b/parser/IHP/HSX/Parser.hs
new file mode 100644
--- /dev/null
+++ b/parser/IHP/HSX/Parser.hs
@@ -0,0 +1,643 @@
+{-# 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
+, HsxSettings (..)
+) where
+
+import Prelude
+import Data.Text (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 Data.String.Conversions
+import qualified Data.List as List
+import Control.Monad (unless)
+import qualified "template-haskell" 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
+import qualified IHP.HSX.HaskellParser as HaskellParser
+
+data HsxSettings = HsxSettings
+    { checkMarkup :: Bool
+    , additionalTagNames :: Set Text
+    , additionalAttributeNames :: Set Text
+    }
+
+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 -- ^ A Comment that is rendered in the final HTML
+    | NoRenderCommentNode -- ^ A comment that is not rendered in the final HTML
+    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 settings position [] hsxText
+parseHsx :: HsxSettings -> SourcePos -> [TH.Extension] -> Text -> Either (ParseErrorBundle Text Void) Node
+parseHsx settings position extensions code =
+    let
+        ?extensions = extensions
+        ?settings = settings
+    in
+        runParser (setPosition position *> parser) "" code
+
+type Parser a = (?extensions :: [TH.Extension], ?settings :: HsxSettings) => Parsec Void Text a
+
+setPosition pstateSourcePos = updateParserState (\state -> state {
+        statePosState = (statePosState state) { pstateSourcePos }
+    })
+
+parser :: Parser Node
+parser = do
+    space
+    node <- manyHsxElement <|> hsxElement
+    space
+    eof
+    pure node
+
+hsxElement :: Parser Node
+hsxElement = try hsxNoRenderComment <|> try hsxComment <|> try hsxSelfClosingElement <|> hsxNormalElement
+
+manyHsxElement :: Parser Node
+manyHsxElement = do
+    children <- many hsxChild
+    pure (Children (stripTextNodeWhitespaces children))
+
+hsxSelfClosingElement :: Parser Node
+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 :: Parser Node
+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 :: Parser (Text, [Attribute])
+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))
+
+hsxNoRenderComment :: Parser Node
+hsxNoRenderComment = do
+    string "{-"
+    manyTill (satisfy (const True)) (string "-}")
+    space
+    pure NoRenderCommentNode
+
+
+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
+    (pos, name) <- between (do char '{'; optional space; string "...") (string "}") do
+            pos <- getSourcePos
+            code <- takeWhile1P Nothing (\c -> c /= '}')
+            pure (pos, code)
+    space
+    haskellExpression <- parseHaskellExpression pos (cs name)
+    pure (SpreadAttributes haskellExpression)
+
+parseHaskellExpression :: SourcePos -> Text -> Parser Haskell.Exp
+parseHaskellExpression sourcePos input = do
+    case HaskellParser.parseHaskellExpression sourcePos ?extensions (cs input) of
+        Right expression -> pure expression
+        Left (line, col, error) -> do
+            pos <- getSourcePos
+            setPosition pos { sourceLine = mkPos line, sourceColumn = mkPos col }
+            fail (show error)
+
+hsxNodeAttribute :: Parser Attribute
+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
+        let checkingMarkup = ?settings.checkMarkup
+        unless (isValidAttributeName name || not checkingMarkup) (fail $ "Invalid attribute name: " <> cs name)
+        pure name
+    where
+        isValidAttributeName name =
+            "data-" `Text.isPrefixOf` name
+            || "aria-" `Text.isPrefixOf` name
+            || "hx-" `Text.isPrefixOf` name
+            || name `Set.member` attributes
+            || name `Set.member` ?settings.additionalAttributeNames
+
+        rawAttribute = takeWhile1P Nothing (\c -> Char.isAlphaNum c || c == '-' || c == '_')
+
+
+hsxQuotedValue :: Parser AttributeValue
+hsxQuotedValue = do
+    value <- between (char '"') (char '"') (takeWhileP Nothing (\c -> c /= '\"'))
+    pure (TextValue value)
+
+hsxSplicedValue :: Parser AttributeValue
+hsxSplicedValue = do
+    (pos, value) <- between (char '{') (char '}') do
+        pos <- getSourcePos
+        code <- takeWhile1P Nothing (\c -> c /= '}')
+        pure (pos, code)
+    haskellExpression <- parseHaskellExpression pos (cs value)
+    pure (ExpressionValue haskellExpression)
+
+hsxClosingElement name = (hsxClosingElement' name) <?> friendlyErrorMessage
+    where
+        friendlyErrorMessage = show (Text.unpack ("</" <> name <> ">"))
+        hsxClosingElement' name = do
+            _ <- string ("</" <> name)
+            space
+            char ('>')
+            pure ()
+
+hsxChild :: Parser Node
+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
+        (pos, expression) <- doParse
+        haskellExpression <- parseHaskellExpression pos (cs expression)
+        pure (SplicedNode haskellExpression)
+    where
+        doParse = do
+            (pos, tree) <- node
+            let value = (treeToString "" tree)
+            pure (pos, Text.init $ Text.tail value)
+
+        parseTree = (snd <$> node) <|> leaf
+        node = between (char '{') (char '}') do
+                pos <- getSourcePos
+                tree <- many parseTree
+                pure (pos, TokenNode tree)
+        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 == '-' || c == '!')
+    let isValidParent = name `Set.member` parents
+    let isValidLeaf = name `Set.member` leafs
+    let isValidCustomWebComponent = "-" `Text.isInfixOf` name 
+                                  && not (Text.isPrefixOf "-" name)
+                                  && not (Char.isNumber (Text.head name))
+    let isValidAdditionalTag = name `Set.member` ?settings.additionalTagNames
+    let checkingMarkup = ?settings.checkMarkup
+    unless (isValidParent || isValidLeaf || isValidCustomWebComponent || isValidAdditionalTag || not checkingMarkup) (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", "slot"
+        , "kind"
+        , "html"
+        , "sse-connect", "sse-swap"
+        , "muted", "onkeypress"
+        ]
+
+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"
+        , "!DOCTYPE"
+        ]
+
+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 = []
