Compare commits

..

No commits in common. "a11fcd37f7208ddba4b49d50ec7604f1466c4e3e" and "c6bfc90897d06c74563992820523b594e4ce143b" have entirely different histories.

17 changed files with 217 additions and 368 deletions

View File

@ -1,6 +1,6 @@
# the sampu Haskell blog engine
https://eversole.co
https://eversole.co (not live yet!)
a _work-in-progress_ blog engine using simple flat-file Markdown content storage
@ -14,46 +14,18 @@ Therefore, `la sampu cu sampu lo ka samtci`!
- [Haskell](https://www.haskell.org)
- [Twain](https://github.com/alexmingoia/twain)
- [Lucid](https://github.com/chrisdone/lucid)
- [Clay](https://github.com/sebastiaanvisser/clay)
- [Lucid2](https://chrisdone.com/posts/lucid2)
## Goal
Provide a simple blog engine that is easily customizable via HTML fragments.
## Build and Deployment
## Deployment
Only Nix build instructions are provided below.
We're not there yet! This project is built and packaged with Nix,
so I will provide directions on deploying with Nix as well as via OCI
containers once there's something viable to run.
### No Containers
1) Clone this repository
2) Build the application (with flakes enabled): `nix build '.#'`
3) Set the environment variables
- File: `cp data/.env.example ./.env; $EDITOR ./.env`
- If you want to set them in a different way, you already know how.
4) Run the application: `./result/bin/sampu`
### Containers
1) Clone this repository
2) Build the container image (with flakes enabled): `nix build .#sampu-container`
3) Load the container image: `podman load -i result`
4) Run the container using your favorite orchestrator or...
5) Use a NixOS configuration:
```
virtualisation.oci-containers.containers.sampu = {
image = "sampu";
ports = [ "${SAMPUR_EXTERNAL_PORT}:3000" ];
volumes = [
"/PATH/TO/SAMPU/data:/app/data"
];
environment = {
SAMPU_PORT = "3000";
SAMPU_TITLE = "Your Blog Title Here!";
SAMPU_BASEURL = "http://example.public.tld";
};
};
```
## Development and Support
Per the permissive ISC license, you are free to do what you wish with this

View File

@ -1,3 +1,2 @@
SAMPU_PORT="3000"
SAMPU_TITLE="Anon's Blog"
SAMPU_BASEURL="http://localhost:3000"
APPLICATIONPORT="3000"
BLOGTITLE="Anon's Blog"

1
data/assets/public/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
html{font-family:Monospace;background-color:#f1f6f0;color:#222323}
a{text-decoration:none}
h2{text-transform:uppercase}
h3{margin:0.25em 0 0.25em 0}
p{margin:0.4em 0 0.4em 0}
a{color:#6D92AD}
body {
margin: 1% 2%;
font-size: 1.25em;
font-weight: 300;
text-align: left
}
body li {
list-style-type: "~> ";
}
.main {
margin: 1em auto;
max-width: 60%;
}
.navContainer {
width: 100%;
text-align: center;
}
.mainNav {
margin: 0 auto;
padding: 0;
overflow: hidden;
box-shadow: 4px 4px 6px #ccc;
display: inline-flex;
}
.mainNav li {
list-style-type: none;
}
.mainNav li a {
display: block;
text-align: center;
padding: 0.25em 0.3em;
text-transform: lowercase;
}
.notFound {
margin: 0 auto;
text-align: center;
}
.notFound h1 {
font-size: 500%;
font-weight: 200;
color:#6D92AD
}
.postList {
font-size: 1.5em;
}

View File

@ -1 +0,0 @@
Copyright [Your Name](youremail@address.local)

30
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": {
"haskell-flake": {
"locked": {
"lastModified": 1728845985,
"narHash": "sha256-0KkAWCRBNpno3f+E1rvV9TOr0iuweqncWGn1KtbrGmo=",
"lastModified": 1707835791,
"narHash": "sha256-oQbDPHtver9DO8IJCBMq/TVbscCkxuw9tIfBBti71Yk=",
"owner": "srid",
"repo": "haskell-flake",
"rev": "2393b55948866f39afcfa7d8a53893a096bcd284",
"rev": "5113f700d6e92199fbe0574f7d12c775bb169702",
"type": "github"
},
"original": {
@ -33,14 +33,20 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1727825735,
"narHash": "sha256-0xHYkMkeLVQAMa7gvkddbPqpxph+hDzdu1XdGPJR+Os=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
"dir": "lib",
"lastModified": 1706550542,
"narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "97b17f32362e475016f942bbdfda4a4a72a8a652",
"type": "github"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz"
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"parts": {
@ -48,11 +54,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1727826117,
"narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=",
"lastModified": 1706830856,
"narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1",
"rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f",
"type": "github"
},
"original": {

View File

@ -1,6 +1,6 @@
cabal-version: 3.0
name: sampu
version: 0.10.0
version: 0.3.0
license: ISC
author: James Eversole
maintainer: james@eversole.co
@ -16,14 +16,12 @@ executable sampu
ghc-options: -threaded -rtsopts -with-rtsopts=-N -optl-pthread -fPIC
build-depends: base
, bytestring >= 0.11.5.0
, clay >= 0.14.0
, commonmark >= 0.2.4
, directory >= 1.3.7.0
, dotenv >= 0.11.0.0
, feed >= 1.3.2.0
, filemanip >= 0.3.6.1
, filepath >= 1.4.2.2
, http-types
, lucid >= 2.11.0
, text >= 2.0
, time >= 1.12.0
@ -39,5 +37,5 @@ executable sampu
Core.HTTP
Core.Rendering
Fragments.Base
Fragments.Styles
Fragments.NotFound
default-language: GHC2021

View File

@ -35,17 +35,11 @@ main = do
++ "All required environment variables:\n"
++ unlines required
-- The port to run the web server on
port :: IO String
port = getEnv "SAMPU_PORT"
appPort :: IO String
appPort = getEnv "APPLICATIONPORT"
-- The site's title; used for HTML title and XML feed title
title :: IO String
title = getEnv "SAMPU_TITLE"
-- The site's public-facing base url with no trailing slash
baseUrl :: IO String
baseUrl = getEnv "SAMPU_BASEURL"
appTitle :: IO String
appTitle = getEnv "BLOGTITLE"
requiredEnvVars :: [String]
requiredEnvVars = [ "SAMPU_PORT", "SAMPU_TITLE", "SAMPU_BASEURL" ]
requiredEnvVars = [ "APPLICATIONPORT", "BLOGTITLE" ]

View File

@ -11,7 +11,6 @@ import Text.XML (def, rsPretty)
data Post = Post { _date :: Text
, _url :: Text
, _title :: Text
, _content :: Text
}
@ -25,7 +24,4 @@ renderFeed = fromJust . Export.textFeedWith def{rsPretty = True} . AtomFeed
-- Convert a Post to an Atom Entry
toEntry :: Post -> Atom.Entry
toEntry (Post date url title content) = (Atom.nullEntry url (Atom.TextString title) date)
{ Atom.entryLinks = [Atom.nullLink url]
, Atom.entryContent = Just (Atom.HTMLContent content)
}
toEntry (Post date url content) = (Atom.nullEntry url (Atom.TextString content) date)

View File

@ -6,41 +6,48 @@ import qualified Core.Handlers as Handle
import Control.Monad ( mapM_ )
import Data.String ( fromString )
import Network.Wai.Handler.Warp ( Port, run )
import Network.Wai.Middleware.RequestLogger ( logStdout, logStdoutDev )
import Network.Wai.Middleware.Static ( staticPolicy, noDots, addBase, (>->) )
import Network.Wai.Middleware.RequestLogger ( logStdoutDev )
import Network.Wai.Middleware.Static
import System.FilePath ( takeFileName )
import Web.Twain
-- Get the port to listen on from the ENV and start the webserver.
main :: IO ()
main = do
port <- Conf.port
let app = preProcessors
++ routes
main :: [FilePath] -> IO ()
main postNames = do
port <- Conf.appPort
let app = preProcessors
++ (routes postNames)
++ postProcessors
run (read port) $
run (read port) $
foldr ($) (notFound Handle.missing) app
-- These Middlewares are executed before any routes are reached.
preProcessors :: [Middleware]
preProcessors = [ logStdoutDev
, staticPolicy (noDots >-> addBase "data/assets/public")
]
preProcessors = [ logStdoutDev
, staticPolicy $ noDots >-> addBase "data/assets/public"
]
-- These Middlewares are executed after all other routes are exhausted
postProcessors :: [Middleware]
postProcessors = []
postProcessors = []
-- Core routes expressed as a list of WAI Middlewares.
routes :: [Middleware]
routes =
[ get "/" Handle.index
, get "/style.css" Handle.theme
, get "/posts" Handle.postsIndex
, get "/posts/:name" Handle.posts
, get "/contact" Handle.contact
, get "/atom.xml" Handle.feed
, get "/feed" Handle.feed
{- The application's core routes expressed as a list of WAI Middlewares.
The list of post names is required so that the postsIndex handler can
automatically build an index of posts available to view. -}
routes :: [FilePath] -> [Middleware]
routes postNames =
[ get "/" Handle.index
, get "/posts" $ Handle.postsIndex postNames
] ++ (buildMdRoutes postNames) ++
[ get "/contact" Handle.contact
, get "/feed" $ Handle.feed postNames
]
-- Takes a post's name extracted from the filepath and returns a valid route
mdFileToRoute :: FilePath -> Middleware
mdFileToRoute postName = get (fromString $ "/posts/" ++ postName) (Handle.posts postName)
buildMdRoutes :: [FilePath] -> [Middleware]
buildMdRoutes postNames = map mdFileToRoute postNames

View File

@ -1,139 +1,87 @@
module Core.Handlers where
import qualified Core.Configuration as Conf
import Core.Feed (Post(..), autoFeed, renderFeed)
import Core.Rendering
import Core.Rendering
import Core.Feed (Post(..), autoFeed, renderFeed)
import Fragments.Base
import Fragments.Styles as S
import Fragments.NotFound
import qualified Text.Atom.Feed as Atom
import Control.Monad.IO.Class (liftIO)
import Data.ByteString.Lazy (ByteString)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as TLE
import Data.Time.Clock (UTCTime(..), getCurrentTime)
import Data.Time.Format (formatTime, defaultTimeLocale)
import Lucid (Html)
import Network.HTTP.Types (status200, hContentType)
import System.Directory (doesFileExist, getModificationTime)
import System.FilePath.Find ( always, extension, fileName, find, (&&?)
, (/~?), (==?) )
import System.FilePath ( dropExtension, takeFileName )
import Web.Twain hiding (fileName)
import Control.Monad.IO.Class (liftIO)
import Data.Text
import qualified Data.Text.Lazy.Encoding as LTE
import Data.Time.Clock (UTCTime(..), getCurrentTime)
import Data.Time.Format (formatTime, defaultTimeLocale)
import Lucid (Html)
import System.Directory (getModificationTime)
import Web.Twain
-- A ResponoderM capable of lifting to IO monad; constructs response to clients
index :: ResponderM a
index = do
(title, homeMd, footerMd) <- liftIO $ (,,)
<$> Conf.title
<*> mdFileToLucid "./data/posts/home.md"
<*> mdFileToLucid "./data/posts/footer.md"
index = do
-- Query the system environment for the BLOGTITLE environment variable
title <- liftIO Conf.appTitle
-- Read a Commonmark Markdown file and process it to HTML
homeMd <- liftIO $ mdFileToLucid "./data/posts/home.md"
-- Respond to request with fragments compositionally to create a home page
sendLucidFragment $ basePage title (baseHome homeMd) footerMd
sendLucidFragment $ basePage title (baseHome homeMd)
-- Responds with processed Commonmark -> HTML for posts
posts :: ResponderM a
posts = do
postName <- param "name"
postValid <- liftIO $ postExists postName
case postValid of
False -> missing
True -> do
(title, footerMd, postMd) <- liftIO $ (,,)
<$> Conf.title
<*> mdFileToLucid "./data/posts/footer.md"
<*> (mdFileToLucid $ postPath postName)
sendLucidFragment $ basePage title (basePost postMd) footerMd
where
postExists :: T.Text -> IO Bool
postExists postName = doesFileExist $ postPath postName
-- Responds with processed Commonmark -> HTML for posts existing at app init
posts :: FilePath -> ResponderM a
posts postName = do
title <- liftIO Conf.appTitle
postMd <- liftIO $ mdFileToLucid ("./data/posts/" ++ postName ++ ".md")
sendLucidFragment $ basePage title (basePost postMd)
postPath :: T.Text -> FilePath
postPath postName = "./data/posts/" ++ T.unpack postName ++ ".md"
-- Builds an index of all posts
postsIndex :: ResponderM a
postsIndex = do
(postNames, title, footerMd) <- liftIO $ (,,)
<$> mdPostNames
<*> Conf.title
<*> mdFileToLucid "./data/posts/footer.md"
sendLucidFragment $ basePage title (postIndex postNames) footerMd
-- Builds an index of all posts on filesystem as of application init
postsIndex :: [FilePath] -> ResponderM a
postsIndex postNames = do
title <- liftIO Conf.appTitle
sendLucidFragment $ basePage title (postIndex postNames)
-- Generates the XML feed at /feed
feed :: ResponderM a
feed = do
(postNames, title, baseUrl, time) <- liftIO $ (,,,)
<$> mdPostNames
<*> Conf.title
<*> Conf.baseUrl
<*> fmap (\x -> timeFormat x) getCurrentTime
feedData <- liftIO $ mapM (makePost baseUrl) postNames
feed :: [FilePath] -> ResponderM a
feed postNames = do
title <- liftIO Conf.appTitle
time <- liftIO $ fmap (\x -> timeFormat x) getCurrentTime
-- Create Atom [Post] to populate the feed
feedData <- liftIO $ mapM makePost postNames
-- Send an XML response with an automatically populated Atom feed
send $ atom
$ TLE.encodeUtf8
$ renderFeed
$ autoFeed (baseFeed title time baseUrl) feedData
send $ xml $ LTE.encodeUtf8 $ renderFeed
$ autoFeed (baseFeed title time) feedData
where
-- Atom feed response headers
atom :: ByteString -> Response
atom f = withHeader (hContentType, "application/atom+xml") $ raw status200 [] $ f
-- Base feed data structure which we populate with entries
baseFeed :: String -> String -> String -> Atom.Feed
baseFeed title time baseUrl = Atom.nullFeed
(T.pack $ baseUrl ++ "/feed")
(Atom.TextString $ T.pack title)
(T.pack $ time ++ " UTC")
baseFeed :: String -> String -> Atom.Feed
baseFeed title time = Atom.nullFeed
"https://eversole.co/feed"
(Atom.TextString $ pack title)
(pack $ time ++ " UTC")
-- Create an Atom Post for each markdown post present
makePost :: String -> FilePath -> IO (Post)
makePost baseUrl postName = do
date <- getModificationTime $ "./data/posts/" ++ postName ++ ".md"
postContent <- mdFileToText $ "./data/posts/" ++ postName ++ ".md"
return $ Post
(T.pack $ (timeFormat date) ++ " UTC")
(T.pack $ baseUrl ++ "/posts/" ++ postName)
(T.pack $ show postName)
(postContent)
makePost :: FilePath -> IO (Post)
makePost x = do
date <- getModificationTime $ "./data/posts/" ++ x ++ ".md"
return $ Post
(pack $ (timeFormat date) ++ " UTC")
(pack $ "https://eversole.co/posts/" ++ x)
(pack $ show x)
-- YYYY-MM-DD HH:MM | 2024-02-24 16:36
timeFormat :: UTCTime -> String
timeFormat date = formatTime defaultTimeLocale "%Y-%m-%d %H:%M" date
timeFormat x = formatTime defaultTimeLocale "%Y-%m-%d %H:%M" x
-- Refer to index comments
contact :: ResponderM a
contact = do
(title, contactMd, footerMd) <- liftIO $ (,,)
<$> Conf.title
<*> mdFileToLucid "./data/posts/contact.md"
<*> mdFileToLucid "./data/posts/footer.md"
sendLucidFragment $ basePage title (baseContact contactMd) footerMd
title <- liftIO Conf.appTitle
contactMd <- liftIO $ mdFileToLucid "./data/posts/contact.md"
sendLucidFragment $ basePage title (baseContact contactMd)
-- Respond with primary processed CSS
theme :: ResponderM a
theme = send $ css $ TLE.encodeUtf8 $ S.cssRender S.composedStyles
-- Helper function for responding in ResponderM from Html
-- Helper function for responding in ResponderM from Html
sendLucidFragment :: Html () -> ResponderM a
sendLucidFragment x = send $ html $ lucidToTwain x
-- 404 handler
missing :: ResponderM a
missing = sendLucidFragment pageNotFound
-- List of all non-hidden .md posts that aren't part of templating
mdPostNames :: IO [FilePath]
mdPostNames = mapM (pure . dropExtension . takeFileName )
=<< find isVisible fileFilter "./data/posts"
where
isVisible = fileName /~? ".?*"
isMdFile = extension ==? ".md"
isHome = fileName /~? "home.md"
isContact = fileName /~? "contact.md"
isFooter = fileName /~? "footer.md"
fileFilter = isMdFile
&&? isVisible
&&? isHome
&&? isContact
&&? isFooter

View File

@ -5,7 +5,6 @@ import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString as B
import Data.Text
import Data.Text.Encoding (decodeUtf8)
import Data.Text.Lazy (toStrict)
import qualified Lucid as LU
import System.IO ()
@ -19,8 +18,3 @@ mdToLucid cmtextinput = case (commonmark "" cmtextinput) of
mdFileToLucid :: FilePath -> IO (LU.Html ())
mdFileToLucid path = fmap (mdToLucid . decodeUtf8) (B.readFile path)
mdFileToText :: FilePath -> IO (Text)
mdFileToText path = do
htmlContent <- mdFileToLucid path
return $ toStrict $ LU.renderText htmlContent

View File

@ -15,9 +15,10 @@ baseDoc title bodyContent = doctypehtml_ $ do
link_ [rel_ "stylesheet", type_ "text/css", href_ "/style.css"]
body_ bodyContent
baseFooter :: Html () -> Html ()
baseFooter content = footer_ $ do
p_ $ content
baseFeed :: Html ()
baseFeed = div_ [class_ "main"] $ do
h2_ "Oops, I haven't been implemented yet."
h3_ "Check back in a couple days!"
baseHome :: Html () -> Html ()
baseHome content = div_ [class_ "main"] content
@ -30,24 +31,19 @@ baseNav = div_ [class_ "navContainer"] $ do
li_ $ a_ [href_ "/contact"] "Contact"
li_ $ a_ [href_ "/feed"] "Feed"
basePage :: String -> Html () -> Html () -> Html ()
basePage title body footer = baseDoc title $ baseNav <> body <> baseFooter footer
basePage :: String -> Html () -> Html()
basePage title body = baseDoc title $ baseNav <> body
basePost :: Html () -> Html ()
basePost content = div_ [class_ "main"] content
postIndex :: [FilePath] -> Html ()
postIndex postNames = div_ [class_ "postList"] $ do
postIndex postNames = div_ [class_ "main"] $ do
h1_ [class_ "title"] "All Posts"
ul_ [] $ do
ul_ [class_ "postList"] $ do
mapM_
(\x -> li_ $ a_ [href_ (pack $ "/posts/" ++ x)] (fromString x))
postNames
pageNotFound :: Html ()
pageNotFound = baseDoc "404" baseNav <>
(div_ [class_ "notFound"] $ h1_ "404 NOT FOUND")
none :: Text
none = mempty

View File

@ -0,0 +1,9 @@
module Fragments.NotFound where
import Fragments.Base
import Lucid
pageNotFound :: Html ()
pageNotFound = baseDoc "404" baseNav <>
(div_ [class_ "notFound"] $ h1_ "404 NOT FOUND")

View File

@ -1,150 +0,0 @@
module Fragments.Styles where
import Clay hiding (main_)
import qualified Clay.Media as M
import Data.Text.Lazy hiding (center)
import Prelude hiding (div)
cssRender :: Css -> Text
cssRender css = renderWith compact [] css
priColor, secColor, terColor :: Color
priColor = "#f1f6f0"
secColor = "#222323"
terColor = "#6D92AD"
composedStyles :: Css
composedStyles = do
core_
main_
nav_
notFound_
postList_
mobileFriendly_
core_ :: Css
core_ = do
a_
body_
code_
footer_
html_
p_
pre_
a_ :: Css
a_ = do
a ? do
textDecoration none
color terColor
body_ :: Css
body_ = do
body ? do
display flex
minHeight $ vh 100
flexDirection column
fontFamily [] [monospace]
fontSize $ em 1.25
fontWeight $ weight 300
textAlign start
margin (em 0) auto (em 0) auto
strong ? do
fontWeight $ weight 600
li ? do
listStyleType $ other "\"~> \""
footer_ :: Css
footer_ = do
footer ? do
bottom (em 0)
margin auto (em 0) (em 0) (em 0)
width $ pct 100
backgroundColor terColor
textAlign center
padding (em 1) (em 0) (em 1) (em 0)
boxSizing borderBox
p ? do
fontSize $ em 0.75
margin (em 0) (em 2) (em 0) (em 2)
color priColor
a ? do
color priColor
html_ :: Css
html_ = do
html ? do
backgroundColor priColor
color secColor
p_ :: Css
p_ = do
p ? do
margin (em 1) (em 0) (em 0.6) (em 0)
main_ :: Css
main_ = do
".main" ? do
margin (em 0) auto (em 2) auto
width $ pct 60
notFound_ :: Css
notFound_ = do
".notFound" ? do
margin (em 0) auto (em 0) auto
textAlign center
h1 ? do
fontSize $ pct 500
fontWeight $ weight 200
color terColor
postList_ :: Css
postList_ = do
".postList" ? do
margin (em 0) auto (em 0) auto
minWidth (pct 60)
maxWidth (pct 95)
overflow scroll
ul ? do
fontSize (em 1.5)
code_ :: Css
code_ = do
code ? do
color priColor
backgroundColor terColor
overflowX scroll
pre_ :: Css
pre_ = do
pre ? do
color priColor
backgroundColor terColor
overflowX scroll
nav_ :: Css
nav_ = do
".navContainer" ? do
margin (em 1.5) (em 0) (em 1.5) (em 0)
width $ pct 100
textAlign center
".mainNav" ? do
margin (em 0) auto (em 0) auto
padding (em 0.5) (em 0.5) (em 0.5) (em 0.5)
overflow hidden
boxShadow . pure $ bsColor "#ccc" $ shadowWithBlur (px 4) (px 4) (px 6)
display inlineFlex
li ? do
listStyleType none
a ? do
display block
textAlign center
padding (em 0.25) (em 0.3) (em 0.25) (em 0.3)
textTransform lowercase
mobileFriendly_ :: Css
mobileFriendly_ = query M.screen [M.maxWidth 768] $ do
".main" ? do
width $ pct 95
".postList" ? do
width $ pct 95

View File

@ -3,7 +3,26 @@ module Main where
import qualified Core.HTTP as HTTP
import qualified Core.Configuration as Conf
import Control.Monad ( mapM_ )
import System.FilePath.Find ( always, extension, fileName, find, (&&?)
, (/~?), (==?) )
import System.FilePath ( dropExtension, takeFileName )
main :: IO ()
main = do
Conf.main
HTTP.main
Conf.main
mdFilePaths <- getMdFilePaths "./data/posts/"
-- Pass only the post names extracted from their filepath to HTTP.main
let mdFiles = map (dropExtension . takeFileName) mdFilePaths
HTTP.main mdFiles
-- Return a list of all non-hidden .md files except for home.md and contact.md
getMdFilePaths :: FilePath -> IO [FilePath]
getMdFilePaths fp = find isVisible fileFilter fp
where
isMdFile = extension ==? ".md"
isVisible = fileName /~? ".?*"
isHome = fileName /~? "home.md"
isContact = fileName /~? "contact.md"
fileFilter = isMdFile &&? isVisible &&? isHome &&? isContact