0.5.13 • Published 4 years ago

goldpage v0.5.13

Weekly downloads
47
License
-
Repository
github
Last release
4 years ago

     What is Goldpage      How it Works      Learn More      Usage           Getting Started                Basics           CSS & Static Assets           Async Data: addInitialProps           HTML: index.html, <head>, <meta>, <link>, ...                Render Control           Where & when: renderToDom, renderToHtml & renderHtmlAtBuildTime           How: htmlRender & domRender                App Types           SPA & MPA           SSR           Static Website           Mixed Apps & BFA                API Reference           Page Config *.page.js           Goldpage Config goldpage.config.js           CLI                Recipes           Providers: Redux / React Router / Vuex / Vue Router / GraphQL Apollo / Relay / ...           Transpilation: Babel / TypeScript / ES6 / ...           CSS-in-JS: Emotion / styled-components / ...           CSS pre-processors: PostCSS / Sass / Less / ...           Routing: Server-side Routing / Browser-side Routing / React Router / Vue Router / ...           Frontend Libraries: Google Analytics / jQuery / Bootstrap / Semantic UI / ...           Server: Express / Koa / Hapi / Fastify / ...           View Libraries: React / Vue / Preact / ...           Process Managers: Docker / systemd / PM2 / ...

What is Goldpage

Goldpage is a tool to build a frontend.

You define pages:

// A page config
export default {
  route: '/hello/:name',
  view: ({name}) => (
    <div>
      Hello {name}, welcome to Goldpage.
    </div>
  ),
};

The Goldpage CLI takes care of the rest:

$ goldpage build

Your pages are built at .build/browser/. If your pages are static, deploy .build/browser/ to a static host such as Netlify, and you are done.

For server-side rendering add the Goldpage middleware:

// Goldpage can be used with any server framework (Express, Koa, Hapi, ...)
const express = require('express');
const goldpage = require('goldpage');

const app = express();

// Our Goldpage middleware serves your pages.
app.use(goldpage.express);

Goldpage is a tiny do-one-thing-do-it-well library (~4K LOCs) with a simple interface. Yet it is powerful:

  • Render Control - You can choose when and where your pages are rendered: one page can be rendered to HTML and to the DOM (classic SSR), another page can be rendered to HTML only (no browser-side JavaScript for a blazing fast mobile page), and a third page can be rendered to the DOM only.
  • Any app type (SPA, SSR, Static Website, ...) - Goldpage supports all app types and switching from one app type to another is easy; you can start writing your prototype and decide later which app type is right for you. No more endless research whether you should go for a static website or SSR.
  • Any view tool - Goldpage can be used any view framework (React, Vue, RNW, Svelte, ...) any view library (Redux, Vuex, GraphQL, ...).
  • Backend First Apps - Goldpage introduces a new app type the Backend First App (BFA). A BFA uses the view framework (React, Vue, ...) primarly as an HTML template engine and interactivity is used scarcely. For increased productivity and performance.

How it Works

Goldpage uses Webpack to transpile and bundle your pages. Its Webpack config is minimal allowing you to easily modify and extend it.

Goldpage is designed to give you full control over how and where your pages are rendered.

You control where your page is rendered with two page configs:

  • renderToDom - If set to true, your page is rendered to the DOM (browser).
  • renderToHtml - If set to true, your page is rendered to HTML (Node.js).

If you want to add SSR to a page you set renderToHtml: true and renderToDom: true. If you want a page to be an SPA, you set renderToDom: true and renderToHtml: false. You can also set renderToHtml: true and renderToDom: false for a page to be rendered only to HTML with zero browser-side JavaScript. (Good old plain HTML like in the 90s!)

You can control how your pages are rendered by defining the render functions htmlRender and domRender:

// We can use any view framework here (React, Vue, RNW, ...)

import React from 'react';
import ReactDOM from 'react-dom';

export default domRender;

async function domRender({page, initialProps, CONTAINER_ID}) {
  const element = React.createElement(page.view, initialProps);
  const container = document.getElementById(CONTAINER_ID);
  if( page.renderToHtml ){
    ReactDOM.hydrate(element, container);
  } else {
    ReactDOM.render(element, container);
  }
}
const React = require('react');
const ReactDOMServer = require('react-dom/server');

module.exports = htmlRender;

async function htmlRender({page, initialProps, CONTAINER_ID}) {
  const el = React.createElement(page.view, initialProps);
  const html = ReactDOMServer.renderToStaticMarkup(el);
  return html;
}

This render control enables you to:

  • Build any app type (SPA, SSR, static website, ...).
  • Build new kinds of apps, such as Backend First Apps.
  • Use any view framework (React, Vue, RNW, ...).
  • Use any view library (React Router, Vuex, GraphQL, ...).

For server-side rendering, we offer a server middleware that can be used with any server framework (Express, Koa, Hapi, ...).

Learn More

Learning material.

Getting Started

This getting started adds Goldpage to an exisiting app. If you don't have an app yet or if you just want to try out Goldpage, then use a Goldpage starter.

  1. Install Goldpage.

    npm install goldpage

    Install a render plugin, such as @goldpage/react:

    npm install @goldpage/react

    Install a server plugin, such as @goldpage/express:

    npm install @goldpage/express
  2. Add Goldpage to your Node.js server.

    With Express:

    const express = require('express');
    const ssr = require('goldpage');
    
    const app = express();
    app.use(ssr.express);
    const Hapi = require('hapi');
    const ssr = require('goldpage');
    
    (async ()=>{
      const server = Hapi.Server();
      await server.register(ssr.hapi);
    })();
    const Koa = require('koa');
    const ssr = require('goldpage');
    
    const app = new Koa();
    app.use(ssr.koa);

    Goldpage can be used with any server framework. But there is no documentation for this (yet). Open a GitHub issue if you want to use Goldpage with a server framework other than Express, Koa, or Hapi.

  3. Create your first page.

    Create a pages/ directory.

    cd path/to/your/project/ && mkdir pages/

    Create a file at pages/hello.page.js.

    With React:

    // pages/hello.page.js
    
    import React, {useState} from 'react';
    
    export default {
      route: '/hello/:name',
      addInitialProps,
      view: Hello,
      title,
      renderToHtml: true,
    };
    
    async function addInitialProps({name}) {
      // We simulate a network request delay
      await sleep(0.5);
    
      const nameReversed = name.split('').reverse().join('');
      return {nameReversed};
    }
    
    function Hello({name, nameReversed}) {
      const [isReversed, setReverse] = useState(false);
    
      return (
        <div>
          Hello <span>{isReversed ? nameReversed : name}</span>, welcome to Goldpage.
          <br/>
          <button onClick={() => setReverse(!isReversed)}>Reverse name</button>
        </div>
      );
    }
    
    function title({name}) {
      return 'Hi '+name;
    }
    
    function sleep(seconds) {
      let resolve;
      const promise = new Promise(r => resolve=r);
      setTimeout(resolve, seconds*1000);
      return promise;
    }
    // pages/hello.page.js
    
    import Hello from './Hello.vue';
    
    export default {
      route: '/hello/:name',
      addInitialProps,
      view: Hello,
      title,
      renderToHtml: true,
    };
    
    async function addInitialProps({name}) {
      // We simulate a network request delay
      await sleep(0.5);
    
      const nameReversed = name.split('').reverse().join('');
      return {nameReversed};
    }
    
    function title({name}) {
      return 'Hi '+name;
    }
    
    function sleep(seconds) {
      let resolve;
      const promise = new Promise(r => resolve=r);
      setTimeout(resolve, seconds*1000);
      return promise;
    }
    // pages/Hello.vue
    
    <template>
      <div>
        Hello {{isReversed?nameReversed:name}}, welcome to <code>Goldpage</code>.
        <br/>
        <button v-on:click="toggleReverse">Reverse name</button>
      </div>
    </template>
    
    <script>
    import Vue from "vue";
    
    export default {
      props: ['name', 'nameReversed'],
      data() {
        return {
          isReversed: false,
          bundler: "Parcel"
        };
      },
      methods: {
        toggleReverse: function() {
          this.isReversed = !this.isReversed;
        },
      },
    };
    </script>
    
    <style scoped>
    code {
      color: gold;
    }
    </style>
  4. Add the Goldpage scripts to your package.json:

    {
      "scripts": {
        "dev": "goldpage dev ./path/to/your/server.js",
        "prod": "npm run build && npm run start",
        "build": "goldpage build ./path/to/your/server.js",
        "start": "export NODE_ENV='production' && node ./.build/nodejs/server"
      }
    }

    :information_source: Goldpage also builds your server's source code so that you can use the same language, for example TypeScript, for browser-side code as well as for server-side code.

That's it: you can now run npm run dev (yarn dev) and go to http://localhost:3000/hello/jon.

CSS & Static Assets

To load a CSS file, simply import it:

import './path/to/style.css';

Importing static assets such as images, fonts, or videos returns an URL:

import imageUrl from './path/to/image.png';
// `imageUrl` is the URL that serves `./path/to/image.png`.
// Do something with imageUrl, e.g. `await fetch(imageUrl)` or `<img src={imageUrl}/>`.

You can also reference static assets in CSS:

.logo {
    /* Your file's path on your disk `./path/to/image.png`
       will automatically be replaced with the URL that serves `./path/to/image.png` */
    background-image: url('./path/to/image.png');
}
@font-face {
    font-family: 'MyAwesomeFont';
    /* Your file's path on your disk `./path/to/my-awesome-font.ttf`
       will automatically be replaced with the URL that serves `./path/to/my-awesome-font.ttf` */
    src: url('./path/to/my-awesome-font.ttf') format('truetype');
}

Example of a page that uses all kinds of static assets:

Async Data: addInitialProps

You can load and render data by adding an addInitialProps function to your page config:

import React from 'react';
import fetchProduct from './fetchProduct';
import Product from './Product';

export default {
  route: '/product/:productId',

  // Goldpage calls `addInitialProps()` before rendering `view` to HTML or to the DOM.
  // Alls props returned in `addInitialProps()` are passed to the `view`'s props.
  addInitialProps: async ({productId}) => {
    const product = await fetchProduct(productId);
    return {product};
  },

  // The `product` returned by `addInitialProps` is available to `view`.
  view: initialProps => {
    const {product} = initialProps;
    return (
      <Product product={product}/>
    );
  },

  // The initial props are also available for generating HTML code.
  title: initialProps => {
    const {product, productId} = initialProps;
    return (
      product.name+' ('+productId+')'
    );
  },

  renderToHtml: true,
};

Alternatively, you can fetch data in a stateful component. But the page's content is then rendered to the DOM only (and not to HTML).

We further explain the difference between using a stateful component and addInitialProps at:

HTML: index.html, <head>, <meta>, <link>, ...

To set the HTML meta tags of all your pages, create an index.html file somewhere in your project's directory. Your index.html needs to contain !HEAD and !BODY:

<!DOCTYPE html>
<html>
  <head>
    !HEAD
  </head>
  <body>
    !BODY
    <script src="https://example.org/some-lib.js" type="text/javascript"></script>
  </body>
</html>

To set the HTML meta tags of only one page, use the page config:

// /examples/html/pages/products.page.js

import React from 'react';
import logoUrl from './logo.png';
import manifestUrl from './manifest.webmanifest';
import fetchProduct from './fetchProduct';
import Product from './Product';

export default {
  view: Product,

  // Goldpage uses the package @brillout/html (https://github.com/brillout/html) to generate HTML.
  // All @brillout/html's options are available over the page config.

  // Adds <title>Welcome</title>
  title: 'Product Page',

  // Adds <link rel="shortcut icon" href="/logo.hash_85dcecf7a6ad1f1ae4d590bb3078e4b1.png">
  favicon: logoUrl,

  // Adds <meta name="description" content="A welcome page.">
  description: 'Describes a product',

  // Adds <script src="https://example.org/awesome-lib.js" type="text/javascript"></script>
  scripts: [
    'https://example.org/awesome-lib.js',
  ],

  // Adds <link href="https://example.org/awesome-lib.css" rel="stylesheet">
  styles: [
    'https://example.org/awesome-lib.css',
  ],

  // Adds <link rel="manifest" href="/manifest.hash_bb5e0038d1d480b7e022aaa0bdce25a5.webmanifest">
  head: [
    '<link rel="manifest" href="'+manifestUrl+'"/>',
    // All HTML in this array are added to `<head>`.
    // Make sure that the HTML you inject here is safe and escape all user generated content.
  ],

  body: [
    '<script>console.log("hello from injected script")</script>',
    // All HTML in this array are added to `<body>`.
    // Make sure that the HTML you inject here is safe and escape all user generated content.
  ],

  // You can also generate HTML dynamically:
  route: '/products/:productId',
  addInitialProps: async ({productId}) => {
    const product = await fetchProduct(productId);
    return {product};
  },
  title: ({product, productId}) => product.name+' ('+productId+')',
  description: ({product}) => product.description,
  head: ({product}) => [
    // Open Graph tags
    '<meta property="og:title" content="'+product.name+'">',
    '<meta property="og:description" name="description" content="'+product.description+'">',
  ],

  renderToHtml: true,
};
// /examples/html/pages/about.page.js

import React from 'react';

export default {
  // `html` allows you to override the `index.html` file for a specific page:
  html: (
`<!DOCTYPE html>
<html>
  <head>
    <title>Title set over \`html\` option.</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    !HEAD
  </head>
  <body>
    !BODY
  </body>
</html>
`
  ),

  route: '/about',
  view: () => <h1>About Page</h1>,
  renderToHtml: true,
};

See @brillout/html's documentation for the list of all options.

Example:

SPA & MPA

:information_source: You can use Goldpage without reading this section.

:warning: This section is meant for readers that know what SPAs and MPAs are.

If what you want is an MPA then you don't have to do anything: Goldpage's default app type is an MPA.

  • By default, your pages are rendered only in the browser. (That is: renderToDom: true and renderToHtml: false. We explain renderToHtml and renderToDom at Where & when: renderToDom, renderToHtml & renderHtmlAtBuildTime.)
  • By default, your pages are routed on the server-side. (We explain what "server-side routing" means at Routing: Server-side Routing / Browser-side Routing / React Router / Vue Router / ....)

If what you want is an SPA then:

  • Create a single page with a catch-all route. (That is, create only one page config with route: '/:param* to have all URLs routed to this single page.)
  • Use a browser-side routing library such as React Router. (We elaborate more at Routing: Server-side Routing / Browser-side Routing / React Router / Vue Router / ....)

Note that an MPA is usually better than an SPA. You most likely want an MPA instead of an SPA. (An MPA is basically the same than an SPA but with server-side routing and code-splitting.)

FYI, an SPA is what you get when you use create-react-app, vue-cli, Webpack, or Parcel.

SSR

:information_source: You can use Goldpage without reading this section.

:warning: This section is meant for readers that know what an SSR app is.

If you want an SSR app, then:

  • Set renderToHtml: true and renderToDom: true to all your page configs. (We explain renderToHtml and renderToDom at Where & when: renderToDom, renderToHtml & renderHtmlAtBuildTime.)

(FYI, an SSR app is what you get when you use Next.js or Nuxt.js.)

Static Website

:information_source: You can use Goldpage without reading this section.

:warning: This section is meant for readers that know what a static website is.

If you want a static website, then:

  • Set renderHtmlAtBuildTime: true to all your page configs. (We explain renderHtmlAtBuildTime at Where & when: renderToDom, renderToHtml & renderHtmlAtBuildTime.)

By setting renderHtmlAtBuildTime: true to all your page configs, all browser-side code and assets are generated at built-time.

This means that no Node.js server is required to serve your frontend and you can deploy your frontend using a static host such as Netlify, Amazon S3, or GitHub Pages.

To deploy, simply run $ goldpage build (or better: npm run build while require('./package.json').scripts.build === 'goldpage build') and deploy the directory .build/browser/ (this is the directory that contains all browser assets) to your static host.

(FYI, a static website is what you get when you use Gastby.)

Mixed Apps & BFA

:information_source: You can use Goldpage without reading this section.

:warning: Prerequisite Knowledge For this section you need to know:      CSR & SSR      renderToDom & renderToHtml      Interactive VS non-interactive You can learn all this at CSR & SSR Explained.

Tools usually implement CSR and SSR in an all-or-nothing way: either all your pages are CSR'd or all your pages are SSR'd.

Our renderToHtml/renderToDom interface gives you a fine grain control you can mix CSR and SSR: one page can be CSR'd (renderToDom: true & renderToHtml: false) while another page can be SSR'd (renderToDom: false & renderToHtml: true).

For example, if your app is non-interactive with the exception of one interactive page, then you can set CSR for that interactive page (renderToDom: true and renderToHtml: false) and set SSR for all your other pages (renderToDom: false and renderToHtml: true).

This mixing enables a whole new range of app types that where previously not possible to achieve.

For example, what we call a BFA (Backend First App). The idea of a BFA is to use React (or Vue) primarily as an HTML template engine and only secondarily to implement interactive views. To achieve:

  • Fast mobile pages
  • High development speed
  • Reliable SEO More at BFA.

Where & when: renderToDom, renderToHtml & renderHtmlAtBuildTime

:information_source: You can use Goldpage without reading this section.

:warning: Prerequisite Knowledge This section requires you to know what CSR and SSR are (which we explain at CSR & SSR Explained).

There are three page configs that allow you to control when your page is rendered:

  • renderToDom - If set to true, your page is rendered in the browser (to the DOM).
  • renderToHtml - If set to true, your page is rendered to HTML (in Node.js).
  • renderHtmlAtBuildTime - Whether your page is rendered to HTML at request-time or at build-time.

The default values are renderToDom: true and renderToHtml: false, which means that by default your page is rendered only in the browser.

Configuring these three page configs are about achieving improvements in:

  • Search Engines All search engines other than Google (Bing, Baidu, Yandex, DuckDuckGo, ...) cannot crawl content rendered to the DOM. If you want your pages to appear in these search engines, then you need to render them to HTML.
  • Google Search Google is capable of crawling content rendered to the DOM but with limitations. Rendering your pages to HTML can still be beneficial.
  • Social Sharing Previews When your page is shared on a social site (Facebook, Twitter, ...) then a little preview (title, description, and image) of your page is shown. To have a correct preview the meta data of your page needs to be rendered to HTML. (No social site can crawl meta data rendered dynamically to the DOM.)
  • Performance Your page's performance can vastly differ depending on whether your page is rendered to the DOM and/or to HTML.
  • Mobile Performance Browser-side JavaScript is a performance killer for low-end devices such as mobile phones. On mobile, rendering your page to HTML is drastically faster then rendering it to the DOM.

At Client-side Rendering (CSR) VS Server-side Rendering (SSR), we elaborate how to set renderToHtml, renderToDom and renderHtmlAtBuildTime in order to achieve improvements in the above points. But before you learn more about these page configs and what you can achieve with them, we recommend that you first implement a prototype and learn more only after you need improvements in the above mentioned points.

How: htmlRender & domRender

You can control how your pages are rendered to HTML and the DOM.

For that, save a goldpage.config.js file at your project's root directory (the directory that contains your package.json) and add the htmlRender and domRender configs:

// goldpage.config.js

module.exports = {
  htmlRender: './render/htmlRender.js',
  domRender: './render/domRender.js',
};

Then create the domRender and htmlRender files.

With React:

// render/domRender.js

import React from 'react';
import ReactDOM from 'react-dom';

export default domRender;

async function domRender({page, initialProps, CONTAINER_ID}) {
  const element = React.createElement(page.view, initialProps);
  const container = document.getElementById(CONTAINER_ID);
  if( page.renderToHtml ){
    ReactDOM.hydrate(element, container);
  } else {
    ReactDOM.render(element, container);
  }
}
// render/htmlRender.js

const React = require('react');
const ReactDOMServer = require('react-dom/server');

module.exports = htmlRender;

async function htmlRender({page, initialProps, CONTAINER_ID}) {
  const html = (
    ReactDOMServer.renderToStaticMarkup(
      React.createElement(page.view, initialProps)
    )
  );

  // Altnertively, you can return a `@brillout/html` options object
  // in order to have more control over the generated HTML. See all
  // html options at https://github.com/brillout/html
  // return {
  //   head: [
  //     '<style>body{background: blue;}</style>',
  //   ],
  //   body: [
  //     '<div>Some additional HTML</div>',
  //     '<div id='+CONTAINER_ID+'>'+html+'</div>',
  //   ]
  // };

  return html;
}

This allows you to add providers such as Redux's <Provider store={store} /> or React Router's <BrowserRouter />.

// render/domRender.js

import Vue from 'vue';
import getVueInstance from './getVueInstance';

export default domRender;

async function domRender({page, initialProps, CONTAINER_ID}) {
  const vm = getVueInstance(page.view, initialProps);

  vm.$mount('#'+CONTAINER_ID);
}
// render/htmlRender.js

const VueServerRenderer = require('vue-server-renderer');
const getVueInstance = require('./getVueInstance');

module.exports = htmlRender;

async function htmlRender({page, initialProps}) {
  const renderer = VueServerRenderer.createRenderer();

  const vm = getVueInstance(page.view, initialProps);

  const html = await renderer.renderToString(vm);

  return html;
}
// render/getVueInstance.js

let Vue = require('vue');
Vue = Vue.default || Vue;

module.exports = getViewInstance;

function getViewInstance(view, initialProps) {
  if( view instanceof Function ){
    return view(initialProps);
  } else {
    return (
      new Vue({
        render: createElement => createElement(view, {props: initialProps}),
      })
    );
  }
}

This allows you to add providers such as for Vuex or Vue Router.

Examples:

Page Config *.page.js

The following page config showcases an overview of the available page configs.

// pages/*.page.js

import React from 'react';
import fetchProduct from './fetchProduct';
import getHtmlOptions from './getHtmlOptions';
import assert_initialProps from './assert_initialProps';
import Product from './Product';

export default getPageConfig();

function getPageConfig() {
  return {
    // The url of the page.
    // The routing is done by `path-to-regexp` (https://github.com/pillarjs/path-to-regexp).
    route: '/product-details/:productId',

    // Add additional inital props, for example data loaded from an API.
    // `addInitialProps` can be async and Goldpage awaits `addInitialProps` before
    // rendering `view` to the DOM / HTML.
    addInitialProps,

    // The content of your page.
    // `view` is rendered by the render plugin you installed.
    view: Product,

    // Control when the page is rendered.
    // More in a section below.
    renderToDom: true, // (default value)
    renderToHtml: false, // (default value)
    renderHtmlAtBuildTime: false, // default value

    // The definition of `getHtmlOptions` is shown in a section below
    // and shows all HTML configs.
    ...getHtmlOptions()
  };
}

async function addInitialProps(initialProps) {
  // The definition of `assert_initialProps` is shown in a section below
  // and shows all `initialProps`.
  assert_initialProps(initialProps);

  const {productId} = initialProps;

  const product = await fetchProduct(productId);

  return {product};
}

We now list all configs.

  • All initial props initialProps
  • All page configs for the page's HTML document
  • All render page configs: renderToDom, renderToHtml & renderHtmlAtBuildTime
All initial props initialProps

This initialProps assertion showcases all initialProps.

import assert from '@brillout/reassert';

export default assert_initialProps;

function assert_initialProps(initialProps){
  const {
    // Route arguments
    productId,

    // Query paramaters
    productColor,

    // Whether the code is being run in Node.js or in the browser
    isNodejs,

    // URL props
    url,
    origin,
    protocol,
    hostname,
    port,
    pathname,
    query,
    queryString,
    hash,

    // The request object is available here.
    // The page config as well.
    // See below.
    ...initialProps__rest
  } = initialProps;

  assert.internal(url.startsWith('http'));
  if( url==='http://localhost:3000/products/123?productColor=blue#reviews' ){
    // Url params
    assert(productId==='123');
    assert(productColor==='blue');

    // Url props
    assert(origin==='http://localhost:3000');
    assert(protocol==='http:');
    assert(hostname==='localhost');
    assert(port==='3000');
    assert(pathname==='/products/123');
    assert(query.productColor==='blue');
    assert(queryString==='?productColor=blue');
    assert(hash==='#reviews');
  }

  assert([true, false].includes(isNodejs));

  // The server framework's request object is also available.
  // For example, to get the HTTP request headers `req.headers`:
  const {headers} = initialProps__rest;

  // The page config is available at `initialProps`
  assert(initialProps__rest.route);
  assert(initialProps__rest.view);

  // Since all props are flat-merged into one object, there can be conflicts.
  // In case of a prop name conflict, you can access all props over `__sources`.
  const {__sources} = initialProps;
  // Props returned by `addInitialProps`
  assert(__sources.addInitialProps__result===null || __sources.addInitialProps__result.product);

  // The request object returned by your server framework (Express / Koa / Hapi / ...)
  assert(isNodejs===false || __sources.requestObject);
  assert(isNodejs===false || __sources.requestObject.headers);

  // The url props returned by `@brillout/parse-url` (https://github.com/brillout/parse-url)
  assert(__sources.urlProps);

  // The route params
  assert(__sources.routeArguments);
  assert(__sources.routeArguments.productId);

  // The page config
  assert(__sources.pageConfig);
  assert(__sources.pageConfig.route);
  assert(__sources.pageConfig.view);

  // Whether the code is running on the server or in the browser.
  assert(__sources.isNodejs===isNodejs);
}
!VAR HTML_CONFIGS

List of all HTML configs:

import manifestUrl from './manifest.webmanifest';

export default getHtmlOptions;

function getHtmlOptions() {
  // Goldpage uses `@brillout/html` (https://github.com/brillout/html) to generate HTML.
  // All `@brillout/html` options are available over the page config.

  return {
    // Adds <title>Title shown in browser tab.</title>
    title: 'Title shown in browser tab.',
    /* Altneratively:
    title: initialProps => {
      assert_initialProps(initialProps);
      // Props returned by `addInitialProps` are also available to the `@brillout/html` options
      return initialProps.product.product.name;
    },
    */

    // <meta name="description" content="Description of page shown in search engines.">
    description: 'Description of page shown in search engines.',
    /*
    // Not only `title` but all options can be dynmically generated with a function
    description: initialProps => initialProps.product.product.name,
    */

    // <link rel="shortcut icon" href="/logo.hash_85dcecf7a6ad1f1ae4d590bb3078e4b1.png">
    favicon: require('./logo.png'),

    head: [
      '<link rel="manifest" href="'+manifestUrl+'">',
    ],
    body: [
      '<script>console.log("hello from injected script")</script>',
    ],
    /* Again, we can use a function to dynammically generate HTML.
    body: initialProps => {
      return [
        '<script>console.log("hello from '+initialProps.product.productname+' page.")</script>',
      ];
    },
    */

    // You can fully control the HTML:
    html: (
`<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    !HEAD
  </head>
  <body>
    !BODY
  </body>
</html>
`
    ),
    /* Dynammically as well:
    html: initialProps => '...',
    */

    // See https://github.com/brillout/html for the list of all options
  };
}
All render page configs: renderToDom, renderToHtml & renderHtmlAtBuildTime

A page has three render configs:

  • renderToDom
  • renderToHtml
  • renderHtmlAtBuildTime

In this section we only explain what each config does. At Where & when: renderToDom, renderToHtml & renderHtmlAtBuildTime we explain how to set these three render configs.

renderToDom

With renderToDom you control whether your page is rendered in the browser (to the DOM).

  • renderToDom: true (default value)
    • Slower Performance. The page's views (e.g. React components) and view libraries (e.g. React) are loaded, executed, and rendered in the browser. This can be very slow on mobile devices.
    • Interactive. Because your page is rendered to the browser's DOM, your page's views (e.g. React components) can be stateful and interactive.
  • renderToDom: false
    • Increased performance. The page's views and view libraries are not loaded nor executed in the browser. Considerably less JavaScript is loaded/executed in the browser. A page that has (or very little) browser-side JavaScript is blazing fast on mobile.
    • Non-interactive. Because your page is not rendered to the browser's DOM, your page connot have stateful views / interactive views.

In a nutshell: If your page is interactive then you have to render it in the browser and set renderToDom to true. But if your page isn't interactive then you can set renderToDom to false for increased performance and a blazing fast page on mobile devices.

renderToHtml

With renderToDom you control whether your page is rendered to HTML (in Node.js).

  • renderToHtml: false (default value) Your page's view component is not rendered to HTML.
  • renderToHtml: true Your page's view component is rendered to HTML.
renderHtmlAtBuildTime

With renderHtmlAtBuildTime you can control whether the page's HTML is rendered statically at build-time or dynamically at request-time.

  • renderHtmlAtBuildTime: false (default value) The page is rendered to HTML at request-time. The page is (re-)rendered to HTML every time the user requests the page.
  • renderHtmlAtBuildTime: true The page is rendered to HTML at build-time. The page is rendered to HTML only once, when Goldpage transpiles and builds your pages.

By default, a page is rendered to HTML at request-time. But if the page's content is static (a landing page, an about page, a blog post, a personal homepage, etc.) it is wasteful to re-render its HTML on every page request.

By setting renderHtmlAtBuildTime: true to all your pages, you can remove the need for a Node.js server and have your frontend be a static website. You can then deploy your frontend to a static host such as Netlify or GitHub Pages.

Goldpage Config goldpage.config.js

We try to keep Goldpage as zero-config as possible and ssr.config.js has only few options.

// ssr.config.js

module.exports = {
  // Control how pages are rendered. (See section "How: `htmlRender` & `domRender`").
  htmlRender: require.resolve('./path/to/your/htmlRender'),
  domRender: require.resolve('./path/to/your/domRender'),

  // Make Goldpage silent in the terminal (but it will still prints errors).
  silent: true,
};

CLI

Goldpage has two commands:

  • $ goldpage dev ./path/to/your/server.js

    The dev command starts your server with auto-reload: whenever you make a change to your code, your code is rebuilt and the pages open in your browser are reloaded. The dev command is meant to be used while you develop your app.

  • $ goldpage build ./path/to/your/server.js

    The build command builds your code for production. (E.g. it minifies your browser-side code whereas the dev command doesn't.) Once build is finished you can repeatedly start and shut down your server. If your server-side code is written in JavaScript then you can call your server entry directly: $ node ./path/to/your/server.js. If not, then run the transpiled version which is located in the .build directory: $ node ./build/nodejs/server.js.

Do not install Goldpage globally and do not call the Goldpage CLI directly. Use your package.json's scripts instead:

{
  "dependencies": {
    "goldpage": "^0.3.3",
  },
  "scripts": {
    "dev": "goldpage dev ./path/to/your/server.js",
    "prod": "npm run build && npm run start",
    "build": "goldpage build ./path/to/your/server.js",
    "start": "export NODE_ENV='production' && node ./.build/nodejs/server"
  }
}

You can then install a local Goldpage copy ($ npm install / $ yarn) and then call $ npm run dev (/ $ yarn dev).

A local install has couple of advantages over a global install:

  • Many projects can have many different Goldpage versions.
  • Removing your project's directory also removes Goldpage.

Providers: Redux / React Router / Vuex / Vue Router / GraphQL Apollo / Relay / ...

By controlling the rendering of your pages you can add providers for Redux, GraphQL, etc.

See How: htmlRender & domRender for how to take over control of the rendering of your pages.

Examples:

Transpilation: Babel / TypeScript / ES6 / ...

You can configure Babel and the JavaScript transpilation by creating a .babelrc file. See /examples/babel for an example of configuring babel.

Goldpage currently uses Webpack. This means that for custom transpilations beyond babel, modifications to Goldpage's webpack config are required. Instead of modifying Goldpage's webpack config yourself, see if there is a transpilation plugin transpilation plugin that modifies Goldpage's webpack config for you. For exampe, for TypeScript, you can use the TypeScript plugin. If there is no plugin for what you need, then open a GitHub issue and we'll create one together.

Once Parcel v2 is released, Goldpage will use Parcel instead of Webpack. Since Parcel is zero-config, most of your transpilation needs will then just work. (Transpilation plugins will not be required anymore.)

Examples:

CSS-in-JS: Emotion / styled-components / ...

Some CSS-in-JS libraries, such as emotion, work with SSR out of the box and no additional setup is required.

For some others, such as styled-components, you make need to take control over rendering.

Examples:

CSS pre-processors: PostCSS / Sass / Less / ...

If there is a transpilation plugin for the CSS pre-processor you want to use, then simply use it.

If there isn't one, then see controlling transpilation.

Example:

Routing: Server-side Routing / Browser-side Routing / React Router / Vue Router / ...

On the web, there are two ways to do routing: server-side routing and browser-side routing.

Server-side Routing

Routes defined in your page configs

import React from 'react';

export default {
  route: '/hello/:name',
  view: ({name}) => (
    <div>
      Welcome {name}.
    </div>
  ),
};

are server-side routes: when navigating from /hello/jon to /hello/alice the browser terminates the current /hello/jon page and starts a new page /hello/alice. It is the same as if you would close your /hello/jon tab and open a new tab at /hello/alice.

It is the server that does the job of mapping URLs to pages and the browser is not involved in the routing process.

Browser-side Routing

HTML5 introduced a new browser API window.history that allows you to manipulate the browser URL history. This enables browser-side routing: when navigating from /previous-page to /next-page, instead of terminating the current page /previous-page and starting a new page /next-page, the current page /previous-page is preserved, its URL changed to /next-page (with window.history.pushState()), and the content of /next-page is rendered to the DOM replacing the DOM of /previous-page.

Server-side routing is simpler than browser-side routing; whenever possible, server-side routing should be used instead of browser-side rendering.

But if server-side routing is not an option, you can opt to do browser-side routing by taking control over rendering.

For example, if you use React, you can take control over rendering in order to use React Router which does browser-side routing:

// /examples/react-router/render/domRender.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

export default domRender;

async function domRender({page, initialProps, CONTAINER_ID}) {
  ReactDOM.hydrate(
    <BrowserRouter>
      <page.view {...initialProps}/>
    </BrowserRouter>,
    document.getElementById(CONTAINER_ID)
  );
}
// /examples/react-router/render/htmlRender.js

const React = require('react');
const ReactDOMServer = require('react-dom/server');
const {StaticRouter} = require('react-router');

module.exports = htmlRender;

async function htmlRender({page, initialProps}) {
  const {pathname, search, hash} = initialProps;
  return (
    ReactDOMServer.renderToStaticMarkup(
      React.createElement(
        StaticRouter,
        {location: {pathname, search, hash, state: undefined}, context: {}},
        React.createElement(
          page.view,
          initialProps
        )
      )
    )
  );
}
// /examples/react-router/goldpage.config.js

module.exports = {
  htmlRender: './render/htmlRender.js',
  domRender: './render/domRender.js',
};

The example's entire source code is at:

More Resources:

Frontend Libraries: Google Analytics / jQuery / Bootstrap / Semantic UI / ...

To load a frontend library that is hosted on a cdn, add <script>/<style> tags to your HTML, see HTML: index.html, <head>, <meta>, <link>, ....

To load a frontend library that is saved on disk, use a file that is loaded by all your pages:

// /examples/frontend-libraries/pages/commons.js

if(
  // Do not load the frontend libraries on the server
  isBrowser()
){
  require('../frontend/normalize.css');
  require('../frontend/style.css');
  require('../frontend/google-analytics.js');
}

function isBrowser() {
  return typeof window !== "undefined";
}
// /examples/frontend-libraries/pages/landing.page.js

require('./commons.js');

import React from 'react';

export default {
  route: '/',
  view: () => <h1>Landing Page</h1>,
  renderToHtml: true,
};
// /examples/frontend-libraries/pages/about.page.js

require('./commons.js');

import React from 'react';

export default {
  route: '/about',
  view: () => <h1>About Page</h1>,
  renderToHtml: true,
};

Server: Express / Koa / Hapi / Fastify / ...

To use Goldpage with express, koa or hapi, use the corresponding server plugin.

To use Goldpage with another server framework, open a GitHub issue. Goldpage can be used with any server framework but there is no documentation for this (yet).

View Libraries: React / Vue / Preact / ...

If there is a render plugin for the view library you want to use, then simply use it.

If there is no render plugin, then take control over rendering. That way you are able to use any view library.

Process Managers: Docker / systemd / PM2 / ...

You can start your server with any process manager.

For example with pm2:

pm2 start ./.build/nodejs/server

If you don't transpile your server code (see !VAR|LINK), then use your server entry file:

pm2 start ./path/to/your/server.js