Elixir Configuration
Everything is defined into two different files. The first one is the script in use to define what's going to take place (lambdapad.exs
) and the other file is the configuration to get easier change parameters (config.toml
). Both are necessary into your directory. The usual content for config.toml
is as follows:
[blog]
url = "http://localhost:8080"
title = "My Blog"
description = "I put my ideas here... what else?"
Actually, you can include more sections inside of this file which are going to be accessible from the templates and the transformations.
You can add as many deep levels as you need, but we recommend to keep it as simple as possible.
You'll need also define information inside of lambdapad.exs
to know what to generate:
import Lambdapad
blog do
source posts: "posts/**.md"
pages "posts index" do
set from: :posts
set index: true
set uri: "/"
set template: "posts.html"
set var_name: "posts"
end
pages "posts" do
set from: :posts
set uri: "/{{ post.id }}"
set template: "post.html"
set var_name: "post"
end
end
Neat! More or less. There is a lot to comment, but first, there is a convention into the creation of the directories:
-
posts
: where we put explicitly posts when we are to create a blog. -
pages
: where we put the extra pages, like about or the normal pages a website should have. -
templates
: where we put the templates (usually ErlyDTL at the moment). -
assets
: where all of the static files should be placed. Be careful if you are using something like webpack, brunch or similar. This should be the destination directory for these tools and not the directory containing all of the generator and node modules directory.
In the same way the default file for the site it's usually lambdapad.exs
and the configuration (if we are not indicating anything) it's config.toml
.
Configuration Blocks
We defined different blocks to be analysed and in use by lambdapad to generate the website. We are going to see them in detail right now.
config
The config
block is only needed if we want to change the provider (one instead of TOML), the name of the configuration file or we want to perform a change into the configuration. The sets we can configure are the following:
-
from
(string) set the name of the configuration file, by default it'sconfig.toml
. -
format
(atom) set the type of the configuration file. At this moment we can only use:toml
. -
transform
(function) let us set a function to change the configuration. It is only running once when we retrieve the configuration from the file and we can only define one function:
config do
set transform: fn(config) ->
Map.put(config, "site_root", config["blog"]["url"])
end
end
source
This special tag is very simple and is only defining aliases for specific paths:
source posts: "posts/**/*.md"
source pages: "pages/**/*.md"
This way in every from
set for pages
or widget
we can use :posts
or :pages
instead of write the wildcard paths again and again.
assets
This block help us to copy the static files into the destination target. This is the most simple one because only accepts two configurations:
-
from
(string) letting set the wildcard from where we are going to get the files. -
to
(string) setting the directory where these files are going to be placed.
If we set assets/**
this is meaning the whole tree directory under assets
is going to be copied to the destionation directory. We can configure as many assets
blocks as we need:
assets "js" do
set from: "js/dist/*.js"
set to: "site/js/"
end
assets "css" do
set from: "css/dist/*.css"
set to: "site/css/"
end
transform
The transform is the definition of a functional action to be performed to a set of pages or for each page each time, or even to the configuration.
The sets we can use to define a transform are:
-
on
(:config | :page | :item) specifies where the transform will be working. If we are defining an action only to work with individual items, of course, it couldn't be applied for the whole set of pages or for configuration. We'll check what are the differences. -
run
(function) the function to be called. Depending on the previous value, it could be:-
:config
: a function receiving config as first parameter and pages as second one, it should return the modified config. It's configured intransform_on_config
forwidget
andpages
block.
transform "last_updated" do set on: :config set run: fn(config, pages) -> last_updated = pages |> Enum.map(& &1["updated"] || &1["date"]) |> Enum.sort(:desc) |> List.first() Map.put(config, "last_updated", last_updated) end end
The transformation is getting the most recent date from the posts priorizing the existence of the updated over date. Of course, we have to provide one or another inside of the page header.
-
:page
: a function receiving pages as first parameter and config as second one, it should return the modified pages. It's configured intransform_on_page
forwidget
andpages
blocks.
transform "order by date desc" do set on: :page set run: fn(pages, config) -> Enum.sort_by(pages, & &1["date"], :desc) end end
The transformation is ordering the pages based on the
date
information from its header in a desc order.-
:item
: a function receiving page as first parameter and config as second one, it should return the modified page. It's configured intransform_on_item
forwidget
andpages
blocks.
transform "date" do set on: :item set run: fn(post, config) -> %{day: day, month: month, year: year} = Date.from_iso8601!(post["date"]) Map.put(post, "_date", %{ day: String.pad_leading(to_string(day), 2, "0"), month: String.pad_leading(to_string(month), 2, "0"), year: to_string(year) }) end end
-
:persist
: a function receiving data as first parameter and page data as second parameter, it should return the modification of the data. This data will be inserted into:url_data
property list using the URL as key. It's a way to get the list of the generated pages at some specific point (i.e. it's useful for the definition of apages
block to create asitemap.xml
):
transform "sitemap" do set on: :persist set run: fn(data, _page) -> date = Date.utc_today() |> to_string() Map.merge(data, %{"last_updated", date}) end end
The transformation is modifying each post providing a new key
_date
which is containing the keysday
,month
andyear
. -
As you can see every transformation requieres a different function defined. It's accepting different parameters and returning different results.
Note that :page
let us even change the way the posts are stored. This is very useful if we want to create a hierachical store to present the pages based on one specific tag (i.e. category, tags or year)
widget
This is the more powerful block because let us process parts of a page and then we can embedded them into others. For this we have different elements, first we can define as many widget blocks as we need:
widget "recent posts" do
set from: :posts
set template: "recent-posts.html"
set transform_on_page: fn(posts, config) ->
Enum.take(posts, config["blog"]["num_of_recent_posts"])
end
set var_name: "posts"
end
The sets we can use for widgets are the following:
-
from
(string | atom) is telling where to find the files to be processed. We could use a definition fromsource
. -
template
(string) the name of the template file in use for rendering the widget. -
transform_on_page
(function | string | list(function | string)) a lambda with two arguments saying what transformation we want to perform on the set of pages. We can usetransform
block to define a tranformation and use only the name here. We can add as many transformations as we need. Seetransform
section for further information. -
transform_on_item
(function | string | list(function | strings)) a lambda with two arguments saying what transformation we want to perform on a single page each time. This is running once per item into the set of pages retrieved usingfrom
. We can add as many transformations as we need. Seetransform
section for further information. -
headers
(boolean) is telling if the markdown files have headers or not. Default value istrue
. -
excerpt
(boolean) is telling if the markdown files have excerpt or not. Even if they have no excerpt and we configure this value totrue
the code is getting the first paragraph as excerpt. Default value istrue
. -
var_name
(string | :plain) it's setting the list of pages using the name provided here to be in use into the template. The default value is:plain
. -
format
(:erlydtl) yes, it makes no sense to put this value at the moment, but in near future it's desirable to have also support for other template engines like:eex
. Keep in touch!. -
env
(map) it's letting us to define extra parameters for the template in a map format (see pages example for further information).
Note that if the from
is retrieving different files, it will generate a list of elements which cannot be merge with the list of values, therefore :plain
will be changed automatically to "pages"
giving a warning.
The generation of the widgets is inserted into the configuration under the key widgets
. But it's even easier to use, when we are into the template we have only to use:
{% widget "recent posts" %}
And then the widget is put in that place.
The flow it follows is:
-
The information for pages is retrieved (using
from
). Butfrom
isn't required so, it's possible to render only the template. Actually this makes no sense because it's better useinclude
in this case. -
The pages are processed item by item (using
transforms_on_item
). If there are no pages from step 1 or the transformations are not defined this step is ommited. -
The pages are processed as a whole (using
transform_on_page
). If there are no pages from step 1 or the transformations are not defined this step is ommited. - The page is rendered using the template.
- Information is stored into the configuration to be in use into the pages.
pages
This is the important block. This block is in charge of generating the pages into the destination directory. As we read previously this is helped of source
block to define wildcard paths as atoms and transform
to define transformations using names.
The sets we can use with pages
are the following:
-
from
(string | atom) is telling where to find the files to be processed. We could use a definition fromsource
. -
uri
(string | function) the URI used to access to the page. We can use here a string formatted inerlydtl
way:set uri: "/{{ page.id }}"
or using a function where we are going to receive the
index
and generate different pages based on this index. Very useful for pagination:set uri: fn (1) -> "/" (index) -> "/page/#{index}" end
-
uri_type
(:dir
|:file
) theuri
is always being created as a directory and inside aindex.html
file, but if we want to create specifically another file, i.e.atom.xml
, we could specify theuri
including the file and then settinguri_type
as:file
. Default value is:dir
. -
index
(boolean) we say if we are going to process all of the posts in only one template or one post per template. This is helping to create pages like indexes of posts or all of the post pages one by one. -
paginated
(false | integer | function) we are indicating if we are going to paginate a page defined as index (set index: true
) we can set this value asfalse
(the default value), the number of elements per page (positive integer):set paginated: 12
or a function which is returning the number of elements per page (positive integer) receiving the config as parameter:
set paginated: fn(config) -> config["blog"]["max_items_per_page"] end
-
template
(string) the name of the template file in use for rendering the widget. -
transform_on_config
(function | string | list(function | string)) a lambda with two arguments saying what transformation we want to perform on the set of pages. We can usetransform
block to define a tranformation and use only the name here. We can add as many transformations as we need. Seetransform
section for further information. -
transform_on_page
(function | string | list(function | string)) a lambda with two arguments saying what transformation we want to perform on the set of pages. We can usetransform
block to define a tranformation and use only the name here. We can add as many transformations as we need. Seetransform
section for further information. -
transform_on_item
(function | string | list(function | string)) a lambda with two arguments saying what transformation we want to perform on a single page each time. This is running once per item into the set of pages retrieved usingfrom
. We can add as many transformations as we need. Seetransform
section for further information. -
transform_to_persist
(function | string | list(function | string)) a lambda with two arguments saying what information should be persisted for the URL generated. The information will be retrieved for the following pages into the config underurl_data
using the URL as key and the persisted data as value. -
headers
(boolean) is telling if the markdown files have headers or not. Default value istrue
. -
excerpt
(boolean) is telling if the markdown files have excerpt or not. Even if they have no excerpt and we configure this value totrue
the code is getting the first paragraph as excerpt. Default value istrue
. -
var_name
(string |:plain
) it's setting the list of pages using the name provided here to be in use into the template. The default value is:plain
. -
format
(:erlydtl
) yes, it makes no sense to put this value at the moment, but in near future it's desirable to have also support for other template engines like:eex
. Keep in touch!. -
env
(map) it's letting us to define extra parameters for the template in a map format:set env: %{ "environment" => System.get_env("LPAD_ENV") || "dev" }
-
priority
(:high
|:normal
|:low
) indicates when the page will be generated. It's useful when we want to usetransform_to_persist
and generatesitemaps
at the end of the generation of the blog.
The way the pages are rendered into the files depends on the configuration. But mainly they follow these steps:
-
The pages are retrieved (read) from the place we have configure (
from
). But even it's possible avoid this and only render a template without specific content. This step is optional. -
The pages are processed item by item (using
transforms_on_item
). If there are no pages from step 1 this step is ommited. -
The pages are processed as a whole (using
transform_on_page
). If there are no pages from step 1 this step is ommited. -
The config is processed based on the pages (using
transform_on_config
). If there are no pages from step 1 this step is ommited. - The page is rendered using the template and appliying the widgets.
- File is written using the render page.
-
The page data is processed by the
transform_to_persist
function and modify the configuration for the specific URL. If thepages
generates several files, then several URLs are going to be saved into theurl_data
configuration.
checks
When we have to check if our website was correctly built we could inject some specific code which could help us to determine if that was right or not. A typical check we can perform is for broken links:
check "broken local links" do
set on: :finish
set run: fn(config) ->
urls = for {url, _} <- config[:url_data], do: URI.parse(url)
site_root = config["site_root"]
for link <- config[:links] do
if String.starts_with?(link, site_root) do
link =
if String.ends_with?(link, "index.html") do
String.replace_suffix(link, "/index.html", "")
else
link
end
unless URI.parse(link) in urls do
raise """
Broken link: #{link}
"""
end
end
end
config
end
end
:links
and :url_data
are provided by Lambdapad. :url_data
could be modified (its content) by transform_to_persist
transformations (into every page
block).
The only option available at this moment for on
is :finish
. The run
option only accepts a valid closure (or anonymous function) which admits one parameter.
You can perform whatever inside of the check and it's a good idea to return the config again because if you are using more than one check, it's going to run the following one based on the config you returned.