Web server with recipe index page

- Add Roux.Server: WAI application using wai+warp directly
  - Scans .cook files from recipe directory on startup
  - GET / → HTML index page listing all recipes
  - Other routes → 404
- Add Roux.RecipeIndex: directory scanning and recipe parsing
  - scanRecipes: lists .cook files, parses each, extracts titles
  - Falls back to filename when metadata title is absent
- Add Roux.Html: blaze-html rendering with Pico CSS
  - indexPage: renders recipe list with links
  - urlEncode: percent-encodes filenames for URLs
- Update Roux.hs: re-exports Server.app and Types
- Add filepath to dependencies
This commit is contained in:
2026-05-19 07:04:22 -04:00
parent ffb1931c7d
commit 4943d4d42e
6 changed files with 181 additions and 13 deletions
+63
View File
@@ -0,0 +1,63 @@
{- | WAI application for the Roux recipe server.
Handles HTTP requests directly via wai — no framework. Currently serves
a single index page listing all discovered recipes.
-}
module Roux.Server (
app,
) where
import Control.Monad (when)
import Data.ByteString.Lazy (ByteString)
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai as Wai
import qualified Roux.Html as Html
import qualified Roux.RecipeIndex as Idx
{- | Build the WAI 'Application' given a path to the recipe directory.
Recipes are scanned once at startup. A future improvement could watch
the directory for changes and rebuild the index.
-}
app :: FilePath -> IO Wai.Application
app recipeDir = do
putStrLn $ "[roux] scanning recipes from " <> recipeDir
recipes <- Idx.scanRecipes recipeDir
let count = length recipes
ok = length [() | r <- recipes, isRight (Idx.riRecipe r)]
errs = count - ok
putStrLn $ "[roux] found " <> show count <> " recipe(s)"
when (errs > 0) $
putStrLn $
"[roux] " <> show errs <> " had parse errors"
-- Return the application; recipes are captured in the closure.
pure (handleRequest recipes)
-- | Route an incoming request to the appropriate handler.
handleRequest :: [Idx.RecipeInfo] -> Wai.Application
handleRequest recipes request respond =
case Wai.pathInfo request of
[] -> respond (htmlResponse (Html.indexPage recipes))
_ -> respond notFound
-- | Build a 200 HTML response.
htmlResponse :: ByteString -> Wai.Response
htmlResponse body =
Wai.responseLBS
HTTP.status200
[("Content-Type", "text/html; charset=utf-8")]
body
-- | 404 response.
notFound :: Wai.Response
notFound =
Wai.responseLBS
HTTP.status404
[("Content-Type", "text/plain")]
"Not found"
-- | Check if an 'Either' is 'Right'.
isRight :: Either a b -> Bool
isRight (Right _) = True
isRight (Left _) = False