point-of-view

Template plugin for Fastify

Usage no npm install needed!

<script type="module">
  import pointOfView from 'https://cdn.skypack.dev/point-of-view';
</script>

README

point-of-view

CI NPM version Known Vulnerabilities js-standard-style

Templates rendering plugin support for Fastify.

point-of-view decorates the reply interface with the view method for managing view engines, which can be used to render templates responses.

Currently supports the following templates engines:

In production mode, point-of-view will heavily cache the templates file and functions, while in development will reload every time the template file and function.

Note that at least Fastify v2.0.0 is needed.

Note: ejs-mate support has been dropped.

Note: marko support has been dropped. Please use @marko/fastify instead.

Benchmarks

The benchmark were run with the files in the benchmark folder with the ejs engine. The data has been taken with: autocannon -c 100 -d 5 -p 10 localhost:3000

  • Express: 8.8k req/sec
  • Fastify: 15.6k req/sec

Install

npm install point-of-view --save

Quick start

fastify.register is used to register point-of-view. By default, It will decorate the reply object with a view method that takes at least two arguments:

  • the template to be rendered
  • the data that should be available to the template during rendering

This example will render the template and provide a variable text to be used inside the template:

const fastify = require("fastify")();

fastify.register(require("point-of-view"), {
  engine: {
    ejs: require("ejs"),
  },
});

fastify.get("/", (req, reply) => {
  reply.view("/templates/index.ejs", { text: "text" });
});

fastify.listen(3000, (err) => {
  if (err) throw err;
  console.log(`server listening on ${fastify.server.address().port}`);
});

If your handler function is asynchronous, make sure to return the result - otherwise this will result in an FST_ERR_PROMISE_NOT_FULFILLED error:

// This is an async function
fastify.get("/", async (req, reply) => {
  // We are awaiting a functioon result
  const t = await something();

  // Note the return statement
  return reply.view("/templates/index.ejs", { text: "text" });
});

Configuration

fastify.register(<engine>, <options>) accepts an options object.

Options

  • engine: The template engine object - pass in the return value of require('<engine>'). This option is mandatory.
  • layout: Point-of-view supports layouts for EJS, Handlebars, Eta and doT. This option lets you specify a global layout file to be used when rendering your templates. Settings like root or viewExt apply as for any other template file. Example: ./templates/layouts/main.hbs
  • propertyName: The property that should be used to decorate reply and fastify - E.g. reply.view() and fastify.view() where "view" is the property name. Default: "view".
  • root: The root path of your templates folder. The template name or path passed to the render function will be resolved relative to this path. Default: "./".
  • includeViewExtension: Setting this to true will automatically append the default extension for the used template engine if ommited from the template name . So instead of template.hbs, just template can be used. Default: false.
  • viewExt: Let's you override the default extension for a given template engine. This has precedence over includeViewExtension and will lead to the same behavior, just with a custom extension. Default "". Example: "handlebars".
  • defaultContext: The template variables defined here will be available to all views. Variables provided on render have precendence and will override this if they have the same name. Default: {}. Example: { siteName: "MyAwesomeSite" }.

Example:

fastify.register(require("point-of-view"), {
  engine: {
    handlebars: require("handlebars"),
  },
  root: path.join(__dirname, "views"), // Points to `./views` relative to the current file
  layout: "./templates/template", // Sets the layout to use to `./views/templates/layout.handlebars` relative to the current file.
  viewExt: "handlebars", // Sets the default extension to `.handlebars`
  propertyName: "render", // The template can now be rendered via `reply.render()` and `fastify.render()`
  defaultContext: {
    dev: process.env.NODE_ENV === "development", // Inside your templates, `dev` will be `true` if the expression evaluates to true
  },
  options: {}, // No options passed to handlebars
});

Rendering the template into a variable

The fastify object is decorated the same way as reply and allows you to just render a view into a variable instead of sending the result back to the browser:

// Promise based, using async/await
const html = await fastify.view("/templates/index.ejs", { text: "text" });

// Callback based
fastify.view("/templates/index.ejs", { text: "text" }, (err, html) => {
  // Handle error
  // Do something with `html`
});

Registering multiple engines

Registering multiple engines with different configurations is supported. They are dinguished via their propertyName:

fastify.register(require("point-of-view"), {
  engine: { ejs: ejs },
  layout: "./templates/layout-mobile.ejs",
  propertyName: "mobile",
});

fastify.register(require("point-of-view"), {
  engine: { ejs: ejs },
  layout: "./templates/layout-desktop.ejs",
  propertyName: "desktop",
});

fastify.get("/mobile", (req, reply) => {
  // Render using the `mobile` render function
  return reply.mobile("/templates/index.ejs", { text: "text" });
});

fastify.get("/desktop", (req, reply) => {
  // Render using the `desktop` render function
  return reply.desktop("/templates/index.ejs", { text: "text" });
});

Providing a layout on render

Point-of-view supports layouts for EJS, Handlebars, Eta and doT. These engines also support providing a layout on render.

Please note: Global layouts and provding layouts on render are mutually exclusive. They can not be mixed.

fastify.get('/', (req, reply) => {
  reply.view('index-for-layout.ejs', data, { layout: 'layout.html' })
})

Setting request-global variables

Sometimes, several templates should have access to the same request-sceific variables. E.g. when setting the current username.

If you want to provide data, which will be depended on by a request and available in all views, you have to add property locals to reply object, like in the example below:

fastify.addHook("preHandler", function (request, reply, done) {
  reply.locals = {
    text: getTextFromRequest(request), // it will be available in all views
  };

  done();
});

Properties from reply.locals will override those from defaultContext, but not from data parameter provided to reply.view(template, data) function.

Minifying HTML on render

To utilize html-minifier in the rendering process, you can add the option useHtmlMinifier with a reference to html-minifier, and the optional htmlMinifierOptions option is used to specify the html-minifier options:

// get a reference to html-minifier
const minifier = require('html-minifier')
// optionally defined the html-minifier options
const minifierOpts = {
  removeComments: true,
  removeCommentsFromCDATA: true,
  collapseWhitespace: true,
  collapseBooleanAttributes: true,
  removeAttributeQuotes: true,
  removeEmptyAttributes: true
}
// in template engine options configure the use of html-minifier
  options: {
    useHtmlMinifier: minifier,
    htmlMinifierOptions: minifierOpts
  }

To utilize html-minify-stream in the rendering process with template engines that support streams, you can add the option useHtmlMinifyStream with a reference to html-minify-stream, and the optional htmlMinifierOptions option is used to specify the options just like html-minifier:

// get a reference to html-minify-stream
const htmlMinifyStream = require('html-minify-stream')
// optionally defined the html-minifier options that are used by html-minify-stream
const minifierOpts = {
  removeComments: true,
  removeCommentsFromCDATA: true,
  collapseWhitespace: true,
  collapseBooleanAttributes: true,
  removeAttributeQuotes: true,
  removeEmptyAttributes: true
}
// in template engine options configure the use of html-minify-stream
  options: {
    useHtmlMinifyStream: htmlMinifyStream,
    htmlMinifierOptions: minifierOpts
  }

To filter some paths from minification, you can add the option pathsToExcludeHtmlMinifier with list of paths

// get a reference to html-minifier
const minifier = require('html-minifier')
// in options configure the use of html-minifier and set paths to exclude from minification
const options = {
  useHtmlMinifier: minifier,
  pathsToExcludeHtmlMinifier: ['/test']
}

fastify.register(require("point-of-view"), {
  engine: {
    ejs: require('ejs')
  },
  options
});

// This path is excluded from minification
fastify.get("/test", (req, reply) => {
  reply.view("./template/index.ejs", { text: "text" });
});

Engine-specific settings

Mustache

To use partials in mustache you will need to pass the names and paths in the options parameter:

  options: {
    partials: {
      header: 'header.mustache',
      footer: 'footer.mustache'
    }
  }

Handlebars

To use partials in handlebars you will need to pass the names and paths in the options parameter:

  options: {
    partials: {
      header: 'header.hbs',
      footer: 'footer.hbs'
    }
  }

To use layouts in handlebars you will need to pass the layout parameter:

fastify.register(require("point-of-view"), {
  engine: {
    handlebars: require("handlebars"),
  },
  layout: "./templates/layout.hbs",
});

fastify.get("/", (req, reply) => {
  reply.view("./templates/index.hbs", { text: "text" });
});

Nunjucks

To configure nunjucks environment after initialisation, you can pass callback function to options:

options: {
  onConfigure: (env) => {
    // do whatever you want on nunjucks env
  };
}

Liquid

To configure liquid you need to pass the engine instance as engine option:

const { Liquid } = require("liquidjs");
const path = require("path");

const engine = new Liquid({
  root: path.join(__dirname, "templates"),
  extname: ".liquid",
});

fastify.register(require("point-of-view"), {
  engine: {
    liquid: engine,
  },
});

fastify.get("/", (req, reply) => {
  reply.view("./templates/index.liquid", { text: "text" });
});

doT

When using doT the plugin compiles all templates when the application starts, this way all .def files are loaded and both .jst and .dot files are loaded as in-memory functions. This behaviour is recommended by the doT team here. To make it possible it is necessary to provide a root or templates option with the path to the template directory.

const path = require("path");

fastify.register(require("point-of-view"), {
  engine: {
    dot: require("dot"),
  },
  root: "templates",
  options: {
    destination: "dot-compiled", // path where compiled .jst files are placed (default = 'out')
  },
});

fastify.get("/", (req, reply) => {
  // this works both for .jst and .dot files
  reply.view("index", { text: "text" });
});

Miscellaneous

Using point-of-view as a dependency in a fastify-plugin

To require point-of-view as a dependency to a fastify-plugin, add the name point-of-view to the dependencies array in the plugin's opts.

fastify.register(myViewRendererPlugin, {
  dependencies: ["point-of-view"],
});

Forcing a cache-flush

To forcefully clear cache when in production mode, call the view.clearCache() function.

fastify.view.clearCache();

Note

By default views are served with the mime type 'text/html; charset=utf-8', but you can specify a different value using the type function of reply, or by specifying the desired charset in the property 'charset' in the opts object given to the plugin.

Acknowledgements

This project is kindly sponsored by:

License

Licensed under MIT.