mirror of
https://github.com/srid/ema.git
synced 2024-12-02 09:15:10 +03:00
123 lines
5.2 KiB
Markdown
123 lines
5.2 KiB
Markdown
# Tutorial
|
|
|
|
Make sure that you have have followed [the previous section](start.md) in order to have the [template repo](https://github.com/srid/ema-template) checked out and running locally. Here, **our goal** is to replace the source code of the template repo and write a basic site from scratch.
|
|
|
|
1. Follow the template repo's [README](https://github.com/srid/ema-template#getting-started) and have it open in Visual Studio Code while running the dev server. Your website should be viewable at <http://localhost:9001/>
|
|
1. Open `src/Main.hs`
|
|
1. Delete everything in it, and replace it with the following
|
|
|
|
```haskell
|
|
module Main where
|
|
|
|
import qualified Ema
|
|
|
|
main :: IO ()
|
|
main = do
|
|
let speaker :: Text = "Ema"
|
|
Ema.runEmaPure $ \_ ->
|
|
encodeUtf8 $ "<b>Hello</b>, from " <> speaker
|
|
```
|
|
|
|
This is the *minimum* amount of code necessary to run an Ema site. Notice that as you replace and save this file, your browser (which is at <http://locahost:9001>) will [hot reload](concepts/hot-reload.md) to display "Hello, Ema". Congratulations, you just created your first website!
|
|
|
|
## Expanding on Hello World
|
|
|
|
Okay, but that's just *one* page. But we want to add a second page. And might as well add more content than "Hello, Ema". Let's do that next. The first step is define the [route](guide/routes.md) type that corresponds to our site's pages. Add the following:
|
|
|
|
```haskell
|
|
data Route
|
|
= Index -- Corresponds to /
|
|
| About -- Corresponds to /about
|
|
deriving (Bounded, Enum, Show)
|
|
```
|
|
|
|
Next, let's define a [model](guide/model.md). A model will hold the state of our website used to render its HTML. Let's put the `speaker` variable in it, as that's all we are using:
|
|
|
|
```haskell
|
|
data Model = Model { speaker :: Text }
|
|
```
|
|
|
|
We should now tell Ema how to convert our `Route` to actual file paths (which is used to determine the URL too). Let's do that by making an instance of the `Ema` [class](guide/class.md):
|
|
|
|
```haskell
|
|
import Ema (Ema (..))
|
|
|
|
instance Ema Model Route where
|
|
encodeRoute _model = \case
|
|
Index -> "index.html" -- To /
|
|
About -> "about.html" -- To /about
|
|
decodeRoute _model = \case
|
|
"index.html" -> Just Index -- From /
|
|
"about.html" -> Just About -- From /about
|
|
_ -> Nothing -- Everything else, are bad routes
|
|
```
|
|
|
|
Now, we write the `main` entry point:
|
|
|
|
```haskell
|
|
import Control.Concurrent (threadDelay)
|
|
import qualified Data.LVar as LVar
|
|
|
|
main :: IO ()
|
|
main = do
|
|
Ema.runEma render $ \_act model -> do
|
|
LVar.set model $ Model "Ema"
|
|
liftIO $ threadDelay maxBound
|
|
```
|
|
|
|
The `runEma` function is explained [here](guide/class.md), but in brief: it takes a render function (see below) as well as an IO action that allows us to create and update the model [lvar](concepts/lvar.md). Note that `threadDelay maxBound` here? That is because our IO action must not exit; in the dev server mode of real-world websites, you would continue to monitor the external world (such as Markdown files) and update the model, to facilitate [hot reload](concepts/hot-reload.md) of data used by your site.
|
|
|
|
On final piece of the puzzle is to write the aforementioned `render` function:
|
|
|
|
```haskell
|
|
import Data.Some (Some)
|
|
import qualified Ema
|
|
import qualified Ema.CLI
|
|
import Text.Blaze.Html5 ((!))
|
|
import qualified Text.Blaze.Html5 as H
|
|
import qualified Text.Blaze.Html5.Attributes as A
|
|
import qualified Text.Blaze.Html.Renderer.Utf8 as RU
|
|
|
|
render :: Some Ema.CLI.Action -> Model -> Route -> Ema.Asset LByteString
|
|
render _emaAction model r = Ema.AssetGenerated Ema.Html . RU.renderHtml $
|
|
H.html $ do
|
|
H.head $ do
|
|
H.title "Basic site"
|
|
H.base ! A.href "/" -- This is important.
|
|
H.body $ do
|
|
H.div ! A.class_ "container" $ do
|
|
case r of
|
|
Index -> do
|
|
H.toHtml $
|
|
"You are on the index page. The name is " <> speaker model
|
|
routeElem About "Go to About"
|
|
About -> do
|
|
"You are on the about page. "
|
|
routeElem Index "Go to Index"
|
|
where
|
|
routeElem targetRoute w =
|
|
H.a
|
|
! A.style "text-decoration: underline"
|
|
! A.href (H.toValue $ Ema.routeUrl model targetRoute) $ w
|
|
```
|
|
|
|
If everything compiles, you should see the site update in the web browser. A couple of quick points about the `render` function:
|
|
|
|
1. It should return the raw HTML as a `ByteString`. Here, we use [blaze-html](https://hackage.haskell.org/package/blaze-html) as HTML DSL. You can also use your own HTML templates of course.
|
|
1. It uses `Ema.routeUrl` function to create a URL out of our `Route` type. This function uses the [`Ema` typeclass](guide/class.md), so it uses the `encodeRoute` function defined further above.
|
|
|
|
On final note, you will note that nothing is actually *generated* so far. This is because Ema has been running in the dev server mode, which is quite useful during development. To actually generate the files, you can use the `gen` command when running the [CLI](concepts/cli.md):
|
|
|
|
```sh
|
|
mkdir ~/output
|
|
nix run . -- gen $(pwd)/output
|
|
```
|
|
|
|
## Exercises
|
|
|
|
1. Discover how to manage static files like images and PDFs (hint: see `AssetStatic` in `Ema.Asset`)
|
|
2. What happens if you `throw` an exception or use `error` in the `render` function?
|
|
|
|
{.last}
|
|
[Next]{.next}, checkout the [Guide](guide.md) series for information on specific topics.
|