Documentation

Fasty is a Blazing Fast CMS built on top of openresty & arangoDB

Requirements

You'll need the following tools installed on your computer

  • NodeJS
  • NPM or Yarn
  • docker & docker-compose
  • foxxy NPM module installed globally
npm install foxxy -g
npm install foxx-cli -g

# optional for using your own code editor
npm install fasty-cli -g 

You'll have to update your ~/.foxxrc file with 

[server.foxxy]
url=http://localhost:8529
username=root
password=password

Get Started

Fasty is based on Openresty, Lua & ArangoDB. You'll have the full stack installed via Docker. Pull the repository and inside it run :

docker-compose build
docker-compose run --rm cms moonc **/*.moon
docker-compose up cms

You have first to create a foxxy/app/js/config.js file (you can copy content from foxxy/app/js/config.js.sample)

The application should run on http://demo.localhost:8080 and http://demo.localhost:8080/static/admin . The database is running on http://localhost:8529 (login : root, password: password)

You'll need to create first a database called db_demo and run within the foxxy folder

foxxy upgrade settings --server foxxy --database db_demo
foxxy upgrade --server foxxy --database db_demo

You should be able now to access the admin URL with the following credentials : 

demo@foxxy.ovh :: 977cebdd

Don't forget to change this default account via the /static/admin page !

Folders rights

You'll also need to update ownership of <app_folder>/install_service & <app_folder>/scripts to be writable by your docker process. If you are unsure use the same user/group from <app_folder>/fastcgi_temp folder. Without that, you won't be able to install API or install scripts (nodejs)

Create certificates for google cloud storage (optional)

Fasty allows you to store your assets in the google cloud. For that you just need to create a folder called `/certs` in the root of your application. For your multi-tenant apps you can define one file per sub domain. 

./certs
  -- cms.json
  -- app1.json
  -- default.json

it will fallback to `default.json` if no match. Here a sample of the json data :

{
  "type": "service_account",
  "project_id": "<your_project_id>",
  "private_key_id": "1234567890abcdef",
  "private_key": "-----BEGIN PRIVATE KEY-----\n ... <your_private_key>\n-----END PRIVATE KEY-----\n",
  "client_email": "client_email",
  "client_id": "client_id",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "<link_to_your_file>"
}

Layouts

Layouts allow you to define the global template of your application. You can have multiple layouts. Each pages can be linked to any layout.

In this basic sample we can see interesting things :

css & css_vendors are CSS content defined in the layout
js & js_vendors are JS content defined in the layout

@headers will be replaced by generated headers

will display the content of your page

you can also notice the en to display the current language & some {{ partials | ... }} to display shared parts

css_vendors :

{{ external | https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css }}
{{ external | https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/railscasts.min.css }}

js_vendors

{{ external | https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js }}
{{ external | https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/languages/json.min.js }}
{{ external | https://cdnjs.cloudflare.com/ajax/libs/riot/3.13.2/riot+compiler.min.js }}

You can define as many external resources you need.

Pages

Pages are the pages of your website. You can create as many pages you want.

Notice that the slug must be unique across all pages. The default url schema is : /:lang/:all/:slug(/*)

:lang is the current lang (e.g. en, fr, etc.)
:all can be anything you want (e.g. -, my, anything, useful, etc.)
:slug
is your page's slug (e.g. home, page, etc)

Any additional path element will be stored in the splat params.
Sample : /en/my/home/page/10

splat params will looks like { page: 10 }


Shortcuts

page

Display any page content via it's slug

{{ page | <slug_or_field> (| <dataset>) }}

{{ page | my_famous_page }} << Display page content of the my_famous_page page

{{ page | slug | posts }} << This one will display the html field from your posts dataset using the slug defined in the URL


partial

Display partials via it's slug

{{ partial | <partial_name> (| <dataset> | <options>) }}

But a partial can have a dataset (arango, rest or use_params) and you can also use do_not_eval as dataset if you just want to include etlua file without evaluating it directly.

{{ partial | demo | arango | aql#FOR user IN users RETURN user }}

{{ partial | pagination | use_params | current_page#1#pages#10 }}

[string "etlua"]:5: attempt to index field '_params' (a nil value)

lang

Display the current lang

{{ lang }}

settings

Display an element set in the settings/home section (json) 

{{ settings | <mailgun_api> }} will fetch data from json data

spa

DIsplay a single page application

{{ spa | <my-app> }}

riot

Mount a riot tag.

{{ riot | tag1 }} Load the widget but don't mount it

{{ riot | tag1 | mount }} Load and mount the widget

{{ riot | tag1#tag2 }} Load tag1 & tag2 (reduce http calls)

tr

Display a translated text. If the key don't exist it will create an entry in the traductions list.

{{ tr | welcome }}

It will display a missing message if the translation is not existing:  welcome

splat

Display a value from the splat params (see pages section)

{{ splat | <splat_key> }}

helper

Display a helper (AQL + partial). You can now add options

{{ helper | <helper_name> (| key1#value1#key2#value2...) }}

AQL

If you need to execute a request just use

{{ aql | <your_aql_request> }}

og_data

You can define a request per page to define headers dynamically. 

Let say we have this AQL request defined : 

FOR doc IN datasets
  FILTER doc.type == "articles" AND doc.slug == @slug
  RETURN { title: doc.title }

You could use then this shortcut in any header fields to get the content. You can also define a default value if og_data is not provided or empty.

{{ og_data | title | <default> }}

html

To display a content from a dataset within the page partial.

{{ html | <key> | <field> }}

You can also use the og:aql request to get the json object to be transformed within the page partial

{{ html | <key_defined_in_og:aql> }}

json

If you need to load an external JSON resource and extract / display a specific key you need to use :

{{ json | url | key }}

The key can be a nested one (e.g owner.login)

Dataset

Display a specific key from a dataset

{{ dataset | key | field | <args> }}

Can be useful for loading JS or CSS content from a dataset record

e.g. :
{{ dataset | slug=myscript | js }}

The second one will return the URL only as js file (using the right content-type)

Components

Components are JS components (RiotJS for now)

<todo>

  <!-- layout -->
  <h3>{ opts.title }</h3>

  <ul>
    <li each={ item, i in items }>{ item }</li>
  </ul>

  <form onsubmit={ add }>
    <input ref="input">
    <button>Add #{ items.length + 1 }</button>
  </form>

  <!-- style -->
  <style>
    h3 {
      font-size: 14px;
    }
  </style>

  <!-- logic -->
  <script>
    this.items = []

    add(e) {
      e.preventDefault()
      var input = this.refs.input
      this.items.push(input.value)
      input.value = ''
    }
  </script>

</todo>

Partials

Partials are .etlua files ... It can be a simple raw HTML content or can embed some logic via LUA code.

You can call a partial with a specific dataset (AQL request, or external JSON data)

<div class="field is-grouped is-grouped-multiline">
  <% for k, item in pairs(dataset.results) do %>
  <div class="control">
    <div class="tags has-addons">
      <span class="tag"><%= item.name[lang] %></span>
      <span class="tag is-primary"><%= item.state[lang] %></span>
    </div>
  </div>
  <% end %>
</div>

With the following AQL request

FOR doc IN datasets
FILTER doc.type == 'features'
SORT doc.order ASC
RETURN doc

You can define a helper called features to load this partial & AQL and then call this helper with {{ helper | features }}

Single Page Applications

Create as many SPA you want. An application has on HTML tag to load components and one JS tag to set the router
{{ riot | account#plans#tchins#offer_tchins#tchins_history#friends#pending_invitations#pending_approvals#blocked }}

<div id="app"></div>
document.addEventListener('DOMContentLoaded', function() {
  route('/', function(name) { riot.mount('div#app', 'account') })
  route('/plans', function(name) { riot.mount('div#app', 'plans') })
  route('/tchins', function(name) { riot.mount('div#app', 'tchins') })
  route('/offer_tchins', function(name) { riot.mount('div#app', 'offer_tchins') })
  route('/tchins_history', function(name) { riot.mount('div#app', 'tchins_history') })
  route('/friends', function(name) { riot.mount('div#app', 'friends') })
  route('/pending_invitations', function(name) { riot.mount('div#app', 'pending_invitations') })
  route('/pending_approvals', function(name) { riot.mount('div#app', 'pending_approvals') })
  route('/blocked', function(name) { riot.mount('div#app', 'blocked') })
  route.start(true)
})

AQLs

Store your AQL requests and then use them within Helpers

Datatypes & Datasets

Datatypes allow you to create dynamic forms for the content editors.

{
  "model": [
    { "r": true, "c": "1-1", "n": "name", "t": "string", "j": "joi.string().required()", "l": "Name", "tr": true },
    { "r": true, "c": "1-1", "n": "state", "t": "string", "j": "joi.string().required()", "l": "State", "tr": true }
  ],
  "columns": [ { "name": "name", "tr": true } ],
  "sortable": true
}

Once a dataset defined; a new dataype will appear and you'll be able to create & manage your data

Helpers

Choose a partial, a request ... Then you can call it anywhere you want using the helper shortcut

Redirections

Redirections allow you to define a specific route for a specific helper

API Builder

The API Builder allow you to write your Foxx service directly via Fasty. You can also deploy it with one click !

Server side scripts

You can edit your Nodejs server side scripts and restart them directly from Fasty. You'll have first to set the docker-compose file :
chatroom:
  build:
    context: .
    dockerfile: Dockerfile_node
  command: /bin/bash -c "yarn && yarn start"
  restart: always
  ports:
    - 8000:8000
  volumes:
    - ./scripts/your_folder/chatroom/:/workspace
  links:
    - arangodb:arangodb
  networks:
    frontend: