Rehydrated static files.

Hydrator - rehydrated static files

Hydrator is a small web application framework for semi-static sites. It maps URL paths to files, compiling certain kinds of assets on-the-fly, as appropriate. Hydrator also allows for dynamic content, with CoffeeScript files that can be executed to generate a response.

Static files (.html, .js, .css, .jpg, …) are passed through. Compilable files (.md, .coffee, .styl) are compiled and returned. .coffee files in the root project folder are treated as dynamic content handlers. Instead of being compiled and returned, they are executed inside a sandbox and given helpers for generating a response.

The bare minimum for a dynamic file is calling response.ok() with response text:

response.ok """
        <h1>Hello, world!</h1>
        <p>#{ new Date() }</p>

Hydrator works as a CLI, and is best used when installed globally:

$ npm install -g hydrator

The Hydrator CLI has two commands: create, serve.

Create a Hydrator project of the given name:

$ hydrator create <project_name>

This creates a folder named <project_name>, and populates it with the files necessary to run the project as a Heroku app. It also initializes it as a git repository.


Start the Hydrator server using the given current project.

$ hydrator serve <project_name>

$ hydrator serve . also works. If using the Heroku toolbelt, the recommended way of running the project is $ foreman start from inside the project, because this will automatically load environment variables from the .env file.

A Hydrator project consists of a folder with a package.json and a subfolder, named www, that contains the actual web content. Other files, like those generated above, can be used to run the project on hosts like Heroku.

For example, the following project structure maps to these URLs:

        index.html          - /
        about.md            - /about/               or /about.html
        api.coffee          - /api/*
        data.json           - /data.json
        _info.md            - 404
            service.coffee  - 404
            index.md        - /some-path/           or /some-path/index.html
            info.md         - /some-path/info/      or /some-path/info.html
            script.coffee   - /assets/script.js
            style.styl      - /assets/style.css
            icon.png        - /assets/icon.png
            module.coffee   - /other-files/module.js
            library.js      - /other-files/library.js

A request to / is served by the /index.html file (could also be /index.coffee or /index.md). /about/ is handled by the /about.md file, which is compiled into HTML on-the-fly. /assets/script.js matches the corresponding CoffeeScript source file, which is compiled to JavaScript and served. /api/, and any paths under that, are handled by /api.coffee, which is compiled to JavaScript and executed inside a sandbox. The file /package.json is unaccessible externally.

URLs generally match the file by path. Non-compilable or -executable files are simply returned as is. Files that can be compiled from Markdown or CoffeeScript match a URL suitable for their output (and cannot be served in raw form). CoffeeScript files at the root level will match any path starting with their name. The *.coffee file can be used as a catch-all, and if present will handle any request if it’s not matched by something else. Like compiled files, these executable files cannot be served directly. Note: files and folders beginning with an underscore are considered private; they may be accessed by executable files, but will return a 404 if attempted to be reached directly. URLs that do not end in a trailing slash but should (as in not directed at a file) are redirected to the URL with a trailing slash. A URL like /about goes to /about/, while /script.js does not.

Executable CoffeeScript files are run inside a sandbox that is given globals with information about the current request, functions to send a response, and various helpers and utilities. These files cannot require any modules.

The sandbox globals are:

  • env

    Environment variables.

  • restler

    restler HTTP request library.

    • get(url, headers={}, query={})
    • post(url, headers={}, query={}, data={})
    • put(url, headers={}, query={}, data={})
    • patch(url, headers={}, query={}, data={})
    • delete(url, headers={}, query={})
  • request

    Information about the current request.

    • url

      The full URL of the request, eg 'http://example.com/hello/world/?parameter=value'.

    • path

      The path of the URL in component form, eg ['hello','world'].

    • path_string

      The path of the URL as a string, eg '/hello/world/'.

    • query

      The parsed query parameters of the URL, eg { parameter: 'value' }.

    • query_string

      The raw query parameter string, eg '?parameter=value'.

    • host

      The host of the URL, eg 'example.com'.

    • method

      The method of the request, eg 'PUT'.

    • headers

      The headers of the request, eg { 'Accepts': 'application/json' }.

    • getForm

      Get the submitted form from a POST request: request.getForm (fields) ->

  • response

    Functions for sending a response. Each corresponds to a specific response status code. Only one function can be called once per request. These methods can generally all take JSON-serializable data in addition to Strings, and will serialize and set the appropriate Content-Type if that is the case.

    They generally follow the signature function(response_data, headers={}).

    • (200) ok
    • (201) created
    • (204) noContent - Does not take response_data
    • (301) permanentRedirect - response_data becomes the Location header
    • (302) temporaryRedirect - response_data becomes the Location header.
    • (400) badRequest
    • (401) unauthorized
    • (403) forbidden
    • (404) notFound
    • (405) methodNotAllowed
    • (410) gone
    • (500) internalServerError
  • cache

    Basic key-value caching of JSON-serializable data for 1 second to 1 hour (default 240 seconds).

    Set the cache with cache.set('key', value, expires=240). Get a cached value cache.get('key', function(val) { }).

    In multi-tenant setups, the keys are segregated by site host.

A single project can support multiple sites, differentiated by host. To do this, group the content for each site into its own folder within the project’s www/ folder. Then, in package.json, add an entry that maps the hosts to the folder names, like so:

    "sites": {
      "example.com": "site_name",
      "otherhost": "other_site"

Each folder name will be the root of the site as matched by the host. Note: you can omit the www. from the domain. It will be stripped when matching, so www.example.com and example.com would both match site_name.

Some example single-serving sites:

Some are relatively low traffic and share infrastructure using the multi-tenant feature. This way they, can keep the Heroku instance awake and have improved response time.

Alec Perkins