Skip to main content

@podium/layout

In Podium a layout server is mainly responsible for fetching HTML fragments (podlets) and stitching these fragments together into an HTML page (a layout).

The @podium/layout module is used for composing HTML pages (layouts) out of page fragments (podlets).

The @podium/layout module provide three core features:

  • A client used to fetch content from podlets
  • A context used to set request bound information on requests from a layout to its podlets when fetching content from them
  • A proxy that makes it possible to publicly expose podlet data endpoints (or any backend services) via the layout

This module is to be used in conjunction with a Node.js HTTP server. For this, Express js, Hapi and Fastify are all supported. It's also possible to write your server using other HTTP frameworks or even just using the core Node.js HTTP libraries.

Connect compatible middleware based frameworks (such as Express) are considered first class in Podium and as such the layout module provides a .middleware() method for convenience.

For writing layout servers with other HTTP frameworks, see HTTP Framework Compatibility.

Installation

$ npm install @podium/layout

Getting started

Building a simple layout server including two podlets:

import express from "express";
import Layout from "@podium/layout";

const layout = new Layout({
name: "myLayout",
pathname: "/",
});

const podletA = layout.client.register({
name: "myPodletA",
uri: "http://localhost:7100/manifest.json",
});

const podletB = layout.client.register({
name: "myPodletB",
uri: "http://localhost:7200/manifest.json",
});

const app = express();
app.use(layout.middleware());

app.get(layout.pathname(), async (req, res, next) => {
const incoming = res.locals.podium;

const [a, b] = await Promise.all([
podletA.fetch(incoming),
podletB.fetch(incoming),
]);

res.podiumSend(`
<section>${a.content}</section>
<section>${b.content}</section>
`);
});

app.listen(7000);

Constructor

Create a new layout instance.

const layout = new Layout(options);

options

optiontypedefaultrequireddetails
namestringnullName that the layout identifies itself by
pathnamestringnullPathname of where a layout is mounted in an HTTP server
loggerobjectnullA logger which conforms to the log4j interface
contextobjectnullOptions to be passed on to the internal @podium/context constructor
clientobjectnullOptions to be passed on to the internal @podium/client constructor
proxyobjectnullOptions to be passed on to the internal @podium/proxy constructor
name

The name that the layout identifies itself by. This value must be in camelCase.

Example:

const layout = new Layout({
name: "myLayoutName",
pathname: "/foo",
});
pathname

The Pathname to where the layout is mounted in an HTTP server. It is important that this value matches the entry point of the route where content is served in the HTTP server since this value is used to mount the proxy and inform podlets (through the Podium context) where they are mounted and where the proxy is mounted.

If the layout is mounted at the server "root", set the pathname to /:

const app = express();
const layout = new Layout({
name: 'myLayout',
pathname: '/',
});

app.use(layout.middleware());

app.get('/', (req, res, next) => {
[ ... ]
});

If the layout is mounted at /foo, set the pathname to /foo:

const app = express();
const layout = new Layout({
name: 'myLayout',
pathname: '/foo',
});

app.use('/foo', layout.middleware());

app.get('/foo', (req, res, next) => {
[ ... ]
});

app.get('/foo/:id', (req, res, next) => {
[ ... ]
});

There is also a helper method for retrieving the set pathname which can be used to get the pathname from the layout object when defining routes. See .pathname() for further details.

logger

Any log4j compatible logger can be passed in and will be used for logging. Console is also supported for easy test / development.

Example:

const layout = new Layout({
name: "myLayout",
pathname: "/foo",
logger: console,
});

Under the hood abslog is used to abstract out logging. Please see abslog for further details.

context

Options to be passed on to the context parsers.

optiontypedefaultrequireddetails
debugobjectnullConfig object passed on to the debug parser
localeobjectnullConfig object passed on to the locale parser
deviceTypeobjectnullConfig object passed on to the device type parser
mountOriginobjectnullConfig object passed on to the mount origin parser
mountPathnameobjectnullConfig object passed on to the mount pathname parser
publicPathnameobjectnullConfig object passed on to the public pathname parser

Example of setting the debug context to default true:

const layout = new Layout({
name: "myLayout",
pathname: "/foo",
context: {
debug: {
enabled: true,
},
},
});
client

Options to be passed on to the client.

optiontypedefaultrequireddetails
retriesnumber4Number of times the client should retry settling a version number conflict before terminating
timeoutnumber1000Default value, in milliseconds, for how long a request should wait before the connection is terminated
maxAgenumberInfinityDefault value, in milliseconds, for how long manifests should be cached

Example of setting retries on the client to 6:

const layout = new Layout({
name: "myLayout",
pathname: "/foo",
client: {
retries: 6,
},
});
proxy

Options to be passed on to the proxy.

optiontypedefaultrequireddetails
prefixstringpodium-resourcePrefix used to namespace the proxy so that it's isolated from other routes in the HTTP server
timeoutnumber6000Default value, in milliseconds, for how long a request should wait before the connection is terminated

Example of setting the timeout on the proxy to 30 seconds:

const layout = new Layout({
name: "myLayout",
pathname: "/foo",
proxy: {
timeout: 30000,
},
});

Layout Instance

The layout instance has the following API:

.middleware()

A Connect/Express compatible middleware function which takes care of the various operations needed for a layout to operate correctly. This function is more or less just a wrapper for the .process() method.

Important: This middleware must be mounted before defining any routes.

Example

const app = express();
app.use(layout.middleware());

The middleware will create an HttpIncoming object for each request and place it on the response at res.locals.podium.

Returns an Array of middleware functions which perform the tasks described above.

.js(options|[options])

Set relative or absolute URLs to JavaScript assets for the layout.

When set, the values will be internally kept and made available for the document template to include.

This method can be called multiple times with a single options object to set multiple assets or one can provide an array of options objects to set multiple assets.

options

optiontypedefaultrequireddetails
valuestringRelative or absolute URL to the JavaScript asset
strategy"beforeInteractive" | "afterInteractive" | "lazy""afterInteractive"Specify how the JavaScript should be loaded
scope"content" | "fallback" | "all""all"Specify what routes the JavaScript should apply to
prefixbooleanfalseWhether the pathname defined on the constructor should be prepend, if relative, to the value
typestringdefaultWhat type of JavaScript (eg. esm, default, cjs)
referrerpolicystringCorrelates to the same attribute on a HTML <script> element
crossoriginstringCorrelates to the same attribute on a HTML <script> element
integritystringCorrelates to the same attribute on a HTML <script> element
nomodulebooleanfalseCorrelates to the same attribute on a HTML <script> element
asyncbooleanfalseCorrelates to the same attribute on a HTML <script> element
deferbooleanfalseCorrelates to the same attribute on a HTML <script> element
value

Sets the pathname for the layout's JavaScript assets. This value is usually the [URL] at which the layouts's user facing JavaScript is served and can be either a URL [pathname] or an absolute URL.

Serve a javascript file at /assets/main.js:

const app = express();
const layout = new Layout({
name: "myLayout",
pathname: "/",
});

app.get("/assets.js", (req, res) => {
res.status(200).sendFile("./src/js/main.js", (err) => {});
});

layout.js({ value: "/assets.js" });

Serve assets from a static file server and set a relative URI to the JS files:

const app = express();
const layout = new Layout({
name: "myLayout",
pathname: "/",
});

app.use("/assets", express.static("./src/js"));

layout.js([{ value: "/assets/main.js" }, { value: "/assets/extra.js" }]);

Set an absolute URL to where the JavaScript file is located:

const layout = new Layout({
name: "myLayout",
pathname: "/",
});

layout.js({ value: "http://cdn.mysite.com/assets/js/e7rfg76.js" });
prefix

Sets whether the method should prepend the value with the pathname value that was set in the constructor.

The prefix will be ignored if value is an absolute URL.

type

Sets the type for the script which is set. If not set, default will be used.

The following are valid values:

  • esm or module for ECMAScript modules
  • cjs for CommonJS modules
  • amd for AMD modules
  • umd for Universal Module Definition
  • default if the type is unknown.

The type field provides a hint for further use of the script in the layout. Typically this is used in the document template when including the <script> tags or when optimizing JavaScript assets with a bundler.

.css(options|[options])

Set relative or absolute URLs to Cascading Style Sheets (CSS) assets for the layout.

When set the values will be internally kept and made available for the document template to include.

This method can be called multiple times with a single options object to set multiple assets or one can provide an array of options objects to set multiple assets.

options

optiontypedefaultrequireddetails
valuestringRelative or absolute URL to the CSS asset
strategy"beforeInteractive" | "afterInteractive" | "lazy" | "shadow-dom""beforeInteractive"Specify how the CSS should be loaded
scope"content" | "fallback" | "all""all"Specify what routes the CSS should apply to
prefixbooleanfalseWhether the pathname defined on the constructor should be prepend, if relative, to the value
crossoriginstringCorrelates to the same attribute on a HTML <link> element
disabledbooleanfalseCorrelates to the same attribute on a HTML <link> element
hreflangstringCorrelates to the same attribute on a HTML <link> element
titlestringCorrelates to the same attribute on a HTML <link> element
mediastringCorrelates to the same attribute on a HTML <link> element
typestringtext/cssCorrelates to the same attribute on a HTML <link> element
relstringstylesheetCorrelates to the same attribute on a HTML <link> element
asstringCorrelates to the same attribute on a HTML <link> element
value

Sets the pathname to the layout's CSS assets. This value can be an relative or absolute URL at which the podlet's user facing CSS is served. . Serve a CSS file at /assets/main.css:

const app = express();
const layout = new Layout({
name: "myLayout",
pathname: "/",
});

app.get("/assets.css", (req, res) => {
res.status(200).sendFile("./src/js/main.css", (err) => {});
});

layout.css({ value: "/assets.css" });

Serve assets from a static file server and set a relative URI to the CSS files:

const app = express();
const layout = new Layout({
name: "myLayout",
pathname: "/",
});

app.use("/assets", express.static("./src/css"));

layout.css([{ value: "/assets/main.css" }, { value: "/assets/extra.css" }]);

Set an absolute URL to where the CSS file is located:

const layout = new Layout({
name: "myLayout",
pathname: "/",
});

layout.css({ value: "http://cdn.mysite.com/assets/css/3ru39ur.css" });
prefix

Sets whether the method should prepend the value with the pathname value that was set in the constructor.

The prefix will be ignored if value is an absolute URL.

.pathname()

A helper method used to retrieve the pathname value that was set in the constructor. This can be handy when defining routes since the pathname set in the constructor must also be the base path for the layout's main content route

Example:

const layout = new Layout({
name: 'myLayout',
pathname: '/foo',
});

app.get(layout.pathname(), (req, res, next) => {
[ ... ]
});

app.get(`${layout.pathname()}/bar`, (req, res, next) => {
[ ... ]
});

app.get(`${layout.pathname()}/bar/:id`, (req, res, next) => {
[ ... ]
});

.view(template)

Sets the default document template.

Takes a template function that accepts an instance of HttpIncoming, a content string as well as any additional markup for the document's head section:

(incoming, body, head) => `Return an HTML string here`;

In practice this might look something like:

layout.view((incoming, body, head) => `<!doctype html>
<html lang="${incoming.context.locale}">
<head>
<meta charset="${incoming.view.encoding}">
<title>${incoming.view.title}</title>
${head}
</head>
<body>
${body}
</body>
</html>`;
);

.render(HttpIncoming, fragment, [args])

Method to render the document template. By default this will render a default document template provided by Podium unless a custom one is set by using the .view method.

In most HTTP frameworks this method can be ignored in favour of res.podiumSend(). If present, res.podiumSend() has the advantage that it's not necessary to pass in HttpIncoming as the first argument.

Returns a String.

This method takes the following arguments:

HttpIncoming (required)

An instance of the HttpIncoming class.

fragment

A String that is intended to be a fragment of the final HTML document (Everything to be displayed in the HTML body).

[args]

All following arguments given to the method will be passed on to the document template.

Additional arguments could be used to pass on parts of a page to the document template as shown:

layout.view = (incoming, body, head) => {
return `
<html>
<head>${head}</head>
<body>${body}</body>
</html>
`;
};

app.get(layout.pathname(), (req, res) => {
const incoming = res.locals.podium;

const head = `<meta ..... />`;
const body = `<section>my content</section>`;

const document = layout.render(incoming, body, head);

res.send(document);
});

.process(HttpIncoming)

Method for processing an incoming HTTP request. This method is intended to be used to implement support for multiple HTTP frameworks and in most cases it won't be necessary for layout developers to use this method directly when creating a layout server.

What it does:

  • Runs context parsers on the incoming request and sets an object with the context at HttpIncoming.context which can be passed on to the client when requesting content from podlets.
  • Mounts a proxy so that each podlet can do transparent proxy requests as needed.

Returns a Promise which will resolve with the HttpIncoming object that was passed in.

If the inbound request matches a proxy endpoint the returned Promise will resolve with a HttpIncoming object where the .proxy property is set to true.

This method takes the following arguments:

HttpIncoming (required)

An instance of the HttpIncoming class.

.client

A property that exposes an instance of the client for retrieving content from podlets.

Example of registering two podlets and retrieving their content:

const layout = new Layout({
name: 'myLayout',
pathname: '/',
});

const podletA = layout.client.register({
name: 'myPodletA',
uri: 'http://localhost:7100/manifest.json',
});

const podletB = layout.client.register({
name: 'myPodletB',
uri: 'http://localhost:7200/manifest.json',
});

[ ... ]

app.get(layout.pathname(), async (req, res, next) => {
const incoming = res.locals.podium;

const [a, b] = await Promise.all([
podletA.fetch(incoming),
podletB.fetch(incoming),
]);

[ ... ]
});

.client.register(options)

Registers a podlet such that the podlet's content can later be fetched.

Example:

const podlet = layout.client.register({
name: "myPodlet",
uri: "http://localhost:7100/manifest.json",
});

Returns a Podlet Resource which is also stored on the layout client instance using the registered name value as its property name.

Example:

const layout = new Layout({
name: "myLayout",
pathname: "/",
});

layout.client.register({
uri: "http://foo.site.com/manifest.json",
name: "fooBar",
});
layout.client.fooBar.fetch();

options (required)

optiontypedefaultrequireddetails
uristringUri to the manifest of a podlet
namestringName of the component. This is used to reference the component in your application, and does not have to match the name of the component itself
retriesnumber4The number of times the client should retry to settle a version number conflict before terminating. Overrides the retries option in the layout constructor
timeoutnumber1000Defines how long, in milliseconds, a request should wait before the connection is terminated. Overrides the timeout option in the layout constructor
throwablebooleanfalseDefines whether an error should be thrown if a failure occurs during the process of fetching a podlet. See Fallbacks.
excludeByobjectLets you define a set of rules where a fetch call will not be resolved if it matches.
includeByobjectInverse of excludeBy. Setting both at the same time will throw.
excludeBy and includeBy

These options are used by fetch to conditionally skip fetching the podlet content based on values on the request. It's an alternative to conditionally fetching podlets in your request handler. Setting both at the same time will throw.

Example: exclude a header and footer in a hybrid web view.

import Client from "@podium/client";
const client = new Client();

const footer = client.register({
uri: "http://footer.site.com/manifest.json",
name: "footer",
excludeBy: {
deviceType: ["hybrid-ios", "hybrid-android"], // when footer.fetch(incoming) is called, if the incoming request has the header `x-podium-device-type: hybrid-ios`, `fetch` will return an empty response.
},
});

.client.refreshManifests()

Refreshes the manifests of all registered resources. Does so by calling the .refresh() method on all resources under the hood.

layout.client.register({
uri: "http://foo.site.com/manifest.json",
name: "foo",
});

layout.client.register({
uri: "http://bar.site.com/manifest.json",
name: "bar",
});

await layout.client.refreshManifests();

.client.state

What state the client is in.

The value will be one of the following values:

  • instantiated - When a Client has been instantiated but no requests to any podlets have been made.
  • initializing - When one or more podlets are requested for the first time.
  • unstable - When an update of a podlet is detected and the layout is in the process of re-fetching the manifest.
  • stable - When all registered podlets are using cached manifests and only fetching content.
  • unhealthy - When an podlet update never settled.

.client Events

The Client instance emits the following events:

state

When there is a change in state.

layout.client.on("state", (state) => {
console.log(state);
});

const podlet = layout.client.register({
uri: "http://foo.site.com/manifest.json",
name: "foo",
});

podlet.fetch();

The event will fire with one the following values:

  • instantiated - When a Client has been instantiated but no requests to any podlets have been made.
  • initializing - When one or multiple podlets are requested for the very first time.
  • unstable - When an update of a podlet is detected and is in the process of refetching the manifest.
  • stable - When all registered podlets are using cached manifests and only fetching content.
  • unhealthy - When an update of a podlet never settled.

.context

A property that exposes the instance of the @podium/context used to create the context which is appended to the requests to each podlet.

.context.register(name, parser)

The context is extensible so it is possible to register third party context parsers to it.

Example of registering a custom third party context parser to the context:

import Parser from "my-custom-parser";

const layout = new Layout({
name: "myLayout",
pathname: "/",
});

layout.context.register("customParser", new Parser("someConfig"));

name (required)

A unique name for the parser. Used as the key for the parser's value in the context.

parser (required)

The parser object to be registered.

.metrics

Property that exposes a metric stream. This stream joins all internal metrics streams into one stream resulting in all metrics from all sub modules being exposed here.

See @metrics/metric for full documentation.

Podlet Resource

A registered podlet is stored in a Podlet Resource object.

The podlet Resource object contains methods for retrieving the content of a podlet. The URI of the content of a component is defined in the component's manifest. This is the content root of the component.

A podlet resource object has the following API:

.fetch(HttpIncoming, options)

Fetches the content of the podlet. Returns a Promise which resolves with a Podlet Response object containing the keys content, headers, css and js.

HttpIncoming (required)

An HttpIncoming object. This is normally provided by the "middleware" which runs on the incoming request to the layout prior to the process of fetching podlets.

The HttpIncoming object is normally found on a request bound property of the request or response object.

const podlet = layout.client.register({
name: "myPodlet",
uri: "http://localhost:7100/manifest.json",
});

app.get(layout.pathname(), async (req, res, next) => {
const incoming = res.locals.podium;

const response = await podlet.fetch(incoming);
res.podiumSend(`
<section>${response.content}</section>
`);
});

options (optional)

optiontypedefaultrequireddetails
pathnamestringA path which will be appended to the content root of the podlet when requested
headersobjectAn Object which will be appended as HTTP headers to the request to fetch the podlets's content
queryobjectAn Object which will be appended as query parameters to the request to fetch the podlets's content

return value

const result = await component.fetch();
console.log(result.content);
console.log(result.headers);
console.log(result.js);
console.log(result.css);

.stream(HttpIncoming, options)

Streams the content of the component. Returns a ReadableStream which streams the content of the component. Before the stream starts flowing a beforeStream event with a Podlet Response object, containing headers, css and js references is emitted.

HttpIncoming (required)

An HttpIncoming object. This is normally provided by the middleware which runs on the incoming request to the layout prior to the process of fetching podlets.

The HttpIncoming object is normally found on a request bound property of the request or response object.

const podlet = layout.client.register({
name: "myPodlet",
uri: "http://localhost:7100/manifest.json",
});

app.get(layout.pathname(), async (req, res, next) => {
const incoming = res.locals.podium;

const stream = podlet.stream(incoming);
stream.pipe(res);
});

options (optional)

optiontypedefaultrequireddetails
pathnamestringA path which will be appended to the content root of the podlet when requested
headersobjectAn object which will be appended as HTTP headers to the request to fetch the podlets's content
queryobjectAn object which will be appended as query parameters to the request to fetch the podlets's content

Event: beforeStream

A beforeStream event is emitted before the stream starts flowing. A response object with keys headers, js and css is emitted with the event.

headers will always contain the response headers from the podlet. If the resource manifest defines JavaScript assets, js will contain the value from the manifest file otherwise js will be an empty string. If the resource manifest defines CSS assets, css will contain the value from the manifest file otherwise css will be an empty string.

const podlet = layout.client.register({
name: "myPodlet",
uri: "http://localhost:7100/manifest.json",
});

app.get(layout.pathname(), async (req, res, next) => {
const incoming = res.locals.podium;

const stream = podlet.stream(incoming);
stream.once("beforeStream", (data) => {
console.log(data.headers);
console.log(data.css);
console.log(data.js);
});

stream.pipe(res);
});

.refresh()

This method will refresh a resource by reading its manifest and fallback if defined in the manifest. The method will not call the content URI of a component.

If the internal cache in the client already has a manifest cached, this will be thrown away and replaced when the new manifest is successfully fetched. If a new manifest cannot be successfully fetched, the old manifest will be kept in cache.

If a manifest is successfully fetched, this method will resolve with a true value. If a manifest is not successfully fetched, it will resolve with false.

const podlet = layout.client.register({
uri: "http://foo.site.com/manifest.json",
name: "foo",
});

const status = await podlet.refresh();

console.log(status); // true

.name

A property returning the name of the Podium resource. This is the name provided during the call to register.

.uri

A property returning the location of the Podium resource.

Podlet Response

When a podlet is requested by the .client.fetch() method it will return a Promise which will resolve with a podlet response object. If a podlet is requested by the .client.stream() method a beforeStream event will emit a podlet response object.

This object hold the response of the HTTP request to the content URL of the podlet which was requested.

An podlet response instance has the following properties:

propertytypegettersetterdefaultdetails
contentstringThe content of the podlet. Normally a string of HTML.
headersobject{}The HTTP headers the content route of the podlet responded with.
cssarray[]An array of AssetCSS objects holding the CSS references registered by the podlet.
jsarray[]An array of AssetJS objects holding the JS references registered by the podlet.

res.podiumSend(fragment)

Method on the http.ServerResponse object for sending HTML fragments. Calls the send / write method on the http.ServerResponse object.

This method wraps the provided fragment in a default HTML document before dispatching. You can use the .view() method to disable using a template or to set a custom template.

Example of sending an HTML fragment:

app.get(layout.pathname(), (req, res) => {
res.podiumSend("<h1>Hello World</h1>");
});