diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 
@@ -5,23 +6,29 @@
 
 import qualified Control.Monad.Fail as Fail
 import qualified Data.Aeson as Aeson
+#ifdef VERSION_attoparsec_aeson
+import qualified Data.Aeson.Parser as Aeson
+#endif
 import qualified Data.Attoparsec.ByteString as Parsec
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Lazy as ByteString.Lazy
+import Data.HashMap.Strict (HashMap)
+import Data.Text (Text)
 import qualified Data.Text as Text
 import qualified Data.Text.Encoding as Text.Encoding
 import qualified Data.Text.Lazy.IO as Text.Lazy.IO
 import qualified Options.Applicative as Options
 import qualified Options.Applicative.Help.Pretty as Pretty
-import qualified System.Exit as Exit
-import qualified Text.EDE as EDE
 import qualified Prettyprinter as PP
 import qualified Prettyprinter.Render.Terminal as PP
+import qualified System.Exit as Exit
+import qualified Text.EDE as EDE
+import qualified Text.EDE.Internal.Compat as Compat
 
 data Options = Options
   { templateFile :: FilePath,
     jsonFile :: Maybe FilePath,
-    jsonObject :: Maybe Aeson.Object
+    jsonObject :: Maybe (HashMap Text Aeson.Value)
   }
 
 optionsParser :: Options.Parser Options
@@ -38,8 +45,8 @@
           ( Options.long "context-file"
               <> Options.metavar "PATH"
               <> Options.help
-                "Path of a file containing a JSON object which is used \
-                \as the template context"
+                ("Path of a file containing a JSON object which is used " <>
+                 "as the template context")
           )
       )
     <*> Options.optional
@@ -60,26 +67,32 @@
   where
     usage =
       Pretty.vcat
-        [ Pretty.empty,
+        [ Pretty.emptyDoc,
           "The --context-file and --context-json options are processed as follows:",
           Pretty.indent 2 "1.)"
             Pretty.<+> ( Pretty.align
-                           ( "Both are provided"
-                               Pretty.<$$> "Both objects are merged into one with the keys of \
-                                           \--context-json taking precedence over those in the \
-                                           \file provided by --context-file."
+                           ( Pretty.vcat
+                               [ "Both are provided",
+                                 "Both objects are merged into one with the keys of " <>
+                                 "--context-json taking precedence over those in the " <>
+                                 "file provided by --context-file."
+                               ]
                            )
                        ),
           Pretty.indent 2 "2.)"
             Pretty.<+> ( Pretty.align
-                           ( "None of them are provided"
-                               Pretty.<$$> "The JSON object is read from STDIN."
+                           ( Pretty.vcat
+                               [ "None of them are provided",
+                                 "The JSON object is read from STDIN."
+                               ]
                            )
                        ),
           Pretty.indent 2 "3.)"
             Pretty.<+> ( Pretty.align
-                           ( "One of them is provided"
-                               Pretty.<$$> "The JSON object is read from the supplied option."
+                           ( Pretty.vcat
+                               [ "One of them is provided",
+                                 "The JSON object is read from the supplied option."
+                               ]
                            )
                        )
         ]
@@ -117,7 +130,7 @@
         EDE.Failure err -> PP.putDoc (err <> PP.hardline) >> Exit.exitFailure
         EDE.Success output -> Text.Lazy.IO.putStr output
 
-stdinParser :: Parsec.Parser Aeson.Object
+stdinParser :: Parsec.Parser (HashMap Text Aeson.Value)
 stdinParser = Aeson.json >>= requireObject
 
 readValue :: Options.ReadM Aeson.Value
@@ -129,10 +142,10 @@
     . Text.Encoding.encodeUtf8
     . Text.pack
 
-readObject :: Options.ReadM Aeson.Object
+readObject :: Options.ReadM (HashMap Text Aeson.Value)
 readObject = readValue >>= requireObject
 
-requireObject :: Fail.MonadFail m => Aeson.Value -> m Aeson.Object
+requireObject :: Fail.MonadFail m => Aeson.Value -> m (HashMap Text Aeson.Value)
 requireObject = \case
-  Aeson.Object obj -> pure obj
+  Aeson.Object obj -> pure (Compat.toHashMapText obj)
   _ -> fail "JSON value must be an object"
diff --git a/ede.cabal b/ede.cabal
--- a/ede.cabal
+++ b/ede.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               ede
-version:            0.3.2.0
+version:            0.3.4.0
 synopsis:
   Templating language with similar syntax and features to Liquid or Jinja2.
 
@@ -9,7 +9,7 @@
 license-file:       LICENSE
 author:             Brendan Hay
 maintainer:         Brendan Hay <brendan.g.hay@gmail.com>
-copyright:          Copyright (c) 2013-2020 Brendan Hay
+copyright:          Copyright (c) 2013-2022 Brendan Hay
 stability:          Experimental
 category:           Text, Template, Web
 build-type:         Simple
@@ -45,13 +45,16 @@
   type:     git
   location: git://github.com/brendanhay/ede.git
 
+flag AttoparsecAeson
+  description: Build attoparsec-aeson.
+
 common base
   default-language: Haskell2010
   ghc-options:
     -Wall -funbox-strict-fields -fwarn-incomplete-uni-patterns
-    -fwarn-incomplete-record-updates
+    -fwarn-incomplete-record-updates -fwarn-unused-packages
 
-  build-depends:    base >=4.12 && <5
+  build-depends:    base >=4.13 && <5
   autogen-modules:  Paths_ede
   other-modules:    Paths_ede
 
@@ -62,6 +65,7 @@
     Text.EDE
     Text.EDE.Filters
     Text.EDE.Internal.AST
+    Text.EDE.Internal.Compat
     Text.EDE.Internal.Eval
     Text.EDE.Internal.Filters
     Text.EDE.Internal.Parser
@@ -70,8 +74,8 @@
     Text.EDE.Internal.Types
 
   build-depends:
-    , aeson                        >=0.7
-    , bytestring                   >=0.9
+    , aeson                        >=1.4
+    , bytestring                   >=0.10.4
     , comonad                      >=4.2
     , directory                    >=1.2
     , filepath                     >=1.2
@@ -79,7 +83,7 @@
     , lens                         >=4.0
     , mtl                          >=2.1.3.1
     , parsers                      >=0.12.1.1
-    , prettyprinter                >=1.6
+    , prettyprinter                >=1.7
     , prettyprinter-ansi-terminal  >=1.1
     , scientific                   >=0.3.1
     , text                         >=1.2
@@ -94,14 +98,22 @@
   main-is:        Main.hs
   ghc-options:    -threaded -rtsopts -with-rtsopts=-A128m
   build-depends:
-    , aeson                        >=0.8
-    , attoparsec
+    , attoparsec                   >=0.13
     , bytestring                   >=0.10.4
     , ede
-    , optparse-applicative         >=0.11
-    , prettyprinter                >=1.6
+    , optparse-applicative         >=0.18
+    , prettyprinter                >=1.7
     , prettyprinter-ansi-terminal  >=1.1
     , text                         >=1.2
+    , unordered-containers         >=0.2.3
+
+  if flag(AttoparsecAeson)
+    build-depends:
+      , aeson                      >=2.2
+      , attoparsec-aeson           >=2.2
+  else
+    build-depends:
+      , aeson                      >=1.4
 
 test-suite tests
   import:         base
diff --git a/lib/Text/EDE.hs b/lib/Text/EDE.hs
--- a/lib/Text/EDE.hs
+++ b/lib/Text/EDE.hs
@@ -3,7 +3,7 @@
 
 -- |
 -- Module      : Text.EDE
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -109,12 +109,15 @@
 
     -- ** Let Expressions
     -- $let
+
+    -- ** Set
+    -- $set
   )
 where
 
 import qualified Control.Monad as Monad
 import Data.Aeson ((.=))
-import Data.Aeson.Types (Object)
+import Data.Aeson.Types (Value)
 import Data.ByteString (ByteString)
 import qualified Data.ByteString as ByteString
 import qualified Data.Foldable as Foldable
@@ -125,9 +128,9 @@
 import qualified Data.Text as Text
 import qualified Data.Text.Lazy as Text.Lazy
 import qualified Data.Text.Lazy.Builder as Text.Builder
-import Data.Text.Prettyprint.Doc (Pretty (..))
 import Data.Version (Version)
 import qualified Paths_ede as Paths
+import Prettyprinter (Pretty (..))
 import qualified System.Directory as Directory
 import qualified System.FilePath as FilePath
 import qualified Text.EDE.Internal.Eval as Eval
@@ -211,14 +214,15 @@
   -- | Strict 'ByteString' template definition.
   ByteString ->
   m (Result Template)
-parseWith config f name =
-  result failure resolve
-    . Parser.runParser config name
+parseWith config f name input =
+  case Parser.runParser config name input of
+    Success (u, is', es', bs) -> do
+      is <- Foldable.foldrM include (Success (HashMap.singleton name u)) (HashMap.toList is')
+      es <- Foldable.foldrM extends (Success mempty) (HashMap.toList es')
+      pure (Template name u <$> is <*> es <*> pure bs)
+    Failure err ->
+      failure err
   where
-    resolve (u, is) =
-      Foldable.foldrM include (Success (HashMap.singleton name u)) (HashMap.toList is)
-        >>= result failure (success . Template name u)
-
     -- Presuming self is always in self's includes, see singleton above.
     -- FIXME: utilise the list of deltas for failures
     include (_, _) (Failure err) = failure err
@@ -226,6 +230,11 @@
       f config key delta
         >>= result failure (success . mappend ss . _tmplIncl)
 
+    extends (_, _) (Failure err) = failure err
+    extends (key, delta :| _) (Success ss) =
+      f config key delta
+        >>= result failure (success . mappend ss . _tmplIncl)
+
 -- | 'HashMap' resolver for @include@ expressions.
 --
 -- The 'identifier' component of the @include@ expression is treated as a lookup
@@ -270,26 +279,30 @@
     then failure ("file " <> pretty path <> " doesn't exist.")
     else ByteString.readFile path >>= success
 
--- | Render an 'Object' using the supplied 'Template'.
+-- | Render an 'HashMap Text Value' using the supplied 'Template'.
 render ::
   -- | Parsed 'Template' to render.
   Template ->
   -- | Bindings to make available in the environment.
-  Object ->
+  HashMap Text Value ->
   Result Text.Lazy.Text
 render = renderWith mempty
 
--- | Render an 'Object' using the supplied 'Template'.
+-- | Render an 'HashMap Text Value' using the supplied 'Template'.
 renderWith ::
   -- | Filters to make available in the environment.
   HashMap Id Term ->
   -- | Parsed 'Template' to render.
   Template ->
   -- | Bindings to make available in the environment.
-  Object ->
+  HashMap Text Value ->
   Result Text.Lazy.Text
-renderWith fs (Template _ u ts) =
-  fmap Text.Builder.toLazyText . Eval.render ts fs u
+renderWith fs t =
+  fmap Text.Builder.toLazyText .
+    Eval.render
+      (_tmplIncl t <> _tmplExtends t)
+      fs
+      (_tmplExp t)
 
 -- | /See:/ 'parse'
 eitherParse :: ByteString -> Either String Template
@@ -312,7 +325,7 @@
 -- | /See:/ 'render'
 eitherRender ::
   Template ->
-  Object ->
+  HashMap Text Value ->
   Either String Text.Lazy.Text
 eitherRender t = eitherResult . render t
 
@@ -320,7 +333,7 @@
 eitherRenderWith ::
   HashMap Id Term ->
   Template ->
-  Object ->
+  HashMap Text Value ->
   Either String Text.Lazy.Text
 eitherRenderWith fs t = eitherResult . renderWith fs t
 
@@ -333,15 +346,15 @@
 --
 -- >>> tmpl <- parse "{% if var %}\nHello, {{ var }}!\n{% else %}\nnegative!\n{% endif %}\n" :: Result Template
 --
--- Then an 'Object' is defined containing the environment which will be
+-- Then an 'HashMap Text Value' is defined containing the environment which will be
 -- available to the 'Template' during rendering:
 --
--- >>> let env = fromPairs [ "var" .= "World" ] :: Object
+-- >>> let env = fromPairs [ "var" .= "World" ] :: HashMap Text Value
 --
 -- Note: the 'fromPairs' function above is a wrapper over Aeson's 'object'
--- which removes the outer 'Object' 'Value' constructor, exposing the underlying 'HashMap'.
+-- which removes the outer 'Data.Aeson.Object' 'Value' constructor, exposing the underlying 'HashMap'.
 --
--- Then, the 'Template' is rendered using the 'Object' environment:
+-- Then, the 'Template' is rendered using the 'HashMap Text Value' environment:
 --
 -- >>> render tmpl env :: Result Text
 -- > Success "Hello, World!"
@@ -379,7 +392,7 @@
 -- which can be cached for future use.
 --
 -- * Rendering takes a 'HashMap' of custom 'Fun's (functions available in the
--- template context), an 'Object' as the binding environment, and a parsed
+-- template context), an 'HashMap Text Value' as the binding environment, and a parsed
 -- 'Template' to subsitute the values into.
 -- The result is a Lazy 'Text.Lazy.Text' value containing the rendered output.
 
@@ -487,12 +500,12 @@
 --
 -- Variables are substituted directly for their renderable representation.
 -- An error is raised if the varaible being substituted is not a literal type
--- (ie. an 'Array' or 'Object') or doesn't exist in the supplied environment.
+-- (ie. an 'Array' or 'HashMap Text Value') or doesn't exist in the supplied environment.
 --
 -- > {{ var }}
 --
--- Nested variable access is also supported for variables which resolve to an 'Object'.
--- Dot delimiters are used to chain access through multiple nested 'Object's.
+-- Nested variable access is also supported for variables which resolve to an 'HashMap Text Value'.
+-- Dot delimiters are used to chain access through multiple nested 'HashMap Text Value's.
 -- The right-most accessor must resolve to a renderable type as with the previous
 -- non-nested variable access.
 --
@@ -564,7 +577,7 @@
 
 -- $loops
 --
--- Iterating over an 'Array' or 'Object' can be acheived using the 'for ... in' section syntax.
+-- Iterating over an 'Array' or 'HashMap Text Value' can be acheived using the 'for ... in' section syntax.
 -- Attempting to iterate over any other type will raise an error.
 --
 -- Example:
@@ -578,13 +591,13 @@
 -- The iteration branch is rendering per item with the else branch being (which is optional)
 -- being rendered if the @{{ list }}@ variable is empty.
 --
--- When iterating over an 'Object', a stable sort using key equivalence is applied, 'Array's
+-- When iterating over an 'HashMap Text Value', a stable sort using key equivalence is applied, 'Array's
 -- are unmodified.
 --
 -- The resulting binding within the iteration expression (in this case, @{{ var }}@) is
--- an 'Object' containing the following keys:
+-- an 'HashMap Text Value' containing the following keys:
 --
--- * @key        :: Text@: They key if the loop target is an 'Object'
+-- * @key        :: Text@: They key if the loop target is an 'HashMap Text Value'
 --
 -- * @value      :: a@: The value of the loop target
 --
@@ -620,8 +633,8 @@
 -- Will render each item with its (1-based) loop index as a prefix, separated
 -- by a blank newline, without a trailing at the end of the document.
 --
--- Valid loop targets are 'Object's, 'Array's, and 'String's, with only 'Object's
--- having an available @{{ <var>.key }}@ in scope.
+-- Valid loop targets are 'HashMap Text Value's, 'Array's, and 'String's, with
+-- only 'HashMap Text Value's having an available @{{ <var>.key }}@ in scope.
 
 -- $includes
 --
@@ -696,3 +709,55 @@
 -- > {{ var }}
 -- > ...
 -- > {% endlet %}
+
+-- $set
+--
+-- You can also bind an identifier to whole templates which will be available within
+-- the following expression scope. The identifier will be available in subsequent template.
+--
+-- For example:
+--
+-- > {% set var %}
+-- >   ...
+-- > {% endset %}
+-- > ...
+-- > {{ var }}
+
+-- $block
+--
+-- Blocks are used for inheritance and act as both placeholders and replacements at the same time: The most powerful
+-- part of @ED-E@ is template inheritance. Template inheritance allows you to build a base "skeleton" template that
+-- contains all the common elements of your site and defines blocks that child templates can override.
+--
+-- Base template:
+--
+-- > <!doctype html>
+-- > {% block head %}
+-- > <title>{% block title %}{% endblock %}</title>
+-- > {% endblock %}
+-- > {% block body %}{% endblock %}
+--
+-- Child template:
+--
+-- > {% extends "base.html" %}
+-- > {% block title %}Index{% endblock %}
+-- > {% block head %}
+-- >  {{ super }}
+-- >  <style type="text/css">
+-- >   .important { color: #336699; }
+-- > </style>
+-- > {% endblock %}
+-- > {% block body %}
+-- >  <h1>Index</h1>
+-- >  <p class="important">
+-- >    Welcome to my awesome homepage.
+-- >  </p>
+-- > {% endblock %}
+--
+-- The @{% extends %}@ tag is the key here. It tells the template engine that this template "extends" another template.
+-- When the template system evaluates this template, it first locates the parent. The extends tag should be the first tag in the template.
+--
+-- As you can see it's also possible to render the contents of the parent block by calling super(). You can’t define multiple {% block %}
+-- tags with the same name in the same template. This limitation exists because a block tag works in “both” directions. That is, a block
+-- tag doesn’t just provide a placeholder to fill - it also defines the content that fills the placeholder in the parent. If there were two
+-- similarly-named {% block %} tags in a template, that template's parent wouldn’t know which one of the blocks’ content to use.
diff --git a/lib/Text/EDE/Filters.hs b/lib/Text/EDE/Filters.hs
--- a/lib/Text/EDE/Filters.hs
+++ b/lib/Text/EDE/Filters.hs
@@ -1,6 +1,6 @@
 -- |
 -- Module      : Text.EDE.Filters
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
diff --git a/lib/Text/EDE/Internal/AST.hs b/lib/Text/EDE/Internal/AST.hs
--- a/lib/Text/EDE/Internal/AST.hs
+++ b/lib/Text/EDE/Internal/AST.hs
@@ -3,7 +3,7 @@
 
 -- |
 -- Module      : Text.EDE.Internal.AST
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -56,6 +56,21 @@
 elet :: Maybe (Id, Exp a) -> Exp a -> Exp a
 elet m e = maybe e (\(i, b) -> Comonad.extract b :< ELet i b e) m
 {-# INLINEABLE elet #-}
+
+eset :: Id -> Exp a -> Exp a -> Exp a
+eset i e k =
+  elet (Just (i, e)) k
+{-# INLINEABLE eset #-}
+
+eblock :: Id -> Exp a -> Exp a
+eblock i e =
+  Comonad.extract e :< EBlock i e
+{-# INLINEABLE eblock #-}
+
+eOverrideBlock :: Id -> Exp a -> Exp a -> Exp a
+eOverrideBlock i b e =
+  Comonad.extract b :< EOverrideBlock i b e
+{-# INLINEABLE eOverrideBlock #-}
 
 ecase ::
   Exp a ->
diff --git a/lib/Text/EDE/Internal/Compat.hs b/lib/Text/EDE/Internal/Compat.hs
new file mode 100644
--- /dev/null
+++ b/lib/Text/EDE/Internal/Compat.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE CPP #-}
+
+-- |
+-- Module      : Text.EDE.Internal.Eval
+-- Copyright   : (c) 2022 Brendan Hay <brendan.g.hay@gmail.com>
+-- License     : This Source Code Form is subject to the terms of
+--               the Mozilla Public License, v. 2.0.
+--               A copy of the MPL can be found in the LICENSE file or
+--               you can obtain it at http://mozilla.org/MPL/2.0/.
+-- Maintainer  : Brendan Hay <brendan.g.hay@gmail.com>
+-- Stability   : experimental
+-- Portability : non-portable (GHC extensions)
+--
+-- /Warning/: this is an internal module, and does not have a stable
+-- API or name. Functions in this module may not check or enforce
+-- preconditions expected by public modules. Use at your own risk!
+module Text.EDE.Internal.Compat
+  ( toHashMapText,
+    fromHashMapText,
+  )
+where
+
+#if MIN_VERSION_aeson(2,0,0)
+import Data.Aeson.KeyMap (toHashMapText, fromHashMapText)
+#else
+import Data.HashMap.Strict (HashMap)
+import Data.Aeson.Types (Value)
+import Data.Text (Text)
+
+toHashMapText :: HashMap Text Value -> HashMap Text Value
+toHashMapText = id
+
+fromHashMapText :: HashMap Text Value -> HashMap Text Value
+fromHashMapText = id
+#endif
diff --git a/lib/Text/EDE/Internal/Eval.hs b/lib/Text/EDE/Internal/Eval.hs
--- a/lib/Text/EDE/Internal/Eval.hs
+++ b/lib/Text/EDE/Internal/Eval.hs
@@ -5,7 +5,7 @@
 
 -- |
 -- Module      : Text.EDE.Internal.Eval
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -26,20 +26,22 @@
 import Control.Monad.Trans (lift)
 import Data.Aeson ((.=))
 import qualified Data.Aeson as Aeson
-import Data.Aeson.Types (Object, Value (..))
+import Data.Aeson.Types (Value (..))
 import qualified Data.Foldable as Foldable
 import Data.HashMap.Strict (HashMap)
 import qualified Data.HashMap.Strict as HashMap
 import Data.List.NonEmpty (NonEmpty (..))
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Scientific (isFloating)
+import Data.Text (Text)
 import qualified Data.Text as Text
 import Data.Text.Lazy.Builder (Builder)
 import qualified Data.Text.Lazy.Builder as Text.Builder
 import Data.Text.Lazy.Builder.Scientific (FPFormat (Fixed), formatScientificBuilder)
 import Data.Text.Manipulate (toOrdinal)
-import Data.Text.Prettyprint.Doc ((<+>))
-import qualified Data.Text.Prettyprint.Doc as PP
+import Prettyprinter ((<+>))
+import qualified Prettyprinter as PP
+import Text.EDE.Internal.Compat
 import Text.EDE.Internal.Filters (stdlib)
 import Text.EDE.Internal.Quoting
 import Text.EDE.Internal.Types
@@ -47,9 +49,11 @@
 import qualified Text.Trifecta.Delta as Trifecta.Delta
 
 data Env = Env
-  { _templates :: HashMap Id (Exp Delta),
+  { _extends :: Bool,
+    _templates :: HashMap Id (Exp Delta),
     _quoted :: HashMap Id Term,
-    _values :: HashMap Id Value
+    _values :: HashMap Id Value,
+    _blocks :: HashMap Id (Exp Delta)
   }
 
 type Context = ReaderT Env Result
@@ -61,7 +65,7 @@
   HashMap Id Value ->
   Result Builder
 render ts fs e o =
-  Reader.runReaderT (eval e >>= nf) (Env ts (stdlib <> fs) o)
+  Reader.runReaderT (eval e >>= nf) (Env False ts (stdlib <> fs) o mempty)
   where
     nf (TVal v) = build (Trifecta.Delta.delta e) v
     nf _ =
@@ -83,6 +87,26 @@
   x <- eval a
   y <- eval b
   binding d x y
+eval (d :< EOverrideBlock i b e) =
+  setExtended True $ do
+    q <- HashMap.lookup i <$> Reader.asks _blocks
+    case q of
+      Nothing ->
+        bindBlock (HashMap.insert i b) (eval e)
+      Just eb ->
+        bindBlock (HashMap.insert i (d :< ELet "super" b eb)) (eval e)
+eval (_ :< EBlock i b) = do
+  extends <- Reader.asks _extends
+  q <- HashMap.lookup i <$> Reader.asks _blocks
+  if extends then
+    pure (qprim (String mempty))
+  else do
+    x <- eval b
+    v <- lift (unquote i 0 x)
+    maybe
+      (pure x)
+      (bind (HashMap.insert "super" v) . eval)
+      q
 eval (_ :< ELet k rhs bdy) = do
   q <- eval rhs
   v <- lift (unquote k 0 q)
@@ -162,22 +186,45 @@
 eval (d :< EIncl i) = do
   ts <- Reader.asks _templates
   case HashMap.lookup i ts of
-    Just e -> eval e
+    Just e ->
+      -- Don't inherit any blocks declared so far.
+      setExtended False $
+        bindBlock (\_ -> mempty) (eval e)
     Nothing ->
       throwError d $
         "template"
           <+> bold (pp i)
           <+> "is not in scope:"
           <+> PP.brackets (pp (Text.intercalate "," $ HashMap.keys ts))
+eval (d :< EExt i) = do
+  ts <- Reader.asks _templates
+  case HashMap.lookup i ts of
+    Just e ->
+      setExtended False (eval e)
+    Nothing ->
+      throwError d $
+        "template"
+          <+> bold (pp i)
+          <+> "is not in scope:"
+          <+> PP.brackets (pp (Text.intercalate "," $ HashMap.keys ts))
 {-# INLINEABLE eval #-}
 
-bind :: (Object -> Object) -> Context a -> Context a
+bind :: (HashMap Text Value -> HashMap Text Value) -> Context a -> Context a
 bind f = Reader.withReaderT (\x -> x {_values = f (_values x)})
 {-# INLINEABLE bind #-}
 
+bindBlock :: (HashMap Id (Exp Delta) -> HashMap Id (Exp Delta)) -> Context a -> Context a
+bindBlock f = Reader.withReaderT (\x -> x {_blocks = f (_blocks x)})
+{-# INLINEABLE bindBlock #-}
+
+setExtended :: Bool -> Context a -> Context a
+setExtended extends =
+  Reader.withReaderT (\x -> x { _extends = extends })
+{-# INLINEABLE setExtended #-}
+
 variable :: Delta -> Var -> Context Value
 variable d (Var is) =
-  Reader.asks _values >>= go (NonEmpty.toList is) [] . Object
+  Reader.asks _values >>= go (NonEmpty.toList is) [] . Object . fromHashMapText
   where
     go [] _ v = pure v
     go (k : ks) r v = do
@@ -189,8 +236,8 @@
       where
         cur = Var (k :| r)
 
-        nest :: Value -> Context Object
-        nest (Object o) = pure o
+        nest :: Value -> Context (HashMap Text Value)
+        nest (Object o) = pure (toHashMapText o)
         nest x =
           throwError d $
             "variable"
diff --git a/lib/Text/EDE/Internal/Filters.hs b/lib/Text/EDE/Internal/Filters.hs
--- a/lib/Text/EDE/Internal/Filters.hs
+++ b/lib/Text/EDE/Internal/Filters.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE ExtendedDefaultRules #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE LambdaCase #-}
@@ -7,7 +8,7 @@
 
 -- |
 -- Module      : Text.EDE.Internal.Filters
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -22,7 +23,7 @@
 module Text.EDE.Internal.Filters where
 
 import qualified Data.Aeson as Aeson
-import Data.Aeson.Types (Array, Object, Value (..))
+import Data.Aeson.Types (Value (..))
 import qualified Data.Char as Char
 import Data.HashMap.Strict (HashMap)
 import qualified Data.HashMap.Strict as HashMap
@@ -33,9 +34,11 @@
 import qualified Data.Text.Lazy as Text.Lazy
 import qualified Data.Text.Lazy.Encoding as Text.Lazy.Encoding
 import qualified Data.Text.Manipulate as Text.Manipulate
-import Data.Text.Prettyprint.Doc ((<+>))
 import qualified Data.Text.Unsafe as Text.Unsafe
+import Data.Vector (Vector)
 import qualified Data.Vector as Vector
+import Prettyprinter ((<+>))
+import Text.EDE.Internal.Compat
 import Text.EDE.Internal.Quoting
 import Text.EDE.Internal.Types
 
@@ -110,8 +113,8 @@
       qlist1 "init" initT initV,
       "at" @: (\x i -> x Vector.! i :: Value),
       -- object
-      "keys" @: (HashMap.keys :: Object -> [Text]),
-      "elems" @: (HashMap.elems :: Object -> [Value]),
+      "keys" @: (HashMap.keys :: HashMap Text Value -> [Text]),
+      "elems" @: (HashMap.elems :: HashMap Text Value -> [Value]),
       -- , "map"        @: undefined
       -- , "filter"     @: undefined
       -- , "zip"        @: undefined
@@ -119,7 +122,7 @@
 
       -- polymorphic
       "show" @: (Text.Lazy.Encoding.decodeUtf8 . Aeson.encode :: Value -> Text.Lazy.Text),
-      "singleton" @: (pure :: Value -> Vector.Vector Value)
+      "singleton" @: (pure :: Value -> Vector Value)
       -- FIXME: existence checks currently hardcoded into the evaluator:
       -- "default"
       -- "defined"
@@ -146,7 +149,7 @@
   (Quote a, Quote b) =>
   Id ->
   (Text -> a) ->
-  (Array -> b) ->
+  (Vector Value -> b) ->
   (Id, Term)
 qlist1 k f g = (k,) . TLam $ \case
   TVal (String t) -> pure . quote k 0 $ f t
@@ -161,12 +164,12 @@
   (Quote a, Quote b, Quote c) =>
   Id ->
   (Text -> a) ->
-  (Object -> b) ->
-  (Array -> c) ->
+  (HashMap Text Value -> b) ->
+  (Vector Value -> c) ->
   (Id, Term)
 qcol1 k f g h = (k,) . TLam $ \case
   TVal (String t) -> pure . quote k 0 $ f t
-  TVal (Object o) -> pure . quote k 0 $ g o
+  TVal (Object o) -> pure . quote k 0 $ g (toHashMapText o)
   TVal (Array v) -> pure . quote k 0 $ h v
   x ->
     Failure $
@@ -178,7 +181,7 @@
 tailT = text Text.Unsafe.unsafeTail
 initT = text Text.init
 
-headV, lastV, tailV, initV :: Array -> Value
+headV, lastV, tailV, initV :: Vector Value -> Value
 headV = vec Vector.unsafeHead
 lastV = vec Vector.unsafeLast
 tailV = vec (Array . Vector.unsafeTail)
@@ -187,7 +190,7 @@
 text :: (Text -> Text) -> Text -> Value
 text f = String . safe mempty Text.null f
 
-vec :: (Array -> Value) -> Array -> Value
+vec :: (Vector Value -> Value) -> Vector Value -> Value
 vec = safe (Array Vector.empty) Vector.null
 
 safe :: b -> (a -> Bool) -> (a -> b) -> a -> b
diff --git a/lib/Text/EDE/Internal/Parser.hs b/lib/Text/EDE/Internal/Parser.hs
--- a/lib/Text/EDE/Internal/Parser.hs
+++ b/lib/Text/EDE/Internal/Parser.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE ConstraintKinds #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
@@ -12,7 +11,7 @@
 
 -- |
 -- Module      : Text.EDE.Internal.Parser
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -31,12 +30,11 @@
 import Control.Comonad.Cofree (Cofree ((:<)))
 import Control.Lens ((%=))
 import qualified Control.Lens as Lens
-import Control.Monad (MonadPlus, void, unless)
+import Control.Monad (MonadPlus, unless, void)
 import Control.Monad.State.Strict (MonadState, StateT)
 import qualified Control.Monad.State.Strict as State
 import Control.Monad.Trans (lift)
-import Data.Aeson.Types (Array, Object, Value (..))
-import qualified Data.Bifunctor as Bifunctor
+import Data.Aeson.Types (Value (..))
 import Data.ByteString (ByteString)
 import qualified Data.Char as Char
 import Data.HashMap.Strict (HashMap)
@@ -48,8 +46,10 @@
 import Data.Text (Text)
 import qualified Data.Text as Text
 import qualified Data.Text.Encoding as Text.Encoding
+import Data.Vector (Vector)
 import qualified Data.Vector as Vector
 import Text.EDE.Internal.AST
+import Text.EDE.Internal.Compat
 import Text.EDE.Internal.Syntax
 import Text.EDE.Internal.Types
 import qualified Text.Parser.Expression as Expression
@@ -63,7 +63,9 @@
 
 data Env = Env
   { _settings :: !Syntax,
-    _includes :: HashMap Text (NonEmpty Delta)
+    _includes :: HashMap Text (NonEmpty Delta),
+    _extends :: HashMap Text (NonEmpty Delta),
+    _blocks :: HashMap Text (Exp Delta)
   }
 
 $(Lens.makeLenses ''Env)
@@ -73,9 +75,7 @@
 
 type Parser m =
   ( Monad m,
-#if MIN_VERSION_base(4,13,0)
     MonadFail m,
-#endif
     MonadState Env m,
     Trifecta.TokenParsing m,
     Trifecta.DeltaParsing m,
@@ -89,9 +89,7 @@
       Applicative,
       Alternative,
       Monad,
-#if MIN_VERSION_base(4,13,0)
       MonadFail,
-#endif
       MonadPlus,
       Trifecta.Parsing,
       Trifecta.CharParsing,
@@ -123,24 +121,35 @@
   Syntax ->
   Text ->
   ByteString ->
-  Result (Exp Delta, HashMap Text (NonEmpty Delta))
+  Result (Exp Delta, HashMap Text (NonEmpty Delta), HashMap Text (NonEmpty Delta), HashMap Text (Exp Delta))
 runParser o n = res . Trifecta.parseByteString (runEDE run) pos
   where
-    run = State.runStateT (pragma *> document <* Trifecta.eof) (Env o mempty)
+    run = State.runStateT (pragma *> document <* Trifecta.eof) (Env o mempty mempty mempty)
 
     pos = Trifecta.Delta.Directed (Text.Encoding.encodeUtf8 n) 0 0 0 0
 
     res = \case
-      Trifecta.Success x -> Success (Bifunctor.second _includes x)
+      Trifecta.Success (x, env) ->
+        if null (_extends env) then
+          Success (x, _includes env, _extends env, _blocks env)
+        else
+          -- In case the template extends another template we arrange the contained
+          -- blocks in a way that make it easy to evaluate the "inheritance" mechanism.
+          let
+            x' =
+              HashMap.foldrWithKey eOverrideBlock x (_blocks env)
+          in
+            Success (x', _includes env, _extends env, _blocks env)
       Trifecta.Failure e -> Failure (Trifecta._errDoc e)
 
 pragma :: Parser m => m ()
 pragma =
   void . Trifecta.many $ do
-    !xs <- pragmal
-      *> Trifecta.symbol "EDE_SYNTAX"
-      *> Trifecta.sepBy field spaces
-      <* trimr pragmar
+    !xs <-
+      pragmal
+        *> Trifecta.symbol "EDE_SYNTAX"
+        *> Trifecta.sepBy field spaces
+        <* trimr pragmar
 
     mapM_ (uncurry Lens.assign) xs
   where
@@ -186,6 +195,9 @@
       cases,
       loop,
       include,
+      set,
+      block',
+      extends',
       binding,
       raw,
       comment
@@ -194,7 +206,7 @@
 block :: Parser m => String -> m a -> m a
 block k p =
   Trifecta.try (multiLine (keyword k) p)
-  <|> singleLine (keyword k) p
+    <|> singleLine (keyword k) p
 
 multiLine :: Parser m => m b -> m a -> m a
 multiLine s =
@@ -259,6 +271,13 @@
         (,) <$> (keyword "with" *> identifier)
           <*> (Trifecta.symbol "=" *> term)
 
+extends' :: Parser m => m (Exp Delta)
+extends' = block "extends" $ do
+  d <- Trifecta.position
+  k <- Trifecta.stringLiteral
+  extends %= HashMap.insertWith (<>) k (d :| [])
+  pure (d :< EExt k)
+
 binding :: Parser m => m (Exp Delta)
 binding =
   elet . Just
@@ -270,6 +289,22 @@
     <*> document
     <* exit "endlet"
 
+set :: Parser m => m (Exp Delta)
+set =
+  eset
+  <$> block "set" identifier
+  <*> document
+  <* exit "endset"
+  <*> document
+
+block' :: Parser m => m (Exp Delta)
+block' = do
+  k <- block "block" identifier
+  e <- document
+  _ <- exit "endblock"
+  blocks %= HashMap.insert k e
+  pure (eblock k e)
+
 raw :: Parser m => m (Exp Delta)
 raw = ann (ELit <$> body)
   where
@@ -342,7 +377,7 @@
 collection = ann (EVar <$> variable <|> ELit <$> col)
   where
     col =
-      Object <$> object
+      (Object . fromHashMapText) <$> object
         <|> Array <$> array
         <|> String <$> Trifecta.stringLiteral
 
@@ -351,7 +386,7 @@
   Bool <$> bool
     <|> Number <$> number
     <|> String <$> Trifecta.stringLiteral
-    <|> Object <$> object
+    <|> (Object . fromHashMapText) <$> object
     <|> Array <$> array
 
 number :: Parser m => m Scientific
@@ -364,7 +399,7 @@
   Trifecta.symbol "true" *> pure True
     <|> Trifecta.symbol "false" *> pure False
 
-object :: Parser m => m Object
+object :: Parser m => m (HashMap Text Value)
 object = HashMap.fromList <$> Trifecta.braces (Trifecta.commaSep pair)
   where
     pair =
@@ -372,7 +407,7 @@
         <$> (Trifecta.stringLiteral <* Trifecta.spaces)
         <*> (Trifecta.char ':' *> Trifecta.spaces *> literal)
 
-array :: Parser m => m Array
+array :: Parser m => m (Vector Value)
 array = Vector.fromList <$> Trifecta.brackets (Trifecta.commaSep literal)
 
 operator :: Parser m => Text -> m Delta
@@ -413,10 +448,10 @@
 
 triml :: Parser m => m a -> m a
 triml p = do
-    c <- Trifecta.Delta.column <$> Trifecta.position
-    if c == 0
-        then Trifecta.spaces *> p
-        else fail "left whitespace removal failed"
+  c <- Trifecta.Delta.column <$> Trifecta.position
+  if c == 0
+    then Trifecta.spaces *> p
+    else fail "left whitespace removal failed"
 
 trimr :: Parser m => m a -> m a
 trimr p = p <* Trifecta.newline -- <* Trifecta.newline
diff --git a/lib/Text/EDE/Internal/Quoting.hs b/lib/Text/EDE/Internal/Quoting.hs
--- a/lib/Text/EDE/Internal/Quoting.hs
+++ b/lib/Text/EDE/Internal/Quoting.hs
@@ -1,5 +1,5 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE DefaultSignatures #-}
-{-# LANGUAGE ExtendedDefaultRules #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE LambdaCase #-}
@@ -8,7 +8,7 @@
 
 -- |
 -- Module      : Text.EDE.Internal.Quoting
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -26,9 +26,10 @@
 import Control.Monad ((>=>))
 import Data.Aeson (FromJSON, ToJSON)
 import qualified Data.Aeson as Aeson
-import Data.Aeson.Types (Array, Object, Value (..))
+import Data.Aeson.Types (Value (..))
 import qualified Data.Bifunctor as Bifunctor
 import qualified Data.ByteString.Char8 as ByteString.Char8
+import Data.HashMap.Strict (HashMap)
 import qualified Data.HashMap.Strict as HashMap
 import Data.List (sortBy)
 import Data.Ord (comparing)
@@ -41,15 +42,18 @@
 import Data.Text.Lazy.Builder (Builder)
 import qualified Data.Text.Lazy.Builder as Text.Builder
 import Data.Text.Manipulate (toOrdinal)
-import Data.Text.Prettyprint.Doc (Pretty (..), (<+>))
-import qualified Data.Text.Prettyprint.Doc as PP
+import Data.Vector (Vector)
 import qualified Data.Vector as Vector
+import Prettyprinter (Pretty (..), (<+>))
+import qualified Prettyprinter as PP
 import Text.EDE.Internal.Types
 import Text.Trifecta.Delta (Delta)
 import qualified Text.Trifecta.Delta as Trifecta.Delta
 import qualified Text.Trifecta.Rendering as Trifecta.Rendering
 
-default (AnsiDoc, Double, Integer)
+#if MIN_VERSION_aeson(2,0,0)
+import Data.Aeson.KeyMap (KeyMap)
+#endif
 
 -- | A HOAS representation of (possibly partially applied) values
 -- in the environment.
@@ -96,6 +100,14 @@
 
 instance Unquote Value
 
+#if MIN_VERSION_aeson(2,0,0)
+instance Unquote (KeyMap Value)
+#endif
+
+instance Unquote (HashMap Text Value)
+
+instance Unquote (Vector Value)
+
 instance Unquote Text
 
 instance Unquote [Text]
@@ -108,10 +120,6 @@
 
 instance Unquote Scientific
 
-instance Unquote Object
-
-instance Unquote Array
-
 instance Unquote Int where
   unquote k n =
     unquote k n
@@ -164,6 +172,14 @@
 
 instance Quote Value
 
+#if MIN_VERSION_aeson(2,0,0)
+instance Quote (KeyMap Value)
+#endif
+
+instance Quote (HashMap Text Value)
+
+instance Quote (Vector Value)
+
 instance Quote [Value]
 
 instance Quote Text
@@ -181,10 +197,6 @@
 instance Quote Double
 
 instance Quote Scientific
-
-instance Quote Object
-
-instance Quote Array
 
 instance Quote Builder where
   quote k n = quote k n . Text.Builder.toLazyText
diff --git a/lib/Text/EDE/Internal/Syntax.hs b/lib/Text/EDE/Internal/Syntax.hs
--- a/lib/Text/EDE/Internal/Syntax.hs
+++ b/lib/Text/EDE/Internal/Syntax.hs
@@ -2,7 +2,7 @@
 
 -- |
 -- Module      : Text.EDE.Internal.Syntax
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -104,7 +104,11 @@
       "_",
       ".",
       "true",
-      "false"
+      "false",
+      "set",
+      "endset",
+      "block",
+      "endblock"
     ]
 
 pragmaStyle :: TokenParsing m => IdentifierStyle m
diff --git a/lib/Text/EDE/Internal/Types.hs b/lib/Text/EDE/Internal/Types.hs
--- a/lib/Text/EDE/Internal/Types.hs
+++ b/lib/Text/EDE/Internal/Types.hs
@@ -11,7 +11,7 @@
 
 -- |
 -- Module      : Text.EDE.Internal.Types
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
@@ -30,7 +30,7 @@
 import Control.Comonad.Cofree (Cofree)
 import qualified Control.Lens as Lens
 import qualified Data.Aeson as Aeson
-import Data.Aeson.Types (Object, Pair, Value (..))
+import Data.Aeson.Types (Pair, Value (..))
 import qualified Data.Functor.Classes as Functor.Classes
 import Data.HashMap.Strict (HashMap)
 import qualified Data.List as List
@@ -38,9 +38,10 @@
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Text (Text)
 import qualified Data.Text as Text
-import Data.Text.Prettyprint.Doc (Doc, Pretty (..))
-import qualified Data.Text.Prettyprint.Doc as PP
-import qualified Data.Text.Prettyprint.Doc.Render.Terminal as PP
+import Prettyprinter (Doc, Pretty (..))
+import qualified Prettyprinter as PP
+import qualified Prettyprinter.Render.Terminal as PP
+import Text.EDE.Internal.Compat
 import Text.Trifecta.Delta (Delta, HasDelta)
 import qualified Text.Trifecta.Delta as Trifecta.Delta
 
@@ -86,7 +87,7 @@
 $(Lens.makePrisms ''Result)
 
 instance Monad Result where
-  return = Success
+  return = pure
   {-# INLINE return #-}
 
   Success x >>= k = k x
@@ -94,7 +95,7 @@
   {-# INLINE (>>=) #-}
 
 instance Applicative Result where
-  pure = return
+  pure = Success
   {-# INLINE pure #-}
 
   Success f <*> Success x = Success (f x)
@@ -165,7 +166,9 @@
 data Template = Template
   { _tmplName :: !Text,
     _tmplExp :: !(Exp Delta),
-    _tmplIncl :: HashMap Id (Exp Delta)
+    _tmplIncl :: HashMap Id (Exp Delta),
+    _tmplExtends :: HashMap Id (Exp Delta),
+    _tmplBlocks :: HashMap Id (Exp Delta)
   }
   deriving (Eq)
 
@@ -202,9 +205,12 @@
   | EFun !Id
   | EApp !a !a
   | ELet !Id !a !a
+  | EBlock !Id !a
+  | EOverrideBlock !Id !a !a
   | ECase !a [Alt a]
   | ELoop !Id !a !a
   | EIncl !Text
+  | EExt !Text
   deriving (Eq, Show, Functor)
 
 instance Functor.Classes.Eq1 ExpF where
@@ -213,11 +219,14 @@
   liftEq _ (EFun a) (EFun b) = a == b
   liftEq c (EApp a1 a2) (EApp b1 b2) = a1 `c` b1 && a2 `c` b2
   liftEq c (ELet a0 a1 a2) (ELet b0 b1 b2) = a0 == b0 && a1 `c` b1 && a2 `c` b2
+  liftEq c (EBlock a0 a1) (EBlock b0 b1) = a0 == b0 && a1 `c` b1
+  liftEq c (EOverrideBlock a0 a1 a2) (EOverrideBlock b0 b1 b2) = a0 == b0 && a1 `c` b1 && a2 `c` b2
   liftEq c (ECase a as) (ECase b bs) = a `c` b && (List.all (uncurry altEq) $ zip as bs)
     where
       altEq (pA, a') (pB, b') = pA == pB && a' `c` b'
   liftEq c (ELoop a0 a1 a2) (ELoop b0 b1 b2) = a0 == b0 && a1 `c` b1 && a2 `c` b2
   liftEq _ (EIncl a) (EIncl b) = a == b
+  liftEq _ (EExt a) (EExt b) = a == b
   liftEq _ _ _ = False
 
 type Exp = Cofree ExpF
@@ -228,15 +237,15 @@
 -- | Unwrap a 'Value' to an 'Object' safely.
 --
 -- See Aeson\'s documentation for more details.
-fromValue :: Value -> Maybe Object
-fromValue (Object o) = Just o
+fromValue :: Value -> Maybe (HashMap Text Value)
+fromValue (Object o) = Just (toHashMapText o)
 fromValue _ = Nothing
 
 -- | Create an 'Object' from a list of name/value 'Pair's.
 --
 -- See Aeson\'s documentation for more details.
-fromPairs :: [Pair] -> Object
+fromPairs :: [Pair] -> HashMap Text Value
 fromPairs xs =
   case Aeson.object xs of
-    Object o -> o
+    Object o -> toHashMapText o
     _other -> mempty
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 -- Module      : Main
--- Copyright   : (c) 2013-2020 Brendan Hay <brendan.g.hay@gmail.com>
+-- Copyright   : (c) 2013-2022 Brendan Hay <brendan.g.hay@gmail.com>
 -- License     : This Source Code Form is subject to the terms of
 --               the Mozilla Public License, v. 2.0.
 --               A copy of the MPL can be found in the LICENSE file or
diff --git a/test/resources/ext-base.ede b/test/resources/ext-base.ede
new file mode 100644
--- /dev/null
+++ b/test/resources/ext-base.ede
@@ -0,0 +1,7 @@
+{}
+---
+<!doctype html>
+{% block head %}
+<title>{% block title %}{% endblock %}</title>
+{% endblock %}
+{% block body %}{% endblock %}
diff --git a/test/resources/ext-base.golden b/test/resources/ext-base.golden
new file mode 100644
--- /dev/null
+++ b/test/resources/ext-base.golden
@@ -0,0 +1,2 @@
+<!doctype html>
+<title></title>
diff --git a/test/resources/ext-child.ede b/test/resources/ext-child.ede
new file mode 100644
--- /dev/null
+++ b/test/resources/ext-child.ede
@@ -0,0 +1,16 @@
+{}
+---
+{% extends "ext-base.ede" %}
+{% block title %}Index{% endblock %}
+{% block head %}
+  {{ super }}
+  <style type="text/css">
+    .important { color: #336699; }
+  </style>
+{% endblock %}
+{% block body %}
+  <h1>Index</h1>
+  <p class="important">
+    Welcome to my awesome homepage.
+  </p>
+{% endblock %}
diff --git a/test/resources/ext-child.golden b/test/resources/ext-child.golden
new file mode 100644
--- /dev/null
+++ b/test/resources/ext-child.golden
@@ -0,0 +1,13 @@
+{}
+---
+<!doctype html>
+  <title>Index</title>
+
+  <style type="text/css">
+    .important { color: #336699; }
+  </style>
+  <h1>Index</h1>
+  <p class="important">
+    Welcome to my awesome homepage.
+  </p>
+
diff --git a/test/resources/polymorphic-show.golden b/test/resources/polymorphic-show.golden
--- a/test/resources/polymorphic-show.golden
+++ b/test/resources/polymorphic-show.golden
@@ -4,4 +4,4 @@
 1.0
 ["a","b","c","d","e"]
 [1,2,3,4,5]
-{"foo":"x","bar":"y"}
+{"bar":"y","foo":"x"}
diff --git a/test/resources/set.ede b/test/resources/set.ede
new file mode 100644
--- /dev/null
+++ b/test/resources/set.ede
@@ -0,0 +1,17 @@
+{
+  "var": true
+}
+---
+{% set x %}
+
+  {% set y %}
+    This is a nested block {{var}}
+  {% endset %}
+
+  This is a big expression! {{var}}
+
+  {{ y }}
+
+{% endset %}
+
+{{x}}
diff --git a/test/resources/set.golden b/test/resources/set.golden
new file mode 100644
--- /dev/null
+++ b/test/resources/set.golden
@@ -0,0 +1,8 @@
+
+
+
+  This is a big expression! true
+
+      This is a nested block true
+
+
diff --git a/test/resources/whitespace.golden b/test/resources/whitespace.golden
--- a/test/resources/whitespace.golden
+++ b/test/resources/whitespace.golden
@@ -6,6 +6,8 @@
 
   
 
+       
        test  
+    
   
 
