ozymandias/build/Config.hs

119 lines
4.1 KiB
Haskell

{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Site-wide configuration loaded from @site.yaml@ at the project root.
--
-- The config is exposed as top-level pure values via @unsafePerformIO@ +
-- @NOINLINE@. This is safe because:
-- 1. The config is a build-time input that never changes during a build.
-- 2. The static site generator is single-shot — no concurrency.
-- 3. NOINLINE prevents GHC from duplicating the value.
--
-- 'Main.main' forces 'siteConfig' before Hakyll starts so a missing or
-- malformed @site.yaml@ fails loudly with a parse error rather than
-- crashing midway through a build.
module Config
( SiteConfig(..)
, NavLink(..)
, Portal(..)
, siteConfig
, siteHost
, defaultAuthor
) where
import Data.Aeson (FromJSON(..), withObject, (.:), (.:?), (.!=))
import Data.Yaml (decodeFileThrow)
import Data.Text (Text)
import qualified Data.Text as T
import System.IO.Unsafe (unsafePerformIO)
-- ---------------------------------------------------------------------------
-- Types
-- ---------------------------------------------------------------------------
data SiteConfig = SiteConfig
{ siteName :: Text
, siteUrl :: Text
, siteDescription :: Text
, siteLanguage :: Text
, authorName :: Text
, authorEmail :: Text
, feedTitle :: Text
, feedDescription :: Text
, license :: Text
, sourceUrl :: Text
, gpgFingerprint :: Text
, gpgPubkeyUrl :: Text
, navLinks :: [NavLink]
, portals :: [Portal]
} deriving (Show)
data NavLink = NavLink
{ navHref :: Text
, navLabel :: Text
} deriving (Show)
data Portal = Portal
{ portalSlug :: Text
, portalName :: Text
} deriving (Show)
-- ---------------------------------------------------------------------------
-- JSON instances
-- ---------------------------------------------------------------------------
instance FromJSON SiteConfig where
parseJSON = withObject "SiteConfig" $ \o -> SiteConfig
<$> o .: "site-name"
<*> o .: "site-url"
<*> o .: "site-description"
<*> o .:? "site-language" .!= "en"
<*> o .: "author-name"
<*> o .: "author-email"
<*> o .:? "feed-title" .!= ""
<*> o .:? "feed-description" .!= ""
<*> o .:? "license" .!= ""
<*> o .:? "source-url" .!= ""
<*> o .:? "gpg-fingerprint" .!= ""
<*> o .:? "gpg-pubkey-url" .!= "/gpg/pubkey.asc"
<*> o .:? "nav" .!= []
<*> o .:? "portals" .!= []
instance FromJSON NavLink where
parseJSON = withObject "NavLink" $ \o -> NavLink
<$> o .: "href"
<*> o .: "label"
instance FromJSON Portal where
parseJSON = withObject "Portal" $ \o -> Portal
<$> o .: "slug"
<*> o .: "name"
-- ---------------------------------------------------------------------------
-- Global config value
-- ---------------------------------------------------------------------------
-- | Loaded from @site.yaml@ at the project root on first access.
-- @NOINLINE@ prevents GHC from duplicating the I/O. If @site.yaml@ is
-- missing or invalid, evaluation throws a parse exception.
{-# NOINLINE siteConfig #-}
siteConfig :: SiteConfig
siteConfig = unsafePerformIO (decodeFileThrow "site.yaml")
-- | The site's hostname, derived from 'siteUrl'. Used by 'Filters.Links'
-- to distinguish self-links from external links.
siteHost :: Text
siteHost = extractHost (siteUrl siteConfig)
where
extractHost url
| Just rest <- T.stripPrefix "https://" url = hostOf rest
| Just rest <- T.stripPrefix "http://" url = hostOf rest
| otherwise = T.toLower url
hostOf rest =
let withPort = T.takeWhile (\c -> c /= '/' && c /= '?' && c /= '#') rest
in T.toLower (T.takeWhile (/= ':') withPort)
-- | Default author name as a 'String', for Hakyll metadata APIs that use
-- 'String' rather than 'Text'.
defaultAuthor :: String
defaultAuthor = T.unpack (authorName siteConfig)