diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@
 
 ## [Unreleased]
 
+### Changed
+- Modernize the build: target GHC 9.6 and build with `cabal` via a `cabal.project`.
+  This drops the Nix, Stack and Travis/CircleCI setup in favor of GitHub Actions.
+- Update dependency bounds for current releases, notably Pandoc 3, `text` 2 and
+  `aeson` 2. Removed the unused `semigroups` dependency.
+- Run the full doctests through Cabal (`cabal repl --with-compiler=doctest`,
+  exposed as `make doctest`).
+
 ## [1.0.1]
 
 Tests were failing from the sdist distribution due to missing files in
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![CircleCI](https://circleci.com/gh/elben/pencil/tree/master.svg?style=svg)](https://circleci.com/gh/elben/pencil/tree/master)
+[![CI](https://github.com/elben/pencil/actions/workflows/ci.yml/badge.svg)](https://github.com/elben/pencil/actions/workflows/ci.yml)
 
 # Pencil
 
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/examples/Docs/site/tutorials/01-getting-started.markdown b/examples/Docs/site/tutorials/01-getting-started.markdown
--- a/examples/Docs/site/tutorials/01-getting-started.markdown
+++ b/examples/Docs/site/tutorials/01-getting-started.markdown
@@ -12,7 +12,7 @@
 you have it installed. Let's create our project:
 
 ```sh
-stack new my-website simple --resolver=lts-13.21
+stack new my-website simple
 cd my-website
 ```
 
@@ -135,8 +135,7 @@
 Let's build our project and try it out.
 
 ```
-stack build
-stack exec my-website
+stack run
 ```
 
 This should create an `out/` directory with an `index.html` file, which contains
@@ -264,8 +263,7 @@
 To generate and serve our website, run the following commands:
 
 ```
-stack build
-stack exec my-website
+stack run
 cd out && python -m SimpleHTTPServer 8000
 ```
 
diff --git a/examples/Docs/site/tutorials/03-blogging.markdown b/examples/Docs/site/tutorials/03-blogging.markdown
--- a/examples/Docs/site/tutorials/03-blogging.markdown
+++ b/examples/Docs/site/tutorials/03-blogging.markdown
@@ -78,7 +78,7 @@
 With these first few lines of code, you should be able to see something working. As usual:
 
 ```
-stack exec my-website
+stack run
 cd out && python -m SimpleHTTPServer 8000
 ```
 
diff --git a/pencil.cabal b/pencil.cabal
--- a/pencil.cabal
+++ b/pencil.cabal
@@ -1,5 +1,6 @@
+cabal-version:       2.4
 name:                pencil
-version:             1.0.1
+version:             1.0.2
 synopsis: Static site generator
 description:
   Pencil is a static site generator. Use Pencil to build a personal website, a
@@ -7,19 +8,20 @@
   Sass/Scss support, templating, blogging, and tagging. Designed with the
   Haskell beginner in mind, but flexible enough to extend for your own needs.
 homepage:            https://github.com/elben/pencil
-license:             BSD3
+license:             BSD-3-Clause
 license-file:        LICENSE
 author:              Elben Shira
 maintainer:          elben@shira.im
 copyright:           2018 Elben Shira
 category:            Web
 build-type:          Simple
-extra-source-files:  README.md
+extra-doc-files:     README.md
                    , CHANGELOG.md
+extra-source-files:
 
                   -- List out test example files so that they are included in the `cabal new-sdist`
                   -- distribution, so that tests can run.
-                   , examples/Simple/site/*.html
+                    examples/Simple/site/*.html
                    , examples/Simple/site/*.scss
                    , examples/Simple/site/*.markdown
                    , examples/Blog/site/*.html
@@ -53,15 +55,11 @@
                    , examples/Docs/site/tutorials/*.markdown
                    , examples/Docs/site/tutorials/images/*.png
 
-                  --  Once cabal 2.4 is enforced, we could just list them out as:
-                  --  , examples/**/*.html
-                  --  , examples/**/*.markdown
-                  --  , examples/**/*.scss
-cabal-version:       >=1.10
-tested-with:         GHC == 8.0.2,
-                     GHC == 8.2.2
-                     GHC == 8.4
-                     GHC == 8.6
+                   --  Once cabal 2.4 is enforced, we could just list them out as:
+                   --  , examples/**/*.html
+                   --  , examples/**/*.markdown
+                   --  , examples/**/*.scss
+tested-with:         GHC == 9.6.7
 
 library
   hs-source-dirs:      src
@@ -76,21 +74,21 @@
                      , Pencil.Env
                      , Pencil.Env.Internal
                      , Pencil.Parser
-  build-depends:       base >= 4.8 && < 5
-                     , data-default >= 0.7 && < 1
-                     , directory >= 1.2.5.0 && < 1.4
+  build-depends:       aeson >= 2.0 && < 2.3
+                     , base >= 4.14 && < 5
+                     , data-default >= 0.7 && < 0.9
+                     , directory >= 1.3 && < 1.4
                      , edit-distance >= 0.2.2.1 && < 0.3
-                     , filepath >= 1.4 && < 1.5
-                     , hashable >= 1.2.6.0 && <= 1.3
+                     , filepath >= 1.4 && < 1.6
+                     , hashable >= 1.2.6.0 && < 1.6
                      , hsass >= 0.8 && < 1
                      , mtl >= 2.2 && < 3
-                     , pandoc >= 2.0 && < 3
+                     , pandoc >= 3.11 && < 4
                      , parsec >= 3.1 && < 3.2
-                     , semigroups >= 0.18.2 && < 0.20
-                     , text >= 1.2.2 && < 1.3
+                     , text >= 1.2.2 && < 2.2
                      , time >= 1.5.0.1 && < 2
                      , unordered-containers >= 0.2.7.2 && < 0.3
-                     , vector >= 0.12.0 && < 0.13
+                     , vector >= 0.12.0 && < 0.14
                      , xml >= 1.3.10 && < 1.4
                      , yaml >= 0.8.32 && < 0.12
   default-language:    Haskell2010
diff --git a/src/Pencil/App/Internal.hs b/src/Pencil/App/Internal.hs
--- a/src/Pencil/App/Internal.hs
+++ b/src/Pencil/App/Internal.hs
@@ -8,7 +8,7 @@
 import Control.Monad.Except
 import Control.Monad.Reader (ReaderT(..))
 import Data.Typeable (Typeable)
-import GHC.IO.Exception (IOException(ioe_description, ioe_filename, ioe_type), IOErrorType(NoSuchThing))
+import GHC.IO.Exception (IOException(ioe_filename, ioe_type), IOErrorType(InvalidArgument, NoSuchThing))
 
 import qualified Data.Text as T
 
@@ -59,7 +59,7 @@
 -- | Returns true if the IOError is an invalid byte sequence error. This
 -- suggests that the file is a binary file.
 isInvalidByteSequence :: IOError -> Bool
-isInvalidByteSequence e = ioe_description e == "invalid byte sequence"
+isInvalidByteSequence e = ioe_type e == InvalidArgument
 
 -- | Returns true if the IOError is due to missing file.
 isNoSuchFile :: IOError -> Bool
diff --git a/src/Pencil/Config.hs b/src/Pencil/Config.hs
--- a/src/Pencil/Config.hs
+++ b/src/Pencil/Config.hs
@@ -57,7 +57,7 @@
 --  , 'configPandocReaderOptions' = Text.Pandoc.def {
 --       Text.Pandoc.readerExtensions = 'Text.Pandoc.Extensions.disableExtension' 'Text.Pandoc.Extensions.Ext_tex_math_dollars' ('Text.Pandoc.Extensions.getDefaultExtensions' "markdown")
 --    }
---  , 'configPandocWriterOptions' = Text.Pandoc.def { Text.Pandoc.writerHighlightStyle = Just Text.Pandoc.Highlighting.monochrome }
+--  , 'configPandocWriterOptions' = Text.Pandoc.def { Text.Pandoc.writerHighlightMethod = Text.Pandoc.Skylighting Text.Pandoc.Highlighting.monochrome }
 --  , 'configDisplayValue = 'toText'
 --  }
 -- @
@@ -81,10 +81,10 @@
   -- directives. For example, this renders weird (as of Pandoc 2.7.2):
   -- > **${name}** and **${age}**
   , configPandocReaderOptions = P.def {
-      P.readerExtensions = disableExtension Ext_tex_math_dollars (P.getDefaultExtensions "markdown")
+      P.readerExtensions = disableExtension Ext_tex_math_dollars (P.getDefaultExtensions (T.pack "markdown"))
   }
   , configPandocWriterOptions = P.def {
-      P.writerHighlightStyle = Just Text.Pandoc.Highlighting.monochrome
+      P.writerHighlightMethod = P.Skylighting Text.Pandoc.Highlighting.monochrome
   }
   , configDisplayValue = toText
   }
diff --git a/src/Pencil/Content.hs b/src/Pencil/Content.hs
--- a/src/Pencil/Content.hs
+++ b/src/Pencil/Content.hs
@@ -69,7 +69,7 @@
 import Pencil.Env.Internal
 import Pencil.Parser
 
-import Control.Monad (forM_, foldM, filterM)
+import Control.Monad (forM_, foldM, filterM, when)
 import Control.Monad.Except
 import Control.Monad.Reader
 import Data.List.NonEmpty (NonEmpty(..)) -- Import the NonEmpty data constructor, (:|)
@@ -265,7 +265,7 @@
 
 -- | Escape XML tags in the given Text.
 escapeForXml :: T.Text -> T.Text
-escapeForXml text = T.pack (XML.escapeStringForXML (T.unpack text))
+escapeForXml = XML.escapeStringForXML
 
 -- | Sorts pages by an ordering function.
 sortByVar :: T.Text
diff --git a/src/Pencil/Content/Nodes.hs b/src/Pencil/Content/Nodes.hs
--- a/src/Pencil/Content/Nodes.hs
+++ b/src/Pencil/Content/Nodes.hs
@@ -9,6 +9,7 @@
 import Pencil.Content.Internal
 import Pencil.Config
 
+import Control.Monad (foldM)
 import Control.Monad.Reader
 import Control.Monad.Except
 import Control.Exception (tryJust)
diff --git a/src/Pencil/Env/Internal.hs b/src/Pencil/Env/Internal.hs
--- a/src/Pencil/Env/Internal.hs
+++ b/src/Pencil/Env/Internal.hs
@@ -9,6 +9,8 @@
 
 import Data.Text.Encoding (encodeUtf8)
 
+import qualified Data.Aeson.Key as K
+import qualified Data.Aeson.KeyMap as KM
 import qualified Data.HashMap.Strict as H
 import qualified Data.Maybe as M
 import qualified Data.Text as T
@@ -80,11 +82,11 @@
 -- blank Env.
 findEnv :: [P.PNode] -> Env
 findEnv nodes =
-  aesonToEnv $ M.fromMaybe H.empty (P.findPreambleText nodes >>= (A.decodeThrow . encodeUtf8 . T.strip))
+  aesonToEnv $ M.fromMaybe KM.empty (P.findPreambleText nodes >>= (A.decodeThrow . encodeUtf8 . T.strip))
 
 -- | Converts an Aeson Object to an Env.
 aesonToEnv :: A.Object -> Env
-aesonToEnv = H.foldlWithKey' maybeInsertIntoEnv H.empty
+aesonToEnv = KM.foldrWithKey (\k v env -> maybeInsertIntoEnv env (K.toText k) v) H.empty
 
 -- | Convert known Aeson 'Aeson.Value' into a Pencil
 -- 'Pencil.Env.Internal.Value', and insert into the env. If there is no
