{-| \"Blaaargh\!\" is a simple filesystem-based content management system for static or semi-static publishing. You can run it standalone (...or will be able to soon) or integrate it as a component in a larger happstack application. Like the venerable blosxom (<http://blosxom.sourceforge.net/>) package, Blaaargh\! relies on plain-text files on the file system as its content database. FEATURES: * simple on-disk content database * posts\/pages written in markdown format * pages formatted into HTML by a flexible cascading template system based on HStringTemplate (<http://hackage.haskell.org/package/HStringTemplate>). Page templates can be overridden on a per-page or per-folder basis. * directories can be given indices using the templating system. Various views of the directory contents (e.g. N most recent pages, pages in forward/reverse chronological order, pages in alphabetical order) are exposed to the index templates. * directories with defined indices get Atom-format syndication feeds * configuration parameters (site title, site URL, base URL, etc.) specified in an INI-style config file MISSING FEATURES: * a web-accessible \"administrative area\" * a comment system (for my homepage I'm currently just outsourcing this to Disqus) INSTRUCTIONS: The Blaaargh directory consists of the following contents: [@config@] an INI-style configuration file [@templates\/@] a tree of @.st@ template files, served by HStringTemplate [@content\/@] a tree of content files. Markdown-formatted files (with @.md@ extensions) are parsed and read as \"posts\" that are rendered to HTML via the templating mechanism. Other files are served as static content. Let's say you have some files in here: > config > content/grumble.md > content/index.md > content/posts/bar.md > content/posts/bar/photo.jpg > content/posts/foo.md > content/static/spatula.md > templates/404.st > templates/post.st > templates/static/index.st > templates/static/post.st > templates/static/spatula.st Think of it like this -- you request content, either a single post, a directory, or an atom feed (hardcoded as @\/[dir]\/feed.xml@). Blaaargh searches for the closest matching template. For @.md@ files (\"posts\" or \"pages\"), the templates /cascade/: e.g. for a file @content\/foo\/bar\/baz.md@, we would search @templates\/@ for the following templates, in order: * @foo\/bar\/baz.st@ * @foo\/bar\/post.st@ * @foo\/post.st@ * @post.st@ * @404.st@ For directories, the templates don't cascade; a directory needs to have a matching template in order to be served. E.g. for a directory @\/foo\/bar\/quux@, we search @templates\/@ for \"@foo\/bar\/quux\/index.st@\", and if @content\/foo\/bar\/quux\/index.md@ exists, we read it into the template as content text. (More about this in \"TEMPLATING\" below.) CONFIGURATION A Blaaargh\! @config@ file looks like this: > [default] > # what's the domain name? > siteurl = http://example.com > > # blaaargh content will be served at this base URL > baseurl = /foo > > [feed] > # site title > title = Example dot com > # authors > authors = John Smith <john@example.com> > # Atom icon > icon = /static/icon.png > > # posts on this list (or directories containing posts) won't be included in > # directory indices, nor in atom feeds > skipurls = static Blaaargh\! uses the ConfigFile library for configuration and post header parsing. (<http://hackage.haskell.org/package/ConfigFile>) POST FORMATTING Posts are (mostly) in Markdown format, with the exception of a key-value header prefixed by pipe (@\|@) characters. Example: > | title: Example post > | author: John Smith <john@example.com> > | published: 2009-09-15T21:18:00-0400 > | summary: A short summary of the post contents > > This is an example post in *markdown* format. > Blaaargh\! accepts the following key-values for posts: [@title@] The title of the post [@author@] The post's author [@authors@] same as @author@ [@summary@] a summary of the post [@updated@] the post's last update time in RFC3339 format [@published@] the post's publish date in RFC3339 format The headers are parsed as follows: any lines starting with the @\|@ character (up until the first line not starting with @\|@) have the prefix stripped and are sent through ConfigFile. Please see its haddock for input syntax. DATA MODEL The documents get indexed into a key-value mapping by id. (Where an \"id\" for a post is defined as its relative path from @content\/@, with the @.md@ suffix removed.) Files with an @\'.md\'@ suffix are treated as \"posts\"\/\"pages\", and are expected to be in markdown format. Files with other suffices are served as static files. TEMPLATING Blaaargh\! uses templates to present the content of posts and lists of posts in HTML. For an individual post (either postname-specific @/postname/.st@ or generic @post.st@), Blaaargh exports a template variable called @$post$@ which is a map containing the following attributes: @ $id$ $date$ $url$ $title$ $content$ $summary$ $authors$ @ So in other words, within your template the post's URL can be accessed using @$post.id$@. For directory templates (@index.st@), we collect the posts within that directory and present them to the templating system as a list of post objects (i.e. containing the @$id$@\/@$date$@\/etc. fields listed above): @ $alphabeticalPosts$ $recentPosts$ -- N.B. 5 most recent posts $chronologicalPosts$ $reverseChronologicalPosts$ @ -} module Blaaargh ( initBlaaargh , serveBlaaargh , runBlaaarghHandler , addExtraTemplateArguments , BlaaarghException , blaaarghExceptionMsg , BlaaarghMonad , BlaaarghHandler , BlaaarghState ) where ------------------------------------------------------------------------------ import Control.Exception import Control.Monad import qualified Data.ByteString.Char8 as B import Data.Char import qualified Data.ConfigFile as Cfg import Data.Either import Data.List import System.Directory import System.FilePath import qualified Text.Atom.Feed as Atom import Text.Printf ------------------------------------------------------------------------------ import Blaaargh.Internal.Handlers import Blaaargh.Internal.Post import Blaaargh.Internal.Types import qualified Blaaargh.Internal.Util.ExcludeList as EL import Blaaargh.Internal.Util.ExcludeList (ExcludeList) import Blaaargh.Internal.Util.Templates ------------------------------------------------------------------------------ {-| Initialize a Blaaargh instance. Given the name of a directory on the disk, 'initBlaaargh' searches it for configuration, content, and template files, and produces a finished 'BlaaarghState' value. Throws a 'BlaaarghException' if there was an error reading the state. -} initBlaaargh :: FilePath -- ^ path to blaaargh directory -> IO BlaaarghState initBlaaargh path = do -- make sure directories exist mapM_ failIfNotDir [path, contentDir, templateDir] (feed, siteURL, baseURL, excludeList) <- readConfig configFilePath cmap <- buildContentMap baseURL contentDir templates <- readTemplateDir templateDir return BlaaarghState { blaaarghPath = path , blaaarghSiteURL = siteURL , blaaarghBaseURL = baseURL , blaaarghPostMap = cmap , blaaarghTemplates = templates , blaaarghFeedInfo = feed , blaaarghFeedExcludes = excludeList , blaaarghExtraTmpl = id } where -------------------------------------------------------------------------- unlessM :: IO Bool -> IO () -> IO () unlessM b act = b >>= flip unless act -------------------------------------------------------------------------- failIfNotDir :: FilePath -> IO () failIfNotDir d = unlessM (doesDirectoryExist d) (throwIO $ BlaaarghException $ printf "'%s' is not a directory" path) -------------------------------------------------------------------------- configFilePath = path </> "config" contentDir = path </> "content" templateDir = path </> "templates" ------------------------------------------------------------------------------ getM :: Cfg.Get_C a => Cfg.ConfigParser -> String -> String -> Maybe a getM cp section = either (const Nothing) Just . Cfg.get cp section readConfig :: FilePath -> IO (Atom.Feed, String, String, ExcludeList) readConfig fp = do cp <- parseConfig fp either (throwIO . BlaaarghException . show) return (mkFeed cp) where ensurePrefix :: Char -> String -> String ensurePrefix p s = if [p] `isPrefixOf` s then s else p:s stripSuffix :: Char -> String -> String stripSuffix x s = if [x] `isSuffixOf` s then init s else s mkFeed :: Either Cfg.CPError Cfg.ConfigParser -> Either Cfg.CPError (Atom.Feed, String, String, ExcludeList) mkFeed cfg = do cp <- cfg title <- Cfg.get cp "feed" "title" authors <- Cfg.get cp "feed" "authors" baseURL' <- Cfg.get cp "default" "baseurl" siteURL' <- Cfg.get cp "default" "siteurl" let icon = getM cp "feed" "icon" let skip = maybe EL.empty (EL.fromPathList . B.pack) (getM cp "feed" "skipurls") let siteURL = stripSuffix '/' siteURL' let baseURL = stripSuffix '/' $ ensurePrefix '/' baseURL' let feedURL = (siteURL ++ baseURL) let feed = Atom.nullFeed feedURL (Atom.TextString title) "" let feed' = feed { Atom.feedAuthors = parsePersons authors , Atom.feedIcon = icon , Atom.feedLinks = [ Atom.nullLink feedURL ] } return (feed', siteURL, baseURL, skip) parseConfig :: FilePath -> IO (Either Cfg.CPError Cfg.ConfigParser) parseConfig = Cfg.readfile Cfg.emptyCP