diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
new file mode 100644
index 0000000..c93df66
--- /dev/null
+++ b/.github/workflows/publish.yaml
@@ -0,0 +1,22 @@
+
+name: "Publish"
+on:
+ # Run only when pushing to master branch
+ push:
+ branches:
+ - master
+jobs:
+ neuron:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build emanote site 🔧
+ run: |
+ mkdir -p output
+ docker run -v $PWD:/data sridca/emanote emanote -C /data/docs gen /data/output
+ - name: Deploy to gh-pages 🚀
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./output/
+ cname: ema.srid.ca
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f84fac6..2b845e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,4 @@ cabal.project.local~
.ghc.environment.*
result
result-*
-docs-output/
\ No newline at end of file
+output
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbd571b..7c2d082 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased (0.2.0.0)
+- `routeUrl`: now returns relative URLs (ie. without the leading `/`)
+ - Use the `` tag to specify an explicit prefix for relative URLs in generated HTML. This way hosting on GitHub Pages without CNAME will continue to have functional links.
- `Ema.Slug`
- Add `Ord`, `Generic`, `Data` and Aeson instances to `Slug`
- Unicode normalize slugs using NFC
@@ -21,7 +23,7 @@
- add wikilink helpers
- TODO(doc) Add `Ema.Helper.PathTree`
- Examples
- - Remove Ex03_Documentation.hs (moved to separate repo, `ema-docs`)
+ - ~~Remove Ex03_Documentation.hs (moved to separate repo, `ema-docs`)~~ Back to ./docs, but using Emanote.
- Add Ex03_Basic.hs example
## 0.1.0.0 -- 2021-04-26
diff --git a/README.md b/README.md
index 60564b9..44a6e25 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# ema
-
+
[![Hackage](https://img.shields.io/hackage/v/ema.svg?logo=haskell)](https://hackage.haskell.org/package/ema)
[![FAIR](https://img.shields.io/badge/FAIR-pledge-blue)](https://www.fairforall.org/about/)
diff --git a/docs/concepts.md b/docs/concepts.md
new file mode 100644
index 0000000..8cdc297
--- /dev/null
+++ b/docs/concepts.md
@@ -0,0 +1,11 @@
+---
+order: 9
+---
+
+# Concepts
+
+* [Hot Reload](concepts/hot-reload.md)
+* [LVar](concepts/lvar.md)
+* [Slug](concepts/slug.md)
+* [CLI](concepts/cli.md)
+* [Logging](concepts/logging.md)
\ No newline at end of file
diff --git a/docs/concepts/cli.md b/docs/concepts/cli.md
new file mode 100644
index 0000000..330c8d8
--- /dev/null
+++ b/docs/concepts/cli.md
@@ -0,0 +1,11 @@
+---
+order: 4
+---
+# CLI
+
+Ema apps have a basic CLI argument structure that takes two kinds of input:
+
+1. `-C
`: specifies the "input directory" (current working directory by default)
+2. `gen` subcommand: generates the static site, instead of starting up the dev server
+
+Ema (`runEma`) will change the [current working directory](https://hackage.haskell.org/package/directory-1.3.6.1/docs/System-Directory.html#v:getCurrentDirectory) to the "input directory" before running your application code. It, along with the "gen" subcommand (if used), is passed as the `Ema.CLI.Action` type to your `render` function. You can also use `runEmaWith` if you are manually handling the CLI arguments yourself.
diff --git a/docs/concepts/hot-reload.md b/docs/concepts/hot-reload.md
new file mode 100644
index 0000000..0ae9ae2
--- /dev/null
+++ b/docs/concepts/hot-reload.md
@@ -0,0 +1,30 @@
+---
+order: 1
+---
+# Hot Reload
+
+**Hot Reloading** is a feature of Ema's dev server wherein any changes to your Haskell source or data files (such as Markdown files or HTML templates) _propagate instantly_ to the web browser without requiring any manual intervention like a full browser refresh. In practice, this is a such a delightful feature to work with. Imagine changing CSS style of an element, and see it reflect on your site in a split second.
+
+## How Ema implements hot reload
+
+### Websocket
+
+The Ema dev server uses websockets to keep a bi-directional connection open between the web browser and the backend server. When you click on a link or when something changes in the backend, they are communicated via this connection. In a statically generated site, however, no such activity happens - and a link click behaves like a normal link, in that the browser makes a full HTTP request to the linked page.
+
+### DOM patching
+
+When switching to a new route or when receiving the new HTML, Ema uses [morphdom](https://github.com/patrick-steele-idem/morphdom) to _patch_ the existing DOM tree rather than replace it in its entirety. This, in addition to use of websockets, makes it possible to support **instant** hot reload with nary a delay.
+
+### Haskell reload
+
+Finally, hot reload on _code_ changes are supported via [ghcid](https://github.com/ndmitchell/ghcid). The [template repo](https://github.com/srid/ema-template)'s `bin/run` script uses ghcid underneath. Any HTML DSL (like blaze-html -- as used by the [Tailwind helper](guide/helpers/tailwind.md)) or CSS DSL automatically gets supported for hot-reload. If you choose to use a file-based HTML template language, you can enable hot-reload on template change using the [FileSystem helper](guide/helpers/filesystem.md).
+
+Note that if your application makes use of threads, it is important to setup cleanup handlers so that `ghcid` doesn't leave [ghost](https://stackoverflow.com/q/24999636/55246) processes behind. Helpers like [`race_`](https://hackage.haskell.org/package/async-2.2.3/docs/Control-Concurrent-Async.html#v:race_) will do this automatically (incidentally it is used by `runEma` for running the user IO action).
+
+### Data reload
+
+For anything outside of the Haskell code, your code becomes responsible for monitoring and updating the model [LVar](concepts/lvar.md). The [filesystem helper](guide/helpers/filesystem.md) already provides utilities to facilitate this for monitoring changes to files and directories.
+
+## Handling errors
+
+If your code throws a Haskell exception, they will be gracefully handled and displayed in the browser, allowing you to recover without breaking hot-reload flow.
diff --git a/docs/concepts/logging.md b/docs/concepts/logging.md
new file mode 100644
index 0000000..740113d
--- /dev/null
+++ b/docs/concepts/logging.md
@@ -0,0 +1,10 @@
+---
+order: 5
+---
+# Logging
+
+`runEma`'s action monad supports the `MonadLoggerIO` constraint, as defined by [monad-logger](https://hackage.haskell.org/package/monad-logger). This means that you can use any of the logging functions from `monad-logger` to add logging to your application. [monad-logger-extras](https://hackage.haskell.org/package/monad-logger-extras) is used to colorize the logs.
+
+```haskell
+logInfoNS "myapp" "This is an info message"
+logDebugNS "myapp" "This is a debug message info"
\ No newline at end of file
diff --git a/docs/concepts/lvar.md b/docs/concepts/lvar.md
new file mode 100644
index 0000000..85899aa
--- /dev/null
+++ b/docs/concepts/lvar.md
@@ -0,0 +1,8 @@
+---
+order: 2
+---
+# LVar
+
+If you are familiar with Haskell's `stm` package, a `LVar` is essentially a [`TMVar`](https://hackage.haskell.org/package/stm-2.5.0.0/docs/Control-Concurrent-STM-TMVar.html) but with an extra ability for other threads to observe changes. Ema uses it for [hot reload](concepts/hot-reload.md), and your application code is expected to set and update its [model](guide/model.md) through the LVar.
+
+Documentation on `LVar` is available [on Hackage](https://hackage.haskell.org/package/lvar).
\ No newline at end of file
diff --git a/docs/concepts/slug.md b/docs/concepts/slug.md
new file mode 100644
index 0000000..d6d11be
--- /dev/null
+++ b/docs/concepts/slug.md
@@ -0,0 +1,16 @@
+---
+order: 3
+---
+# Slug
+
+A slug is a component of a URL or file path. In an _URL_ like `/foo/bar`, there are two _slugs_: "foo" and "bar". URLs (as well as filepaths) can therefore be represented as lists of slugs (`[Slug]`).
+
+```haskell
+import Ema (Slug)
+
+type URL = [Slug]
+```
+
+Slugs are integral to Ema's routing system. When defining [route](guide/routes.md) encoders and decoders (via [Ema class instance](guide/class.md)) you are effectively writing functions that convert back and forth between your route type and `[Slug]`. These functions are ultimately used to determine the *filename* of the statically generated HTML (i.e., `./foo/bar.html`) as well as the linking URL in the rendered HTML (i.e., `/foo/bar`).
+
+Slugs are also automatically [unicode normalized](https://www.unicode.org/faq/normalization.html) to NFC to ensure that route links work reliably regardless of the underlying representation of any non-ascii link text.
\ No newline at end of file
diff --git a/docs/favicon.svg b/docs/favicon.svg
new file mode 100644
index 0000000..fcfc3c7
--- /dev/null
+++ b/docs/favicon.svg
@@ -0,0 +1,114 @@
+
+
+
diff --git a/docs/guide.md b/docs/guide.md
new file mode 100644
index 0000000..0113b53
--- /dev/null
+++ b/docs/guide.md
@@ -0,0 +1,13 @@
+---
+order: 2
+---
+
+# Guide
+
+After having familiarized yourself with Ema by following the [earlier section](start/tutorial.md), you are now ready to dive deep into learning how to achieve specific things.
+
+* [Defining your model](guide/model.md) -- [Define your site model such that it supports hot reload]{.item-intro}
+* [Working with routes](guide/routes.md) -- [Unless you site has a single page (`index.html`), you will need to manage a set of routes]{.item-intro}
+* [Defining Ema instance](guide/class.md) -- [Constrain your `model` and `route` to work with static sites]{.item-intro}
+* [Rendering HTML](guide/render.md) -- [You could use plain strings to build HTML, or use templates, or use one of the delightful Haskell DSLs]{.item-intro}
+* [Helpers](guide/helpers.md) -- [Bring Your Own Libraries, or choose from existing helpers]{.item-intro}
diff --git a/docs/guide/class.md b/docs/guide/class.md
new file mode 100644
index 0000000..867edac
--- /dev/null
+++ b/docs/guide/class.md
@@ -0,0 +1,52 @@
+---
+order: 3
+---
+# Defining Ema instance
+
+Once you have [model](guide/model.md) and [route](guide/routes.md) types in place, we must tell the Haskell compiler that they are suitable for generating static sites. We do this by creating an instance of the `Ema` typeclass.
+
+Using some `MyModel` and the route `Route` shown in the [previous](guide/routes.md) section, we can create an instance as follows:
+
+```haskell
+class Ema MyModel Route where
+ -- Convert our route to browser URL, represented as a list of slugs
+ encodeRoute = \case
+ Index -> [] -- An empty slug represents the index route: index.html
+ About -> ["about"]
+
+ -- Convert back the browser URL, represented as a list of slugs, to our route
+ decodeRoute = \case
+ [] -> Just Index
+ ["about"] -> Just About
+ _ -> Nothing
+
+ -- The third method is optional; and used during static site generation.
+ -- This tells Ema which routes to generate .html files for.
+ -- By default, Enum & Bounded will be used to determine this list.
+ staticRoutes model =
+ [Index, About]
+
+ -- The fourth method is also optional; if you have static assets to serve, specify
+ -- them here. Paths are relative to current working directory.
+ staticAssets Proxy =
+ ["css", "images", "favicon.ico", "resume.pdf"]
+```
+
+The `Ema` typeclass has four methods, with the last two of them being optional with default implementations:
+
+1. Define `encodeRoute` that converts our route type to a browser URL [slug](concepts/slug.md) path representing relative URLs like `/foo/bar`
+2. Define `decodeRoute` that does the *reverse* converstion (the conversion must be isomorphic)
+3. _Optionally_, define `staticRoutes` indicating the routes to statically generate
+4. _Optionally_, define the list of static assets to copy over as-is during static generation
+
+## `runEma`
+
+The `Ema` constraint is used by the `runEma` function that acts as the main entry point to your static site generator. It takes two arguments:
+
+1. `render` function that renders your HTML (we'll go over this in [the next](guide/render.md) section)
+2. an IO action that takes [`LVar model`](guide/model.md) as an argument.
+
+This IO action is expected to be a long-running one, wherein you have full control over setting the value of the model over time.
+
+{.last}
+[Next]{.next}, with our model and routes in place constrained by `Ema` type class, [we will define the HTML for our site](guide/render.md) using Ema.
diff --git a/docs/guide/helpers.md b/docs/guide/helpers.md
new file mode 100644
index 0000000..8b1d2ea
--- /dev/null
+++ b/docs/guide/helpers.md
@@ -0,0 +1,10 @@
+---
+order: 5
+---
+# Helpers
+
+Beyond the model and route types, Ema leaves it up to you as to how to develop your site. The following are not *required* when using Ema; nevertheless they are useful inasmuch as they capture common patterns in writing a good static site:
+
+* [Tailwind + Blaze](guide/helpers/tailwind.md) -- [We recommend--but not mandate--Tailwind for CSS and blaze-html as HTML DSL]{.item-intro}
+* [Working with files](guide/helpers/filesystem.md) -- [Ema provides a helper to support hot-reload on files]{.item-intro}
+* [Converting Markdown](guide/helpers/markdown.md) -- [Pointers on how to work with Markdown files]{.item-intro}
\ No newline at end of file
diff --git a/docs/guide/helpers/filesystem.md b/docs/guide/helpers/filesystem.md
new file mode 100644
index 0000000..f71483e
--- /dev/null
+++ b/docs/guide/helpers/filesystem.md
@@ -0,0 +1,56 @@
+---
+order: 2
+---
+# Working with files
+
+If your static site is generated depending on local files on disk, the general flow of things is as follows:
+
+```haskell
+runEma render $ \model -> do
+ -- Load everything on launch
+ initialModel <- loadFilesAndBuildModel
+ LVar.set model initialModel
+ -- Continue to monitor and update the model
+ observeFileSystem $ \action ->
+ LVar.modify model $ applyAction action
+```
+
+For monitoring local files on disk you would typically use something like [fsnotify](https://hackage.haskell.org/package/fsnotify) in place of `observeFileSystem`. What is the point of doing this? To support [hot reload](concepts/hot-reload.md) on _data_ change. Imagine that your static site is generated based on Markdown files as well as HTML templates on disk. If either the Markdown file, or a HTML template file is modified, we want the web browser to hot reload the updated HTML *instantly*. This is enabled by storing both these kinds of files in the application [model](guide/model.md) and using [LVar](concepts/lvar.md) to update it *over time*.
+
+For filesystem changes, Ema provides a helper based on `fsnotify` in the `Ema.Helper.FileSystem` module. You can use it as follows
+
+```haskell
+import qualified Ema.Helper.FileSystem as FileSystem
+
+type Model = Map FilePath Text
+
+Ema.runEma render $ \model -> do
+ LVar.set model =<< do
+ mdFiles <- FileSystem.filesMatching "." ["**/*.md"]
+ forM mdFiles readFileText
+ <&> Map.fromList
+ FileSystem.onChange "." $ \fp -> \case
+ FileSystem.Update ->
+ when (takeExtension fp == ".md") $ do
+ log $ "Update: " <> fp
+ s <- readFileText fp
+ LVar.modify model $ Map.insert fp s
+ FileSystem.Delete ->
+ whenJust (takeExtension fp == ".md") $ do
+ log $ "Delete: " <> fp
+ LVar.modify model $ Map.delete fp
+```
+
+In most cases, however, you probably want to use the higher level function `mountOnLVar`. It "mounts" the files you specify onto the [model LVar](concepts/lvar.md) such that any changes to them are *automatically* reflected in your [model](guide/model.md) value.
+
+```haskell
+Ema.runEma render $ \model -> do
+ FileSystem.mountOnLVar "." ["**/*.md"] model $ \fp -> \case
+ FileSystem.Update -> do
+ s <- readFileText fp
+ pure $ Map.insert fp s
+ FileSystem.Delete ->
+ pure $ Map.delete fp
+```
+
+[Full example here](https://github.com/srid/ema-template/blob/master/src/Main.hs).
\ No newline at end of file
diff --git a/docs/guide/helpers/markdown.md b/docs/guide/helpers/markdown.md
new file mode 100644
index 0000000..7d764cc
--- /dev/null
+++ b/docs/guide/helpers/markdown.md
@@ -0,0 +1,33 @@
+---
+order: 3
+---
+
+# Using Markdown
+
+There are quite a few packages to convert Markdown to HTML,
+
+- [Pandoc](https://hackage.haskell.org/package/pandoc) -- [Supports formats other than Markdown]{.item-intro}
+- [commonmark-hs](https://github.com/jgm/commonmark-hs) -- [Lightweight parser by the same author of Pandoc]{.item-intro}
+- [mmark](https://github.com/mmark-md/mmark) -- [*Strict* Markdown parser]{.item-intro}
+
+## Helper
+
+Ema provides a helper to parse Markdown files with YAML frontmatter, using commonmark-hs. If you are parsing front matter, you can use any type that has a [`FromYAML`](https://hackage.haskell.org/package/HsYAML-0.2.1.0/docs/Data-YAML.html#t:FromYAML) instance.
+
+```haskell
+import qualified Ema.Helper.Markdown as Markdown
+
+-- Front matter metadata can be any type with a `FromYAML` instance
+--
+-- Using a `Map` is a lazy way to capture metadata, but in real code we
+-- generally define a sum type and manually derive `FromYAML` for it.
+type Metadata = Map Text Text
+
+-- Returns `Either Text (Metadata, Pandoc)`
+Markdown.parseMarkdownWithFrontMatter @Metadata
+ "test.md" "Hello *world*"
+```
+
+The template repo, as well as [Emanote](https://github.com/srid/emanote) (used to generate this site), uses this helper to parse Markdown files into Pandoc AST. Consult [the template repo's source code](https://github.com/srid/ema-template/blob/master/src/Main.hs) for details.
+
+Note that with Ema you can get [hot reload](concepts/hot-reload.md) support for your Markdown files using [filesystem notifications](guide/helpers/filesystem.md).
diff --git a/docs/guide/helpers/tailwind.md b/docs/guide/helpers/tailwind.md
new file mode 100644
index 0000000..f08e04a
--- /dev/null
+++ b/docs/guide/helpers/tailwind.md
@@ -0,0 +1,19 @@
+---
+order: 1
+---
+# Using Tailwind CSS
+
+The `Ema.Helper.Tailwind` module provides a `layout` function that uses [twind](https://twind.dev/) shim that is used in the statically generated site, and otherwise uses Tailwind CSS from CDN in the dev server mode. This helper is for those that **use [Tailwind CSS](https://tailwindcss.com/) in conjunction with [blaze-html](https://hackage.haskell.org/package/blaze-html) DSL**.
+
+To use the layout helper in your [render](guide/render.md) function:
+
+```haskell
+render :: Ema.CLI.Action -> MyModel -> MyRoute -> LByteString
+render emaAction model route = do
+ Tailwind.layout emaAction (H.title "My site") $ do
+ H.p "Hello world"
+```
+
+The very site you are viewing (ema.srid.ca) is a live demonstration of this helper.
+
+**Note** that because the [twind JS shim](https://twind.dev/handbook/the-shim.html) is used to support Tailwind styles your site will not render properly on web browsers with JavaScript disabled if you use this helper; it might also have trouble interoperating with other JS initializers on the site. See [this issue](https://github.com/srid/ema/issues/20) for upcoming alternatives.
diff --git a/docs/guide/model.md b/docs/guide/model.md
new file mode 100644
index 0000000..6f03230
--- /dev/null
+++ b/docs/guide/model.md
@@ -0,0 +1,33 @@
+---
+order: 1
+---
+# Defining your model
+
+A "*model*" in Ema represents the state to use to generate your site. It could be as simple as a variable, or it could be a list of parsed Markdown files (as in the case of a weblog). Ema's model is also conceptually similar to [Elm](https://guide.elm-lang.org/architecture/)'s model, in that - changing the model [automatically](concepts/hot-reload.md) changes the [view](guide/render.md).
+
+Here's an example model:
+
+```haskell
+newtype BlogPosts = BlogPosts (Map Slug Text}
+```
+
+Here `BlogPosts` is the model type. If we are generating a weblog site, then all the "data" we need is loaded into memory as a value of `BlogPosts`.
+
+## Modifying the model
+
+Ema's dev server supports [hot reload](concepts/hot-reload.md); it will observe changes to your model, in addition to code. To facilitate this you will manage your model as a [LVar](concepts/lvar.md). The `runEma` function ([described here](guide/class.md)) takes an IO action that gets `LVar model` as an argument.
+
+For example,
+
+```haskell
+runEma render $ \model ->
+ forever $ do
+ LVar.set model =<< liftIO getCurrentTime
+ liftIO $ threadDelay $ 1 * 1000000
+```
+
+In this contrived example ([full code here](https://github.com/srid/ema/blob/master/src/Ema/Example/Ex02_Clock.hs)), we are using `UTCTime` as the model. We set the initial value using `LVar.set`, and then continually update the current time every second. Every time the model gets updated, the web browser will [hot reload](concepts/hot-reload.md) to display the up to date value. For the `BlogPosts` model, you would typically use [fsnotify](https://hackage.haskell.org/package/fsnotify) to monitor changes to the underlying Markdown files, but note that Ema provides [a helper](guide/helpers/filesystem.md) for that.
+
+
+{.last}
+[Next]{.next}, we will [talk about routes](guide/routes.md).
\ No newline at end of file
diff --git a/docs/guide/render.md b/docs/guide/render.md
new file mode 100644
index 0000000..a185a78
--- /dev/null
+++ b/docs/guide/render.md
@@ -0,0 +1,44 @@
+---
+order: 4
+---
+# Rendering HTML
+
+Once you have [model](guide/model.md) and [routes](guide/routes.md) in place and [constrained](guide/class.md), the last piece of the puzzle is to write a function that takes both as arguments and returns the HTML string (lazy bytestring, to be exact). This function can be as simple as the following:
+
+```haskell
+render :: MyModel -> Route -> ByteString
+render model route =
+ "Hello, world!"
+```
+
+Of course we want it to be real, by using our model value, as well as generate the HTML based on the route. We will also use the [blaze-html](https://hackage.haskell.org/package/blaze-html) library to make writing HTML in Haskell palatable (see also [the layout helper](guide/helpers/tailwind.md)). A more realistic starting point (if not the finishing product) would be:
+
+```haskell
+render :: MyModel -> Route -> ByteString
+render model route = Blaze.renderHtml $
+ H.html $ do
+ H.head $ do
+ H.title "My site"
+ H.body $ do
+ H.h1 "My site"
+ case route of
+ Index ->
+ H.h1 "Welcome to my website!"
+ H.p $ do
+ "Checkout the"
+ H.a ! A.href (H.toValue $ Ema.routeUrl About) $ "About"
+ " page."
+ About ->
+ H.div $ H.p "This website is managed by yours truly"
+ H.footer $ do
+ A.a ! A.href "https://github.com/user/repo" $ "Source on GitHub"
+```
+
+Note that Ema provides a `routeUrl` helper function that serializes your route to the final URL (here, `/about`) for linking to.
+
+Spend a few moments trying to appreciate how this is *much simpler* to write than dealing with HTML template files spread across the disk as is the case with traditional static site generators. If you [choose](https://vrom911.github.io/blog/html-libraries) to go the DSL route, Haskell's type-safety now applies to your HTML as well. On top of it, Ema's [hot reload](concepts/hot-reload.md) will instantly update the dev server's browser view whenever you change your HTML (or any of the Haskell source code).
+
+Of course when using Ema nothing prevents you from choosing to use traditional HTML templates, and you can get [hot reload](concepts/hot-reload.md) on them too with [a little bit of plumbing](guide/helpers/filesystem.md).
+
+{.last}
+[Next]{.next}, you might want to peruse [the helper topics](guide/helpers.md) if you need some extra functionality provided.
diff --git a/docs/guide/routes.md b/docs/guide/routes.md
new file mode 100644
index 0000000..a3918c7
--- /dev/null
+++ b/docs/guide/routes.md
@@ -0,0 +1,40 @@
+---
+order: 2
+---
+# Working with routes
+
+Ema gives you the freedom to use any Haskell type for representing your site routes. You don't need complicated rewrite rules. Routes are best represented using what are known as *sum types* (or ADT, short for *Abstract Data Type*). Here's an example of a route type:
+
+```haskell
+data Route
+ = Index
+ | About
+```
+
+This type represents two routes pointing to -- the index page (`/`) and the about page (`/about`). Designing the route type is only half the job; you will also need to tell Ema how to convert it to / from the browser URL. We will explain this in the next section.
+
+## Advanced example
+
+Here's one possible way to design the route type for a weblog site,
+
+```haskell
+data Route
+ = Home
+ | Blog BlogRoute
+ | Tag TagRoute
+
+data BlogRoute
+ = BlogIndex
+ | BlogPost Ema.Slug
+
+data TagRoute
+ = TagListing
+ | Tag Tag
+
+newtype Tag = Tag Text
+```
+
+Defining *hierarchical routes* like this is useful if you want to render *parts* of your HTML as being common to only a subset of your site, such as adding a blog header to all blog pages, but not to tag pages.
+
+{.last}
+[Next]{.next}, with our model and routes in place, [we will make them work with Ema](guide/class.md) by defining their static site behaviour.
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..375dd35
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,28 @@
+# Ema
+
+:::{.avatar}
+![](/favicon.svg){.w-32 .h-32 .float-right}
+:::
+
+{.text-xl .mb-8}
+[Ema](https://github.com/srid/ema) is a next-gen **Haskell** library toolkit for building [jamstack-style](https://jamstack.org/) static sites. Ema sites are *change-aware*; in addition to good ol' static site generation, it provides a live server supporting **fast hot-reload** in the browser on code *or* data change.
+
+{.text-gray-600}
+The ultimate goal of ema is to facilitate creating with ease your own [neuron](https://neuron.zettel.page/), 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.
+
+:::{.my-8}
+* [Getting Started](start.md)
+* [Guide](guide.md)
+* [Concepts](concepts.md)
+* [Ema News](https://notes.srid.ca/ema) ([RSS Feed](https://notes.srid.ca/ema.xml))
+:::
+
+:::{.flex .justify-center .items-center .mb-8}
+```{=video}
+/static/ema-demo.mp4
+```
+:::
+
+:::{.flex .justify-center .items-center}
+[![FAIR](https://img.shields.io/badge/FAIR-pledge-blue)](https://www.fairforall.org/about/)
+:::
\ No newline at end of file
diff --git a/docs/index.yaml b/docs/index.yaml
new file mode 100644
index 0000000..1ba17f6
--- /dev/null
+++ b/docs/index.yaml
@@ -0,0 +1,28 @@
+template:
+ theme: pink
+ iconUrl: /favicon.svg
+ # Disable collapsing of folders since we don't have large number of notes.
+ sidebar:
+ collapsed: false
+
+pandoc:
+ rewriteClass:
+ # The "Next" navigation in series
+ last: mt-8 border-t-2 border-pink-500 pb-1 pl-1 bg-gray-50 rounded
+ next: py-2 text-xl italic font-bold
+ # For description of items in guide listing
+ item-intro: text-gray-500
+
+# Put page-specific metadata here. Override them in Markdown frontmatter or
+# per-folder YAML as necessary.
+page:
+ siteName: Ema
+ siteTitle: Ema
+ description: |
+ Ema static site generator (Jamstack) in Haskell with live server and reload.
+ headHtml: |
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/start.md b/docs/start.md
new file mode 100644
index 0000000..008539d
--- /dev/null
+++ b/docs/start.md
@@ -0,0 +1,17 @@
+---
+order: 1
+---
+
+# Getting Started
+
+As first steps, perform the following before proceeding to the tutorial section below:
+
+1. [Install Nix](https://nixos.org/download.html) (see [platform-specific notes here](https://neuron.zettel.page/install))
+1. [Enable Flakes](https://nixos.wiki/wiki/Flakes#Installing_flakes)
+1. Clone [the template repository](https://github.com/srid/ema-template) locally
+1. Run `bin/run` and access the site at
+
+That should start the Ema dev server displaying a simple website. Go ahead and try modifying either the Markdown content in `./content` or the Haskell source in `./src/Main.hs`, and observe how the web view updates [instantly](concepts/hot-reload.md).
+
+{.last}
+[Next]{.next}, [in the tutorial](start/tutorial.md) let's try using this template repo to create a basic website.
diff --git a/docs/start/tutorial.md b/docs/start/tutorial.md
new file mode 100644
index 0000000..ec4a1e7
--- /dev/null
+++ b/docs/start/tutorial.md
@@ -0,0 +1,120 @@
+# 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
+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 $ "Hello, 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 ) 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 URL paths. 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 = \case
+ Index -> [] -- To /
+ About -> ["about"] -- To /about
+ decodeRoute = \case
+ [] -> Just Index -- From /
+ ["about"] -> 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 $ \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 qualified Ema.CLI
+import qualified Ema.Helper.Tailwind as Tailwind
+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 :: Ema.CLI.Action -> Model -> Route -> LByteString
+render _emaAction model r = RU.renderHtml $
+ H.html $ do
+ H.head $ do
+ H.title "Basic site"
+ 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 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 . -- -C ./content gen ./output
+```
+
+## Exercises
+
+1. Figure out how to use static assets (images, files) in your static sites (hint: the typeclass)
+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.
diff --git a/docs/static/ema-demo.mp4 b/docs/static/ema-demo.mp4
new file mode 100644
index 0000000..80ec27c
Binary files /dev/null and b/docs/static/ema-demo.mp4 differ
diff --git a/docs/static/manifest.json b/docs/static/manifest.json
new file mode 100644
index 0000000..577feb6
--- /dev/null
+++ b/docs/static/manifest.json
@@ -0,0 +1,10 @@
+{
+ "short_name": "Ema",
+ "name": "Ema static site generator",
+ "description": "Ema static site generator (Jamstack) in Haskell",
+ "start_url": "/",
+ "background_color": "#DB2777",
+ "display": "standalone",
+ "scope": "/",
+ "theme_color": "#DB2777"
+}
\ No newline at end of file
diff --git a/flake.nix b/flake.nix
index 7ad8288..7eff8dd 100644
--- a/flake.nix
+++ b/flake.nix
@@ -47,12 +47,5 @@
# Used by `nix develop`
devShell = emaProject true;
-
- # Used by `nix run` (for docs)
- apps.${name} = flake-utils.lib.mkApp {
- drv = ema;
- exePath = "/bin/ema-docs";
- };
- defaultApp = apps.${name};
});
}
diff --git a/hie.yaml b/hie.yaml
index 566ec27..08ab24f 100644
--- a/hie.yaml
+++ b/hie.yaml
@@ -2,5 +2,3 @@ cradle:
cabal:
- path: "./src"
component: "library:ema"
- - path: "./docs"
- component: "exe:ema-docs"
diff --git a/src/Ema/Route.hs b/src/Ema/Route.hs
index 60adccf..268aab3 100644
--- a/src/Ema/Route.hs
+++ b/src/Ema/Route.hs
@@ -17,12 +17,16 @@ import Ema.Route.Slug (Slug (unSlug), decodeSlug, encodeSlug)
import Ema.Route.UrlStrategy
( UrlStrategy (..),
slugFileWithStrategy,
- slugUrlWithStrategy,
+ slugRelUrlWithStrategy,
)
+-- | Return the relative URL of the given route
+--
+-- As the returned URL is relative, you will have to either make it absolute (by
+-- prepending with `/`) or set the `` URL in your HTML head element.
routeUrl :: forall a r. Ema a r => r -> Text
routeUrl r =
- slugUrlWithStrategy def (encodeRoute @a r)
+ slugRelUrlWithStrategy def (encodeRoute @a r)
routeFile :: forall a r. Ema a r => r -> FilePath
routeFile r =
diff --git a/src/Ema/Route/UrlStrategy.hs b/src/Ema/Route/UrlStrategy.hs
index 63eaec3..b331e81 100644
--- a/src/Ema/Route/UrlStrategy.hs
+++ b/src/Ema/Route/UrlStrategy.hs
@@ -19,17 +19,17 @@ data UrlStrategy
instance Default UrlStrategy where
def = UrlStrategy_HtmlOnlySansExt
-slugUrlWithStrategy :: UrlStrategy -> [Slug] -> Text
-slugUrlWithStrategy strat slugs =
+slugRelUrlWithStrategy :: UrlStrategy -> [Slug] -> Text
+slugRelUrlWithStrategy strat slugs =
case strat of
UrlStrategy_FolderOnly ->
- "/" <> T.intercalate "/" (encodeSlug <$> slugs)
+ T.intercalate "/" (encodeSlug <$> slugs)
UrlStrategy_HtmlOnlySansExt ->
case nonEmpty slugs of
Nothing ->
- "/"
+ ""
Just (removeLastIf (decodeSlug "index") -> slugsWithoutIndex) ->
- "/" <> T.intercalate "/" (encodeSlug <$> slugsWithoutIndex)
+ T.intercalate "/" (encodeSlug <$> slugsWithoutIndex)
where
removeLastIf :: Eq a => a -> NonEmpty a -> [a]
removeLastIf x xs =