node package manager

feral

Feral

Express + Zombie caching content server.

Store static HTML snapshots of JavaScript applications, conditionally serving those static snapshots to clients that are not capable of running the application.

Installation

npm install feral

feral.create

Create a new Feral + Express server. Create takes a single object with the following keys as it's only argument:

  • port - Number
  • public - String, path to serve static files from
  • cacheDir - String, path to write
  • targets - Object, name: callback pairs. callback will receive an http.ServerRequest object and must return boolean.
  • cache - Object, name: Object option pairs. options can contain:
    • url - String, route. May be relative or a fully qualified URL.
    • targets - Object, name: true || callback pairs. If specifying a callback it will receive a zombie.Browser instance and a continuation callback. The continuation callback must be called.
    • callback - Function. Will receive a zombie.Browser instance and a continuation callback. The continuation callback must be called.
    • debug - Boolean
    • runScripts - Boolean
    • userAgent - String

The following is used to serve functionsource.com:

feral = require 'feral'

server = feral.create
  port: 3000
  public: __dirname + '/public/'
  cacheDir: __dirname + '/cache/'
  targets: [
    {
      mobile: (request) ->
        request.agent().name in ['ios','android']
    }
    {
      html5: (request) ->
        request.agent().historyAvailable
    }
    {
      legacy: (request) ->
        not request.agent().historyAvailable
    }
  ]
  cache:
    index:
      url: '/:page?'
      targets:
        mobile: true
        legacy: (browser,next) ->
          # do something only when taking a snapshot for a legacy target
          next()
      callback: (browser,next) ->
        # do something when taking a snapshot for any target
        next()
    tag:
      url: '/tags/:tag/:page?'
      targets:
        legacy: true
    post: 
      url: '/post/:slug'
      targets:
        legacy: true

To make an existing Express server Feral, pass it as the first argument (you must still pass the port number in the options object):

server = express.createServer()
# server setup logic here
feral.create server, options

Targets & Cache Middleware

Feral installs a middleware method which is similar to express.static, but will conditionally serve files based on the target. The following files would be served when a request for "/" is initiated:

mobile: /cache/mobile/index.html
legacy: /cache/legacy/index.html
html5: (no match, let other middleware respond)

Like middleware, order matters when defining targets and the first target callback that returns true will determine which file will be served. In our example above a browser may be both HTML5 capable and a mobile device, but mobile was specified first, so the request will be treated as a "mobile" target.

HTTP API

For each cache name specified Feral exposes a corresponding HTTP GET method. Visiting this URL will trigger Feral to send a Zombie to cache the page for each target. Given the example shown in feral.create visiting:

http://localhost:3000/feral/post?slug=slug_name

Will trigger Feral to send a Zombie to:

http://localhost:3000/post/slug_name

And will cache the result at:

/cache/legacy/post/slug_name

server.cache

A wrapper for the HTTP API

server.cache post: slug: 'slug_name', -> # on complete callback

Can also be used in cache definition callbacks to have one cache trigger another (i.e. when caching a blog post also cache the front page of the blog).

Cache Sweeping

Like Rails, sweeping the cache is an application specific task that is up to you to implement. Two convenience methods are provided to assist.

server.urlForCache (cache_name,params) - Returns the url that would be visited by a Zombie.

server.urlForCache("post",slug: "post_name") == "/post/post_name"

server.pathForCache (cache_name,target_name,params) - Returns the path in the filesystem where the cache would be stored.

server.pathForCache("post","mobile",slug: "post_name") == "/cache/mobile/post/post_name"

Request

Feral adds an agent method to every http.ServerRequest request object which is useful in creating targets. The agent method returns an object with the following attributes:

  • name - String, name of the user agent, will be one of the following:
    • chrome
    • firefox
    • safari
    • ios
    • android
    • opera
    • ie
    • bing
    • google
    • ask
    • yahoo
    • unknown
  • version - String, user agent version.
  • historyAvailable - Boolean, does the agent support the HTML5 history API?
  • robot - Boolean, is the agent a robot?
  • targets - Object, containing target_name: boolean pairs for each of the specified targets.

Browser Object

Every browser object in Feral is a zombie.Browser object with the following additional methods:

  • addScript (src) - Add a script tag to the browser.window's head.
  • removeScript (src = false) - Remove a script tag from the browser.window's head. Not specifying a src will remove all.

Single File Targets

Serving a single file to a given target (i.e. an HTML page that will load a JS application) for all requests can be accomplished with an Express middleware method. The following code is used in conjunction with Feral on functionsource.com:

for path in [
  "/:page?"
  "/tags/:tag/:page?"
  "/post/:slug"
]
  server.get path, (request,response,next) ->
    return next() if not request.agent().targets.html5
    response.sendfile __dirname + '/cache/html5/index.html'

Protecting Feral

By default the Feral HTTP API is publicly accessible. It can be protected with an Express middleware method:

server = feral.create ...

server.get /^\/feral\/.*/, (request,response,next) ->
  if not request.query.admin?
    next new Error 'Not admin'
  else
    next()