The Server Architecture

There is an important change coming from the previous version: we want to have full serverless support.

Serverless functions are single files with this structure:

module.exports = (req, res) => {
  res.end(`Hello from my serverless function!`)
}

Constrains:

  • Single file only, no node_modules.
    We’ll have to use webpack or ncc to create a single-file bundle.
  • The smaller the better.
    We’ll have to pay attention to the size of all modules we use in production.
  • Export a function that handles an HTML request.
    Serverless functions don’t need to worry about starting a server or handling static assets: they just handle the HTML request.

That server bundle (and the client one) is what users get when they run frontity build.

Running Frontity on Node

On top of that, we need something like express in case the user wants to start a regular Node app to serve both the HTML and static assets:

This Node app is what users get when they run frontity serve.

How are the statics served in this case?

AFAIK, serverless functions are not used to serve static files so users have to configure a static server or CDN.

For that reason, I will keep with our previous approach of supporting two different URLs, one for ssr or dynamic assets and the other for static assets. That way this type of configuration is really simple.

Yesterday I added both headers and robots.txt settings to the first draft of frontity.config.js. That way users can override those settings using their own frontity.config.js file.

But after that, I realized that it’d be much powerful to let users hack into the express server. If we do so, things like headers or robots.txt could be changed from extensions and that means final users can configure them in the Frontity Admin instead of a file.

Maybe something like this to control other urls:

const robots = ({ app, settings }) => {
  const router = Router();
  router.get("/robots.txt", (req, res) => {
    res.type("text/plain");
    res.send(settings.robots.text);
  });
  app(router);
};

And something like this to add middleware:

const headers = ({ app, settings }) => {
  app((req, res, next) => {
    Object.entries(settings.headers.dynamicHeaders).forEach(([key, value]) => {
      res.header(key, value);
    });
    next();
  })
};

This would change our architecture to:

But doesn’t seem problematic:


Right now there are three problems creating the serverless bundle:

1. webpack-hot-server-middleware needs a function that returns a function:

The 'server' compiler must export a function
in the form of `(options) => (req, res, next) => void`

In serverless we need: (req, res) => void.

2. loadable-components creates a file that then is accessed by node in runtime:

// This is the stats file generated by webpack loadable plugin
const statsFile = path.resolve('../dist/loadable-stats.json')

In serverless we need that the whole bundle is contained in one file.

3. Webpack doesn’t like express because it has a dynamic request:

WARNING in ./node_modules/express/lib/view.js 74:13-25
    Critical dependency: the request of a dependency is an expression

In serverless we need to now all the imports at bundle time.


So maybe the serverless bundle needs to be created afterwards with something like ncc. I wonder how ncc works with express because it seems like it works.

ncc doesn’t complain about the dynamic require of webpack but I don’t think it’s a good option. Maybe what we need is a wrapper of the whole webpack bundle. Something like this:

const stats = require("dist/loadable-stats.json");
const app = require("dist/main.node.js");

module.exports = (req, res) => app({ stats })(req, res);

And use koa or an alternative to express.

I have studied Koa and I think it’s pretty neat. It’s not the standard (express is) but it’s smaller, faster, it has a better API, doesn’t have dynamic imports (bad for webpack) and takes advantage of await/async.

I’m going to see if code splitting still works when I create the bundle with Webpack and then wrap it with the stats jsons.

I really like the idea to use Koa to extend Frontity because it makes the whole server extensible, like explained here.

With Koa, changing headers or robots becomes as simple as:

export const server = ({ app, settings }) => {
  // Change dynamic headers.
  app.use((ctx, next) => {
    Object.entries(settings.headers.dynamicHeaders).forEach(([key, value]) => {
      ctx.set(key, value);
    });
    next();
  });
  // Add robots.txt support
  app.use(route('/robots.txt', ctx => {
    ctx.body = settings.robots.text;
  }));
}

There is something that might cause problems: the order of the middleware. For example, if one extension adds middleware to look for any *.txt file:

export const server = ({ app }) => {
  app.use(route('/(.*).txt', ctx => {
    ctx.body = 'Sorry, missing txt';
  }));
}

It will break our robots.txt middleware unless it is in the correct order:

  1. robots-extension
  2. missing-txt-extension

An idea is to instruct the dev to use the order of his frontity.package.js:

export default [
  "robots-extension",
  "missing-txt-extension"
];

I’ve been thinking about this because I had another idea: use Koa middleware for theme templates.

We can populate frontity all the stuff we used to pass to the template in the old framework (revised, of course):

pwaTemplate({
          html,
          styles,
          preloads,
          publicPath,
          stores,
          chunks,
        }),

And it’d work something like:

const myTemplate = frontity => `<html><head>..${frontity stuff}..</html>`;
const myAmpTemplate = frontity => `<html amp><head>..${frontity stuff}..</html>`

export const server = ({ app, frontity }) => {
  app.use((ctx, next) => {
    ctx.body = frontity.match === 'amp'
      ? myTemplate(frontity)
      : myAmpTemplate(frontity);
    next();
  });
}

That way we don’t have to come up with yet another API for the templates.


I’m going to open another post to talk about the extensions export API.

EDIT: I’ve done it here: The packages export API

I’ve just thought about a couple of extra design principles:

  • Everything should be hackable.
  • Everything should have a default (meaning: it works without config).

With those in mind, I really like the idea that the template is hackable but… ctx.body can be already populated with a default template coming from the framework for both default and amp modes. If a theme wants to use its own template, it can replace or modify ctx.body.


I should probably open an express vs koa thread to document it and a Design Principles thread to talk about them.

Just as a quick reminder of why koa instead of express:

  • It is smaller: half the size.
  • The router is simpler: koa-route vs express.Router.
  • It’s ~20-30% faster.
  • The API is simpler.
  • It allows us to pass a template (ctx.body) but don’t send it yet, so it can be substituted or modified by extensions. That could in theory be done in express but with extra APIs we need to inject and teach people how to use.
  • It doesn’t have any dynamic require. Well, it has one, any-promise, but we can replace it for by promise-monofill with webpack. Express has one dynamic require that cannot be solved because it is internal.

And a reminder of some of our Design Principles:

  • It should be as simple as possible to learn.
    • The least number of concepts the developer needs to learn as possible.
    • The least number of APIs the developer needs to learn as possible.
  • It should be as simple as possible to use.
    • In case of conflict, the final-user experience goes first, then developer experience and finally our own development experience.
  • Everything should have a default (meaning: no configuration).
    • If not possible, ensure the least amount of configuration.
    • Everything that’s configurable should be hackable as well.

My progress with serverless:

1. loadable-components creates a file that is accessed by Node in runtime

It turns out loadable-components only needs the client stats on the server. I have managed to wait until the client bundle has finished and then create the server bundle including the client stats.

Problem solved.

2. Webpack doesn’t like express because it has a dynamic request

We switched to Koa.

Problem solved.

3. webpack-hot-server-middleware needs a function that returns a function

This is our last problem.

Now that I’ve managed to include the client stats with Webpack, I’d love to have the serverless bundle directly from Webpack as well. That way we make sure it is as simple as possible and we don’t break anything in a recompilation step.

With that in mind, I think the best solution is to fork webpack-hot-server-middleware and change its getServerRenderer function to accept a normal function: https://github.com/60frames/webpack-hot-server-middleware/blob/master/src/index.js#L61-L77

Maybe will accept a PR that makes it optional :slight_smile:

1 Like

Who is the final user?

Uhm… I was thinking about a WordPress owner that wants to use Frontity but only though the Frontity Admin (it’s not a developer).

It comes from this conversation where we talking about one solution that would force the WordPress owner to install a myriad of plugins instead of just one.

Maybe instead of final-user we can use wordpress-admin or wordpress-owner.