diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,6 +1,11 @@
-
 # `gemmula-altera` Change Log
 
+## 2.1.1 (July 23, 2024)
+* Add discontinuation notice.
+* Strip the final newline in the `body` variable of `gemalter web`.
+* Implement the test for `Text.Gemini.Web.getTitle`.
+* Clean up some documentations.
+
 ## 2.1.0 (January 19, 2024)
 * Reimplement the stdin functionality of `gemalter`.
 * Default HTML title for `gemalter` inputs don't include the file extension now.
@@ -12,4 +17,3 @@
 
 ## 1.0.0 (January 14, 2024)
 * Initial release!
-
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,15 +1,11 @@
-
-# [gemmula-altera](https://hackage.haskell.org/package/gemmula-altera)
+# [gemmula-altera](https://hackage.haskell.org/package/gemmula-altera) ***(Discontinued)***
 
-A tiny Gemtext (`text/gemini`) converter, built on [gemmula](https://hackage.haskell.org/package/gemmula) library.
+A tiny Gemtext (`text/gemini`) converter, built on the [gemmula](https://hackage.haskell.org/package/gemmula) library.
 
 Supports encodings for:
 * [Markdown](https://hackage.haskell.org/package/gemmula-altera/docs/Text-Gemini-Markdown.html) ([CommonMark](https://spec.commonmark.org/current))
 * [HTML](https://hackage.haskell.org/package/gemmula-altera/docs/Text-Gemini-Web.html)
 
-See the repository [page](https://codeberg.org/sena/gemmula) for (also) tiny command line app usage.
+See the repository [page](https://codeberg.org/sena/gemmula/src/commit/2fbca325abf875db71739a4242fe181de8a84cf6/gemmula-altera#readme) for the (also) tiny command line app usage.
 
 The documentation is available at [Hackage](https://hackage.haskell.org/package/gemmula-altera).
-
-You can find more information and related modules at the [Codeberg repository](https://codeberg.org/sena/gemmula).
-
diff --git a/Setup.lhs b/Setup.lhs
--- a/Setup.lhs
+++ b/Setup.lhs
@@ -5,4 +5,3 @@
 
 > main :: IO ()
 > main = defaultMain
-
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,4 +1,6 @@
-{-# LANGUAGE OverloadedStrings, LambdaCase, NamedFieldPuns, TupleSections #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TupleSections #-}
 
 -- |
 -- Module      :  Main
@@ -11,54 +13,57 @@
 --
 -- A tiny command line helper for converting Gemini capsules.
 --
--- Converts Gemtext to Markdown and HTML.
+-- Converts gemtext to Markdown and HTML.
 
 module Main (main) where
 
-import Options.Applicative
-import Control.Monad (when)
 import Control.Arrow (first, second)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Maybe (fromMaybe)
-import Data.List (isPrefixOf)
+import Control.Monad (when)
 import Data.Bool (bool)
 import Data.Function ((&))
+import Data.List (isPrefixOf)
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Options.Applicative
 
-import System.IO (hReady, stdin)
 import System.Directory
 import System.FilePath
+import System.IO (hReady, stdin)
 
-import Paths_gemmula_altera (version)
+import Data.Text.IO (putStr, readFile, writeFile)
 import Data.Version (showVersion)
-import Data.Text.IO (readFile, writeFile, putStr)
-import Prelude hiding (readFile, writeFile, putStr)
+import Paths_gemmula_altera (version)
+import Prelude hiding (putStr, readFile, writeFile)
 
 import Text.Gemini (GemDocument)
 import qualified Text.Gemini as G
 import qualified Text.Gemini.Markdown as M
 import qualified Text.Gemini.Web as W
 
-
 -- | Process the input files (or piped input) according to the command and write them.
 run :: Opts -> IO ()
-run opt@Opts { mode }
-    | null $ inputs mode = hReady stdin >>= bool (errorWithoutStackTrace "No inputs found.")
-                                   (putStr =<< processStdin opt convert)
+run opt@Opts{mode}
+    | null $ inputs mode =
+        hReady stdin
+            >>= bool
+                (errorWithoutStackTrace "No inputs found.")
+                (putStr =<< processStdin opt convert)
     | otherwise = writeFiles =<< processFiles opt convert ext
-    where (convert, ext) = case mode of
-                               Markdown {} -> (markdown opt, "md")
-                               Web {} -> (web opt, "html")
+  where
+    (convert, ext) = case mode of
+        Markdown{} -> (markdown opt, "md")
+        Web{} -> (web opt, "html")
 
 
--- | Convert Gemtext to Markdown.
+-- | Convert gemtext to Markdown.
 --
 -- Rewrites all local @.gmi@ links as @.md@ and encodes the file using 'M.encode'.
 -- Non-local links are webified using 'W.webifyLink' if the @webify@ flag is used.
 markdown :: Opts -> FilePath -> GemDocument -> IO Text
-markdown (Opts { webify }) _ = fmap (M.encode . map M.rewriteLink) . bool return (mapM W.webifyLink) webify
+markdown (Opts{webify}) _ = fmap (M.encode . map M.rewriteLink) . bool return (mapM W.webifyLink) webify
 
--- | Convert Gemtext to HTML.
+-- | Convert gemtext to HTML.
 --
 -- Rewrites all local @.gmi@ links as @.html@ and encodes the file using 'W.encode'.
 -- Non-local links are webified using 'W.webifyLink' if the @webify@ flag is used.
@@ -70,36 +75,50 @@
 --
 -- The file path variables are inserted while processing the inputs.
 web :: Opts -> FilePath -> GemDocument -> IO Text
-web (Opts { mode, webify }) path doc
+web (Opts{mode, webify}) path doc
     | body mode = fmap (W.encode . map W.rewriteLink) . bool return (mapM W.webifyLink) webify $ doc
-    | otherwise = let title = fromMaybe (T.pack $ takeBaseName path) $ W.getTitle doc
-                      defaultTemplate = T.unlines
-                          [ "<!DOCTYPE html>"
-                          , "<html lang=\"en\">"
-                          , "  <head>"
-                          , "    <meta charset=\"utf-8\" />"
-                          , "    <meta name=\"viewport\" content=\"width=device-width, \
-                              \ initial-scale=1.0, maximum-scale=1.0\" />"
-                          , "    <link href=\"<!--#VAR style#-->\" rel=\"stylesheet\" />"
-                          , "    <title><!--#VAR title#--></title>"
-                          , "  </head>"
-                          , "  <body>"
-                          , ""
-                          , "<!--#VAR body#-->"
-                          , "  </body>"
-                          , "</html>"
-                          ]
-          in do templ <- maybe (return defaultTemplate) readFile $ template mode
-                body <- fmap (W.encode . map W.rewriteLink) . bool return (mapM W.webifyLink) webify $ doc
+    | otherwise =
+        let title = fromMaybe (T.pack $ takeBaseName path) $ W.getTitle doc
+            defaultTemplate =
+                T.unlines
+                    [ "<!DOCTYPE html>"
+                    , "<html lang=\"en\">"
+                    , "  <head>"
+                    , "    <meta charset=\"utf-8\" />"
+                    , "    <meta name=\"viewport\" content=\"width=device-width, \
+                      \ initial-scale=1.0, maximum-scale=1.0\" />"
+                    , "    <link href=\"<!--#VAR style#-->\" rel=\"stylesheet\" />"
+                    , "    <title><!--#VAR title#--></title>"
+                    , "  </head>"
+                    , "  <body>"
+                    , ""
+                    , "<div id=\"gemalter-body\">"
+                    , "<!--#VAR body#-->"
+                    , "</div>"
+                    , ""
+                    , "  </body>"
+                    , "</html>"
+                    ]
+            stripLastNewline t = bool t (T.dropEnd 1 t) ("\n" `T.isSuffixOf` t)
+         in do
+                templ <- maybe (return defaultTemplate) readFile $ template mode
+                body <-
+                    fmap (stripLastNewline . W.encode . map W.rewriteLink)
+                        . bool return (mapM W.webifyLink) webify
+                        $ doc
                 return $ insertVars templ [("body", body), ("title", title)]
 
 
 -- | Write the processed files to their output paths.
 writeFiles :: [(FilePath, Either FilePath Text)] -> IO ()
-writeFiles files = do when (null files) $ errorWithoutStackTrace "No files to copy or write found."
-                      mapM_ (\(out, content) -> case content of
-                                                    Left src -> copyFile src out
-                                                    Right text -> writeFile out text) files
+writeFiles files = do
+    when (null files) $ errorWithoutStackTrace "No files to copy or write found."
+    mapM_
+        ( \(out, content) -> case content of
+            Left src -> copyFile src out
+            Right text -> writeFile out text
+        )
+        files
 
 -- | Process the input files with the given converter function and extension.
 -- The output is a tuple with the output path of the file; and the content of the
@@ -111,61 +130,77 @@
 -- Otherwise, they are put in a subdirectory in the output dir.
 --
 -- The file path variables are inserted here, after the subdir is stripped if needed.
-processFiles :: Opts -> (FilePath -> GemDocument -> IO Text) -> String -> IO [(FilePath, Either FilePath Text)]
-processFiles (Opts { mode, output, copy }) convert ext = let
-    stripSubdir fs = if all (isPrefixOf (head $ splitDirectories (fst $ head fs)) . fst) fs
-                     then return $ map (first ((\p -> joinPath $ bool p (tail p) (length p > 1)) . splitPath)) fs
-                     else return fs
-
-    insertPathVars = case mode of
-        Web {} -> mapM (\(path, content) -> case content of
-                      Left _ -> return (path, content)
-                      Right t -> (path,) . Right . insertVars t <$> mapM (pathVar path) (filevars mode))
-        _ -> return
-
-    outputPath fs
-        | length fs <= 1 = maybe (return fs)
-              (\o -> (fs &) . map . bool (first (const o)) (first (o </>)) <$> doesDirectoryExist o) output
-        | otherwise = let out = fromMaybe "result" output
-                       in do createDirectoryIfMissing True out
-                             mapM_ (\f -> createDirectoryIfMissing True (out </> takeDirectory (fst f))) fs
-                             return $ map (first (out </>)) fs
+processFiles
+    :: Opts -> (FilePath -> GemDocument -> IO Text) -> String -> IO [(FilePath, Either FilePath Text)]
+processFiles (Opts{mode, output, copy}) convert ext =
+    let stripSubdir fs =
+            if all (isPrefixOf (head $ splitDirectories (fst $ head fs)) . fst) fs
+                then return $ map (first ((\p -> joinPath $ bool p (tail p) (length p > 1)) . splitPath)) fs
+                else return fs
 
-    in outputPath =<< insertPathVars =<< stripSubdir . concat =<< mapM (process "") (inputs mode)
+        insertPathVars = case mode of
+            Web{} ->
+                mapM
+                    ( \(path, content) -> case content of
+                        Left _ -> return (path, content)
+                        Right t -> (path,) . Right . insertVars t <$> mapM (pathVar path) (filevars mode)
+                    )
+            _ -> return
 
-    where process :: FilePath -> FilePath -> IO [(FilePath, Either FilePath Text)]
-          process root path = ((path &) . bool (single root) (recurse root)) =<< doesDirectoryExist path
+        outputPath fs
+            | length fs <= 1 =
+                maybe
+                    (return fs)
+                    (\o -> (fs &) . map . bool (first (const o)) (first (o </>)) <$> doesDirectoryExist o)
+                    output
+            | otherwise =
+                let out = fromMaybe "result" output
+                 in do
+                        createDirectoryIfMissing True out
+                        mapM_ (\f -> createDirectoryIfMissing True (out </> takeDirectory (fst f))) fs
+                        return $ map (first (out </>)) fs
+     in outputPath =<< insertPathVars =<< stripSubdir . concat =<< mapM (process "") (inputs mode)
+  where
+    process :: FilePath -> FilePath -> IO [(FilePath, Either FilePath Text)]
+    process root path = ((path &) . bool (single root) (recurse root)) =<< doesDirectoryExist path
 
-          single :: FilePath -> FilePath -> IO [(FilePath, Either FilePath Text)]
-          single root path
-              | takeExtension path /= ".gmi" = return $ bool [] [(root </> takeFileName path, Left path)] copy
-              | otherwise = let out = root </> replaceExtension (takeFileName path) ext
-                             in (:[]) . (out,) . Right <$> (convert out . G.decode =<< readFile path)
+    single :: FilePath -> FilePath -> IO [(FilePath, Either FilePath Text)]
+    single root path
+        | takeExtension path /= ".gmi" = return $ bool [] [(root </> takeFileName path, Left path)] copy
+        | otherwise =
+            let out = root </> replaceExtension (takeFileName path) ext
+             in (: []) . (out,) . Right <$> (convert out . G.decode =<< readFile path)
 
-          recurse :: FilePath -> FilePath -> IO [(FilePath, Either FilePath Text)]
-          recurse root path = let proc p = process (root </> last (splitDirectories path)) (path </> p)
-                               in fmap concat . mapM proc =<< listDirectory path
+    recurse :: FilePath -> FilePath -> IO [(FilePath, Either FilePath Text)]
+    recurse root path =
+        let proc p = process (root </> last (splitDirectories path)) (path </> p)
+         in fmap concat . mapM proc =<< listDirectory path
 
 processStdin :: Opts -> (FilePath -> GemDocument -> IO Text) -> IO Text
-processStdin (Opts { mode }) convert = let
-    insertPathVars t = case mode of
-        Web {} -> insertVars t <$> mapM getVar (filevars mode)
-        _ -> return t
-    in insertPathVars =<< convert "gemalter" . G.decode . T.pack =<< getContents
+processStdin (Opts{mode}) convert =
+    let insertPathVars t = case mode of
+            Web{} -> insertVars t <$> mapM getVar (filevars mode)
+            _ -> return t
+     in insertPathVars =<< convert "gemalter" . G.decode . T.pack =<< getContents
 
 
 -- | Parse the variable option and make it relative to the 'FilePath'.
 pathVar :: FilePath -> Text -> IO (Text, Text)
-pathVar path text = (\(name, val) ->
-    let relative = joinPath (replicate (length . init . splitDirectories $ path) "..") </> val
-     in return (name, T.pack relative)) . second T.unpack =<< getVar text
+pathVar path text =
+    ( \(name, val) ->
+        let relative = joinPath (replicate (length . init . splitDirectories $ path) "..") </> val
+         in return (name, T.pack relative)
+    )
+        . second T.unpack
+        =<< getVar text
 
 -- | Parse a variable option of @"key=val"@ as a pair of key and value.
 getVar :: Text -> IO (Text, Text)
 getVar var
     | T.null val = errorWithoutStackTrace "Incorrect variable option. See \"web --help\"."
     | otherwise = return (name, val)
-    where (name, val) = second (T.drop 1) . T.break (== '=') $ var
+  where
+    (name, val) = second (T.drop 1) . T.break (== '=') $ var
 
 -- | Replace the variable keywords in the text with the parsed variables in the list.
 insertVars :: Text -> [(Text, Text)] -> Text
@@ -173,63 +208,105 @@
 
 
 -- | Options to use normally.
-data Opts = Opts { output :: !(Maybe FilePath)
-                 , copy :: !Bool
-                 , webify :: !Bool
-                 , mode :: !Command
-                 } deriving (Show, Eq)
+data Opts = Opts
+    { output :: !(Maybe FilePath)
+    , copy :: !Bool
+    , webify :: !Bool
+    , mode :: !Command
+    }
+    deriving (Show, Eq)
 
-data Command = Markdown { inputs :: ![FilePath] }
-             | Web { body :: !Bool
-                   , template :: !(Maybe FilePath)
-                   , filevars :: ![Text]
-                   , inputs :: ![FilePath]
-                   } deriving (Show, Eq)
+data Command
+    = Markdown {inputs :: ![FilePath]}
+    | Web
+        { body :: !Bool
+        , template :: !(Maybe FilePath)
+        , filevars :: ![Text]
+        , inputs :: ![FilePath]
+        }
+    deriving (Show, Eq)
 
 opts :: Parser Opts
-opts = Opts
-    <$> optional (option str
-        ( long "output"
-       <> short 'o'
-       <> help "The output path"
-       <> metavar "PATH" ))
-    <*> flag True False
-        ( long "no-copy"
-       <> short 'n'
-       <> help "Do not copy non-convertable files to the output dir" )
-    <*> switch
-        ( long "webify"
-       <> short 'w'
-       <> help "Rewrite gemini links as http if they can be reached" )
-    <*> hsubparser
-        ( command "md" (info (Markdown
-                  <$> many (argument str (metavar "FILES"))
-              ) (progDesc "Convert Gemtext files to Markdown."))
-       <> command "web" (info (Web
-                  <$> switch
-                      ( long "body"
-                     <> short 'b'
-                     <> help "Output the HTML body only" )
-                  <*> optional (option str
-                      ( long "template"
-                     <> short 't'
-                     <> help "Template file to use while producing HTML outputs, \
-                            \ where <!--#VAR name#--> are replaced with the \
-                            \ variable values. Special variables are: body, title"
-                     <> metavar "PATH" ))
-                  <*> many (option str
-                      ( long "file"
-                     <> short 'f'
-                     <> help "Set a dynamic file path variable always pointing to \
-                            \ the given path, in the form of \"name=value\", where \
-                            \ value must be a path relative to the output root"
-                     <> metavar "VAR" ))
-                  <*> many (argument str (metavar "FILES"))
-              ) (progDesc "Convert Gemtext files to HTML.")) )
+opts =
+    Opts
+        <$> optional
+            ( option
+                str
+                ( long "output"
+                    <> short 'o'
+                    <> help "The output path"
+                    <> metavar "PATH"
+                )
+            )
+        <*> flag
+            True
+            False
+            ( long "no-copy"
+                <> short 'n'
+                <> help "Do not copy non-convertable files to the output dir"
+            )
+        <*> switch
+            ( long "webify"
+                <> short 'w'
+                <> help "Rewrite gemini links as http if they can be reached"
+            )
+        <*> hsubparser
+            ( command
+                "md"
+                ( info
+                    ( Markdown
+                        <$> many (argument str (metavar "FILES"))
+                    )
+                    (progDesc "Convert gemtext files to Markdown.")
+                )
+                <> command
+                    "web"
+                    ( info
+                        ( Web
+                            <$> switch
+                                ( long "body"
+                                    <> short 'b'
+                                    <> help "Output the HTML body only"
+                                )
+                            <*> optional
+                                ( option
+                                    str
+                                    ( long "template"
+                                        <> short 't'
+                                        <> help
+                                            "Template file to use while producing HTML outputs, \
+                                            \ where <!--#VAR name#--> are replaced with the \
+                                            \ variable values. Special variables are: body, title"
+                                        <> metavar "PATH"
+                                    )
+                                )
+                            <*> many
+                                ( option
+                                    str
+                                    ( long "file"
+                                        <> short 'f'
+                                        <> help
+                                            "Set a dynamic file path variable always pointing to \
+                                            \ the given path, in the form of \"name=value\", where \
+                                            \ value must be a path relative to the output root"
+                                        <> metavar "VAR"
+                                    )
+                                )
+                            <*> many (argument str (metavar "FILES"))
+                        )
+                        (progDesc "Convert gemtext files to HTML.")
+                    )
+            )
 
-main :: IO ()
-main = run =<< execParser (info (opts <**> helper <**> simpleVersioner ("v" <> showVersion version))
-                              ( fullDesc
-                             <> progDesc "Convert Gemtext to Markdown and HTML using gemmula library."
-                             <> header "gemalter - a tiny command line helper for converting Gemini capsules"))
 
+main :: IO ()
+main =
+    run
+        =<< execParser
+            ( info
+                (opts <**> helper <**> simpleVersioner ("v" <> showVersion version))
+                ( fullDesc
+                    <> progDesc "Convert gemtext to Markdown and HTML using the gemmula library."
+                    <> header "gemalter - a tiny command line helper for converting Gemini capsules"
+                )
+            )
diff --git a/gemmula-altera.cabal b/gemmula-altera.cabal
--- a/gemmula-altera.cabal
+++ b/gemmula-altera.cabal
@@ -1,17 +1,16 @@
 name:               gemmula-altera
-version:            2.1.0
-synopsis:           A tiny Gemtext converter for gemmula
-description:        gemmula-altera is a tiny Gemtext converter, built on gemmula library.
+version:            2.1.1
+synopsis:           A tiny gemtext converter for gemmula
+description:        gemmula-altera is a tiny gemtext converter, built on the gemmula library.
                     It provides simple encodings for Markdown and HTML, plus a tiny
                     command line helper.
 license:            AGPL-3
 license-file:       LICENSE
 author:             Sena <jn-sena@proton.me>
 maintainer:         Sena <jn-sena@proton.me>
-tested-with:        GHC == 9.4.8 || == 9.6.3
+tested-with:        GHC == 9.6.5 || == 9.8.2
 category:           Text, Gemini
-homepage:           https://codeberg.org/sena/gemmula
-bug-reports:        https://codeberg.org/sena/gemmula/issues
+homepage:           https://codeberg.org/sena/gemmula/src/commit/2fbca325abf875db71739a4242fe181de8a84cf6
 build-type:         Simple
 extra-source-files: README.md, ChangeLog.md
 cabal-version:      1.18
diff --git a/src/Text/Gemini/Markdown.hs b/src/Text/Gemini/Markdown.hs
--- a/src/Text/Gemini/Markdown.hs
+++ b/src/Text/Gemini/Markdown.hs
@@ -9,80 +9,84 @@
 -- Stability   :  stable
 -- Portability :  portable
 --
--- A tiny Gemtext to Markdown converter for gemmula.
+-- A tiny gemtext to Markdown converter for gemmula.
 --
--- Encodes parsed Gemtext documents and lines as Markdown 'Text'.
+-- Encodes parsed gemtext documents and lines into Markdown 'Text'.
 -- Follows the [CommonMark specification](https://spec.commonmark.org/current).
 
 module Text.Gemini.Markdown
-  ( -- * Encoding documents
-    encode
-    -- * Encoding single items
-  , prettyItem
-  , encodeItem
-    -- * Rewriting links
-  , rewriteLink
-  ) where
+    ( -- * Encoding documents
+      encode
 
+      -- * Encoding single items
+    , prettyItem
+    , encodeItem
+
+      -- * Rewriting links
+    , rewriteLink
+    ) where
+
+import Control.Arrow (second, (***))
 import Control.Monad (join)
-import Control.Arrow ((***), second)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Maybe (fromMaybe, isNothing)
+import Data.Bool (bool)
+import Data.Char (isDigit)
 import Data.Either (isRight)
 import Data.List (groupBy, intercalate)
-import Data.Char (isDigit)
-import Data.Bool (bool)
+import Data.Maybe (fromMaybe, isNothing)
+import Data.Text (Text)
+import qualified Data.Text as T
 import qualified Text.URI as URI
 
 import Text.Gemini (GemDocument, GemItem (..))
 
-
--- | Encode parsed 'GemDocument' as a Markdown file.
--- The output 'Text' uses LF-endings. Uses the 'prettyItem' function below.
+-- | Encode parsed 'GemDocument' into a Markdown file.
+-- The output 'Text' uses LF line breaks.
 --
--- Valid Markdown characters are escaped before encoding.
+-- Valid Markdown characters are escaped before encoding. Uses the 'prettyItem' function below.
 --
 -- The adjacent links are grouped together in a paragraph to make them look pretty.
 --
 -- Empty 'GemText's and 'GemList's are ignored.
 encode :: GemDocument -> Text
 encode = T.unlines . map prettyItem . intercalate [GemText ""] . groupBy links . filter (not . empty)
-    where empty :: GemItem -> Bool
-          empty (GemText line) = T.null . T.strip $ line
-          empty (GemList list) = null list
-          empty _ = False
-
-          links :: GemItem -> GemItem -> Bool
-          links (GemLink _ _) (GemLink _ _) = True
-          links _ _ = False
+  where
+    empty :: GemItem -> Bool
+    empty (GemText line) = T.null . T.strip $ line
+    empty (GemList list) = null list
+    empty _ = False
 
+    links :: GemItem -> GemItem -> Bool
+    links (GemLink _ _) (GemLink _ _) = True
+    links _ _ = False
 
--- | Encode a /single/ parsed 'GemItem' as Markdown text.
--- The output 'Text' uses LF-endings and might be multiple lines.
+-- | Encode a /single/ parsed 'GemItem' into Markdown text.
 --
+-- The output 'Text' might be multiple lines, in which case it uses LF line breaks.
+--
 -- Valid Markdown characters are escaped before encoding.
 --
 -- Unlike 'encodeItem', long lines (> 80) will be split to multiple lines to
 -- make it look prettier. The link items are also put in a seperate line to make them
 -- look nice.
 --
--- /Beware/ that the output text doesn't end with a newline.
+-- /Beware/ that the output text does /not/ end with a newline.
 prettyItem :: GemItem -> Text
 prettyItem (GemText line) = multiline Nothing $ escapeContent line
-prettyItem (GemLink link desc) = let desc' = maybe (escapeContent link) (multiline Nothing . escapeContent) desc
-                                  in " => [" <> desc' <> "](" <> link <> ")  "
+prettyItem (GemLink link desc) =
+    let desc' = maybe (escapeContent link) (multiline Nothing . escapeContent) desc
+     in " => [" <> desc' <> "](" <> link <> ")  "
 prettyItem (GemHeading level text) = "\n" <> T.replicate (min level 6) "#" <> " " <> escapeContent text
 prettyItem (GemList list) = T.intercalate "\n" $ map ((" * " <>) . multiline Nothing . escapeContent) list
 prettyItem (GemQuote text) = multiline (Just " > ") $ escapeContent text
 prettyItem (GemPre text alt) = T.intercalate "\n" $ ["```" <> fromMaybe "" alt] <> map escapePre text <> ["```"]
 
--- | Encode a /single/ parsed 'GemItem' as Markdown text.
--- The output 'Text' uses LF-endings and might be multiple lines.
+-- | Encode a /single/ parsed 'GemItem' into Markdown text.
 --
+-- The output 'Text' might be multiple lines, in which case it uses LF line breaks.
+--
 -- Valid Markdown characters are escaped before encoding.
 --
--- /Beware/ that the output text doesn't end with a newline.
+-- /Beware/ that the output text does /not/ end with a newline.
 encodeItem :: GemItem -> Text
 encodeItem (GemText line) = escapePrefixes $ escapeContent line
 encodeItem (GemLink link desc) = "[" <> escapeContent (fromMaybe link desc) <> "](" <> link <> ")"
@@ -91,7 +95,6 @@
 encodeItem (GemQuote text) = " > " <> escapeContent text
 encodeItem (GemPre text alt) = T.intercalate "\n" $ ["```" <> fromMaybe "" alt] <> map escapePre text <> ["```"]
 
-
 -- | Rewrite @.gmi@ links as @.md@ links.
 --
 -- /Beware/ that this only applies to local 'GemLink's.
@@ -100,53 +103,59 @@
 rewriteLink (GemLink link desc)
     | isNothing (URI.uriPath uri) || isRight (URI.uriAuthority uri) = GemLink link desc
     | otherwise = GemLink (maybe link (<> ".md") $ T.stripSuffix ".gmi" link) desc
-    where uri = fromMaybe URI.emptyURI $ URI.mkURI link
+  where
+    uri = fromMaybe URI.emptyURI $ URI.mkURI link
 rewriteLink item = item
 
-
 -- Split the text to multiple lines if the text is longer than 80 characters.
 -- If given, adds the prefix to the beginning of every line.
 -- Escapes the valid prefixes of every line if the text has any.
 multiline :: Maybe Text -> Text -> Text
 multiline pre text = T.intercalate "\n" $ map (maybe id (<>) pre . escapePrefixes) $ split [] [] $ T.words text
-    where split :: [Text] -> [Text] -> [Text] -> [Text]
-          split line ls (w:ws)
-              | T.length (T.unwords line) < 80 = split (line <> [w]) ls ws
-              | otherwise = split [w] (ls <> [T.unwords line]) ws
-          split line ls [] = ls <> [T.unwords line]
-
+  where
+    split :: [Text] -> [Text] -> [Text] -> [Text]
+    split line ls (w : ws)
+        | T.length (T.unwords line) < 80 = split (line <> [w]) ls ws
+        | otherwise = split [w] (ls <> [T.unwords line]) ws
+    split line ls [] = ls <> [T.unwords line]
 
 -- Escapes the line prefixes such as list items and quotes.
 escapePrefixes :: Text -> Text
 escapePrefixes text = foldr escapePrefix text chars
-    where escapePrefix :: Char -> Text -> Text
-          escapePrefix c t
-              | T.null end = t
-              -- Ordered lists
-              | c == '.' = bool t (pre <> "\\" <> end) (all isDigit before && (not . null $ before))
-              | otherwise = bool t (pre <> "\\" <> end) (null before)
-              where (pre, end) = T.break (== c) t
-                    before = T.unpack . T.stripStart $ pre
+  where
+    escapePrefix :: Char -> Text -> Text
+    escapePrefix c t
+        | T.null end = t
+        -- Ordered lists
+        | c == '.' = bool t (pre <> "\\" <> end) (all isDigit before && (not . null $ before))
+        | otherwise = bool t (pre <> "\\" <> end) (null before)
+      where
+        (pre, end) = T.break (== c) t
+        before = T.unpack . T.stripStart $ pre
 
-          chars = ['.', '-', '+', '#', '>', '*']
+    chars = ['.', '-', '+', '#', '>', '*']
 
 -- Escapes the content of the text, such as the backslashes; as well as the
 -- surround characters, such as emphasis, links and codeblocks.
 escapeContent :: Text -> Text
 escapeContent text = foldr escapeSurround (T.replace "\\" "\\\\" text) chars
-    where escapeSurround :: (Char, Char) -> Text -> Text
-          escapeSurround del@(op, cl) t
-              | T.null t = t
-              | otherwise =
-                    let (pre, (ins, post)) = second (T.break (== cl) . T.drop 1 . (<> " ")) $ T.break (== op) t
-                        (op', cl') = join (***) (("\\" <>) . T.singleton) del
-                     in pre <> T.dropEnd 1 (if T.null post
-                                            then bool (T.singleton op <> ins) ins (T.null ins)
-                                            else op' <> ins <> cl' <> escapeSurround del (T.drop 1 post))
+  where
+    escapeSurround :: (Char, Char) -> Text -> Text
+    escapeSurround del@(op, cl) t
+        | T.null t = t
+        | otherwise =
+            let (pre, (ins, post)) = second (T.break (== cl) . T.drop 1 . (<> " ")) $ T.break (== op) t
+                (op', cl') = join (***) (("\\" <>) . T.singleton) del
+             in pre
+                    <> T.dropEnd
+                        1
+                        ( if T.null post
+                            then bool (T.singleton op <> ins) ins (T.null ins)
+                            else op' <> ins <> cl' <> escapeSurround del (T.drop 1 post)
+                        )
 
-          chars = [('~', '~'), ('`', '`'), ('(', ')'), ('<', '>'), ('[', ']'), ('{', '}'), ('_', '_'), ('*', '*')]
+    chars = [('~', '~'), ('`', '`'), ('(', ')'), ('<', '>'), ('[', ']'), ('{', '}'), ('_', '_'), ('*', '*')]
 
 -- Escapes the preformatted delimiter inside a preformatted text.
 escapePre :: Text -> Text
 escapePre text = bool (T.replace "```" "    ```" text) text (T.null text)
-
diff --git a/src/Text/Gemini/Web.hs b/src/Text/Gemini/Web.hs
--- a/src/Text/Gemini/Web.hs
+++ b/src/Text/Gemini/Web.hs
@@ -1,4 +1,6 @@
-{-# LANGUAGE OverloadedStrings, LambdaCase, TupleSections #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TupleSections #-}
 
 -- |
 -- Module      :  Text.Gemini.Web
@@ -9,52 +11,56 @@
 -- Stability   :  stable
 -- Portability :  portable
 --
--- A tiny Gemtext to HTML converter for gemmula.
+-- A tiny gemtext to HTML converter for gemmula.
 --
--- Encodes parsed Gemtext documents and lines as HTML 'Text'.
+-- Encodes parsed gemtext documents and lines into HTML 'Text'.
 
 module Text.Gemini.Web
     ( -- * Encoding documents
       encode
+
       -- * Encoding single items
     , prettyItem
     , encodeItem
+
       -- * Rewriting links
     , rewriteLink
     , webifyLink
+
       -- * Other
     , getTitle
     ) where
 
-import Control.Exception (catch, SomeException)
-import Data.Text (Text)
-import qualified Data.Text as T
-import Data.Maybe (fromMaybe, fromJust, isNothing, maybeToList)
+import Control.Exception (SomeException, catch)
+import Data.Bool (bool)
 import Data.Either (isRight)
 import Data.List (find)
-import Data.Bool (bool)
+import Data.Maybe (fromJust, fromMaybe, isNothing, maybeToList)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Network.HTTP (getRequest, getResponseCode, simpleHTTP)
 import qualified Text.URI as URI
-import Network.HTTP (simpleHTTP, getRequest, getResponseCode)
 
 import Text.Gemini (GemDocument, GemItem (..))
 
-
--- | Encode parsed 'GemDocument' as a HTML file.
--- The output 'Text' uses LF-endings. Uses the 'prettyItem' function below.
+-- | Encode parsed 'GemDocument' into a HTML file.
 --
--- Valid HTML characters are escaped before encoding.
+-- The output 'Text' uses LF line breaks.
 --
+-- Valid HTML characters are escaped before encoding. Uses the 'prettyItem' function below.
+--
 -- Empty 'GemList's are ignored and empty 'GemText's are replaced with @<br />@.
 encode :: GemDocument -> Text
 encode = T.unlines . map prettyItem . filter (not . empty)
-    where empty :: GemItem -> Bool
-          empty (GemList list) = null list
-          empty _ = False
-
+  where
+    empty :: GemItem -> Bool
+    empty (GemList list) = null list
+    empty _ = False
 
--- | Encode a /single/ parsed 'GemItem' as HTML text.
--- The output 'Text' uses LF-endings and might be multiple lines.
+-- | Encode a /single/ parsed 'GemItem' into HTML text.
 --
+-- The output 'Text' might be multiple lines, in which case it uses LF line breaks.
+--
 -- Valid HTML characters are escaped before encoding.
 --
 -- Unlike 'encodeItem', long lines (> 80) will be split to multiple lines to
@@ -63,23 +69,26 @@
 -- Links have a "scheme" attribute set to "gemini" if the scheme of the URI
 -- is @gemini://@, to make them stylable with CSS.
 --
--- /Beware/ that the output text doesn't end with a newline.
+-- /Beware/ that the output text does /not/ end with a newline.
 prettyItem :: GemItem -> Text
 prettyItem (GemText line) = bool (tag "p" [] $ multiline line) "<br />" (T.null . T.strip $ line)
 prettyItem (GemLink link desc) =
-     let s = maybe "http" URI.unRText $ URI.uriScheme $ fromMaybe URI.emptyURI $ URI.mkURI link
-      in tag "a" ([("href", link)] <> bool [] [("scheme", s)] (s == "gemini")) $ multiline $ fromMaybe link desc
+    let s = maybe "http" URI.unRText $ URI.uriScheme $ fromMaybe URI.emptyURI $ URI.mkURI link
+     in tag "a" ([("href", link)] <> bool [] [("scheme", s)] (s == "gemini")) $
+            multiline $
+                fromMaybe link desc
 prettyItem (GemHeading level text) = "\n" <> tag ("h" <> (T.pack . show $ min level 6)) [] (multiline text)
 prettyItem (GemList list) = "<ul>\n" <> T.unlines (map (("  " <>) . tag "li" []) list) <> "</ul>"
 prettyItem (GemQuote text) = tag "blockquote" [] $ multiline text
 prettyItem (GemPre text alt) = tag "pre" (maybeToList $ ("title",) <$> alt) $ "\n" <> T.unlines text
 
--- | Encode a /single/ parsed 'GemItem' as HTML text.
--- The output 'Text' uses LF-endings and might be multiple lines.
+-- | Encode a /single/ parsed 'GemItem' into HTML text.
 --
+-- The output 'Text' might be multiple lines, in which case it uses LF line breaks.
+--
 -- Valid HTML characters are escaped before encoding.
 --
--- /Beware/ that the output text doesn't end with a newline.
+-- /Beware/ that the output text does /not/ end with a newline.
 encodeItem :: GemItem -> Text
 encodeItem (GemText line) = tag "p" [] line
 encodeItem (GemLink link desc) = tag "a" [("href", link)] $ fromMaybe link desc
@@ -88,7 +97,6 @@
 encodeItem (GemQuote text) = tag "blockquote" [] text
 encodeItem (GemPre text alt) = tag "pre" (maybeToList $ ("title",) <$> alt) $ "\n" <> T.unlines text
 
-
 -- | Rewrite @.gmi@ links as @.html@ links.
 --
 -- /Beware/ that this only applies to local 'GemLink's.
@@ -97,7 +105,8 @@
 rewriteLink (GemLink link desc)
     | isNothing (URI.uriPath uri) || isRight (URI.uriAuthority uri) = GemLink link desc
     | otherwise = GemLink (maybe link (<> ".html") $ T.stripSuffix ".gmi" link) desc
-    where uri = fromMaybe URI.emptyURI $ URI.mkURI link
+  where
+    uri = fromMaybe URI.emptyURI $ URI.mkURI link
 rewriteLink item = item
 
 -- | Rewrite @gemini://@ link as @http://@ if it can be reached over HTTP.
@@ -108,40 +117,44 @@
 webifyLink :: GemItem -> IO GemItem
 webifyLink (GemLink link desc)
     | isRight (URI.uriAuthority uri) && maybe "" URI.unRText (URI.uriScheme uri) == "gemini" =
-          (\(t, _, _) -> GemLink (bool (URI.render uri') link (t > 3)) desc) <$>
-              catch (simpleHTTP (getRequest $ URI.renderStr uri') >>= getResponseCode)
-                  (\e -> let _ = (e :: SomeException) in return (9, 9, 9))
+        (\(t, _, _) -> GemLink (bool (URI.render uri') link (t > 3)) desc)
+            <$> catch
+                (simpleHTTP (getRequest $ URI.renderStr uri') >>= getResponseCode)
+                (\e -> let _ = (e :: SomeException) in return (9, 9, 9))
     | otherwise = return $ GemLink link desc
-    where uri = fromMaybe URI.emptyURI $ URI.mkURI link
-          uri' = uri {URI.uriScheme = Just (fromJust $ URI.mkScheme "http")}
+  where
+    uri = fromMaybe URI.emptyURI $ URI.mkURI link
+    uri' = uri{URI.uriScheme = Just (fromJust $ URI.mkScheme "http")}
 webifyLink item = return item
 
-
 -- | Get the text of the first @<h1>@ in the document, if there's any.
 --
 -- Useful for using as @<title>@.
 getTitle :: GemDocument -> Maybe Text
-getTitle doc = (\case { GemHeading _ t -> Just t; _ -> Nothing })
-                   =<< find (\case { GemHeading l _ -> l == 1; _ -> False }) doc
+getTitle doc =
+    (\case GemHeading _ t -> Just t; _ -> Nothing)
+        =<< find (\case GemHeading l _ -> l == 1; _ -> False) doc
 
 -- Creates a HTML tag with the given name, attributes and body.
 -- The body and attributes are escaped using the functions below.
 tag :: Text -> [(Text, Text)] -> Text -> Text
 tag name attrs body = "<" <> name <> T.concat (map attr attrs) <> ">" <> escapeBody body <> "</" <> name <> ">"
-    where attr :: (Text, Text) -> Text
-          attr (n, v) = " " <> n <> "=\"" <> (bool escapeAttr escapeHref $ n == "href") v <> "\""
+  where
+    attr :: (Text, Text) -> Text
+    attr (n, v) = " " <> n <> "=\"" <> (bool escapeAttr escapeHref $ n == "href") v <> "\""
 
 -- Split the text to multiple lines if the text is longer than 80 characters.
 -- Indents the text by 2 spaces for every line then.
 multiline :: Text -> Text
-multiline text = let result = split [] [] $ T.words text
-                  in bool (T.concat result) ("\n  " <> T.intercalate "\n  " result <> "\n") (length result > 1)
-    where split :: [Text] -> [Text] -> [Text] -> [Text]
-          split line ls (w:ws)
-              | T.length (T.unwords line) < 80 = split (line <> [w]) ls ws
-              | otherwise = split [w] (ls <> [T.unwords line]) ws
-          split line ls [] = ls <> [T.unwords line]
-
+multiline text =
+    let result = split [] [] $ T.words text
+     in bool (T.concat result) ("\n  " <> T.intercalate "\n  " result <> "\n") (length result > 1)
+  where
+    split :: [Text] -> [Text] -> [Text] -> [Text]
+    split line ls (w : ws)
+        | T.length (T.unwords line) < 80 = split (line <> [w]) ls ws
+        | otherwise = split [w] (ls <> [T.unwords line]) ws
+    split line ls [] = ls <> [T.unwords line]
 
 -- Escape the relevant characters inside bodies such as ampersands and tag delimiters.
 escapeBody :: Text -> Text
@@ -153,5 +166,4 @@
 
 -- Escape the relevant characters inside links attributes such as quotes.
 escapeHref :: Text -> Text
-escapeHref = T.replace "'" "%27". T.replace "\"" "%22"
-
+escapeHref = T.replace "'" "%27" . T.replace "\"" "%22"
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,13 +1,13 @@
-{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
 
-import Data.Text (Text)
 import Control.Monad
+import Data.Text (Text)
 import System.Exit
 import Test.HUnit
 import Text.RawString.QQ
 
 import Text.Gemini (GemDocument, GemItem (..))
-
 import qualified Text.Gemini.Markdown as M
 import qualified Text.Gemini.Web as W
 
@@ -15,45 +15,81 @@
 -- | Query the test functions.
 main :: IO ()
 main = do
-    count <- runTestTT $ TestList
-        [ markdownEncode
-        , markdownRewrite
-        , htmlEncode
-        , htmlRewrite
-        , webify
-        ]
+    count <-
+        runTestTT $
+            TestList
+                [ -- Markdown functions
+                  markdownEncode
+                , markdownRewrite
+                , -- Web functions
+                  htmlEncode
+                , htmlRewrite
+                , -- Others
+                  getTitle
+                , webify
+                ]
     when (failures count > 0) exitFailure
 
 
 -- | Test the Markdown encoding.
 markdownEncode :: Test
-markdownEncode = TestCase $ assertEqual "Comparing the expected result of Markdown encoding,"
-                     markdown $ M.encode gemtext
+markdownEncode =
+    TestCase
+        $ assertEqual
+            "Comparing the expected result of Markdown encoding,"
+            markdown
+        $ M.encode gemtext
 
 -- | Test the @.md@ local link rewriting.
 markdownRewrite :: Test
-markdownRewrite = TestCase $ assertEqual "Comparing the expected result of Markdown local link rewrite,"
-                      (GemLink "test/file.md" Nothing) $ M.rewriteLink (GemLink "test/file.gmi" Nothing)
+markdownRewrite =
+    TestCase
+        $ assertEqual
+            "Comparing the expected result of Markdown local link rewrite,"
+            (GemLink "test/file.md" Nothing)
+        $ M.rewriteLink (GemLink "test/file.gmi" Nothing)
 
 -- | Test the HTML encoding.
 htmlEncode :: Test
-htmlEncode = TestCase $ assertEqual "Comparing the expected result of HTML encoding,"
-                 html $ W.encode gemtext
+htmlEncode =
+    TestCase
+        $ assertEqual
+            "Comparing the expected result of HTML encoding,"
+            html
+        $ W.encode gemtext
 
 -- | Test the @.html@ local link rewriting.
 htmlRewrite :: Test
-htmlRewrite = TestCase $ assertEqual "Comparing the expected result of HTML local link rewrite,"
-                  (GemLink "test/file.html" Nothing) $ W.rewriteLink (GemLink "test/file.gmi" Nothing)
+htmlRewrite =
+    TestCase
+        $ assertEqual
+            "Comparing the expected result of HTML local link rewrite,"
+            (GemLink "test/file.html" Nothing)
+        $ W.rewriteLink (GemLink "test/file.gmi" Nothing)
 
+-- | Test the title finding.
+getTitle :: Test
+getTitle =
+    TestCase
+        $ assertEqual
+            "Comparing the expected result of getting the title,"
+            (Just "title")
+        $ W.getTitle [GemHeading 3 "test", GemHeading 1 "title", GemHeading 1 "ignore"]
+
 -- | Test HTTP link rewriting.
 webify :: Test
-webify = TestCase $ assertEqual "Comparing the expected result of HTTP link rewriting,"
-             (GemLink "http://geminiprotocol.net/docs/faq.gmi" Nothing)
-                 =<< W.webifyLink (GemLink "gemini://geminiprotocol.net/docs/faq.gmi" Nothing)
+webify =
+    TestCase $
+        assertEqual
+            "Comparing the expected result of HTTP link rewriting,"
+            (GemLink "http://geminiprotocol.net/docs/faq.gmi" Nothing)
+            =<< W.webifyLink (GemLink "gemini://geminiprotocol.net/docs/faq.gmi" Nothing)
 
+
 -- | The supposedly resulting Markdown file.
 markdown :: Text
-markdown = [r|
+markdown =
+    [r|
 # Encoding Test
 
 \- This is the \*gemtext\* file for testing \`gemmula-altera\`.
@@ -84,7 +120,8 @@
 
 -- | The supposedly resulting HTML file.
 html :: Text
-html = [r|
+html =
+    [r|
 <h1>Encoding Test</h1>
 <br />
 <p>- This is the *gemtext* file for testing `gemmula-altera`.</p>
@@ -110,21 +147,21 @@
 <a href="link.gmi">link.gmi</a>
 |]
 
-
 -- | The Gemini document to test the encoding from.
 gemtext :: GemDocument
-gemtext = [ GemHeading 1 "Encoding Test"
-          , GemText ""
-          , GemText " - This is the *gemtext* file for testing `gemmula-altera`."
-          , GemLink "https://hackage.haskell.org/package/gemmula-altera" (Just "gemmula-altera")
-          , GemQuote "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."
-          , GemList ["Emerald Green", "Toxic Artificial Dye."]
-          , GemPre ["This is a preformatted text.", "``` beautiful, isn't it?"] (Just "md")
-          , GemText "```Exciting stuff next!!"
-          , GemText "## Shenanigans!!"
-          , GemText " "
-          , GemList []
-          , GemPre [] Nothing
-          , GemLink "link.gmi" Nothing
-          ]
-
+gemtext =
+    [ GemHeading 1 "Encoding Test"
+    , GemText ""
+    , GemText " - This is the *gemtext* file for testing `gemmula-altera`."
+    , GemLink "https://hackage.haskell.org/package/gemmula-altera" (Just "gemmula-altera")
+    , GemQuote
+        "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."
+    , GemList ["Emerald Green", "Toxic Artificial Dye."]
+    , GemPre ["This is a preformatted text.", "``` beautiful, isn't it?"] (Just "md")
+    , GemText "```Exciting stuff next!!"
+    , GemText "## Shenanigans!!"
+    , GemText " "
+    , GemList []
+    , GemPre [] Nothing
+    , GemLink "link.gmi" Nothing
+    ]
