improved blog tutorial

This commit is contained in:
flupe 2020-09-28 02:58:01 +02:00
parent 47f4f8ed7c
commit d286f603a6
1 changed files with 150 additions and 68 deletions

View File

@ -4,9 +4,13 @@ title: Making a blog from scratch
# Making a blog from scratch
Let's see how to use **achille** for making a static site generator for a blog.
First we decide what will be the structure of our source directory.
We choose the following:
In this tutorial we'll see how to use **achille** for a simple blog generator.
## Content structure
The first step is to settle on the content structure.
For a blog, we will simply store each article in a separate markdown file,
inside the `posts/` folder.
```bash
content
@ -16,87 +20,165 @@ content
   └── 2020-05-21-some-more.md
```
We define the kind of metadata we want to allow in the frontmatter header
of our markdown files:
We require every article to have a title in their Front Matter header.
We also give them the possibility to specify a summary.
```haskell
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAny #-}
```md
---
title: Hello World!
draft: true
---
import GHC.Generics
import Data.Aeson
This is my first article on *this* blog.
It is powered by [achille](https://acatalepsie.fr/projects/achille).
```
## HTML template
Then we figure out how we want to render our blog posts. We need to produce
HTML, and achille doesn't care how we do it, so we are free to use any library
that suits us. My personal favorite is `lucid` so we'll use that here.
```hs
{-# LANGUAGE BlockArguments, OverloadedStrings #-}
import Lucid
import Data.Text (Text)
data Meta = Meta
{ title :: Text
} deriving (Generic, FromJSON)
```
This way we enfore correct metadata when retrieving the content of our files.
Every markdown file will have to begin with the following header for our
generator to proceed:
```markdown
---
title: My first blogpost!
---
```
Then we create a generic template for displaying a page, thanks to lucid:
```haskell
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BlockArguments #-}
import Lucid.Html5
renderPost :: Text -> Text -> Html ()
renderPost title content = wrapContent do
h1_ $ toHtml title
toHtmlRaw content
renderIndex :: [(Text, FilePath)] -> Html ()
renderIndex = wrapContent .
ul_ . mconcat . map \(title, path) ->
li_ $ a_ [href_ path] $ toHtml title
wrapContent :: Html () -> Html ()
wrapContent content = doctypehtml_ do
template :: Html () -> Html ()
template content = doctypehtml_ do
head_ do
meta_ [charset_ "utf-8"]
title_ "my very first blog"
title_ "Jenna's Weblog"
body_ do
header_ $ h1_ "BLOG"
content_
header_ "Jenna's Weblog"
content
footer_ "© Jenna 2020"
```
We define a recipe for rendering every post:
We also tell achille how `Html ()` should be written on disk.
```haskell
buildPosts :: Task IO [(String, FilePath)]
buildPosts =
match "posts/*.md" do
(Meta title, text) <- compilePandocMetadata
saveFileAs (-<.> "html") (renderPost title text)
<&> (title,)
```hs
import Achille.Writable as Writable (Writable)
instance Writable (Html ()) where
write to = Writable.write to . renderBS
```
We can define a simple recipe for rendering the index, given a list of posts:
## Processing articles
```haskell
buildIndex :: [(Text, FilePath)] -> Task IO FilePath
buildIndex posts =
save (renderIndex posts) "index.html"
Now let's define what kind of information is associated with an article.
```hs
{-# LANGUAGE DeriveGeneric, DeriveAny #-}
import GHC.Generics (Generic)
import Data.Aeson (FromJSON)
import Data.Binary (Binary)
import Data.Time (UTCTime)
data PostMeta = PostMeta
{ title :: Text
, summary :: Maybe Text
, draft :: Maybe Bool
} deriving (Generic, Eq, FromJSON)
data Post = Post
{ postTitle :: Text
, postSummary :: Maybe Text
, postIsDraft :: Bool
, postPath :: FilePath
} deriving (Generic, Eq, Binary)
renderPost :: Post -> Text -> Html ()
renderPost post content = template $
article_ do
header_ $ h1 (toHtml $ postTitle post)
toHtmlRaw content
```
Then, it's only a matter of composing the recipes and giving them to **achille**:
Because we derive `FromJSON` for `PostMeta`, we are now able to load a Front
Matter header and convert it directly to a value of type `PostMeta`. **Correct
metadata is thus enforced**. `Post` is a datatype containing all the processed
information about an article, and we derive `Binary` so that we can cache it later.
Notice how we do not store the article content here, there's no use.
```haskell
Now we simply need a recipe for reading an article, processing it and rendering
the appropriate HTML output file:
```hs
import System.FilePath ((-<.>))
import Data.Maybe (fromMaybe)
processPost :: Recipe IO FilePath Post
processPost = do
outputPath <- (-<.> "html") <$> getInput
(meta, doc) <- readPandoc
let post = Post { postTitle = title meta
, postSummary = summary meta
, postIsDraft = fromMaybe False (draft meta)
, postPath = outputPath
}
renderPandoc doc <&> renderPost post >>= write outputPath
return post
```
## Rendering the index
Ok, we now have a recipe for building individual articles, however we want to
be able to display them on the index. We need to filter out drafts. And we need
to sort them from most recent to oldest.
```hs
import Control.Monad (forM_, mapM_)
processPosts :: Task IO ()
processPosts = do
posts <- match "posts/*.md" processPost
let visible = filter (not postIsDraft) posts
watch visible $ write "index.html" (renderIndex visible)
renderIndex :: [Post] -> Html ()
renderIndex posts = do
h2_ "Latest articles"
ul_ $ forM_ posts \ post -> li_ do
a_ [href_ (fromString $ postPath post)] (toHtml $ postTitle post)
forM_ (postSummary post) \summary -> p_ $ toHtml summary
```
## Wrapping things up
Finally, we can forward this task to the top-level achille runner:
```hs
main :: IO ()
main = achille do
posts <- buildPosts
buildIndex posts
main = achille processPosts
```
And that's it, you now have a very minimalist incremental blog generator!
Compile into an executable `blog` and run the command `blog build`. Hold and
behold, if you look into the `_site/` folder, there should be:
```bash
_site
├── index.html
└── posts
   ├── 2020-04-13-hello-world.html
   ├── 2020-04-14-another-article.html
   └── 2020-05-21-some-more.html
```
What's more, your generator is **incremental**. Modify a single article, and
trigger a rebuild.
```bash
$ touch -m content/posts/2020-04-13-hello-world.md
$ blog build
```
You should see that only this article is rebuilt. And because we haven't
actually changed neither the title nor the summary, the index hasn't been rebuilt.
Magic!