mirror of
https://github.com/srid/ema.git
synced 2024-11-22 21:52:56 +03:00
docs: editing, until tutorial
This commit is contained in:
parent
749eb99ada
commit
f4ab820b61
@ -10,10 +10,9 @@ page:
|
||||
:::
|
||||
|
||||
{.text-xl .mb-8}
|
||||
[Ema](https://github.com/srid/ema) is a next-gen [**Haskell**](https://www.srid.ca/haskell) library for building [jamstack-style](https://jamstack.org/) static sites. Ema sites are *change-aware*; in addition to good ol' static site generation, Ema provides a [[live-server|live server]] supporting **fast hot-reload** in the browser, on code *or* data change.
|
||||
[Ema](https://github.com/srid/ema) is a next-gen library for building [jamstack-style](https://jamstack.org/) static sites in [**Haskell**](https://www.srid.ca/haskell). Ema sites are *change-aware*; in addition to good ol' static site generation, Ema provides a [[live-server|live server]] supporting **fast hot-reload** in the browser, on code *or* data change.
|
||||
|
||||
{.text-gray-600}
|
||||
The ultimate purpose of ema is to facilitate creating with ease your own [neuron](https://neuron.zettel.page/),[^emanote] or just about any app that creates a *browser view of arbitrarily changing data* (on disk, database, or whatever). Ema is designed to facilitate creation of apps whose data is normally *edited* via traditional mechanisms (eg: text editor) but *rendered* as a delightful web page - so as to provide an economical read-only view of your data on desktop & mobile. A classic static site is just one type of such [*kind*]{title="See what we did there?"} of apps.
|
||||
{.text-gray-600} The ultimate purpose of ema is to facilitate creating with ease your own [neuron](https://neuron.zettel.page/)^emanote, or just about any app that creates a _browser view of arbitrarily changing data_ (on disk, database, or whatever). In Ema apps, data is _edited_ via traditional mechanisms (e.g., text editor), and its view is _rendered_ as a delightful web page, thus economically providing a read-only view of your data on desktop & mobile. A classic static site is just one type of such [*kind*]{title="See what we did there?"} of apps.
|
||||
|
||||
:::{.my-8}
|
||||
* [[start]]
|
||||
|
@ -10,11 +10,11 @@ The best way to get started with Ema is via **Nix**,[^nix] using the template re
|
||||
1. Clone [the template repository][ema-template] locally
|
||||
1. Run `bin/run` and access the site at <http://localhost:9001>
|
||||
|
||||
Step 3 should start the Ema [[live-server]] displaying a simple website. Go ahead and try modifying the HTML DSL in the Haskell source `./src/Main.hs`, and observe how the browser view updates [[hot-reload|instantly]].
|
||||
Step 3 starts the Ema [[live-server]] displaying a simple website. Try modifying the HTML DSL in the Haskell source `./src/Main.hs`, and observe how the browser-view updates [[hot-reload|instantly]].
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, [[tutorial|in the tutorial series]] we will start from scratch a trivial site and evolve it towards a feature-rich one.
|
||||
[Next]{.next}, [[tutorial|in the tutorial series]] we will create a trivial site from scratch and then evolve it towards a feature-rich one.
|
||||
|
||||
[^nix]: Nix is optional. However, it provides the best development experience. The [template repository][ema-template] may work with plain `cabal`, but this is not officially tested.
|
||||
[^nix]: Nix is optional to use Ema, and the [template repository][ema-template] may work with plain `cabal`. However, Nix provides the best development experience. See [this blog post](https://srid.ca/haskell-nix) if you are a Haskell developer new to Nix.
|
||||
|
||||
[ema-template]: https://github.com/EmaApps/ema-template
|
||||
|
@ -6,7 +6,7 @@ page:
|
||||
|
||||
# Tutorial
|
||||
|
||||
In this tutorial series, we will begin with a hello world Ema app, and then move on to writing a mood tracker that reads a CSV file of moods and renders them as a static site. Source code for this tutorial is available at: https://github.com/srid/MoodTracker-Tutorial
|
||||
In this tutorial series, we will begin with a hello world Ema app and then write a mood tracker that reads a CSV file of moods rendering them as a static site. The source code for this tutorial is available at: https://github.com/srid/MoodTracker-Tutorial
|
||||
|
||||
1. [[00-hello]]
|
||||
2. [[01-routes]]
|
||||
|
@ -4,34 +4,45 @@ order: 0
|
||||
|
||||
# Hello World
|
||||
|
||||
To write a minimal Ema app, we need two things at minimum: a [[route]] type corresponding to the generated HTML file(s) (`Route` below) as well as an [[site|EmaSite]] instance on that route type defining the site render pipeline. The simplest Ema app is presented below:
|
||||
Writing an Ema apps involves two things at a minimum:
|
||||
- a [[route]] type corresponding to the generated HTML file(s), as well as
|
||||
- an [[site|EmaSite]] instance on that route type defining the site render pipeline.
|
||||
|
||||
The simplest Ema app is presented below:
|
||||
|
||||
```haskell
|
||||
import Ema
|
||||
|
||||
-- The Route type of our site
|
||||
newtype Route = Route ()
|
||||
deriving newtype
|
||||
(Show, Eq, Ord, Generic, IsRoute)
|
||||
|
||||
-- Site pipeline (input & output)
|
||||
instance EmaSite Route where
|
||||
siteInput _ _ =
|
||||
-- There is no input in a hello-world site
|
||||
pure $ pure ()
|
||||
siteOutput _ _ _ =
|
||||
-- The output of index.html is simply a hello-world message in HTML
|
||||
pure $ Ema.AssetGenerated Ema.Html "<b>Hello</b>, Ema"
|
||||
|
||||
main :: IO ()
|
||||
main = void $ Ema.runSite @Route ()
|
||||
main =
|
||||
-- Hook everything up in main using runSite.
|
||||
void $ Ema.runSite @Route ()
|
||||
```
|
||||
|
||||
Let's walkthrough this code:
|
||||
Let's walk through this code:
|
||||
|
||||
1. `Route` is a singleton type, because our site has exactly one page -- `index.html`.
|
||||
- The unit type, `()`, already has an `IsRoute` instance, so we derive it via newtype.
|
||||
- In [[01-routes]], you will see how to write more elaborate route types and derive `IsRoute` for them.
|
||||
1. `EmaSite`'s `siteOutput` method is used to render this route.
|
||||
- `siteInput` is responsible for returning the data value (model) used in rendering the routes. In [[02-model]] we will use a custom model, and in [[03-dynamic]] we will make it time-varying.
|
||||
1. `Ema.runSite` takes a route type (via `TypeApplications`), and runs the Ema site.
|
||||
1. Running the resultant executable without arguments runs the [[live-server]], whereas running it with the `gen` subcommand will generate the static site (see [[cli]]).
|
||||
1. The `Route` type represents the pages on our site. As there is only one page (`index.html`) in our hello-world site, we simply use `()`.
|
||||
- The unit type, `()`, already has an `IsRoute` instance, so we derive it via `newtype`.
|
||||
- In [[01-routes]], you will see how to write more elaborate route types and derive `IsRoute` for them. `IsRoute` is what tells Ema that a Haskell type is a route type (with URL encoders and decoders).
|
||||
1. The `EmaSite` typeclass defines the "site pipeline" -- the input [[model]] and the output [[asset]]:
|
||||
1. `siteOutput` renders this route.
|
||||
1. `siteInput` returns the [[model|model]] used in rendering the routes. In [[02-model]] we will use a custom model, and in [[03-dynamic]] we will make it time-varying.
|
||||
1. `Ema.runSite` takes a route type (via `TypeApplications`), and runs the Ema site.
|
||||
1. Running the resultant executable without arguments runs the [[live-server]], whereas running it with the `gen` subcommand will generate the static site (see [[cli]]).
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, [[01-routes|we will explain]] how to write a simple **mood tracker** in Ema.
|
@ -4,27 +4,27 @@ order: 1
|
||||
|
||||
# Add Routes
|
||||
|
||||
We want to write an application to track moods, ie., a [mood tracking](https://en.wikipedia.org/wiki/Mood_tracking) app.
|
||||
A hello-world site is not very interesting. Let's write an application to track moods, i.e., a [mood tracking](https://en.wikipedia.org/wiki/Mood_tracking) app. We will record our moods in [plain-text](https://en.wikipedia.org/wiki/Plain_text) (a CSV file) and view them through an Ema app.
|
||||
|
||||
The first step is to think about the various pages and define their corresponding route types. Our application will have an index page (displaying mood summary) as well as pages specific to the individual days.
|
||||
The first step is to think about the various pages and define their corresponding [[route|route]] types. Since our application will have an index page (displaying mood summary) and pages specific to the individual days, we will use an ADT with two constructors:
|
||||
|
||||
```haskell
|
||||
data Route
|
||||
= Route_Index
|
||||
| Route_Date Date
|
||||
= Route_Index -- /index.html
|
||||
| Route_Date Date -- /date/YYYY-MM-DD.html
|
||||
deriving stock (Show, Eq, Ord, Generic)
|
||||
|
||||
deriveGeneric ''Route
|
||||
deriveIsRoute ''Route [t|'[]|]
|
||||
```
|
||||
|
||||
We use TemplateHaskell to derive `IsRoute` *generically*, instead of hand-writing the instance. We derive `IsRoute` to enrich our route type with three capabilities:
|
||||
We must derive `IsRoute` to enrich our route type with three capabilities:
|
||||
|
||||
1. `RouteModel`: associate a value type ([[model]]) that is used for encoding routes
|
||||
1. `RouteModel`: associate a value type ([[model]]) that will be used for encoding and decoding routes (see next point)
|
||||
2. `routePrism`: produce [[prism]] (a `Prism'`) that we can use to encode routes to URLs and vice versa.
|
||||
3. `routeUniverse`: generate a list of routes to statically generate
|
||||
|
||||
We can of course also derive `IsRoute` manually. In fact, we must do it for the `Date` sub-route type (as it is not an ADT shaped for generic deriving):
|
||||
Here, we use TemplateHaskell to derive `IsRoute` *generically*, instead of hand-writing the instance. We can of course also derive `IsRoute` manually. In fact, we must do it for the `Date` sub-route type (because it is not an ADT, like `Route` above, shaped for [[generic]]):
|
||||
|
||||
|
||||
```haskell
|
||||
@ -38,7 +38,7 @@ newtype Date = Date (Integer, Int, Int)
|
||||
|
||||
instance IsRoute Date where
|
||||
type RouteModel Date = ()
|
||||
routePrism () = toPrism_ $
|
||||
routePrism () = Ema.toPrism_ $
|
||||
prism'
|
||||
( \(Date (y, m, d)) ->
|
||||
formatTime defaultTimeLocale "%Y-%m-%d.html" $
|
||||
@ -51,10 +51,10 @@ instance IsRoute Date where
|
||||
```
|
||||
|
||||
1. We don't need any special [[model]] to encode a `Day` route, thus `RouteModel` is a unit. But we'll modify this in next step (to implement `routeUniverse`).
|
||||
2. `toPrism_` converts the optics-core `Prism'` into a coercible `Prism_` type that Ema internally uses. A route prism knows how to encode and decode the `Day` route. Our route `Prism'` is built using `formatTime` and `parseTimeM`.
|
||||
2. `toPrism_` is an Ema function that converts the optics-core `Prism'` into a coercible `Prism_` type that Ema internally uses. A route prism knows how to encode and decode the `Day` route. Our route `Prism'` is built using `formatTime` and `parseTimeM`.
|
||||
3. We will implement `routeUniverse` in the next step of the tutorial
|
||||
|
||||
The result is that we can use the function `routeUrl` to get the URL to our routes. Let's see this in action in GHCi:
|
||||
The result is that we can use the function `routeUrl` to get the URL to our routes. Let's see this in action in GHCi (run `bin/repl` in the template repository):
|
||||
|
||||
```haskell
|
||||
ghci> -- First get hold of the route Prism, which is passed to `siteOutput`
|
||||
|
@ -4,7 +4,7 @@ order: 2
|
||||
|
||||
# Add a Model
|
||||
|
||||
In order to generate our mood tracker website, we need ... mood data, ie., the mood [[model]]. If we record our mood each day, then Haskell's `Map` type is one way to represent moods overtime.
|
||||
To generate our mood tracker view, we need ... mood data, i.e., the mood [[model]]. If we are recording our mood each day, then Haskell's `Map` type is one way to represent moods over time.
|
||||
|
||||
```haskell
|
||||
data Model = Model
|
||||
@ -14,9 +14,9 @@ data Model = Model
|
||||
data Mood = Bad | Neutral | Good
|
||||
```
|
||||
|
||||
Now we want to *associate* our `Route` type with this `Model`. This can be done as follows:
|
||||
Now we want to *associate* our `Route` type from [[01-routes]] with this `Model`. This can be done as follows:
|
||||
|
||||
1. When [[generic|genericaly deriving]] routes, use `WithModel` option to associate a model for that route.
|
||||
1. When [[generic|genericaly deriving]] routes, use the `WithModel` option to associate a model for that route.
|
||||
2. Use the same[^same] model in the `IsRoute` instance for subroutes (here, `Date`).
|
||||
3. Change `EmaSite`'s `siteInput` method to return the model; and `siteOutput` to use the new model
|
||||
|
||||
@ -33,7 +33,7 @@ To achieve (2):
|
||||
|
||||
```haskell
|
||||
instance IsRoute Date where
|
||||
type RouteModel Date = Model
|
||||
type RouteModel Date = Model -- ^ We changed `()` to `Model`
|
||||
routePrism (Model _moods) = toPrism_ $
|
||||
prism'
|
||||
( \(Date (y, m, d)) ->
|
||||
@ -43,19 +43,19 @@ instance IsRoute Date where
|
||||
( fmap (Date . toGregorian)
|
||||
. parseTimeM False defaultTimeLocale "%Y-%m-%d.html"
|
||||
)
|
||||
routeUniverse (Model moods) = Map.keys moods
|
||||
routeUniverse (Model moods) = Map.keys moods -- ^ We implemented this
|
||||
```
|
||||
|
||||
Notice how this time we are able to properly define `routeUniverse` (it is used during static site generation, to determine which routes to generate on disk), because the model value is available. `routePrism` also gets the model as an argument, but in this case we have no need for it (in theory, we could check that a date exists before decoding successfully).
|
||||
|
||||
Finally, (3) is where we get to produce (`siteInput`) and consume (`siteOutput`) the model when rendering the site. The subsequent section explains this in detail.
|
||||
Finally, (3) is where we get to produce (`siteInput`) and consume (`siteOutput`) the model when rendering the site. The next section explains this in detail.
|
||||
## Use `Model`
|
||||
|
||||
We are yet to *use* our model yet. Let us do it now, by rendering a basic HTML for our routes. Change the `siteOutput` to following (we use blaze-html library):
|
||||
We are yet to *use* our model to do anything meaningful. The most meaningful thing to do here is to render HTML for our routes. Change the `siteOutput` to following (we use blaze-html library):
|
||||
|
||||
```haskell
|
||||
instance EmaSite Route where
|
||||
siteInput _ _ = pure $ pure $ Model mempty
|
||||
siteInput _ _ = pure $ pure $ Model mempty -- Empty model for now
|
||||
siteOutput rp model r =
|
||||
pure . Ema.AssetGenerated Ema.Html . RU.renderHtml $ do
|
||||
H.docType
|
||||
@ -65,6 +65,7 @@ instance EmaSite Route where
|
||||
H.body $ case r of
|
||||
Route_Index -> do
|
||||
H.h1 "Mood tracker"
|
||||
-- Just list the moods
|
||||
forM_ (Map.toList $ modelDays model) $ \(date, mood) -> do
|
||||
H.li $ do
|
||||
let url = Ema.routeUrl rp $ Route_Date date
|
||||
@ -82,14 +83,14 @@ This should render both `/` (`Route_Index`) and, say, `/date/2020-01-01.html` (`
|
||||
|
||||
Ultimately the value for our `Model` will come from elsewhere, such as a CSV file on disk. Let's use [cassava](https://hackage.haskell.org/package/cassava) to parse this CSV and load it into our Model.
|
||||
|
||||
First add a sample CSV file under `./data/moods.csv` containing:
|
||||
First, add a sample CSV file under `./data/moods.csv` containing:
|
||||
|
||||
```csv
|
||||
2022-04-23,Good
|
||||
2022-04-24,Neutral
|
||||
```
|
||||
|
||||
Now change the `siteInput` function to replace `mempty` with the contents of this Csv file loaded as `Model`:
|
||||
Now change the `siteInput` function to replace `mempty` with the contents of this CSV file loaded as `Model`:
|
||||
|
||||
```haskell
|
||||
import Data.Csv qualified as Csv
|
||||
@ -122,9 +123,9 @@ instance Csv.FromField Mood where
|
||||
Right v -> pure v
|
||||
```
|
||||
|
||||
The result of this that our site's index page will display the moods in the CSV file, along with the link to the individual day routes (`Route_Date`).
|
||||
The result is that our site's index page will display the moods in the CSV file, along with the link to the particular day routes (`Route_Date`).
|
||||
|
||||
This is great so far, but we don't have [[hot-reload]]. Changing `data/moods.csv` ought to update our site. This is what the final step our tutorial series will explain.
|
||||
This is great so far---we can track how we feel in `moods.csv` and get an app-like "view" of it. But, we don't have [[hot-reload]]. Changing `data/moods.csv` ought to update our site. The final step of our tutorial series will explain this.
|
||||
|
||||
{.last}
|
||||
[Next]{.next}, [[03-dynamic|we will enable]] hot-reload on this mode.
|
||||
[Next]{.next}, [[03-dynamic|we will enable]] hot-reload on the mood model.
|
||||
|
@ -4,11 +4,13 @@ order: 3
|
||||
|
||||
# Dynamic Model
|
||||
|
||||
In [[02-model]] we modified our mood tracker so that it displayed the moods from a CSV file. Here, we will change it so that any user modifications to the `data/moods.csv` file [[hot-reload|hot reloads]] the [[live-server]] view of our app in the same manner as [Emanote](https://emanote.srid.ca/) does.
|
||||
In [[02-model]], we modified our mood tracker to display the moods from a CSV file. Here, we improve it so that any user modifications to the `data/moods.csv` file will [[hot-reload|hot reload]] the [[live-server]] view of our app in the same manner as [Emanote](https://emanote.srid.ca/) does.
|
||||
|
||||
## Dynamic
|
||||
|
||||
You will note that `siteInput` is defined to return a `Dynamic m (RouteModel r)`. In our case, `r ~ Route` and `RouteModel Route ~ Model`, thus our `siteInput` returns a `Dynamic m Model` in the IO monad. A [[dynamic]] is simply defined as:
|
||||
To do this, we must understand what a [[dynamic]] (which `siteInput` returns) is.
|
||||
|
||||
`siteInput` is defined to return a `Dynamic m (RouteModel r)`. In our case, `r ~ Route` and `RouteModel Route ~ Model`, thus our `siteInput` returns a `Dynamic m Model` in the IO monad. A [[dynamic]] is simply defined as:
|
||||
|
||||
```haskell
|
||||
newtype Dynamic m a
|
||||
@ -20,9 +22,9 @@ newtype Dynamic m a
|
||||
)
|
||||
```
|
||||
|
||||
It is a pair of values: the initial value, and a function that knows how to update that value over time using the user-provided update function (`a -> m ()`). Dynamic's are an `Applicative`, so they compose using `liftA*` family of functions.
|
||||
It is a pair of values: the initial value and a function that knows how to update that value over time using the user-provided update function (`a -> m ()`). Dynamic's are an `Applicative`, so they compose using `liftA*` family of functions.
|
||||
|
||||
In our `siteInput`, so far we return `pure myModel`---it has an initial value, but does *not* update over time. In order to return an actually updating `Dynamic` of that model, we would change it to: `Dynamic (myModel, updater)` and now the task becomes to define the "updater" function itself. Spelled out:
|
||||
In our `siteInput, so far, we return `pure myModel`---it has an initial value but does *not* update over time. In order to return an actually updating `Dynamic` of that model, we would change it to: `Dynamic (myModel, updater)` and now the task becomes to define the "updater" function itself. Spelled out:
|
||||
|
||||
```haskell
|
||||
siteInput _ _ = do
|
||||
@ -36,13 +38,13 @@ siteInput _ _ = do
|
||||
```
|
||||
|
||||
## FSNotify
|
||||
|
||||
In the case of mood tracker, we will use the [fsnotify](https://hackage.haskell.org/package/fsnotify) package (see [[unionmount]] for another option) to fulfill that `<some func that returns the next update>` part. Without further ado, here's the full implementation of the new `siteInput` that returns a fully fledged `Dynamic m Model`:
|
||||
In the case of our mood tracker, we will use the [fsnotify](https://hackage.haskell.org/package/fsnotify) package (see [unionmount] for another option) to fulfill that `<some func that returns the next update>` part. So, without further ado, here's the full implementation of the new `siteInput` that produces a fully-fledged `Dynamic m Model`:
|
||||
|
||||
```haskell
|
||||
siteInput _ _ = do
|
||||
model0 <- readModel "data/moods.csv"
|
||||
pure . Dynamic . (model0,) $ \setModel -> do
|
||||
-- Create a `Dynamic` with initial value (model0) and an updater function
|
||||
pure $ Dynamic $ (model0,) $ \setModel -> do
|
||||
ch <- liftIO $ watchDirForked "data"
|
||||
let loop = do
|
||||
logInfoNS "fsnotify" "Waiting for fs event ..."
|
||||
@ -68,4 +70,8 @@ In the case of mood tracker, we will use the [fsnotify](https://hackage.haskell.
|
||||
pure ch
|
||||
```
|
||||
|
||||
Now if you run the app, and modify the `data/mood.csv` file (eg: change "Neutral" to "Bad"), your app's web view will update in real-time. This concludes the tutorial series, and hopefully you have gained an introductory understanding of what is entailed behind the "*just about any app that creates a browser view of arbitrarily changing data*" claim in the [[index|index]] page. You can view the source code for the mood tracker tutorial at https://github.com/srid/MoodTracker-Tutorial.
|
||||
Now if you run the app and modify the `data/mood.csv` file (e.g., change "Neutral" to "Bad"), your app's web view will update in real-time. Your Ema app updates instantly on code *or* data change.
|
||||
|
||||
This concludes the tutorial series, and hopefully, you have gained an introductory understanding of what is entailed behind the "*just about any app that creates a browser view of arbitrarily changing data" claim on the [[index|index]] page. You can view the source code for the mood tracker tutorial at https://github.com/srid/MoodTracker-Tutorial.
|
||||
|
||||
You may visit [[guide]] or [[topics]] to further your understanding.
|
Loading…
Reference in New Issue
Block a user