This guide is intended to introduce programming beginners to making websites, in particular static websites. It aims to assume little prior knowledge except for the very basics of programming with Clojure.
If you have any questions while following this guide please open an issue or join the
#perunchannel in the http://clojurians.net Slack group. We are happy to help.
Boot is a tool to help authoring Clojure projects. It downloads dependencies and can build your project. Also Perun is built on top of Boot.
Installation instructions can be found in Boot's Readme. If you already have Boot installed make sure you're using the latest version (boot -u).
build.boot fileNote: This tutorial will assume a UNIX-like system (i.e. Linux/Mac), if you're using windows you can either look up the respective commands or try using the Explorer for basic tasks. Here are some basic intro links to working with a terminal: Mac, Linux, Windows
Create a directory:
mkdir my-website # creates the directory
cd my-website # moves you into the directory
and use your favorite text editor to add a file build.boot as below:
(set-env!
:source-paths #{"src" "content"}
:dependencies '[ [perun "0.3.0" :scope "test"] ])
(require '[io.perun :refer :all])
also add a file boot.properties (dont worry about it for now):
#http://boot-clj.com
#Mon Jan 18 23:19:36 CET 2016
BOOT_CLOJURE_NAME=org.clojure/clojure
BOOT_CLOJURE_VERSION=1.7.0
BOOT_VERSION=2.5.5
BOOT_EMIT_TARGET=no
Now let's create the simplest website possible: a single page with some text on it.
Create the directory content and place a file index.markdown inside it, containing the following:
# Hello World
We are making a website!
Note: Markdown is a lightweight markup language with plain text formatting syntax designed so that it can be converted to HTML (among other formats).
Now Boot allows us to run tasks from the terminal. To see what tasks we have at our disposal we can run boot --help from a terminal session.
The printed output will have a section similar to this:
Tasks: add-repo Add all files in project git repo to fileset.
aot Perform AOT compilation of Clojure namespaces.
checkout Checkout dependencies task.
help Print usage info and list available tasks.
install Install project jar to local Maven repository.
jar Build a jar file for the project.
[...]
atom-feed Generate Atom feed
base Adds some basic information to the perun metadata and
build-date Add :date-build attribute to each file metadata and also to the global meta
canonical-url Adds :canonical-url key to files metadata.
collection Render collection files
draft Exclude draft files
global-metadata Read global metadata from `perun.base.edn` or configured file.
gravatar Find gravatar urls using emails
images-dimensions Adds images' dimensions to the file metadata:
images-resize Resize images to the provided resolutions.
inject-scripts Inject JavaScript scripts into html files.
markdown Parse markdown files
permalink Adds :permalink key to files metadata. Value of key will determine target path.
print-meta Utility task to print perun metadata
render Render pages.
rss Generate RSS feed
sitemap Generate sitemap
slug Adds :slug key to files metadata. Slug is derived from filename.
ttr Calculate time to read for each file
word-count Count words in each file
The part below the [...] has been added to the set of available tasks by the code we put into our build.boot file and are part of Perun, the static site generator we will use in this guide.
Perun works, similarily to — and on top of — Boot. Both provide tasks operating on a value and producing a new value. By ensuring that the newly produced value looks similarily to the received value you can mix and match tasks as you wish (this is often called composition).
Since we've just created a index.markdown file, let's try the markdown task:
$ boot markdown
[markdown] - parsed 1 markdown files
Great! We parsed a Markdown file. But where did the resulting HTML go? As mentioned earlier Perun works by passing values from task to task. Most of Perun's tasks don't write files directly but instead add information to this value so that it can all be written to files at a later stage.
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ Value │ Task 1 │ Value │ Task 2 │ Value │ Task 3 │ Value │
│ V0 │───────────▶│ V1 │───────────▶│ V2 │───────────▶│ V3 │
│ │ │ │ │ │ │ │
└───────┘ └───────┘ └───────┘ └───────┘
To inspect the value that is passed from task to task we can use the print-meta task. Below you can see how print-meta first prints an empty list () and after the markdown task ran prints more information including the rendered markdown.
$ boot print-meta markdown print-meta
()
[markdown] - parsed 1 markdown files
({:content "<h1><a href=\"#hello-world\" name=\"hello-world\"></a>hello world</h1>",
:extension "md",
:filename "index.md",
:full-path "/Users/martin/etc/boot/cache/tmp/Users/martin/code/perun-guide/k8y/-grrwi1/index.md",
:original true,
:parent-path "",
:path "index.md",
:short-filename "index"})
Let's do something with that information, let's render it to a file. Perun provides a render task. Let's use boot to figure out what it does:
TODO fix this
$ boot render --help
Render individual pages for entries in perun data.
The symbol supplied as `renderer` should resolve to a function
which will be called with a map containing the following keys:
- `:meta`, global perun metadata
- `:entries`, all entries
- `:entry`, the entry to be rendered
Entries can optionally be filtered by supplying a function
to the `filterer` option.
Filename is determined as follows:
If permalink is set for the file, it is used as the filepath.
If permalink ends in slash, index.html is used as filename.
If permalink is not set, the original filename is used with file extension set to html.
Options:
-h, --help Print this help info.
-o, --out-dir OUTDIR Set the output directory to OUTDIR.
--filterer FILTER Set filter function to FILTER.
-r, --renderer RENDERER Set page renderer (fully qualified symbol which resolves to a function) to RENDERER.
Ok, so rendering individual pages for entries. Given that we only have a single entry right now that sounds like what we want. Let's try just calling the render task after the markdown task:
$ boot markdown render
[markdown] - parsed 1 markdown files
clojure.lang.ExceptionInfo: java.lang.AssertionError: Assert failed: Renderer must be a fully qualified symbol, i.e. 'my.ns/fun
(and (symbol? sym) (namespace sym))
data: {:file
"/var/folders/ss/4qg3hk1d4nv40phg1360ng5w0000gn/T/boot.user1104049499782741856.clj",
:line 19}
java.util.concurrent.ExecutionException: java.lang.AssertionError: Assert failed: Renderer must be a fully qualified symbol, i.e. 'my.ns/fun
(and (symbol? sym) (namespace sym))
java.lang.AssertionError: Assert failed: Renderer must be a fully qualified symbol, i.e. 'my.ns/fun
(and (symbol? sym) (namespace sym))
io.perun/assert-renderer! perun.clj: 375
io.perun/render-in-pod perun.clj: 379
io.perun/eval2098/fn/fn/fn perun.clj: 417
io.perun/eval1600/fn/fn/fn perun.clj: 116
boot.core/run-tasks core.clj: 794
boot.core/boot/fn core.clj: 804
clojure.core/binding-conveyor-fn/fn core.clj: 1916
...
Duh, that didn't work. The error tells us we need to supply a (fully-qualified) symbol to the renderer option pointing to a function. Let's create a namespace with a renderer function for our page. First create the required directory structure:
mkdir -p src/site
Now add a file at src/site/core.clj containing the following:
(ns site.core
(:require [hiccup.page :as hp]))
(defn page [data]
(hp/html5
[:div {:style "max-width: 900px; margin: 40px auto;"}
(-> data :entry :content)]))
We've added a function that renders a bit of HTML and inserts what has previously been parsed by the markdown task (the :content). The rendering is done by Hiccup a library to convert Clojure data structures to HTML. A simplistic example would be:
[:span {:class "foo"} "bar"] ; Clojure produces
<span class="foo">bar</span> ; HTML
Now that we have a function that we can use as renderer let's give it a try:
$ boot markdown render -r site.core/page # -r is a shorthand for --renderer
Error! Our program can't find the Hiccup code because we haven't added it to our list of dependencies. Modify build.boot so it looks like this:
(set-env!
:source-paths #{"src" "content"}
:dependencies '[[perun "0.3.0" :scope "test"]
[hiccup "1.0.5"]])
(require '[io.perun :refer :all])
Note: By adding a dependency to the list of
:dependenciesinbuild.bootyou make it available to the rest of your program.
Now try the command from above again and see that it works:
$ boot markdown render -r site.core/page
[markdown] - parsed 1 markdown files
[render] - rendered 1 pages
Still we don't see any files being generated, to fix that just append the target task to your command:
$ boot markdown render -r site.core/page target
[markdown] - parsed 1 markdown files
[render] - rendered 1 pages
Writing target dir(s)...
Note: Remember the value we spoke about earlier that is passed from task to task? In Boot this value describes a directory structure and files inside it. Whenever the
targettask is used Boot will sync relevant files from this description to an actual target directory.
Now there should be a directory called target containing another directory public, finally containing a file index.html. If you open that file you should see your new website! :tada:
Admittedly it is a very basic website but let's recap what we've done:
Let's add an about page by adding the file content/about.markdown:
# About this site
This site has been made by following the Perun guides
Also so that our visitors can find our new about page let's change our index.markdown file to look like this:
# Hello World
We are making a website! ([about this website](/about.html))
After rebuilding our site by running boot markdown render -r
site.core/page target we can open target/public/index.html again and see that there is a link to our new about page. If we click it however there will be an error.
This is because currently we're just opening those files from our filesystem and not retrieving them from a server as it's usually done with websites. To get closer to the "mode of operation" of an actual website and to fix this problem we need to serve our website over HTTP.
Add [pandeiro/boot-http "0.7.0"] to the list of :dependencies in your build.boot. Also modify the require statement in that file to look like this:
(require '[io.perun :refer :all]
'[pandeiro.boot-http :refer [serve]])
Now use the newly available serve task like this:
boot serve --resource-root public markdown render -r site.core/page wait
There are two new things here:
serve --resource-root public — The serve task starts an HTTP server and serves files from the JAVA classpath, which we can think of as an imaginary directory somewhere on our computer containing lots of files. When we previously used the target task we saw that our generated files were all in a directory public so we tell the server to only respond to requests for files in public. (We could have also used the shorthand -r option instead of --resource-root by the way.)wait tells the task pipeline to wait even after it has finished. This way we ensure that even after all files are generated the server will keep running to serve those files.Now after running this command there will be a line printed like this one:
Started Jetty on http://localhost:3000
Go to http://localhost:3000 — you should see the index.html file with the link to your about page. Clicking on the link will bring you to http://localhost:3000/about.html properly displaying your about page.
Success! Every good website should have a way to go back to it's homepage however so let's modify our renderer function to always show a link to the homepage ("root") of the site. In src/site/core.clj change the page function to look like this
(defn page [data]
(hp/html5
[:div {:style "max-width: 900px; margin: 0 auto;"}
[:a {:href "/"} "Home"] ; <---- We added this
(-> data :entry :content)]))
After restarting our site building & serving command go to http://localhost:3000 again and view your site. On both pages there should now be a little "Home" link at the top bringing you back to the index page.
One more thing before you're done: Currently every time we make a change we have to restart our command. To avoid this you can adapt the command like this:
boot serve -r public watch markdown render -r site.core/page
The watch task will rebuild your page whenever an important file changes. (Because the watch tasks keeps the pipeline running we don't need wait any longer.) Give it a try by editing index.markdown and reloading the browser!
You're at the end of the "Getting Started" tutorial. There are many things still to be explored, proceed with whatever interests you most:
boot serve ... command can be annoying, learn how you can define your own tasks consisting of several sub-tasks in the [Boot Task Guide][task-guide]