node package manager
Orgs are free. Discover, share, and reuse code in your team. Create a free org »

pellet

Pellet

Build SEO friendly rich isomorphic app using React, webpack, nodejs, and FRP

Build Status Issues NPM Version Dependency Status License

Codeship Status for Rebelizer/pellet

Installation

$ npm install -g pellet

Is this you?

You love React/Webpack/NodeJS/Jade. You care about having a SEO friendly application, but you need a dynamic application that can render its pages on the client without any dependencies on your server. You need an easy way to compose your applications making deployment, A/B tests, and reusable components a cinch. Your site needs to support internationalization. You care about instrumentation, logging, and analytics, and you want to track user behavior on your site. You care about testing, and you want good tools for running, building, and deploying. You want to use react, jade, cjs, js/cs and other modern technologies. You need to support not only desktop, but also mobile, tv, and other devices.

Sounds like you? Then you are like VEVO, and we are open sourcing pellet, our tool that does all of this. In under five minutes you can have your first project up and running with all of the above bells and whistles. Check out pellets features below.

Motivation

Pellet was built to support VEVO's isomorphic web platform. Last year VEVO was responsible for 40+ billion videos watched globally and served out a ton of pages in multi languages and devices. The problem that VEVO is facing is the need to have an SEO friendly web platform, but also to have a dynamic RIA web application.

AngularJS, Amber, BACKBONE, meteor are great, but it is hard to build a solution that cleanly manages SEO in multiple languages, devices, support social share bots, etc. These technologies are great for building UI and interactive applications, but not for SEO and social share bots. That is where Pellet comes in. Pellet is letting VEVO build an isomorphic web application that runs as RIA web app, but is SEO friendly, localized, and fast enough to support a large site.

Pellet is letting VEVO build components, pages, and layouts that can run on both the server and client. So when a request for a URI comes in (from a user, SEO/social share bot) the server can generate the markup with the same exact code used on the client. Then the code sent to the client can render any page without the need for the server anymore. This is achieved by Pellet building a client and server version of your code, automatically making your code isomorphic. On the request to the server we get the data, render a page, set headers, and serialize the state on the server so the RIA can pick it up. Then when the page is sent to the browser or bot it's like any other website, but in cases when the request goes to a browser we can initialize the RIA web application with the server's serialize state. In this step the page is bootstrapped and made live, so as the user navigates the site there is no need to ask the server for anything, except for RESTFull API data.

Features

  • Isomorphic environment
  • Robust routing
  • Instrumentation, logging, statsd, and alerting
  • Internationalization
  • FRP, event, and coordinator support
  • Flexible multi-environment configuration (dev, staging, prod)
  • Manifest based composition
  • Modular page, component, and layout/skin framework
  • Very extensible and flexible architecture
  • User tracking and client side metrics
  • Webpack based with asset management
  • Preconfigured to support
    • css, less, stylus
    • jsx, cjs, jade(reactjs transpiler)
    • JavaScript, CoffeeScript, es6
  • Multi device and platform support
    • Web desktop, mobile, tablet
    • Cordova
  • Easy Tooling
    • Managed HTTP/HTTPS/spdy server
    • Polyfill server
    • Watch and reload development mode
    • CLI tools to create projects,
    • Deployment tools

Quick Start

Create your first project

After installing Pellet you can use Pellet to create an empty project (an opinionated boilerplate). Just follow the prompts and answer the questions.

$ cd /tmp
$ pellet init

Example:

Name: (tmp) demo
Version: (0.0.0)
Create Directory: (Y/n)
Template Type: (Use arrow keys)
❯ Jade 
  JSX 
Language: (Use arrow keys)
❯ JavaScript 
  CoffeeScript 
Styles: (Use arrow keys)
❯ stylus 
  css 
  none 
About to:
 Create: /tmp/demo/config
 Create: /tmp/demo/public
 Create: /tmp/demo/src/page-skeleton.ejs
 Create: /tmp/demo/src/page-500.ejs
 Create: /tmp/demo/src/page-404.ejs
 Create: /tmp/demo/frontend
 Create: /tmp/demo/.pellet
 Create: /tmp/demo/frontend/index.jade
 Create: /tmp/demo/frontend/index.js
 Create: /tmp/demo/assets/o.styl
 Create: /tmp/demo/assets/reset.styl
 Create: /tmp/demo/manifest.json
 
Is this ok: (Y/n)

Start your project

Now that we have a new project, let's build and run it. We will use Pellet internal web server to make the setup easy, but you can use your own server if you want.

$ cd /tmp/demo
$ pellet run --build

Wait until you see "Listen on 8080 0.0.0.0" in the terminal, then visit the demo app in your browser http://localhost:8080/. You should see a web page displaying a page with "My index Page". Now let's stop the server and restart it in --watch mode. This will let us edit our files and see our changes without having to start/stop our server each time we make a change. Type control-c in the terminal to stop the server.

$ pellet run --watch --clean

This will now start the server in watch mode. The --clean will make sure the previous build files get cleaned up so you know your environment is clean. Now that the server is running in watch mode reload http://localhost:8080/ in the browser. Now update the files /tmp/demo/frontend/index.jade and /tmp/demo/frontend/index.js to:

index.jade

.index-page
  style
    | .todo-list {clear:both;padding-top:20px}
    | ul {width:300px; padding:0;}
    | li {list-style: none;}
    | li button {float:right}
  h1 Pellet TODO
  input(style={float:'left'}, onKeyDown=this.keydown, ref='text')
  button(style={float:'left'}, onClick=this.add) ADD
  .todo-list
    if !this.state.items || this.state.items.length == 0
      p NO ITEMS
    else
      div
        ul
          for item,ix in this.state.items
            li
              span #{ix}: #{item}
              button(onClick=this.del.bind(this, ix)) X
        p #{this.state.items.length} things todo

index.js

var React = require("react")
  , indexJade = require('./index.jade')
  , pellet = require("pellet");
 
module.exports = indexPage = pellet.createClass({
 
  routes: ["/", "/index"],
 
  getInitialState: function() {
    return {
      items: []
    };
  },
 
  keydown: function(event) {
    if(event.keyCode==13) {
      this.add();
    }
  },
 
  add: function() {
    var el = this.refs.text.getDOMNode();
    this.setState({items: this.state.items.concat(el.value)});
    el.value = '';
    el.focus();
  },
 
  del: function(ix) {
    this.state.items.splice(ix,1);
    this.setState({items: this.state.items});
  },
 
  render: function() {
    return indexJade(this);
  }
});

Now reload http://localhost:8080/ and you should see "My index Page" then a flicker to "Pellet TODO". This is due to the fact that Pellet's file watch will only reload the client side code and does not restart the server. So the markup downloaded to the client is the old code and then Pellet runs the new code on the client forcing a refresh. This renders the latest code and that is the flicker that you see. To fix this problem you just need to restart the server. Just control-c and rerun Pellet via "pellet run --watch --clean" - voila! Refresh and no flicker. Now let's add a new page.

$ cd /tmp/demo/frontend
$ pellet create
 
Type to create: (Use arrow keys)
❯ Component 
  Page 
  Layout 
  Project 
Name: (frontend) hello-world
Version: (0.0.0)
Create Directory: (Y/n)
Template Type: (Use arrow keys)
❯ Jade 
  JSX 
Language: (Use arrow keys)
❯ JavaScript 
  CoffeeScript 
Styles: (Use arrow keys)
❯ stylus 
  css 
  none 
Include unit tests: (Y/n)
Manifest location: (Use arrow keys)
❯ Update /tmp/demo/manifest.json 
  Create /tmp/demo/frontend/manifest.json 
  Create /tmp/demo/frontend/pellet.json 
  None (skip updating manifest) 
 
About to:
 Create: /tmp/demo/frontend/hello-world
 Create: /tmp/demo/frontend/hello-world/hello-world.jade
 Create: /tmp/demo/frontend/hello-world/hello-world.js
 Create: /tmp/demo/frontend/hello-world/hello-world.styl
 Create: /tmp/demo/frontend/hello-world/hello-world.test.js
 Overwrite: /tmp/demo/manifest.json
 
Is this ok: (Y/n)
 
$ pellet run --watch --clean
 

Caveats

Some features of Jade are not supported, like filters, mixins and cases. You can use other things like if/else, inline functions, and require() as alternatives. Additionally if there are more than one root nodes, only the last statement is returned. Same for block statements. This can become a problem if your jade file does not have a single root node, or if your jade code has if or loop statements that do not have a single root node. Using forEach in code instead of the each block will output nothing (forEach returns nothing). For example:

if true
  p line one
  p line two

will output only "line two" because it has more then one root. To fix this problem just add a div to make one root like this:

if true
  div
    p line one
    p line two

If you want to add a react component in your jade code you can reference it by prepend "pellet_" and the component's name in the manifest. The jade arguments will become the react props making it easy to build up the react DOM tree. For example if you have a component named imageTitle that requires a props of {alt:'', src:'', size:10} you could add it to your jade like this:

h1 My image title
pellet_imageTitle(alt='test alt tag', src='http://foobar.com/image.png', size=10)

If you need to debug/see what your Jade code becomes in react code check out the build/[browser|server]/component.js

Because jade files can be included into your JavaScript and CoffeeScript you separate your view/controller code and also have multiple views loaded into one controller. This is useful for A/B testing, reusing view code, A/B testing, etc. For example you can have controller code like this:

var mixin = require('../imgTile.jade');
var panelA = require('./detail-panel-ver-a.jade');
var panelB = require('./detail-panel-ver-b.jade');
 
// react render function 
render: function() {
  this.imgTileMixin = mixin;
  if(showVerAToUser === true) {
    return panelA(this);
  } else {
    return panelB(this);
  }
}
// both panelA, panelB can now have mixin so they can loop over arrays and output 
// imgTile using this markup/logic that can bind back to the shared controller. 

Resources

Installing First project
Pellet Feature Pellet Feature
Internationalization Internals
Pellet Feature Pellet Feature

Slide show

Pellet Feature

Contribution

You are welcome to contribute by opening an issue or a pull request.

You are also welcome to correct any spelling mistakes or any language issues, because my english is not perfect...

Contributors

License

MIT