A lightweight Node.js server that generates sites from Handlebars templates and JSON resources pulled in from almost any API. For any high traffic applications it is best to use it as an origin for a CDN that can provide sufficient caching. It has run in production with Fastly and Edgecast, but has been especially tuned for use with Fastly.
Awesome things you can do with Solidus:
npm install solidus --save
solidus [command]
Some commands (and Solidus itself) have flags which can be used to pass parameters to commands. You can do that like so:
solidus [command] -[flag] [value]
OR
solidus [command] --[longflag] [value]
Most of the time you'll be using solidus start --dev
to start a Solidus server in development mode. To see a full list of commands/options available to you, just use the "help" flag ( -h
, --help
).
You can also run Solidus manually in code if you need to. In order to do this, just include Solidus locally and require
it. The only method available at the moment is solidus.start
, and it's essentially the same as the CLI command:
var solidus = require('solidus');
solidus.start({
port: 9001,
dev: true
});
Starts a Solidus server using the current folder.
solidus start -p 9001
solidus start --port 9001
solidus start --dev
solidus start --loglevel 2
Solidus sites are comprised of views, resources, and assets. Views are Handlebars templates used for page, layout, and partial markup. Resources are JSON API responses from places like YouTube, Tumblr, etc and the JavaScript preprocessors that normalize them. Assets are the CSS, JavasCript, fonts, and images in a Solidus site.
The views
directory contains every page, partial, and layout in a site. Any view with the name layout.hbs
will automatically become a layout. By default, pages will inherit the closest layout and use it, up to the root of the views
directory. Partials and pages are identical and only differ in usage: you can use any view as a partial or a page.
Every view in the views
directory is available as a page. The routes of these pages are generated from their filename and location in the views directory. Here's a quick example:
views/index.hbs
becomes /
views/about.hbs
becomes /about
views/kitties/index.hbs
becomes /kitties
views/kitties/mr-welsey-kins.hbs
becomes /kitties/mr-wesley-kins
Dynamic segments can be defined by using curly braces {}
in the view's filename. A dynamic segment is a placeholder for something variable, like a page number or ID. Here are some examples of views with dynamic segments:
views/doggies/{dog}.hbs
becomes /doggies/ein
, /doggies/marmaduke
, /doggies/pixel
views/articles/{article_id}.hbs
becomes /articles/582890
, /articles/582811
, /articles/600345
Dynamic segments can also be specified with directory names:
views/doggies/{dog}/toys
becomes /doggies/ein/toys
, /doggies/marmaduke/toys
, /doggies/pixel/toys
views/articles/{article_id}/links/{link_id}
becomes /articles/582890/links/2
Page configuration is done with a JSON object nested within a Handlebars comment at the top of a page view. This object can contain the following:
<title>
tag.Here's a quick example of what a page configuration might look like:
index.hbs
{{!
{
"title": "Home",
"resources": {
"kitties": "https://example.com/api/v1/resources/5632ac/tims-favorite-kitties",
"doggies": "https://example.com/api/v1/resources/4657df/tims-favorite-doggies"
},
"preprocessor": "home.js"
}
}}
Every page in a Solidus site has a context, which holds all of the data that's being fed to a view. The context contains things such as page metadata, data fetched from remote resources, query parameters, and dynamic segment values. Contexts can also be modified before they are used in the view by preprocessors. Here's a quick run down of what you'll find in the page context:
?page=5
.Here's an example context:
{
"page": {
"title": "Character Page",
"path": "/Users/Sparkart/solidus-test-site/views/characters/{hero_id}.hbs"
},
"parameters": {
"hero_id": "92352"
},
"query": {
"debug": "true"
},
"resources": {
"character": {
"id": 92352,
"name": "UUUGUUUUUUUU",
"class": "demon-hunter",
"gender": "0"
...
}
},
"assets": {
"scripts": "<script src=\"/compiled/scripts.js\"></script>",
"styles": "<link rel=\"stylesheet\" href=\"/compiled/styles.css\" />"
},
"layout": "layout.hbs"
}
Instead of keeping content in a database Solidus uses data from external APIs. Solidus requests JSON data from third party APIs, preprocesses it, then combines it with a Handlebars template to make a page.
Important: API responses are cached, but expire in only a minute, meaning API requests very often. This may cause any rate limits to be quickly exceeded in production when a site is exposed to traffic. An API proxy is therefore highly recommended.
Here's a quick outline of how resources work:
1) A resource is added to the configuration object of a page view:
kitties/index.hbs
...
"resources": {
"kitties": "https://example.com/api/v1/resources/5632ac/tims-favorite-kitties"
}
...
Resources can also be specified with an options object instead of a string. This object must include
url
, and may includeheaders
andauth
.headers
is an object of request headers, andauth
is a string that represents an HTTP Auth parameter.
doges/index.hbs
...
"resources": {
"doges": {
"url": "https://example.com/api/v2/resources/4521zb/such-doges",
"headers": {
"key": "123"
},
"auth": "user:pass"
}
}
...
2) When the page is requested, the resources are fetched and their data is added to the resources
object in the page's context. It looks something like this:
Context in /kitties
{
...
"resources": {
"kitties": {
"count": 3,
"results": ["Wesley", "Twizzler", "Pixel"]
}
}
...
}
3) The context can also be made available to client side JavaScript like so:
<script>{{{context}}}</script>
<script>
alert( 'Here are the kitties!', solidus.context.resources.kitties.results );
</script>
Sometimes resources will need to be requested with parameters that change dynamically. This can be done by using dynamic segments or query parameters. To create a dynamic resource, all you need to do is replace the dynamic part of your resource URL with a placeholder in curly braces (just like setting up dynamic routes). The name of the placeholder should match up with the name of the dynamic segment or query parameter you want to use to fill it in. In the event that a dynamic segment and query parameter have the same name, the query parameter will be used.
kitties/{resource_id}.hbs
...
"resources": {
"kitties": "https://example.come/api/v1/resources/{resource_id}/kitties?order={order}"
}
...
Context in /kitties/635bc?order=alpha
(result from the resource: https://example.come/api/v1/resources/635bc/kitties?order=alpha
)
{
...
"resources": {
"kitties": {
"count": 3,
"results": ["Pixel", "Twizzler", "Wesley"]
}
}
...
}
Certain resources will require an options, like a query string or header, in order to work. Since these resources can be used across many views, it can quickly get cumbersome to set these options every single time you include a resource. To fix this problem, you can set options for a resource (or group of resources) based on their url globally, using auth.json
. This file is a set of resource configurations that will be mixed in to resources before they are fetched. Here's a simple example:
auth.json
{
"http://proxy.storyteller.io/*": {
"headers": {
"Api-Key": "0000aaaa-aa00-00aa-a00a-aaaa000000"
}
},
"http://services.sparkart.net/*": {
"query": {
"key": "1111bbbb-bb11-11bb-b11b-bbbb111111"
}
}
}
Now any resource that starts with http://proxy.storyteller.io/
will mix in the data under that parameter, and any resource that starts with http://services.sparkart.net/
will mix in the data under that parameter.
If the data returned in a resource isn't quite right for a template, a preprocessor can be used to make the data more palatable. Preprocessors are run after resources are requested, but before pages are rendered, so they can be used to transform data, add new data, merge two resources together, and more. All preprocessors are placed in a site's preprocessors
directory, and are enabled by specifying them in the preprocessor
option in the view configuration. Preprocessors are simply CommonJS modules that export a function which modifies and returns the page's context
. Here's a quick example of a preprocessor that converts the name of the kitties to ALL CAPS:
preprocessors/kitties.js
module.exports = function( context ){
for( var i in context.resources.kitties.results ){
context.resources.kitties.results[i] = context.resources.kitties.results[i].toUpperCase();
}
return context;
};
views/kitties/index.hbs
...
"resources": {
"kitties": "https://example.com/api/v1/resources/5632ac/tims-favorite-kitties"
},
"preprocessor": "kitties.js"
...
Original context in /kitties
{
...
"resources": {
"kitties": {
"count": 3,
"results": ["Wesley","Twizzler","Pixel"]
}
}
}
Processed context in /kitties
{
...
"resources": {
"kitties": {
"count": 3,
"results": ["WESLEY","TWIZZER","PIXEL"]
}
}
}
Preprocessors are Node.js modules like any other. Search npm to find libraries solving common problems in areas like time, data manipulation, text formatting, and many more. Node.js built-in modules are also available.
Here's a quick example:
preprocessors/kitties.js
var _ = require('underscore');
module.exports = function( context ){
context.resources.kitties.results = _.shuffle( context.resources.kitties.results );
return context;
};
Processed context in /kitties
{
...
"resources": {
"kitties": {
"count": 3,
"results": ["Pixel","Twizzler","Wesley"]
}
}
}
Redirects can easily be defined using the redirects.js
file located in the base of the Solidus site's directory. This file simply exports an array of redirection objects with the parameters from
, to
, start
, end
, and permanent
. from
is the route you want to redirect from, and to
is the target destination. start
and end
are datetimes that correspond with when the redirection should be active and are in YYYY-MM-DD HH:MM:SS
format. By default all redirects will be 302 Found, but if permanent
is true they will be served as 301 Moved Permanently.
Here's an example redirects.js
:
module.exports = [
{
from: "/redirect1",
to: "/"
}, {
from: "/redirect2",
to: "/",
"start": "2000-1-1 00:00:00"
}, {
from: "/redirect3",
to: "/",
"start": "3000-1-1 00:00:00"
}, {
from: "/redirect4",
to: "/",
"end": "2000-1-1 00:00:00"
}, {
from: "/redirect5",
to: "/",
"permanent": true
}, {
from: "/redirect6/{dynamic}/{route}",
to: "/new/{route}/{dynamic}"
}, {
from: /\/redirect7\/(\d+)-\d+-(\d+)-(\d+)/,
to: "/new/{1}/{0}/{2}"
}, {
from: "/redirect8/{dynamic}/{route}",
to: function(params) {
return "/new/{route}/" + params.dynamic.toUpperCase();
}
}, {
from: /\/redirect9\/(\d+)-\d+-(\d+)-(\d+)/,
to: function(params) {
return "/new/{1}/{0}/" + (1000 + parseInt(params['2']));
}
}];
Solidus has the capability to serve any static resource you choose, be it stylesheets, JavaScripts, images, fonts, flash files, etc. Just place your assets in the assets
directory or setup the build output from tools like Gulp and Grunt, and Solidus will serve them up.
=======
Solidus uses mocha to run its tests. Any new features should be tested, and resolved bugs should have tests to prevent regression. Tests can be run with the mocha
command.