Tadam is a minimalistic framework for creating dynamic function-oriented websites. All the potential of Clojure simplified for fast development. It could be in someway equivalent to Flask, but a little more modern.
The rest of the magic will come from you.
urls.clj
(defroutes public
(GET "/" [] view-public/index))
views.clj
(defn index
[req]
(render-HTML req "welcome.html"
{ :name "Tadam"
:time (System/currentTimeMillis) }))
welcome.html
<h1>Welcome to {{ name }} Framework</h1>
<p>No bugs since: {{ time }}</p>
π TADAM!!! π
<h1>Welcome to Tadam Framework</h1>
<p>No bugs since: 1590773922089</p>
Installation
1. Make sure you have Openjdk or Oracle-jdk installed.
Debian/Ubuntu
sudo apt install default-jdk
Mac OS
brew install openjdk
2. Install Clojure and Leiningen.
Debian/Ubuntu
sudo apt install clojure leiningen
Mac OS
brew install clojure/tools/clojure leiningen
3. Create a π©Tadamπ© project.
lein new tadam-lite myproject
4. Run.
cd myproject
lein run
Great π. You already have your π₯ own web server π₯.
Now open your browser.
localhost:7404
Directory Structure
When you generate your project you will find the following structure.
config.yaml
project.clj
README.md
resources/
public/
css/
main.css
img/
tadam.svg
js/
main.js
templates/
layouts/
base.html
public/
404.html
welcome.html
src/
myproject/
views/
public.clj
config.clj
core.clj
urls.clj
Let's see the description of the relevant sections.
Root
Name | Type | Description |
---|---|---|
config.yaml | File | The configuration of your application, to which you can add all the variables you need. By default you will find domain, debug and port. You can see more information in Configuration |
project.clj | File | Clojure configuration. Add as many libraries as you need. |
README.md | File | Example of README.md file for your project. |
Resources folder
Everything related to the template system or static files (javascript, images, styles...).
Name | Type | Description |
---|---|---|
public | Folder | Static material. |
templates | Folder | Templates HTML. |
templates/layouts/base.html | File | Example of a template that will contain all the structure that will not change between pages, such as the header or footer. |
templates/public/welcome.html | File | Sample HTML page. |
src folder
Source code in Clojure, the heart of the beast. System of routes, views, business logic...
Name | Type | Description |
---|---|---|
views/ | Folder | The views are invoked by the urls.clj . When a route is visited, a function within the appropriate view is called. |
views/public.clj | File | Example of a public view. In the future it should grow with other private, management, identification or APIs. |
config.clj | File | Place to store the configuration. You can configure as many variables as you need or add them as root in config.yaml |
core.clj | File | First file to be executed in your application. It must have the minimum to start. |
urls.clj | File | Routes of your website. |
Configuration
By default you will have a config.yaml file like the one below.
domain: "http://localhost"
debug: true
port: 7404
- domain: Indicates which will be the domain of your application (http://example.com). In case you work locally or have a reverse proxy, it will not be necessary to modify it.
- debug: If true, it enables an automatic code refresh and ignores CORS. Otherwise it assumes you are working in production and will block the CORS in accordance to the domain.
- port: Port you will use.
Feel free to add as many variables as you need.
For example let's create the wizard variable and use it.
config.yaml
domain: "http://localhost"
debug: true
port: 7404
wizard: Merlin
core.clj
(ns myproject.core
(:require
[myproject.config :refer [config]]))
(defn -main [& args]
;; Main
(prn (config :wizard)))
" Merlin
Routing
Simple route
Inside urls.clj you can find an example where 2 routes are declared and linked to their respective views.
If you want to add new routes you should follow 4 steps.
- Import the View.
- Use or create a group of Routes.
- Define the routes with View.
- Add your group to the set of all routes (Optional, only if it does not exist). You should always leave at the end resources-routes, are used for static content.
(ns myproject.urls
(:require
[compojure.core :refer [defroutes GET]]
[compojure.route :as route]
;; 1) Import View
[myproject.views.public :as view-public]))
;; 2) Set group routes, in the example it is called "public"
(defroutes public
;; 3) Add routes
(GET "/" [] view-public/index)
(GET "/FAQ" [] view-public/faq)
(GET "/about" [] view-public/about))
(defroutes resources-routes
;; Resources (statics)
(route/resources "/")
(route/not-found view-public/page-404))
;; 4) Add your group of routes to all of them.
(def all-routes
;; Wrap routers. "resources-routes" should always be the last.
(compojure.core/routes public resources-routes))
Parameters
In the following example we have routes that require different parameters.
(defroutes public
(GET "/" [] view-public/index)
(GET "/blog/:id" [id] view-public/blog)
(GET "/auth/activate-account/:token/:email/" [token email] view-auth/activate-account))
In the View, the variables are collected as follows.
(defn activate-account
"Activate account"
[req]
(let [token (-> req :params :token)
email (-> req :params :email)]
;; Your magic code
(redirect req "/auth/login/")))
Avoiding repetition
At some point you will need to have a prefix for the routes.
(defroutes user-routes
(GET "/user/auth/login" [] ...)
(GET "/user/auth/signup" [] ...)
(GET "/user/auth/recovery-password" [] ...))
To avoid this you can use a context.
(defroutes user-routes
(context "/user/auth" []
(GET "/login" [] ...)
(GET "/signup" [] ...)
(GET "/recovery-password" [] ...)))
Views
The views are used to include the logic of each route.
In this example, an HTML template is rendered.
(defn index
;; View HTML
[req]
(render-HTML req "public/welcome.html" {}))
In the following example, a simple JSON is printed.
(defn api
;; View JSON
[req]
(render-JSON req {:result true}))
;; { "result": true }
You can see more in Responses.
Templates
HTML templates can be displayed in raw, rendered using parameters or different layouts. As well as it is possible to return Markdown or JSON.
All templates should be located in /resources/templates/.
The syntax is inspired by Django. It is generated by Selmer, you can consult its documentation for more advanced topics such as loops or filters.
HTML
Suppose you have an HTML template in the /resources/templates/theatre.html path.
<!DOCTYPE html>
<html>
<body>
Olympia Theatre
</body>
</html>
Then you just need to use the function render-HTML.
(render-HTML [request] [path] [args]))
Example
;;;; View web
(ns myproject.views.my-view
(:require
[tadam.templates :refer [render-HTML]]))
(defn index
;; View HTML
[req]
(render-HTML req "theatre.html" {}))
HTML with params
Suppose you have an HTML template in the /resources/templates/theatre.html path.
<!DOCTYPE html>
<html>
<body>
<p>The {{ name }} Theatre was founded in {{ opened }} and currently has {{ surface }} seats.</p>
</body>
</html>
The View would be similar to the previous example but indicating its parameters.
(ns myproject.views.my-view
(:require
[tadam.templates :refer [render-HTML]]))
(defn index
;; View HTML
[req]
(render-HTML req "theatre.html" {
:name "Olympia"
:opened 1915
:surface 500
}))
As a result the request is returned.
<!DOCTYPE html>
<html>
<body>
<p>The Olympia Theatre was founded in 1915 and currently has 500 seats.</p>
</body>
</html>
HTML in layout
In case you need to repeat the same HTML structure, then it is possible to extend a template or to define which part will change at each page HTML in layout.
Let's create a template in /resources/templates/layouts/base.html that will our reference to generate new ones. It will contain all the recurrent parts, such as header, footer, navs, etc. Then we define where should be added blocks that will be different at each page.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/main.css">
<script src="/js/main.js"> </script>
<title>{% block title %}{% endblock %} | Olympia Theatre</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/">Welcome</a></li>
<li><a href="/programme">Programme</a></li>
</ul>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
More information in our newsletter
</footer>
</body>
</html>
Now it's time to define the page that will extend base.html with new template in /resources/templates/public/welcome.html.
{% extends "layouts/base.html" %}
{% block title %}
Welcome
{% endblock %}
{% block content %}
<h1 class="title-welcome">Welcome to Olympia Theatre</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Conclusum est enim contra Cyrenaicos satis acute, nihil ad Epicurum. Qui bonum omne in virtute ponit, is potest dicere perfici beatam vitam perfectione virtutis; Ecce aliud simile dissimile. </p>
{% endblock %}
The result is:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/main.css">
<script src="/js/main.js"> </script>
<title>Welcome | Olympia Theatre</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/">Welcome</a></li>
<li><a href="/programme">Programme</a></li>
</ul>
</nav>
</header>
<main>
<h1 class="title-welcome">Welcome to Olympia Theatre</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Conclusum est enim contra Cyrenaicos satis acute, nihil ad Epicurum. Qui bonum omne in virtute ponit, is potest dicere perfici beatam vitam perfectione virtutis; Ecce aliud simile dissimile. </p>
</main>
<footer>
More information in our newsletter
</footer>
</body>
</html>
Markdown
It is possible to use Markdown files that will be transformed into HTML.
(ns myproject.views.my-view
(:require
[tadam.templates :refer [render-markdown]]))
(render-markdown req "theatre.md" {})
JSON
There is a function available to convert collections to JSON.
(render-JSON [request] [collection]))
Example
(ns myproject.views.my-view
(:require
[tadam.templates :refer [render-JSON]]))
(render-JSON req {:name "Olympia" :surface 500 :opened 1915})
It returns a nice JSON with the right header.
{
"name": "Olympia",
"surface": 500,
"opened": 1915
}
Render templates to string
In certain circumstances it is necessary to render HTML templates to obtain a string, for example when we want to prepare content to send an email or because it is going to be saved in a new file.
(render-template [path template] [collection])
Example
(ns myproject.views.my-view
(:require
[tadam.templates :refer [render-template]]))
(render-template "public/template.html" {:name "Olympia" :surface 500 :opened 1915})
Requests
Params
GET
From the following URL we can capture the variable query.
curl https://mydomain.com/?query=Tadam
(-> req :params :query)
;; Tadam
POST
Capturing a parameter by POST is like GET.
curl --data "query=Tadam" https://mydomain.com/
(-> req :params :query)
;; Tadam
If you need to know if you are receiving the POST method you can use:
(ns myproject.views.myview
(:require [tadam.utils :refer [is-post]]))
(is-post req)
;; true or false
JSON
A small utility can be used to obtain the parameters of a JSON.
curl --header "Content-Type: application/json" \
--request POST \
--data '{"query": "Tadam","page": 1}' \
https://mydomain.com/
(ns myproject.views.myview
(:require [tadam.utils :refer [get-JSON]]))
(get-JSON req)
;; {:query "Tadam" :page 1}
HEADERS
If you need to capture the value of a header.
(ns myproject.views.myview
(:require [tadam.utils :refer [get-header]]))
(get-header req "Content-Type")
;; "application/json"
Responses
When used in a View the (render-HTML) function, or any other template element, it is not necessary to specify the request. In case you need to customize it you have a helper.
(response [req] [body] [status] [content-type]))
At least the request and the body must be indicated.
(ns myproject.views.my-view
(:require
[tadam.responses :refer [response]]))
(defn index
;; View HTML
[req]
(response req "Hi Tadam"))
Return to browser.
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Hi Tadam
Although it can be customized with status and content-type.
(ns myproject.views.my-view
(:require
[tadam.responses :refer [response]]))
(defn index
;; View HTML
[req]
(response req "<?xml version="1.0"?>
<Name>
Tadam
</Name>" 201 "text/xml;charset=utf-8"))
Return to browser.
HTTP/1.1 201 OK
Content-Type: text/xml;charset=utf-8
<?xml version="1.0"?> <Name> Tadam </Name>
Redirect
To make a redirect in your View you have within responses the utility redirect.
(redirect [req] [url] [status]))
Example:
(ns myproject.views.my-view
(:require
[tadam.responses :refer [redirect]]))
(defn index
;; View HTML
[req]
(redirect req "/contact/"))
If he's not told otherwise, he'll use status 303 (See Other).
If you need another one, such as 301, you can customize the argument status.
(ns myproject.views.my-view
(:require
[tadam.responses :refer [redirect]]))
(defn index
;; View HTML
[req]
(redirect req "/blog/tadam-is-magic/" 301))
Also redirect-permanent in case you want to use the 308 state directly, although it can also be done like the previous examples.
(redirect-permanent [req] [url]))
Example:
(ns myproject.views.my-view
(:require
[tadam.responses :refer [redirect-permanent]]))
(defn index
;; View HTML
[req]
(redirect-permanent req "/blog/tadam-is-magic/"))
If you want to send emails before you must update the configuration variables in config.yaml.
Add the following.
smtp-from: "no-reply@domain.com"
smtp-host: "smtp.domain.com"
smtp-user: "user"
smtp-password: "password"
smtp-port: 587
smtp-tls: true
Then all you have to do is use send-email.
(send-email [config] [recipient email] [subject] [message HTML] [message plain])
Example:
(ns myproject.views.my-view
(:require
[myproject.config :refer [config]
[tadam.responses :refer [response]]
[tadam.email :refer [send-email]]))
(defn send-message
;; View Send email
[req]
;; Send email
(send-email config "client@email.com" "My subject" "<h1>Title</h1><p>Content</p>" "Title\nContent")
;; Response OK
(response req "Send!!!!"))
You can do this easily by customizing the HTML or plain text with render-template.
(ns myproject.views.my-view
(:require
[myproject.config :refer [config]
[tadam.responses :refer [response]]
[tadam.templates :refer [render-template]]
[tadam.email :refer [send-email]]))
(defn send-message
;; View Send email
[req]
(let [params {:name "Houdini"
:born 1874}]
;; Send email
(send-email config
"client@email.com"
"My subject"
(render-template "emails/contact.html" params)
(render-template "emails/contact.txt" params))
;; Response OK
(response req "Send!!!!")))
Compile
Run the following command to build a jar file.
lein uberjar
After this two files should be created in target/. We will use the standalone version: {project name}-standalone.jar.
Service
Systemctl
To create a service in Linux is done like any application in Java. Below you can see an example.
Create a file in the following path: /etc/systemd/system/tadam-project.service.
Add the content.
[Unit]
Description=Tadam-project
After=network.target
[Service]
Type=simple
Restart=always
WorkingDirectory=/folder/jar/
ExecStart=java -jar tadam-project.jar
[Install]
WantedBy=multi-user.target
Finally enable and start the service.
sudo systemctl enable tadam-project
sudo systemctl start tadam-project
Reverse Proxy
Nginx
With Nginx it's pretty quick and easy. You can use it as a reverse proxy, since Glosa contains its own web server (Jetty). You can see an example of configuration that can be useful.
server {
server_name tadam.domain.com;
access_log /var/log/myproject_access.log;
error_log /var/log/myproject_error.log;
location / {
proxy_pass http://localhost:7404/;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
}
Donation
Success stories
Comments for static sites. Clone of Disqus, but faster, Opensource and sexy.
Do you want to know if a site is made with Tadam?
curl -Is localhost:7404 | grep 'X-Powered-By:'
π
X-Powered-By: Clojure/Tadam
Thanks
Valentina Rubane: Revision of texts in English.