OrisonJS

Orison smoking pipe icon

A static site generator and server based upon lit-html

Single pages

/src/pages/example.js

import { html } from 'orison';

export default context => html`
  ...
`

List pages

/src/pages/list.js

import { html } from 'orison';

export default (context, slug) => [
  {
    name: 'path-segment-1',
    html: html`...`
  },
  {
    name: 'path-segment-2',
    html: html`...`
  }
  ...
]

In the below example we load in a list of blog posts with a made up loadContent method. It is important to remember that when doing orison build the slug parameter will be undefined and when doing orison serve it will be the last path segment in the url. This way orison serve can only load in the data it needs for the currently requested page and orison build can build every page available from your content provider. This logic will need implemented differently depending on what content provider you are using, and is hidden inside the loadContent method in this example.

/src/pages/list.js

import { html } from 'orison';

export default async (context, slug) => {
  const blogPosts = await loadContent(slug);

  blogPosts.map(blogPost => ({
    name: blogPost.urlName,
    html: html`
      ...
    `
  );
}

Partials

/src/partials/example.js

import { html } from 'orison';

export default data => html`
  ...
`

Layouts

/src/pages/layout.js

import { html } from 'orison';

export default context => html`
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>OrisonJS</title>
  </head>
  <body>
    <div>Website header</div>
    ${context.page.html}
    <div>Website footer</div>
  </body>
</html>
`;

Context

context.path
context.data
context.local
context.parent
context.root
context.parents
context.children
context.mdString
context.mdFile

The local, parent, and root properties return OrisonDirectory objects. The parents and children return arrays of OrisonDirectory objects.

Child Directories

The context.local.children property provides an array of OrisonDirectory objects Each item in the array has the following attributes and methods:

These attributes give you access to the child data.json files, the path of each child, and the parent and children methods can be used to programmatically navigate the site structure.

The order of the children will be based on the orison.order property of the corresponding data.json files as shown below.

/src/pages/first/data.json

{
  "orison": {
    "order": 100
  }
}

/src/pages/second/data.json

{
  "orison": {
    "order": 200
  }
}

In this example the /src/pages/first directory will be the first child in the array, and the /src/pages/second directory will be the second child in the array. It is recommended to use larger numbers so that you can reorder directories without having to increment each directory.

This can then be used to create navigation elements as shown below. In this example we are creating the navigation links by getting the children of the root directory.

/src/pages/index.js

const { html } = require('orison');

export default context => html`
  <a href="/" class="${context.page.path === '/index.js' ? 'active' : ''}">Begin</a>
  ${context.local.children.map(child => html`
    <a href="${child.path}"
       class="${context.page.path.startsWith(child.path) ? 'active' : ''}">
       ${child.data.title}
    </a>
  `)}
`;

The context object is also available from html files as well.

/src/pages/index.html

<a href="/" class="${context.page.path === '/index.js' ? 'active' : ''}">Begin</a>
${context.local.children.map(child => html`
  <a href="${child.path}"
     class="${context.page.path.startsWith(child.path) ? 'active' : ''}">
     ${child.data.title}
  </a>
`)}

Root Directory

export default context => html`
  ${context.root}
`

Parent Directories

export default context => html`
  ${context.parents}
`

Metadata

Metadata file are data.json files under the /src/pages hierarchy. They are made available to pages through the context API. The only restraint to the json file is that "orison" is a reserved top level key:

/src/pages/data.json

{
  ...
  "orison": {
    "order": 100
  }
}

The only reserved key of the orison property is the "order" key which determines the order of the children context property.

Public Metadata

Any JSON in the public property will be made available at the corresponding url. For example the following data.json file:

/src/pages/data.json

{
  "public": {
    "example": "Hello, World"
  }
}

Will cause this JSON:

{
  "example": "Hello, World"
}

To be built to the /docs/data.json file and will be available at localhost:3000/data.json.

This allows you to make some metadata publicly available to client side code.

Programatic Usage

Instead of installing OrisonJS as a command line utility you can install it as a project dependency. Then you can interact with a configurable and programatic API for building and serving your site.

npm install orison

You can require the CommonJS module from 'orison'. ES6 Modules are available from './node_modules/orison/bin/orison-esm.js'

Static site generation

Here is an example of programmatically building the src directory into the docs directory. The build method also returns an object with information about what pages were built which can be integrated into a build pipeline for things such as a service worker build tool or other down stream systems or integrations which needs to know about the pages that exist.

const { OrisonGenerator } = require('orison');
const orisonGenerator = new OrisonGenerator({ rootPath: __dirname });
orisonGenerator.build();

Static serving

Here is an example of serving the statically built files.

const { OrisonStaticServer } = require('orison');
const orisonStaticServer = new OrisonStaticServer({ rootPath: __dirname });
orisonStaticServer.start();

Server side generation

Here is an example of serving files and rendering the file during each request.

const { OrisonServer } = require('orison');
const orisonServer = new OrisonServer({ rootPath: __dirname });
orisonServer.start();

Custom command line utility

Or you could create a file that builds, serves, or serves static based on provided command line arguments.

const { OrisonGenerator, OrisonServer, OrisonStaticServer } = require('orison');

if (process.argv.includes('build'))
  new OrisonGenerator({ rootPath: __dirname }).build();

if (process.argv.includes('serve'))
  new OrisonServer({ rootPath: __dirname }).start();

if (process.argv.includes('static'))
  new OrisonStaticServer({ rootPath: __dirname }).start();

Then you can build, serve, or serve static with these commands:

node ./orison.js build
node ./orison.js serve
node ./orison.js static

Unsafe HTML

/src/pages/example.js

import { html, unsafeHTML } from 'orison';
import someCMS from 'some-cms';

const htmlFromCMS = someCMS.get('...');

export default context => html`
  ${unsafeHTML(htmlFromCMS)}
`

Local Development Proxies

During local development you might need a proxy so that you can integrate with other APIs without running into CORS errors. This is quite simple to setup. For example say you are using Netlify Functions and have them running locally on localhost:9000 but are running the Orison development server on localhost:3000. To setup a proxy so that you can make relative url requests from your client side JavaScript do the following:

/build.js

const proxy = require('express-http-proxy');
const { OrisonServer } = require('orison');

const server = new OrisonServer({ rootPath: process.cwd() });
server.app.use('/.netlify/functions/', proxy('http://localhost:9000/.netlify/functions/'));

Now, from your JavaScript in the browser you can use fetch as if Netlify Functions was running on localhost:3000. These requests will go to localhost:3000 and then be proxied to the local Netlify Functions server.

fetch('/.netlify/functions/yourCustomLambdaFunction');

Loaders

If you integrate Orison with APIs it might be useful to cache the API responses. For example let's say that you are rending a hundred blog posts, and each time the layout is called you have to make the same API request to load in the websites title from a CMS. You could connect to the API directly from your layout however then the request will be made for each page. Alternatively you could wrap the API in an Orison Loader which will automatically cache the API response for the duration of the build process.

Loaders are defined in JavaScript files under /src/loaders as shown below:

/src/loaders/example.js

export default async message => {
  return new Promise(resolve => {
    resolve('Message from loader: ' + message);
  });
};

While this code fakes an asynchronous API call, the idea is simple. The loader exports a function which excepts some parameters and then returns a promise of data. If this loader is called with the same parameters then the cache will be hit and a second API call will be avoided. The name of the loader will be the camel cased version of the file name.

This loader can then be used in layouts, pages, and partials by using the context.loaders.example method:

/src/pages/index.js

import { html } from 'orison';

export default async context => html`
  <ol>
    <li>${await context.loaders.example('ABC')}</li>
    <li>${await context.loaders.example('ABC')}</li>
  </ol>
`;

This will produce the output as shown below. The first call will hit the loader defined at /src/loaders.js. The second call will hit the cache and the loader at /src/loader.js will not be hit:

  1. Message from loader: ABC
  2. Message from loader: ABC

Programatic Loaders

Instead of creating loaders as JavaScript files under /src/loaders you can instead pass loaders into the OrisonServer and OrisonGenerator constructors as shown below:

/build.js

const loaders = [
  {
    name: 'anotherExample',
    loader: message => new Promise(resolve => resolve('Message from programatic loader: ' + message))
  }
];

if (process.argv.includes('serve')) {
  new OrisonServer({
    rootPath: process.cwd(),
    loaders: loaders
  }).start();
} else if (process.argv.includes('build')) {
  new OrisonGenerator({
    rootPath: process.cwd(),
    loaders: loaders
  }).build();
}

Now in our page we can use context.loaders.anotherExample to call our programatically defined loader:

/src/pages/index.js

import { html } from 'orison';

export default async context => html`
  <ol>
    <li>${await context.loaders.anotherExample('123')}</li>
    <li>${await context.loaders.anotherExample('123')}</li>
  </ol>
`;

And see the output as shown below. The first call will hit the loader, the second call will hit the cache.

  1. Message from programatic loader: 123
  2. Message from programatic loader: 123