Browse Source

Using go-script in place of makefile, template api starting to gel, normalizing tag/category urls, sitemap generating more correctly(?), config through yaml because overrides are trivially supported through parser and removed ini config

contexts-draft2
Logan McGrath 11 months ago
parent
commit
1529fe95ba
  1. 11
      .drone.yml
  2. 55
      Makefile
  3. 146
      commands.sh
  4. 22
      config.ini
  5. 35
      config.yaml
  6. 150
      go
  7. 5
      green.cabal
  8. 3
      package.yaml
  9. 0
      site/_errors/404.md
  10. 5
      site/_errors/500.md
  11. 14
      site/_layouts/skeleton.html
  12. BIN
      site/images/reasons-my-website-is-offline/card-3072.png
  13. 13
      site/sitemap.xml
  14. 6
      src/Green.hs
  15. 4
      src/Green/Command.hs
  16. 193
      src/Green/Config.hs
  17. 22
      src/Green/Site.hs
  18. 71
      src/Green/Site/Blog.hs
  19. 2
      src/Green/Site/Css.hs
  20. 9
      src/Green/Site/Download.hs
  21. 7
      src/Green/Site/HomePage.hs
  22. 15
      src/Green/Site/Pages.hs
  23. 51
      src/Green/Site/Sitemap.hs
  24. 5
      src/Green/Site/Templates.hs
  25. 19
      src/Green/Template/Compiler.hs
  26. 27
      src/Green/Template/Context.hs
  27. 37
      src/Green/Template/Custom.hs
  28. 17
      src/Green/Template/Custom/Compiler.hs
  29. 15
      src/Green/Template/Custom/Context.hs
  30. 9
      src/Green/Template/Custom/DateField.hs
  31. 2
      src/Green/Template/Custom/GitField.hs
  32. 31
      src/Green/Template/Custom/HtmlField.hs
  33. 4
      src/Green/Template/Field.hs
  34. 1
      src/Green/Template/Source/Parser.hs
  35. 17
      src/Green/Template/Tags.hs
  36. 73
      src/Green/Util.hs
  37. 2
      test/Green/TestSupport/Config.hs
  38. 94
     

11
.drone.yml

@ -1,11 +0,0 @@
kind: pipeline
type: docker
name: build-test-deploy
steps:
- name: build and test
image: haskell:8.10.7
commands:
- stack build
- stack exec test
- stack exec site build

55
Makefile

@ -1,55 +0,0 @@
build:
set -e; source ./commands.sh; build
.PHONY: build
clean:
set -e; source ./commands.sh; clean
.PHONY: clean
clean-all:
set -e; source ./commands.sh; clean_all
.PHONY: clean-all
rebuild:
set -e; source ./commands.sh; rebuild
.PHONY: rebuild
rebuild-all:
set -e; source ./commands.sh; rebuild_all
.PHONY: rebuild-all
watch:
set -e; source ./commands.sh; watch
.PHONY: watch
publish:
set -e; source ./commands.sh; publish
.PHONY: publish
init:
set -e; source ./commands.sh; init
.PHONY: init
test:
set -e; source ./commands.sh; test
.PHONY: test
hpack:
stack exec hpack
.PHONY: hpack
upload:
set -e; source ./commands.sh; upload
.PHONY: upload
datestamp:
set -e; source ./commands.sh; datestamp
.PHONY: datestamp
favicon:
set -e; source ./commands.sh; favicon
.PHONY: favicon
default_og_image:
set -e; source ./commands.sh; default_og_image
.PHONY: default_og_image

146
commands.sh

@ -1,146 +0,0 @@
#!/usr/bin/env bash
set -e
ARGS=()
if [ -n "$VERBOSE" ]; then
ARGS+=("--verbose")
fi
init () {
git config core.hooksPath .githooks
brew bundle
stack install hakyll
if [ $? -ne 0 ]; then
echo "Failed to install Hakyll, check README.md for troubleshooting"
exit 1
fi
echo
echo "Setup completed successfully"
echo
}
build () {
if ! command -v stack &> /dev/null; then
init
fi
stack build
stack exec site build -- ${ARGS[@]}
favicon
default_og_image
}
clean () {
rm -rf _cache/*
}
clean_all () {
clean
stack clean
rm -rf _cache/* _site/*
}
rebuild () {
clean
build
}
touch_all () {
# touch all files so they're built again
touch site
}
rebuild_all () {
clean_all
touch_all
build
}
watch () {
build
stack exec site watch -- ${ARGS[@]}
}
publish () {
init
current_branch="$(git branch --show-current)"
if [[ "$current_branch" -ne "main" ]]; then
echo "Can't publish from current branch: $current_branch"
exit 1
fi
test_sync "main"
build
if [ ! -d _site ] || [ -z "$(ls -A _site)" ]; then
git clone --branch _site "$(git config --get remote.origin.url)" _site
fi
sha="$(git log -1 HEAD --pretty=format:%h)"
pushd ./_site
test_sync "_site"
git add .
git commit -m "Build on $(date) generated from $sha"
git push origin "_site"
scp -r * thisfieldwas.green:/var/www/thisfieldwas.green/
popd
}
test_sync () {
branch=$1
git switch $branch
git fetch origin $branch
rev_parse_remote="$(git rev-parse origin/$branch)"
rev_parse_local="$(git rev-parse $branch)"
if [ "$rev_parse_local" != "$rev_parse_remote" ]; then
echo "ERROR: Branch $branch not in sync with remote!"
exit 1
fi
echo "INFO: Local branch $branch is up to date with remote"
}
test () {
stack test
}
upload () {
rsync -ahp _site/* closet.oflogan.xyz:/usr/share/nginx/thisfieldwas.green/
}
datestamp () {
DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "$DATE" | pbcopy
echo "Copied to clipboard: $DATE"
}
favicon () {
src_file="$(pwd)/site/images/grass.svg"
out_file="$(pwd)/_site/favicon.ico"
mkdir -p _cache/favicon_tmp
pushd _cache/favicon_tmp
sizes=(16 32 48 64 96 128 256)
for x in ${sizes[@]}; do
inkscape -w $x -h $x -y 0 -o $x.png "$src_file"
done
files=("${sizes[@]/%/.png}")
convert "${files[@]}" "$out_file"
identify "$out_file"
popd
}
default_og_image () {
src_file="$(pwd)/site/images/grass.svg"
out_file="$(pwd)/_site/grass_og-image.png"
inkscape -w 1024 -h 1024 -b "aliceblue" -o "$out_file" "$src_file"
identify "$out_file"
}

22
config.ini

@ -1,22 +0,0 @@
[Site]
title = This Field Was Green
description = ""
root = https://thisfieldwas.green
authorName = Logan McGrath
authorEmail = logan.mcgrath@thisfieldwas.green
linkedInProfile = https://www.linkedin.com/in/loganmcgrath
gitWebUrl = https://bitsof.thisfieldwas.green/keywordsalad/thisfieldwas.green/src/commit
[DisplayFormats]
dateShortFormat = %B %e, %Y
dateLongFormat = %B %e, %Y %l:%M %P %EZ
timeFormat = %l:%M %p %EZ
imageWidths = 320, 768, 1024, 1920, 3840
[Hakyll]
providerDirectory = site
destinationDirectory = _site
[Debug]
preview = false
rawCss = false

35
config.yaml

@ -0,0 +1,35 @@
default: &default
site-info:
title: This Field Was Green
root: http://localhost:8000
author-name: Logan McGrath
author-email: logan.mcgrath@thisfieldwas.green
linkedin-profile: https://www.linkedin.com/in/loganmcgrath
twitter-profile: https://twitter.com/thisgreenfield
git-web-url: https://bitsof.thisfieldwas.green/keywordsalad/thisfieldwas.green/src/commit
display-formats:
date-short-format: '%B %e, %Y'
date-long-format: '%B %e, %Y %l:%M %P %EZ'
time-format: '%l:%M %p %EZ'
robot-date: '%Y-%m-%d'
robot-time: '%Y-%m-%dT%H:%M:%S%Ez'
image-widths: [320, 768, 1024, 1920, 3840]
hakyll-config:
provider-directory: site
destination-directory: _site
debug-settings:
preview: true
inflate-css: true
prod:
<<: *default
site-info:
root: https://thisfieldwas.green
debug-settings:
preview: false
inflate-css: false

150
go

@ -0,0 +1,150 @@
#!/usr/bin/env bash
# This "./go" script is the build script.
# For context behind the "./go" script, please read these:
# https://blog.thepete.net/blog/2014/03/28/_-attributes-of-an-amazing-dev-toolchain/
# https://code.ofvlad.xyz/vlad/lightning-runner
set -e
_verify-prerequisites () {
git config core.hooksPath .githooks
if ! command -v stack &> /dev/null
then
_bad-message "Install haskell-stack to continue"
exit 1
fi
if ! command -v hakyll-init &> /dev/null
then
stack install hakyll
if [ $? -ne 0 ]; then
_bad-message "Failed to install Hakyll, check README.md for troubleshooting"
exit 1
fi
fi
}
⚡build () {
_help-line "Compile the site generator and generate the site"
stack build
stack exec site build
⚡favicon
⚡default_og_image
}
⚡clean () {
_help-line "Clean generated site files"
rm -rf _cache/* _site/*
}
⚡clean_all () {
_help-line "Clean generated site files and site generator binaries"
⚡clean
stack clean
}
⚡rebuild () {
_help-line "Clean and then rebuild the generated site"
⚡clean
⚡build
}
⚡rebuild_all () {
_help-line "Clean and then rebuild both the generated site and the site generator binary"
⚡clean_all
⚡build
}
⚡watch () {
_help-line "Build the site generator, generate the site, and then serve it so that it may be viewed in a browser"
⚡build
stack exec site watch
}
⚡publish () {
_help-line "Build the site and then publish it live"
current_branch="$(git branch --show-current)"
if [[ "$current_branch" -ne "main" ]]; then
_bad-message "Can only publish from main branch; tried to publish from $current_branch"
exit 1
fi
⚡test_sync "main"
⚡build
if [ ! -d _site ] || [ -z "$(ls -A _site)" ]; then
git clone --branch _site "$(git config --get remote.origin.url)" _site
fi
sha="$(git log -1 HEAD --pretty=format:%h)"
pushd ./_site
test_sync "_site"
git add .
git commit -m "Build on $(date) generated from $sha"
git push origin "_site"
rsync -ahp * closet.oflogan.xyz:/usr/share/nginx/thisfieldwas.green/
popd
}
⚡test_sync () {
_help-line "Verify that the current or specified local branch is up to date with the remote branch"
branch=${1:-$(git branch --show-current)}
git switch $branch
git fetch origin $branch
rev_parse_remote="$(git rev-parse origin/$branch)"
rev_parse_local="$(git rev-parse $branch)"
if [ "$rev_parse_local" != "$rev_parse_remote" ]; then
_bad-message "Branch $branch not in sync with remote!"
exit 1
fi
_good-message "Local branch $branch is up to date with remote"
}
⚡test () {
_help-line "Run hspec tests"
stack test
}
⚡force-publish () {
_help-line "Publish generated site as-is. Only use this for emergencies!"
rsync -ahp _site/* closet.oflogan.xyz:/usr/share/nginx/thisfieldwas.green/
}
⚡datestamp () {
_help-line "Generate ISO-8601 datestamp with time and offset"
DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "$DATE" | pbcopy
echo "Copied to clipboard: $DATE"
}
⚡favicon () {
_help-line "Generate favicon from grass.svg"
src_file="$(pwd)/site/images/grass.svg"
out_file="$(pwd)/_site/favicon.ico"
mkdir -p _cache/favicon_tmp
pushd _cache/favicon_tmp
sizes=(16 32 48 64 96 128 256)
for x in ${sizes[@]}; do
inkscape -w $x -h $x -b "aliceblue" -o $x.png "$src_file"
done
files=("${sizes[@]/%/.png}")
convert "${files[@]}" "$out_file"
identify "$out_file"
popd
}
⚡default_og_image () {
_help-line "Generate og:image for open graph"
src_file="$(pwd)/site/images/grass.svg"
out_file="$(pwd)/_site/grass_og-image.png"
inkscape -w 1024 -h 1024 -b "aliceblue" -o "$out_file" "$src_file"
identify "$out_file"
}
source ⚡

5
green.cabal

@ -35,7 +35,6 @@ library
Green.Site.BrokenLinks
Green.Site.Code
Green.Site.Css
Green.Site.Download
Green.Site.Feed
Green.Site.HomePage
Green.Site.Images
@ -49,7 +48,6 @@ library
Green.Template.Compiler
Green.Template.Context
Green.Template.Custom
Green.Template.Custom.Compiler
Green.Template.Custom.Context
Green.Template.Custom.DateField
Green.Template.Custom.GitField
@ -115,7 +113,6 @@ library
, base >=4.14 && <5
, binary
, bytestring
, config-ini
, data-default
, directory
, filepath
@ -124,6 +121,7 @@ library
, microlens
, microlens-th
, mtl
, network-uri
, optparse-applicative
, pandoc
, parsec
@ -134,6 +132,7 @@ library
, time
, unordered-containers
, vector
, yaml
default-language: Haskell2010
executable author

3
package.yaml

@ -20,7 +20,6 @@ library:
- aeson
- binary
- bytestring
- config-ini
- data-default
- directory
- filepath
@ -31,6 +30,7 @@ library:
- microlens
- microlens-th
- mtl
- network-uri
- optparse-applicative
- pandoc
- parsec
@ -41,6 +41,7 @@ library:
- time
- unordered-containers
- vector
- yaml
executables:
site:

0
site/_pages/404.md → site/_errors/404.md

5
site/_errors/500.md

@ -0,0 +1,5 @@
---
title: 500 Server Error
layout: page
---
The field has been caught with its pants brown. It will be green again soon.

14
site/_layouts/skeleton.html

@ -6,19 +6,19 @@
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta rel="canonical" href="{{siteRoot}}{{url}}">
<meta property="og:title" content="{{title}}">
<meta property="og:url" content="{{siteRoot}}{{url}}">
<meta property="og:image" content="{{siteRoot}}{{og_image | default '/grass_og-image.png'}}">
<title>{{siteTitle}}{{#if title}} - {{title}}{{#end}}</title>
{{#if author}}<link rel="author" href="{{siteRoot}}/" content="{{author}}">{{#end}}
<link rel="shortcut icon" sizes="16x16 32x32 48x48 64x64 96x96 128x128 256x256" href="{{siteRoot}}/favicon.ico">
<meta property="og:title" content="{{title}}">
<meta property="og:url" content="{{siteRoot}}{{url}}">
<meta property="og:image:url" content="{{siteRoot}}{{og.image.url | default '/grass_og-image.png'}}">
<meta property="og:image:alt" content="{{og.image.alt | default siteTitle}}">
<link rel="stylesheet" href="/css/main.css">
<script src="/js/main.js"></script>
<script src="{{route 'js/main.js'}}"></script>
</head>
<body class='{{bodyClass | default "default"}}'>
<body class="{{bodyClass | default 'default'}}">
{{body}}
</body>
</html>

BIN
site/images/reasons-my-website-is-offline/card-3072.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

13
site/sitemap.xml

@ -7,10 +7,15 @@
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
{{-#for pages}}
<url>
<loc>{{siteRoot}}{{url}}</loc>
<lastmod>{{#if updated}}{{updated}}{{#else if date}}{{date}}{{#end}}</lastmod>
<changefreq>{{#if changefreq}}{{changefreq}}{{#else}}weekly{{#end}}</changefreq>
<priority>{{#if priority}}{{priority}}{{#else}}0.8{{#end}}</priority>
<loc>{{siteRoot}}{{url | escapeHtmlUri}}</loc>
<lastmod>{{
updated
| default date
| default currentDate
| dateAs robotTime
}}</lastmod>
<changefreq>{{changefreq | default "monthly"}}</changefreq>
<priority>{{priority | default 0.8}}</priority>
</url>
{{-#end}}
</urlset>

6
src/Green.hs

@ -1,6 +1,6 @@
module Green where
import qualified Data.Text.IO as TIO
import qualified Data.ByteString as BS
import Data.Time
import Green.Command
import Green.Common
@ -27,10 +27,10 @@ loadSiteConfig :: IO SiteConfig
loadSiteConfig = do
env <- getEnvironment
time <- utcToZonedTime <$> getCurrentTimeZone <*> getCurrentTime
configIniText <- TIO.readFile "config.ini"
configYamlBs <- BS.readFile "config.yaml"
siteConfig <-
either fail return $
parseConfigIni env defaultTimeLocale time configIniText
parseConfigYaml env defaultTimeLocale time configYamlBs
putStrLn $ replicate 80 '-'
print siteConfig

4
src/Green/Command.hs

@ -41,5 +41,5 @@ createDraft :: SiteConfig -> CreateDraftOpts -> IO ()
createDraft _ (CreateDraftOpts title maybeCategory) =
putStrLn $ "Writing post '" ++ title ++ "' to file " ++ draftFilePath
where
draftFilePath = categoryPrefix ++ kebabCase title ++ ".md"
categoryPrefix = maybe "" ((++ "/") . kebabCase) maybeCategory
draftFilePath = categoryPrefix ++ camelToKebab title ++ ".md"
categoryPrefix = maybe "" ((++ "/") . camelToKebab) maybeCategory

193
src/Green/Config.hs

@ -1,15 +1,16 @@
module Green.Config where
import Data.Ini.Config
import Data.Text (Text)
import Data.Aeson.Types
import Data.ByteString (ByteString)
import qualified Data.Text as T
import Data.Yaml
import Green.Common
import Green.Lens
import Hakyll.Core.Configuration as HC
import qualified Hakyll as H
data SiteDebug = SiteDebug
{ _debugPreview :: Bool,
_debugRawCss :: Bool
_debugInflateCss :: Bool
}
deriving stock (Show)
@ -19,31 +20,67 @@ defaultSiteDebug :: SiteDebug
defaultSiteDebug =
SiteDebug
{ _debugPreview = False,
_debugRawCss = False
_debugInflateCss = False
}
instance FromJSON SiteDebug where
parseJSON = withObject "SiteDebug" \debug ->
SiteDebug
<$> debug .:? "preview" .!= (defaultSiteDebug ^. debugPreview)
<*> debug .:? "inflate-css" .!= (defaultSiteDebug ^. debugInflateCss)
data SiteInfo = SiteInfo
{ _siteRoot :: String,
_siteTitle :: String,
_siteDescription :: String,
_siteAuthorName :: String,
_siteAuthorEmail :: String,
_siteLinkedInProfile :: String,
_siteGitWebUrl :: String
}
deriving stock (Show)
makeLenses ''SiteInfo
instance FromJSON SiteInfo where
parseJSON = withObject "SiteInfo" \info ->
SiteInfo
<$> info .: "root"
<*> info .: "title"
<*> info .:? "description" .!= ""
<*> info .: "author-name"
<*> info .: "author-email"
<*> info .: "linkedin-profile"
<*> info .: "git-web-url"
data SiteDisplayFormat = SiteDisplayFormat
{ _displayDateLongFormat :: String,
_displayDateShortFormat :: String,
_displayTimeFormat :: String,
_displayRobotDate :: String,
_displayRobotTime :: String,
_displayImageWidths :: [Int]
}
deriving stock (Show)
makeLenses ''SiteDisplayFormat
instance FromJSON SiteDisplayFormat where
parseJSON = withObject "SiteDisplayFormat" \format ->
SiteDisplayFormat
<$> format .: "date-long-format"
<*> format .: "date-short-format"
<*> format .: "time-format"
<*> format .: "robot-date"
<*> format .: "robot-time"
<*> format .:? "image-widths" .!= []
data SiteConfig = SiteConfig
{ _siteEnv :: [(String, String)],
_siteRoot :: String,
_siteTitle :: String,
_siteDescription :: String,
_siteAuthorName :: String,
_siteAuthorEmail :: String,
_siteLinkedInProfile :: String,
_siteGitWebUrl :: String,
_siteInfo :: SiteInfo,
_siteDebug :: SiteDebug,
_siteHakyllConfiguration :: Configuration,
_siteTime :: ZonedTime,
_siteHakyllConfiguration :: H.Configuration,
_siteCurrentTime :: ZonedTime,
_siteTimeLocale :: TimeLocale,
_siteDisplayFormat :: SiteDisplayFormat
}
@ -55,11 +92,11 @@ siteFeedConfiguration = to f
where
f siteConfig =
FeedConfiguration
{ feedTitle = siteConfig ^. siteTitle,
feedRoot = siteConfig ^. siteRoot,
feedAuthorName = siteConfig ^. siteAuthorName,
feedAuthorEmail = siteConfig ^. siteAuthorEmail,
feedDescription = siteConfig ^. siteDescription
{ feedTitle = siteConfig ^. siteInfo . siteTitle,
feedRoot = siteConfig ^. siteInfo . siteRoot,
feedAuthorName = siteConfig ^. siteInfo . siteAuthorName,
feedAuthorEmail = siteConfig ^. siteInfo . siteAuthorEmail,
feedDescription = siteConfig ^. siteInfo . siteDescription
}
siteDestinationDirectory :: Lens' SiteConfig FilePath
@ -81,17 +118,18 @@ instance Show SiteConfig where
show config =
intercalate "\n" $
[ "SiteConfig:",
" Root: " <> show (config ^. siteRoot),
" Title: " <> show (config ^. siteTitle),
" Description: " <> show (config ^. siteDescription),
" AuthorName: " <> show (config ^. siteAuthorName),
" AuthorEmail: " <> show (config ^. siteAuthorEmail),
" LinkedInProfile: " <> show (config ^. siteLinkedInProfile),
" GitWebUrl: " <> show (config ^. siteGitWebUrl),
" Time: " <> show (config ^. siteTime),
" Time: " <> show (config ^. siteCurrentTime),
" SiteInfo:",
" Root: " <> show (config ^. siteInfo . siteRoot),
" Title: " <> show (config ^. siteInfo . siteTitle),
" Description: " <> show (config ^. siteInfo . siteDescription),
" AuthorName: " <> show (config ^. siteInfo . siteAuthorName),
" AuthorEmail: " <> show (config ^. siteInfo . siteAuthorEmail),
" LinkedInProfile: " <> show (config ^. siteInfo . siteLinkedInProfile),
" GitWebUrl: " <> show (config ^. siteInfo . siteGitWebUrl),
" Debug:",
" Preview: " <> show (config ^. siteDebug . debugPreview),
" RawCss: " <> show (config ^. siteDebug . debugRawCss),
" InflateCss: " <> show (config ^. siteDebug . debugInflateCss),
" HakyllConfiguration:",
" DestinationDirectory: " <> show (config ^. siteDestinationDirectory),
" ProviderDirectory: " <> show (config ^. siteProviderDirectory),
@ -101,71 +139,50 @@ instance Show SiteConfig where
" DateLongFormat: " <> show (config ^. siteDisplayFormat . displayDateLongFormat),
" DateShortFormat: " <> show (config ^. siteDisplayFormat . displayDateShortFormat),
" TimeFormat: " <> show (config ^. siteDisplayFormat . displayTimeFormat),
" RobotDate: " <> show (config ^. siteDisplayFormat . displayRobotDate),
" RobotTime: " <> show (config ^. siteDisplayFormat . displayRobotTime),
" ImageWidths: " <> show (config ^. siteDisplayFormat . displayImageWidths),
" Env:",
" " <> intercalate "\n " ((\(key, val) -> key <> "=" <> show val) <$> config ^. siteEnv)
]
hasEnvFlag :: String -> [(String, String)] -> Bool
hasEnvFlag f e = isJust (lookup f e)
parseConfigIni :: [(String, String)] -> TimeLocale -> ZonedTime -> Text -> Either String SiteConfig
parseConfigIni env timeLocale time iniText = parseIniFile iniText do
hakyllConfiguration <- section "Hakyll" do
providerDirectory' <- fieldOf "providerDirectory" string
destinationDirectory' <- fieldOf "destinationDirectory" string
allowedFiles <- fieldListOf "allowedFiles" string
return
HC.defaultConfiguration
{ providerDirectory = providerDirectory',
destinationDirectory = destinationDirectory',
ignoreFile = customIgnoreFile allowedFiles
customIgnoreFile :: Foldable t => t FilePath -> FilePath -> Bool
customIgnoreFile allowedFiles path =
H.ignoreFile H.defaultConfiguration path
&& takeFileName path `notElem` allowedFiles
parseHakyllConfigurationJSON :: Value -> Parser H.Configuration
parseHakyllConfigurationJSON = withObject "Hakyll.Core.Configuration" \config ->
initConfig
<$> config .: "provider-directory"
<*> config .: "destination-directory"
<*> config .:? "allowed-files" .!= []
where
initConfig providerDirectory' destinationDirectory' allowedFiles =
H.defaultConfiguration
{ H.providerDirectory = providerDirectory',
H.destinationDirectory = destinationDirectory',
H.ignoreFile = customIgnoreFile allowedFiles
}
debugSettings <- sectionDef "Debug" defaultSiteDebug do
SiteDebug
<$> configEnvFlag "preview" "SITE_PREVIEW" False env
<*> configEnvFlag "rawCss" "SITE_RAW_CSS" False env
displayFormat <- section "DisplayFormats" do
SiteDisplayFormat
<$> fieldOf "dateLongFormat" string
<*> fieldOf "dateShortFormat" string
<*> fieldOf "timeFormat" string
<*> fieldListOf "imageWidths" number
section "Site" do
SiteConfig env
<$> fieldOf "root" string
<*> fieldOf "title" string
<*> (fieldOf "description" string <|> return "")
<*> fieldOf "authorName" string
<*> fieldOf "authorEmail" string
<*> fieldOf "linkedInProfile" string
<*> fieldOf "gitWebUrl" string
<*> pure debugSettings
<*> pure hakyllConfiguration
<*> pure time
<*> pure timeLocale
<*> pure displayFormat
where
customIgnoreFile allowedFiles path =
ignoreFile defaultConfiguration path
&& takeFileName path `notElem` allowedFiles
fieldListOf :: Text -> (Text -> Either String a) -> SectionParser [a]
fieldListOf k p = fieldDefOf k (listWithSeparator "," p) []
configEnvFlag :: String -> String -> Bool -> [(String, String)] -> SectionParser Bool
configEnvFlag configKey envKey defaultValue env =
case lookup envKey env of
Just _ -> return True
Nothing -> fieldFlagDef (T.pack configKey) defaultValue
configEnvMbOf :: String -> String -> (Text -> Either String a) -> [(String, String)] -> SectionParser (Maybe a)
configEnvMbOf configKey envKey parseFn env =
fieldFromEnv <|> fieldMbOf (T.pack configKey) parseFn
parseSiteConfigJSON :: [(String, String)] -> TimeLocale -> ZonedTime -> Value -> Parser SiteConfig
parseSiteConfigJSON env timeLocale time = withObject "SiteConfig" \allConfig -> do
config <- allConfig .: T.pack envKey
SiteConfig env
<$> config .: "site-info"
<*> (overrideDebugSettings <$> config .:? "debug-settings" .!= defaultSiteDebug)
<*> (config .: "hakyll-config" >>= parseHakyllConfigurationJSON)
<*> pure time
<*> pure timeLocale
<*> config .: "display-formats"
where
fieldFromEnv = sequence $ getValue . parseFn . T.pack <$> lookup envKey env
getValue (Left e) = error e
getValue (Right v) = return v
envKey = fromMaybe "default" $ lookup "SITE_ENV" env
overrideDebugSettings debug =
debug
& debugInflateCss %~ (\x -> maybe x read $ lookup "SITE_INFLATE_CSS" env)
& debugPreview %~ (\x -> maybe x read $ lookup "SITE_PREVIEW" env)
parseConfigYaml :: [(String, String)] -> TimeLocale -> ZonedTime -> ByteString -> Either String SiteConfig
parseConfigYaml env timeLocale time =
first prettyPrintParseException . decodeEither'
>=> parseEither (parseSiteConfigJSON env timeLocale time)

22
src/Green/Site.hs

@ -6,7 +6,6 @@ import Green.Site.Blog
import Green.Site.BrokenLinks
import Green.Site.Code
import Green.Site.Css
import Green.Site.Download
import Green.Site.Feed
import Green.Site.HomePage
import Green.Site.Images
@ -19,18 +18,17 @@ import Green.Template.Custom.Context
site :: SiteConfig -> Rules ()
site config = do
let context = customContext config
brokenLinks
images
js
scss config
downloads
code
templates
blog context
feed
homePage context
pages context
robotsTxt context
sitemap context
brokenLinks
templatesDependency <- templates
rulesExtraDependencies [templatesDependency] do
let context = customContext config
homePage context
pages context
blog context
code
feed
sitemap context
robotsTxt context

71
src/Green/Site/Blog.hs

@ -7,16 +7,22 @@ import Green.Template
import Green.Template.Custom
import qualified Hakyll as H
buildBlogCategories :: (H.MonadMetadata m) => m Tags
buildBlogCategories = buildCategories "_posts/**" makeCategoryId
buildBlogTags :: (H.MonadMetadata m) => m Tags
buildBlogTags = buildTags "_posts/**" makeTagId
blog :: Context String -> Rules ()
blog context = do
categories <- buildCategories "_posts/**" makeCategoryId
tags <- buildTags "_posts/**" makeTagId
categories <- buildBlogCategories
tags <- buildBlogTags
blogHome categories tags context
posts context
archives context
-- categoriesPages categories context
categoriesPages categories context
tagsPages tags context
draftsIndex context
@ -36,10 +42,9 @@ blogHome categories tags context =
<> recentPosts
<> postContext
<> context
getResourceBody
>>= contentCompiler blogContext
>>= layoutCompiler blogContext
>>= relativizeUrls
(getResourceBody, blogContext) `applyTemplates` do
contentTemplate
layoutTemplate
archives :: Context String -> Rules ()
archives context = do
@ -50,10 +55,9 @@ archives context = do
let archivesContext =
constField "posts" (itemListValue (postContext <> context) publishedPosts)
<> context
getResourceBody
>>= contentCompiler archivesContext
>>= layoutCompiler archivesContext
>>= relativizeUrls
(getResourceBody, archivesContext) `applyTemplates` do
contentTemplate
layoutTemplate
draftsIndex :: Context String -> Rules ()
draftsIndex context = do
@ -64,21 +68,19 @@ draftsIndex context = do
let draftsContext =
constField "posts" (itemListValue (postContext <> context) draftPosts)
<> context
getResourceBody
>>= contentCompiler draftsContext
>>= layoutCompiler draftsContext
>>= relativizeUrls
(getResourceBody, draftsContext) `applyTemplates` do
contentTemplate
layoutTemplate
posts :: Context String -> Rules ()
posts context = do
match "_posts/**" do
route postsRoute
compile $
getResourceBody
>>= contentCompiler postsContext
>>= snapshotCompiler [publishedPostsSnapshot]
>>= layoutCompiler postsContext
>>= relativizeUrls
(getResourceBody, postsContext) `applyTemplates` do
contentTemplate
saveSnapshots [publishedPostsSnapshot]
layoutTemplate
where
postsContext = postContext <> context
@ -94,11 +96,10 @@ drafts context = do
match "_drafts/**" do
route draftsRoute
compile $
getResourceBody
>>= contentCompiler draftsContext
>>= snapshotCompiler [draftPostsSnapshot]
>>= layoutCompiler draftsContext
>>= relativizeUrls
(getResourceBody, draftsContext) `applyTemplates` do
contentTemplate
saveSnapshots [draftPostsSnapshot]
layoutTemplate
where
draftsContext = postContext <> context
@ -121,12 +122,10 @@ categoriesPages categories context =
<> constField "posts" (itemListValue (postContext <> context) categoryPosts)
<> constField "layout" ("page" :: String)
<> context
template <- loadBody "_templates/posts-under-category.html"
makeItem ""
>>= applyTemplate' template categoryContext
>>= pandocCompiler
>>= layoutCompiler categoryContext
>>= relativizeUrls
(makeItem "", categoryContext) `applyTemplates` do
loadAndApplyTemplate "_templates/posts-under-category.html"
withCompiler pandocCompiler
layoutTemplate
tagsPages :: Tags -> Context String -> Rules ()
tagsPages tags context =
@ -140,12 +139,10 @@ tagsPages tags context =
<> constField "posts" (itemListValue (postContext <> context) tagPosts)
<> constField "layout" ("page" :: String)
<> context
template <- loadBody "_templates/posts-under-tag.html"
makeItem ""
>>= applyTemplate' template tagsContext
>>= pandocCompiler
>>= layoutCompiler tagsContext
>>= relativizeUrls
(makeItem "", tagsContext) `applyTemplates` do
loadAndApplyTemplate "_templates/posts-under-tag.html"
withCompiler pandocCompiler
layoutTemplate
postContext :: Context String
postContext =

2
src/Green/Site/Css.hs

@ -12,7 +12,7 @@ scss siteConfig = do
route $ setExtension "css"
compile do
css <- withItemBody compileSass =<< getResourceString
if siteConfig ^. siteDebug . debugRawCss
if siteConfig ^. siteDebug . debugInflateCss
then return css
else return $ compressCss <$> css
where

9
src/Green/Site/Download.hs

@ -1,9 +0,0 @@
module Green.Site.Download where
import Green.Common
downloads :: Rules ()
downloads = do
match "downloads/**" do
route $ setExtension ".txt"
compile copyFileCompiler

7
src/Green/Site/HomePage.hs

@ -15,7 +15,6 @@ homePage siteContext =
constField "recentPosts" (itemListValue siteContext recentPosts)
<> teaserField "teaser" publishedPostsSnapshot
<> siteContext
getResourceBody
>>= contentCompiler context
>>= layoutCompiler context
>>= relativizeUrls
(getResourceBody, context) `applyTemplates` do
contentTemplate
layoutTemplate

15
src/Green/Site/Pages.hs

@ -1,18 +1,19 @@
module Green.Site.Pages where
import Control.Monad (forM_)
import Green.Common
import Green.Route
import Green.Template.Custom
import Hakyll (fromGlob)
pages :: Context String -> Rules ()
pages context =
match "_pages/**" do
pages context = forM_ ["_pages/", "_errors/"] \dir -> do
match (fromGlob $ dir ++ "**") do
route $
gsubRoute "_pages/" (const "")
gsubRoute dir (const "")
`composeRoutes` setExtension "html"
`composeRoutes` indexRoute
compile $
getResourceBody
>>= contentCompiler context
>>= layoutCompiler context
>>= relativizeUrls
(getResourceBody, context) `applyTemplates` do
contentTemplate
layoutTemplate

51
src/Green/Site/Sitemap.hs

@ -1,10 +1,11 @@
module Green.Site.Sitemap where
import Green.Common
import Green.Compiler (loadExistingSnapshots)
import Green.Site.Blog (loadPublishedPosts)
import Green.Site.Blog (buildBlogCategories, buildBlogTags, loadPublishedPosts)
import Green.Template
import Hakyll (recentFirst)
import Green.Template.Custom
import Hakyll ((.||.))
import qualified Hakyll as H
sitemap :: Context String -> Rules ()
sitemap siteContext =
@ -12,31 +13,33 @@ sitemap siteContext =
route idRoute
compile do
context <- sitemapContext siteContext
getResourceBody
>>= applyAsTemplate' context
(getResourceBody, context) `applyTemplates` applyAsTemplate
sitemapContext :: Context String -> Compiler (Context String)
sitemapContext siteContext = do
pages <- concat <$> mapM (`loadExistingSnapshots` "_content") pagePatterns
posts <- recentFirst =<< loadPublishedPosts
let context =
forItemField "updated" latestPostPatterns (\_ -> latestPostUpdated posts)
<> constField "pages" (itemListValue context (pages <> posts))
categoriesPages <- H.loadAll . tagsPattern =<< buildBlogCategories :: Compiler [Item String]
tagsPages <- H.loadAll . tagsPattern =<< buildBlogTags :: Compiler [Item String]
blogPage <- H.load "blog.html" :: Compiler (Item String)
posts <- H.recentFirst =<< loadPublishedPosts
pages <- H.loadAll pagesPattern :: Compiler [Item String]
let allPages =
pages
<> [blogPage]
<> categoriesPages
<> tagsPages
<> posts
context =
constField "updated" (latestPostUpdated posts)
<> constField "pages" (itemListValue context allPages)
<> siteContext
return context
where
pagePatterns =
[ "index.html",
"*.md",
"blog.html",
"archives.html"
]
latestPostPatterns =
fromFilePath
<$> [ "blog.html",
"archives.html",
"categories.html",
"tags.html"
]
pagesPattern =
foldl1 (.||.) $
[ H.fromGlob "_pages/**",
"index.html",
"archives.html"
]
latestPostUpdated (latestPost : _) = tplWithItem latestPost (unContext siteContext "updated")
latestPostUpdated _ = tplTried "latest post updated"
latestPostUpdated [] = tplTried "latest post updated"
tagsPattern tags = H.fromList (H.tagsMap tags <&> \(tag, _) -> H.tagsMakeId tags tag)

5
src/Green/Site/Templates.hs

@ -4,10 +4,11 @@ import Green.Common
import Green.Template
import Hakyll.Core.Identifier.Pattern ((.||.))
templates :: Rules ()
templates =
templates :: Rules Dependency
templates = do
match templatePattern do
compile getResourceTemplate
makePatternDependency templatePattern
where
templatePattern =
"_layouts/**"

19
src/Green/Template/Compiler.hs

@ -23,28 +23,27 @@ compileTemplateItem item = do
let filePath = toFilePath $ itemIdentifier item
either (fail . show) return $ parse filePath (itemBody item)
evalTemplate :: Context String -> (Item String -> TemplateRunner String (Item String)) -> Item String -> Compiler (Item String)
evalTemplate context f item = evalStateT (f item) $ templateRunner context item
loadTemplate :: Identifier -> TemplateRunner a Template
loadTemplate = lift . fmap itemBody . load
loadAndApplyTemplate' :: Identifier -> Context String -> Item String -> Compiler (Item String)
loadAndApplyTemplate' id' context item = do
let s = templateRunner context item
evalStateT (loadAndApplyTemplate id') s
evalStateT (loadAndApplyTemplate id' >> tplItem) s
loadAndApplyTemplate :: Identifier -> TemplateRunner String (Item String)
loadAndApplyTemplate :: Identifier -> TemplateRunner String ()
loadAndApplyTemplate =
loadTemplate
>=> applyTemplate
>=> lift . makeItem
>=> tplPushItem
applyAsTemplate :: Item String -> TemplateRunner String (Item String)
applyAsTemplate :: TemplateRunner String ()
applyAsTemplate =
lift . compileTemplateItem
>=> applyTemplate
>=> lift . makeItem
tplModifyItem do
lift . compileTemplateItem
>=> applyTemplate
>=> lift . makeItem
-- | Applies an item as a template to itself.
applyAsTemplate' :: Context String -> Item String -> Compiler (Item String)
@ -117,6 +116,8 @@ eval = \case
StringValue name -> return name
x -> tplFail $ "invalid field " ++ show x ++ " near " ++ show (getExpressionPos field)
>>= unContext target'
EmptyValue -> return EmptyValue
UndefinedValue {} -> return EmptyValue
x -> tplFail $ "invalid context " ++ show x ++ " near " ++ show pos
FilterExpression x f _ -> apply f x
ContextExpression pairs _ -> do

27
src/Green/Template/Context.hs

@ -44,6 +44,33 @@ templateRunner context item =
tplItem :: TemplateRunner a (Item a)
tplItem = gets $ head . tplItemStack
tplModifyItem :: (Item a -> TemplateRunner a (Item a)) -> TemplateRunner a ()
tplModifyItem f =
tplItem
>>= f
>>= tplReplaceItem
tplReplaceItem :: Item a -> TemplateRunner a ()
tplReplaceItem item = do
stack <-
gets tplItemStack <&> \case
[] -> [item]
_ : rest -> item : rest
modify \s -> s {tplItemStack = stack}
tplPopItem :: TemplateRunner a (Item a)
tplPopItem =
gets tplItemStack >>= \case
[] -> error "tplPopItem: empty stack"
x : xs -> do
modify \s -> s {tplItemStack = xs}
return x
tplPushItem :: Item a -> TemplateRunner a ()
tplPushItem item = do
stack <- gets tplItemStack
modify \s -> s {tplItemStack = item : stack}
tplContext :: TemplateRunner a (Context a)
tplContext = gets $ head . tplContextStack

37
src/Green/Template/Custom.hs

@ -1,14 +1,47 @@
module Green.Template.Custom
( module Green.Template,
module Green.Template.Custom.Compiler,
module Green.Template.Custom,
module Green.Template.Custom.DateField,
module Green.Template.Custom.GitField,
module Green.Template.Custom.HtmlField,
)
where
import Control.Monad.State.Strict (evalStateT, forM_)
import Data.List (nub)
import Green.Common
import Green.Template
import Green.Template.Custom.Compiler
import Green.Template.Custom.DateField
import Green.Template.Custom.GitField
import Green.Template.Custom.HtmlField
applyTemplates :: (Compiler (Item a), Context a) -> TemplateRunner a () -> Compiler (Item a)
applyTemplates (itemM, context) =
applyTemplatesWith itemM context
applyTemplatesWith :: Compiler (Item a) -> Context a -> TemplateRunner a () -> Compiler (Item a)
applyTemplatesWith itemM context templates =
itemM >>= evalStateT (templates >> tplItem) . templateRunner context
contentTemplate :: TemplateRunner String ()
contentTemplate = do
applyAsTemplate
tplModifyItem $ lift . pandocCompiler
layoutTemplate :: TemplateRunner String ()
layoutTemplate = loadAndApplyTemplate "_layouts/from-context.html"
fileTemplate :: FilePath -> TemplateRunner String ()
fileTemplate filePath =
loadAndApplyTemplate (fromFilePath filePath)
withCompiler :: (Item a -> Compiler (Item a)) -> TemplateRunner a ()
withCompiler compiler =
tplModifyItem $ lift . compiler
saveSnapshots :: [String] -> TemplateRunner String ()
saveSnapshots snapshots = do
item <- tplItem
lift $ forM_ snapshots' (`saveSnapshot` item)
where
snapshots' = nub ("_content" : snapshots)

17
src/Green/Template/Custom/Compiler.hs

@ -1,17 +0,0 @@
module Green.Template.Custom.Compiler where
import Control.Monad.State.Strict
import Data.List (nub)
import Green.Common
import Green.Template
contentCompiler :: Context String -> Item String -> Compiler (Item String)
contentCompiler context = pandocCompiler <=< applyAsTemplate' context
layoutCompiler :: Context String -> Item String -> Compiler (Item String)
layoutCompiler = loadAndApplyTemplate' $ fromFilePath "_layouts/from-context.html"
snapshotCompiler :: [String] -> Item String -> Compiler (Item String)
snapshotCompiler snapshots item = foldM (flip saveSnapshot) item snapshots'
where
snapshots' = nub ("_content" : snapshots)

15
src/Green/Template/Custom/Context.hs

@ -33,13 +33,18 @@ customContext config = self
layoutField "applyLayout" "_layouts",
dateFields config,
gitCommits config,
constField "siteTitle" (config ^. siteTitle),
constField "siteRoot" (config ^. siteRoot),
constField "linkedInProfile" (config ^. siteLinkedInProfile),
constField "authorEmail" (config ^. siteAuthorEmail),
constField "siteTitle" (config ^. siteInfo . siteTitle),
constField "siteRoot" (config ^. siteInfo . siteRoot),
constField "currentTime" (formatTime timeLocale robotTime currentTime),
constField "linkedInProfile" (config ^. siteInfo . siteLinkedInProfile),
constField "authorEmail" (config ^. siteInfo . siteAuthorEmail),
escapeHtmlField,
escapeHtmlUriField,
imgField,
youtubeField,
codeField,
linkField,
defaultFields
]
timeLocale = config ^. siteTimeLocale
robotTime = config ^. siteDisplayFormat . displayRobotTime
currentTime = config ^. siteCurrentTime

9
src/Green/Template/Custom/DateField.hs

@ -10,16 +10,18 @@ import Green.Util
dateFields :: SiteConfig -> Context a
dateFields config =
mconcat
[ dateField "date" timeLocale,
[ dateField "date" timeLocale currentTime,
publishedField "published" timeLocale,
updatedField "updated" timeLocale,
constField "longDate" (displayFormat ^. displayDateLongFormat),
constField "shortDate" (displayFormat ^. displayDateShortFormat),
constField "timeOnly" (displayFormat ^. displayTimeFormat),
constField "robotTime" (displayFormat ^. displayRobotTime),
dateFormatField "dateAs" timeLocale
]
where
timeLocale = config ^. siteTimeLocale
currentTime = config ^. siteCurrentTime
displayFormat = config ^. siteDisplayFormat
dateFormatField :: String -> TimeLocale -> Context a
@ -30,13 +32,14 @@ dateFormatField key timeLocale = functionField2 key f
return $ formatTime timeLocale dateFormat date
deserializeTime = parseTimeM' timeLocale normalizedFormat
dateField :: String -> TimeLocale -> Context a
dateField key timeLocale = field key f
dateField :: String -> TimeLocale -> ZonedTime -> Context a
dateField key timeLocale currentTime = field key f
where
f item =
lift $
dateFromMetadata timeLocale ["date", "published"] item
<|> dateFromFilePath timeLocale item
<|> return (formatTime timeLocale "%Y-%m-%dT%H:%M:%S%Ez" currentTime)
publishedField :: String -> TimeLocale -> Context a
publishedField key timeLocale = field key f

2
src/Green/Template/Custom/GitField.hs

@ -22,7 +22,7 @@ instance Binary GitFile where
gitCommits :: SiteConfig -> Context a
gitCommits config =
mconcat
[ constField "gitWebUrl" (config ^. siteGitWebUrl),
[ constField "gitWebUrl" (config ^. siteInfo . siteGitWebUrl),
field "gitSha1" (gitSha1Compiler root),
field "gitMessage" (gitMessageCompiler root),
field "gitBranch" gitBranchCompiler,

31
src/Green/Template/Custom/HtmlField.hs

@ -1,9 +1,9 @@
module Green.Template.Custom.HtmlField where
import qualified Data.HashMap.Strict as M
import Green.Common
import Green.Template
import Green.Util (dropIndex)
import Network.URI (escapeURIString, isUnescapedInURI)
-- | Trims @index.html@ from @$url$@'s
trimmedUrlField :: String -> Context String
@ -35,27 +35,24 @@ imgField = functionField "img" f
defaults = defaultKeys ["id", "src", "title", "alt"]
f (imgFields :: Context String) =
tplWithContext (imgFields <> defaults) do
template <- loadTemplate (fromFilePath "_templates/image.html")
applyTemplate template
loadAndApplyTemplate "_templates/image.html"
itemBody <$> tplPopItem
youtubeField :: Context String
youtubeField = functionField "youtube" f
where
defaults = defaultKeys ["id", "video", "title"]
f (ytFields :: Context String) = do
f (ytFields :: Context String) =
tplWithContext (ytFields <> defaults) do
itemBody <$> loadAndApplyTemplate (fromFilePath "_templates/youtube.html")
loadAndApplyTemplate "_templates/youtube.html"
itemBody <$> tplPopItem
linkField :: Context String
linkField = functionField2 "link" f
escapeHtmlField :: Context String
escapeHtmlField = functionField "escapeHtml" f
where
f (linkPath :: String) (linkContent :: [Block]) = do
let fields =
hashMapField $
M.fromList
[ ("linkPath", intoValue linkPath :: ContextValue String),
("linkContent", intoValue linkContent)
]
tplWithContext fields do
template <- loadTemplate (fromFilePath "_templates/link.html")
applyTemplate template
f = return . escapeHtml
escapeHtmlUriField :: Context String
escapeHtmlUriField = functionField "escapeHtmlUri" f
where
f = return . escapeHtml . escapeURIString isUnescapedInURI

4
src/Green/Template/Field.hs

@ -51,8 +51,8 @@ includeField key basePath = functionField key f
let filePath' = basePath </> filePath <.> "html"
tplWithCall (filePath' ++ " via " ++ show key) do
context <- tplContext
result <- loadAndApplyTemplate (fromFilePath filePath')
return $ itemValue context result
loadAndApplyTemplate (fromFilePath filePath')
itemValue context <$> tplPopItem
layoutField :: String -> FilePath -> Context String
layoutField key basePath = functionField2 key f

1
src/Green/Template/Source/Parser.hs

@ -146,6 +146,7 @@ accessExpression = simpleExpression `chainl1` try accessed <?> "AccessExpression
accessed = withPosition do
withTag DotToken
return f
f pos x (NameExpression id' pos') = f pos x (StringExpression id' pos')
f pos x y = AccessExpression x y pos
simpleExpression :: Parser Expression

17
src/Green/Template/Tags.hs

@ -6,14 +6,21 @@ module Green.Template.Tags
)
where
import Data.Char (toLower)
import Green.Common
import Green.Template.Context
import Green.Util (dropIndex)
import Green.Util
import Hakyll (MonadMetadata, Tags, buildTags, getTags, renderTagCloudWith)
import qualified Hakyll
normalizeTag :: String -> String
normalizeTag tag = toLower <$> sanitized
where
sanitized =
wordsToKebab $ camelToKebab <$> words tag
makeTagId :: String -> Identifier
makeTagId = Hakyll.fromCapture "tags/*.html"
makeTagId = Hakyll.fromCapture "tags/*.html" . normalizeTag
tagsField :: String -> Context a
tagsField key = field key $ lift . getTags . itemIdentifier
@ -36,8 +43,12 @@ tagLinksField key = tagLinksFieldWith key getTags
categoryLinksField :: String -> Context a