diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+## 2.1.0.0 — 2026-07-07
+
+- Added `System.Elm.Compile` with `compileElm` for embedding compiled
+  Elm JavaScript or HTML in your Haskell program at build time.
+- Removed the deprecated `elmSite` function; use `elmSiteDev`
+  instead.
diff --git a/om-elm.cabal b/om-elm.cabal
--- a/om-elm.cabal
+++ b/om-elm.cabal
@@ -1,6 +1,6 @@
 cabal-version:       3.0
 name:                om-elm
-version:             2.0.1.2
+version:             2.1.0.0
 synopsis:            Haskell utilities for building embedded Elm programs.
 description:         This package provides utilities for serving Elm programs
                      directly from your Haskell binary. It uses TemplateHaskell
@@ -18,7 +18,8 @@
 copyright:           2025 Owens Murray, LLC.
 category:            Web
 build-type:          Simple
-extra-source-files:  README.md
+extra-source-files:  CHANGELOG.md
+                     README.md
 
 
 common warnings
@@ -32,7 +33,8 @@
 
 library
   import: warnings
-  exposed-modules:     
+  exposed-modules:
+    System.Elm.Compile
     System.Elm.Middleware
   -- other-modules:       
   -- other-extensions:    
diff --git a/src/System/Elm/Compile.hs b/src/System/Elm/Compile.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Elm/Compile.hs
@@ -0,0 +1,265 @@
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+{- |
+  Core compile-time utilities for embedding Elm programs in Haskell.
+
+  This module runs the @elm@ compiler during Template Haskell
+  splices and returns the compiled output as a 'String'. It is the
+  low-level building block used by higher-level modules such as
+  "System.Elm.Middleware".
+
+  == Quick start
+
+  1) Ensure @elm@ is available when GHC runs Template Haskell. If
+  you use Cabal, add 'System.Elm.Middleware.requireElm' to your
+  @Setup.hs@ (see "System.Elm.Middleware" for details).
+
+  2) Call 'compileElm' from a Template Haskell splice:
+
+  > import Language.Haskell.TH
+  > import System.Elm.Compile
+  >
+  > embeddedJs :: Q String
+  > embeddedJs = compileElm Dev JavaScript "frontend/Main.elm"
+
+  3) Splice the result into your program:
+
+  > myJs :: String
+  > myJs = $(embeddedJs)
+
+  == Choosing a 'Mode'
+
+  'Mode' controls which flags are passed to @elm make@:
+
+  * 'Dev' — no extra flags (the default for local development)
+  * 'Debug' — passes @--debug@ (debug runtime, better errors)
+  * 'Optimize' — passes @--optimize@ (smaller, faster output)
+
+  Examples:
+
+  > compileElm Dev JavaScript "frontend/Main.elm"
+  >
+  > compileElm Debug JavaScript "frontend/Main.elm"
+  >
+  > compileElm Optimize JavaScript "frontend/Main.elm"
+
+  == Choosing an 'ElmOutputFormat'
+
+  'ElmOutputFormat' selects whether @elm make@ produces a standalone
+  JavaScript bundle or a complete HTML page:
+
+  * 'JavaScript' — compile to @.js@ (typical for SPAs loaded by your
+    own HTML template)
+  * 'Html' — compile to a self-contained @.html@ page
+
+  Examples:
+
+  > -- JavaScript bundle for a custom HTML shell
+  > compileElm Dev JavaScript "frontend/Main.elm"
+  >
+  > -- Full HTML page served as-is
+  > compileElm Dev Html "frontend/Main.elm"
+
+  == Embedding in other types
+
+  'compileElm' returns a plain 'String'. Convert it however your
+  application needs:
+
+  > import qualified Data.Text as T
+  >
+  > embeddedText :: Q T.Text
+  > embeddedText = T.pack <$> compileElm Dev JavaScript "frontend/Main.elm"
+
+  > import qualified Data.ByteString.Char8 as BS8
+  >
+  > embeddedBytes :: Q BS8.ByteString
+  > embeddedBytes = BS8.pack <$> compileElm Dev JavaScript "frontend/Main.elm"
+
+  == Using with typed splices
+
+  For a typed expression splice, bind the compiled content in @Q@ and
+  lift it:
+
+  > import Language.Haskell.TH
+  >
+  > myJs :: String
+  > myJs = $(do
+  >   js <- compileElm Optimize JavaScript "frontend/Main.elm"
+  >   [| js |]
+  >   )
+
+  == Recompilation
+
+  'compileElm' registers the @.elm@ source file as a Template Haskell
+  dependency via 'addDependentFile'. GHC can re-run the splice when
+  that file changes, but that only effectively works if the @.elm@
+  file is also listed under @extra-source-files@ in your @.cabal@ file
+  — otherwise Cabal may not rebuild the Haskell module when the Elm
+  source changes.
+
+  Example:
+
+  > extra-source-files:
+  >   frontend/Main.elm
+
+  == Relationship to "System.Elm.Middleware"
+
+  If you want to serve compiled Elm as WAI 'Middleware', use
+  "System.Elm.Middleware" instead — it wraps 'compileElm' for that
+  purpose. Use this module when you want the compiled output
+  directly (for example, embedding JavaScript in a HTML template you
+  write yourself, or serving it through your own HTTP layer).
+
+-}
+module System.Elm.Compile (
+  Mode (..),
+  ElmOutputFormat (..),
+  compileElm,
+)
+where
+
+import Control.Exception.Safe (tryAny)
+import Control.Monad (Monad((>>=)), void)
+import Data.String (IsString)
+import Language.Haskell.TH (Q, runIO)
+import Language.Haskell.TH.Syntax (addDependentFile)
+import Prelude
+  ( Bool(True), Functor(fmap), Maybe(Just, Nothing), MonadFail(fail)
+  , Semigroup((<>)), Show(show), ($), (++), (.), Eq, FilePath, String, putStrLn
+  )
+import System.Directory (createDirectory, removeDirectoryRecursive)
+import System.Exit (ExitCode(ExitSuccess))
+import System.Posix
+  ( ProcessStatus(Exited), executeFile, forkProcess, getProcessStatus
+  )
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Char8 as BS8
+
+data Mode
+  = Optimize
+  | Dev
+  | Debug
+  deriving stock (Eq, Show)
+
+
+data ElmOutputFormat
+  = JavaScript
+  | Html
+  deriving stock (Eq, Show)
+
+
+buildDir :: (IsString a) => a
+buildDir = ".om-elm-build-dir"
+
+
+{- |
+  Compile an Elm program at Template Haskell time.
+
+  This function invokes @elm make@, reads the compiler output from a
+  temporary build directory, and returns the contents as a 'String'.
+  The caller is responsible for interpreting the result — for example,
+  choosing an HTTP content type or converting to 'Text'.
+
+  == Parameters
+
+  * __mode__ — compiler flags ('Dev', 'Debug', or 'Optimize')
+  * __format__ — output shape ('JavaScript' or 'Html')
+  * __elmFile__ — path to the @.elm@ entrypoint, relative to the
+    project root (same path you would pass to @elm make@ on the command
+    line)
+
+  == Basic examples
+
+  Compile a JavaScript bundle with default (dev) settings:
+
+  > compileElm Dev JavaScript "frontend/Main.elm"
+
+  Compile an optimized JavaScript bundle:
+
+  > compileElm Optimize JavaScript "frontend/Main.elm"
+
+  Compile a self-contained HTML page:
+
+  > compileElm Dev Html "frontend/Main.elm"
+
+  == Splicing into top-level definitions
+
+  > myJs :: String
+  > myJs = $(compileElm Dev JavaScript "frontend/Main.elm")
+
+  > myHtml :: String
+  > myHtml = $(compileElm Dev Html "frontend/Main.elm")
+
+  == Splicing into a larger Template Haskell computation
+
+  > import Language.Haskell.TH
+  >
+  > myJs :: String
+  > myJs = $(do
+  >   js <- compileElm Optimize JavaScript "frontend/Main.elm"
+  >   [| js |]
+  >   )
+
+  == Combining mode and format
+
+  > -- Debug runtime, JavaScript output
+  > compileElm Debug JavaScript "frontend/Main.elm"
+  >
+  > -- Optimized production build, HTML output
+  > compileElm Optimize Html "frontend/Main.elm"
+  >
+  > -- Dev build, JavaScript output (most common for SPAs)
+  > compileElm Dev JavaScript "frontend/Main.elm"
+
+  == Requirements
+
+  * The @elm@ executable must be on @PATH@ when GHC runs this splice.
+  * The @elmFile@ path must exist at compile time.
+  * GHC can re-run the splice when @elmFile@ changes (via
+    'addDependentFile'), but that only effectively works if @elmFile@
+    is also listed under @extra-source-files@ in your @.cabal@ file
+    (see the module documentation for an example).
+
+  See the module documentation for setup instructions and more
+  embedding examples.
+
+-}
+compileElm
+  :: Mode
+  -> ElmOutputFormat
+  -> FilePath
+  -> Q String
+compileElm mode format elmFile = do
+  addDependentFile elmFile
+  runIO $ do
+    void . tryAny $ removeDirectoryRecursive buildDir
+    createDirectory buildDir
+    putStrLn $ "Compiling elm file: " ++ elmFile
+    let
+      flags :: [String]
+      flags =
+        case mode of
+          Debug -> ["--debug"]
+          Dev -> []
+          Optimize -> ["--optimize"]
+    forkProcess
+      (
+        executeFile "elm" True ([
+            "make",
+            elmFile,
+            "--output=" <> buildFile format
+          ] ++ flags) Nothing
+      ) >>= getProcessStatus True True >>= \case
+          Nothing -> fail "elm should have ended."
+          Just (Exited ExitSuccess) ->
+            fmap BS8.unpack (BS.readFile (buildFile format))
+          e -> fail $ "elm failed with: " ++ show e
+
+
+buildFile :: ElmOutputFormat -> FilePath
+buildFile = \case
+  JavaScript -> buildDir <> "/elm.js"
+  Html -> buildDir <> "/elm.html"
diff --git a/src/System/Elm/Middleware.hs b/src/System/Elm/Middleware.hs
--- a/src/System/Elm/Middleware.hs
+++ b/src/System/Elm/Middleware.hs
@@ -27,7 +27,7 @@
 
   2) Modify your @Setup.hs@ file, using 'requireElm'.
 
-  3) Include a 'Middleware' template-haskell splice, using 'elmSite',
+  3) Include a 'Middleware' template-haskell splice, using 'elmSiteDev',
   in the appropriate place in your code.
 
   See the function documnetation for more details.
@@ -35,7 +35,6 @@
 -}
 module System.Elm.Middleware
   ( requireElm
-  , elmSite
   , elmSiteDebug
   , elmSiteDev
   , elmSiteOptimize
@@ -43,9 +42,6 @@
   )
 where
 
-
-import Control.Exception.Safe (tryAny)
-import Control.Monad (void)
 import Data.Map (Map)
 import Data.String (IsString(fromString))
 import Data.Text (Text)
@@ -58,30 +54,23 @@
   ( ConfigFlags(configVerbosity), fromFlagOrDefault
   )
 import Distribution.Verbosity (normal)
-import Language.Haskell.TH (Code(examineCode), Q, TExp, runIO)
-import Language.Haskell.TH.Syntax (addDependentFile)
+import Language.Haskell.TH (Code(examineCode), Q, TExp)
 import Network.HTTP.Types (methodNotAllowed405, ok200)
 import Network.Wai
   ( Request(pathInfo, requestMethod), Application, Middleware, responseLBS
   )
 import Prelude
-  ( Bool(True), Eq((==)), Foldable(length), Functor(fmap), Maybe(Just, Nothing)
-  , Monad((>>=)), MonadFail(fail), Semigroup((<>)), Show(show)
-  , Traversable(mapM), ($), (++), (.), (<$>), (=<<), FilePath, String, putStrLn
+  ( Applicative(pure), Bool(True), Eq((==)), Foldable(length), Functor(fmap)
+  , Maybe(Just, Nothing), Traversable(mapM), ($), (++), (=<<), FilePath, String
   , reverse, take
   )
 import Safe (lastMay)
-import System.Directory (createDirectory, removeDirectoryRecursive)
-import System.Exit (ExitCode(ExitSuccess))
-import System.Posix
-  ( ProcessStatus(Exited), executeFile, forkProcess, getProcessStatus
+import System.Elm.Compile
+  ( ElmOutputFormat(Html, JavaScript), Mode(Debug, Dev, Optimize), compileElm
   )
-import qualified Data.ByteString as BS
-import qualified Data.ByteString.Char8 as BS8
 import qualified Data.Map as Map
 import qualified Data.Text as T
 
-
 {- |
   Add the elm program requirements to a set of build hooks. This is
   expected to be used in your Setup.hs file thusly:
@@ -121,7 +110,7 @@
 
   The typed template-haskell splice:
 
-  > $$(elmSite $ Map.fromList [
+  > $$(elmSiteDev $ Map.fromList [
   >     (["app.js"], "elm/Your/Elm/Module/App.elm")
   >   ])
 
@@ -136,12 +125,6 @@
 elmSiteDev = elmSite2 Dev
 
 
-{-| Deprecated. Synonym for 'elmSiteDev'.  -}
-{-# DEPRECATED elmSite "Use elmSiteDev instead. This function will be removed in a future release." #-}
-elmSite :: Map PathInfo FilePath -> Q (TExp Middleware)
-elmSite = elmSite2 Dev
-
-
 {- | Like 'elmSiteDev', but serve the debug elm runtime. -}
 elmSiteDebug :: Map PathInfo FilePath -> Q (TExp Middleware)
 elmSiteDebug = elmSite2 Debug
@@ -154,11 +137,7 @@
 
 elmSite2 :: Mode -> Map PathInfo FilePath -> Q (TExp Middleware)
 elmSite2 mode spec =
-    buildMiddleware =<<
-      mapM (\(u, c) -> (u,) <$> c) [
-        (uriPath, compileElm uriPath elmFile)
-        | (fmap T.unpack -> uriPath, elmFile) <- Map.toList spec
-      ]
+    buildMiddleware =<< mapM compileResource (Map.toList spec)
   where
     {- | Construct the middleware from a set of compiled elm resources. -}
     buildMiddleware :: [([String], (String, String))] -> Q (TExp Middleware)
@@ -167,17 +146,17 @@
         [||
           let
             apps = Map.fromList[
-                (uriPath, buildApp contentType content)
-                | (fmap T.pack -> uriPath, (contentType, content)) <- resources
+                (uriPath, buildApp contentTypeHeader content)
+                | (fmap T.pack -> uriPath, (contentTypeHeader, content)) <- resources
               ]
             {- | Build the application that serves a single elm resource. -}
             buildApp :: String -> String -> Application
-            buildApp contentType content req respond = respond $
+            buildApp contentTypeHeader content req respond = respond $
               case requestMethod req of
                 "GET" ->
                   responseLBS
                     ok200
-                    [("Content-Type", fromString contentType)]
+                    [("Content-Type", fromString contentTypeHeader)]
                     (fromString content)
                 _ -> responseLBS methodNotAllowed405 [("Allow", "GET")] ""
           in
@@ -187,66 +166,35 @@
                 Just app -> app req respond
         ||]
 
-    compileElm :: [String] -> FilePath -> Q (String, String)
-    compileElm uriPath elmFile = do
-        addDependentFile elmFile
-        runIO $ do
-          void . tryAny $ removeDirectoryRecursive buildDir
-          createDirectory buildDir
-          putStrLn $ "Compiling elm file: " ++ elmFile
-          let
-            flags :: [String]
-            flags =
-              case mode of
-                Debug -> ["--debug"]
-                Dev -> []
-                Optimize -> ["--optimize"]
-          forkProcess
-            (
-              executeFile "elm" True ([
-                  "make",
-                  elmFile,
-                  "--output=" <> buildFile
-                ] ++ flags) Nothing
-            ) >>= getProcessStatus True True >>= \case
-                Nothing -> fail "elm should have ended."
-                Just (Exited ExitSuccess) ->
-                  (contentType,)
-                  . BS8.unpack
-                  <$> BS.readFile buildFile
-                e -> fail $ "elm failed with: " ++ show e
-      where
-        {- |
-          The name of the build directory. We have to have a build
-          directory because elm won't output compile results to
-          stdout. It will only output them to files.
-        -}
-        buildDir :: (IsString a) => a
-        buildDir = ".om-elm-build-dir"
+    compileResource
+      :: (PathInfo, FilePath)
+      -> Q ([String], (String, String))
+    compileResource (uriPathText, elmFile) = do
+      let
+        uriPath :: [String]
+        uriPath = fmap T.unpack uriPathText
 
-        {- | Figure out if we are compiling to javascript or html. -}
-        contentType :: String
-        contentType = case lastMay uriPath of
-          Just (endsWith ".js" -> True) -> "text/javascript"
-          _ -> "text/html"
+        format :: ElmOutputFormat
+        format = outputFormat uriPath
+      content <- compileElm mode format elmFile
+      pure (uriPath, (contentType format, content))
 
-        buildFile :: FilePath
-        buildFile = buildDir <> case lastMay uriPath of
-          Just (endsWith ".js" -> True) -> "/elm.js"
-          _ -> "/elm.html"
+    outputFormat :: [String] -> ElmOutputFormat
+    outputFormat uriPath =
+      case lastMay uriPath of
+        Just (endsWith ".js" -> True) -> JavaScript
+        _ -> Html
 
-        endsWith :: String -> String -> Bool
-        endsWith ending str =
-          take (length ending) (reverse str) == reverse ending
+    contentType :: ElmOutputFormat -> String
+    contentType = \case
+      JavaScript -> "text/javascript"
+      Html -> "text/html"
 
+    endsWith :: String -> String -> Bool
+    endsWith ending str =
+      take (length ending) (reverse str) == reverse ending
 
+
 {- | A WAI uri path, as per the meaning of 'pathInfo'. -}
 type PathInfo = [Text]
-
-
-data Mode
-  = Optimize
-  | Dev
-  | Debug
-
 
