Skip to content

Migrating from n–ui to Page Kit

Bren Brightwell edited this page Mar 5, 2020 · 36 revisions

No apps use n-ui any more. This guide is included for posterity and doesn't necessarily reflect the current state of Page Kit.

Table of contents

Preparation

  • Clone your application from GitHub, cd into the directory
  • Install, build and run the unaltered branch to be sure it runs as expected. If it doesn't please raise appropriate bugfix PRs now.
  • If everything is working as expected create a new page-kit-migration branch.

Remove n-ui

NOTE: This is quite a long step and you may need a notepad and pen.

  • Delete n-ui-build.config.js in the app root directory.
  • In .gitignore, replace all the public/* lines with a single public/ line.
  • Remove n-ui scripts from the makefile. We will add replacement scripts later.
  • Remove n-ui as a dependency from bower.json and package.json (but leave n-ui-foundations, we'll still need that).
  • Now is a good time to bump some dependencies to versions which support Page Kit.
    • Bump n-gage to v4.0.0 or higher.
    • Bump n-heroku-tools to v8.3.0 or higher.

Client-side

Sass

  • Open the Sass entry point (usually client/main.scss) and replace n-ui with n-ui-foundations, you may need to install this package as a Bower dependency.

    -@import 'n-ui/main';
    +@import 'n-ui-foundations/mixins'
  • Remove any nUiStylesheetStart/nUiStylesheetEnd mixins from the Sass source code. There is no equivalent in Page Kit.

JavaScript

  • Open the client-side JS entry point (usually client/main.js).
  • Comment out any references to flags imported from n-ui, we will bring them back later.
  • Comment out any references to tracking imported from n-ui, we will bring them back later.
  • Delete any dependency on n-ui or @financial-times/n-ui.
  • Delete any references to allstylesloaded and hoist the contents of its callback into the parent scope.
  • Delete any onAppInitialised() calls.

Server-side

  • Install n-express:

    npm install -S @financial-times/n-express
  • Open the application entry point (usually server/app.js) and replace the dependency on n-ui:

    - const express = require('@financial-times/n-ui');
    + const express = require('@financial-times/n-express');
  • Update the Express server initialisation options. At a minimum add the appName, graphiteName and set withFlags, withConsent and withAnonMiddleware to true.

    const app = express({
    +   appName: 'application name',
    +   graphiteName: 'application name',
    +   withFlags: true,
    +   withConsent: true,
    +   withAnonMiddleware: true
        ...
    });
  • If any handlebars helpers are being registered, comment out these dependencies now and then remove the helpers option from the express server initialisation.

  • Make a note of what features app.locals.nUiConfig = {} is configuring and then delete it.

    • The preset value will be either discrete or complete. Check the list of features included in each one here and make a note of the features being used in your application.
    • Please note: Later we will need to initialise any client-side components that are not included in Page Kit (such as image lazy loading, n-feedback, o-date, etc.)
  • If the .snyk file contains a patch for n-ui, delete the patch now.

  • Finally, search for any remaining references to n-ui (or nui) and delete or comment them out.

  • Commit your work with the message "Completely remove n-ui integration from this app"

Implement Page Kit Handlebars as Express view engine

NOTE: This is probably the hardest step and this will vary between applications depending on how it extends or works around the limitations of Handlebars.

  • Install the Handlebars package:

    npm install -S @financial-times/dotcom-server-handlebars
  • In the application entry point (usually server/app.js), register Handlebars as an Express view engine and configure the template helpers:

    const { PageKitHandlebars, helpers } = require('@financial-times/dotcom-server-handlebars');
    app.engine('.html', new PageKitHandlebars({ helpers }).engine);

    If your app was using any additional Handlebars helpers (e.g x-handlebars) configure them now.

  • Update all response.render() calls in the application's controllers to include the .html file extension. If you miss any response.render() calls, you'll see an error like this:

    Error: No default engine was specified and no extension was provided.
  • Remove the layout property from the template data passed to response.render() (usually layout: 'wrapper').

  • If your application is using Handlebars directly (require('handlebars')):

    • Don't! Handlebars is a singleton ...
    • ... and n-ui implemented a hack to load partial templates on application startup and append them to this.
    • If necessary, refactor the application to use a shared instance of PageKitHandlebars. You may prefer to add a new server/handlebars-setup.js module to achieve this.
  • Run the application and load it in your browser.

    make build run-local
    • This is the point in the migration where you will find out if your application is using any unsupported Handlebars helpers. For instance, Page Kit does not support {{#defineBlock}}{{/defineBlock}} as it uses a different mechanism for inserting content into the document <head> and before/after the header and footer slots (using the layout options). Handlebars supports an inline partials mechanism which you can use instead if necessary.
    • Comment out the use of any other unsupported helpers for now and make a note of them.

    It's important to get the application running and verify that it is delivering the expected HTML at this point as we will verify each of the following stages by running the application and checking it in the browser.

  • Commit your work and have a lovely cup of tea, you've earned it. ☺️

Set up basic build task for client-side JavaScript and Sass

NOTE: This is probably the second hardest step and may vary between applications and the dependencies it uses.

  • Install the Page Kit build packages:

    npm install -D \
        @financial-times/dotcom-page-kit-cli \
        @financial-times/dotcom-build-js \
        @financial-times/dotcom-build-sass \
        @financial-times/dotcom-build-bower-resolve
  • Create a page-kit.config.js file in the repository root:

    const path = require('path');
    
    module.exports = {
        plugins: [
            require('@financial-times/dotcom-build-js').plugin(),
            require('@financial-times/dotcom-build-sass').plugin(),
            require('@financial-times/dotcom-build-bower-resolve').plugin()
        ],
        settings: {
            build: {
                entry: {
                    scripts: './client/main.js',
                    styles: './client/main.scss'
                },
                outputPath: path.resolve('./public')
            }
        }
    };
  • Configure Page Kit build steps in the application's makefile:

    build:
    +	page-kit build --development
    build-production:
    +	page-kit build
    watch:
    +	page-kit build --development --watch
  • Build the application:

    • There may be a number of warnings output to the console, inspect these but they can usually be ignored.
    • If there are any errors resolve these now. In our tests the most common cause of problems is CJS/ESM interoperability.
    • Open the public/ folder and ensure the expected JS and CSS files are being generated along with a manifest.json file.

    It's important to get the application building correctly without any errors at this stage. If you are unsure run make build-production which will fail if any problems are found.

  • Commit your work.

Set up basic integration with the Page Kit shell

  • Install the shell package:

    npm install -S @financial-times/dotcom-ui-shell
  • Install react, react-dom and the eslint react plugin (this is required by n-gage):

    npm install -S react react-dom \
    && npm install -D eslint-plugin-react
  • Create a page-kit-wrapper.js file in the application's /server directory with the following content:

    const React = require('react');
    const ReactDOM = require('react-dom/server');
    const { Shell } = require('@financial-times/dotcom-ui-shell');
    
    module.exports = ({ response, next, shellProps }) => {
        return (error, html) => {
            if (error) {
                return next(error);
            }
    
            const document = React.createElement(
                Shell,
                { ...shellProps, contents: html }
            );
    
            response.send('<!DOCTYPE html>' + ReactDOM.renderToStaticMarkup(document));
        };
    };
  • Integrate the new page-kit-wrapper.js module in the application's controller files.

    • Require the module.
    • Create a shellProps object.
    • Create a pageKitArgs object passing in Express route handler params and shellProps.
    • Pass the shell with its arguments as a third parameter to the response.render() call.
    const pageKitWrapper = require('./server/page-kit-wrapper');
    ...
    const shellProps = {
      pageTitle: 'application-title'
    };
    
    const pageKitArgs = { request, response, next, shellProps };
    ...
    response.render('layout.html', templateData, pageKitWrapper(pageKitArgs));
  • Add any additional properties that your application needs to shellProps alongside pageTitle, e.g. description, openGraph and jsonLd.

  • Build and run the application and check the output in the browser.

    • The expected bootstrap scripts and meta tags should be present in the rendered HTML.
    • The page should have a pink background.
  • Commit your work.

Implement the layout component with navigation data

  • Install the layout component and the navigation middleware:

    npm install -S \
        @financial-times/dotcom-ui-layout \
        @financial-times/dotcom-middleware-navigation
  • Require the navigation middleware in your application's server file and add it to the list of middlewares being used by your application.

    + const navigationMiddleware = require('@financial-times/dotcom-middleware-navigation');
    ...
    app.use(
    +   navigationMiddleware.init()
        ...
    );
  • If your app has subnavigation, add the enableSubNavigation option in the init():

    + navigationMiddleware.init({ enableSubNavigation: true }),
  • Integrate the layout component with your page-kit-wrapper.js module.

    • Require the module.
    • Pass layoutProps to the existing document component.
    • Update the layoutProps object with navigationData and headerOptions.
    • Add layoutProps to the function arguments.
    +const { Layout } = require('@financial-times/dotcom-ui-layout');
    ...
    -module.exports = ({ response, next, shellProps }) => {
    +module.exports = ({ response, next, shellProps, layoutProps }) => {
    +   layoutProps.navigationData = response.locals.navigation;
    +   layoutProps.headerOptions = { ...response.locals.anon};
        ...
        const document = React.createElement(
            Shell,
    -       { ...shellProps, contents: html }
    +       { ...shellProps },
    +       React.createElement(Layout, { ...layoutProps, contents: html })
        );
    };
  • Build and run the application and check the output in the browser.

    • The header, footer and navigation elements should be present in the rendered html.
    • Site skip-links should be present in the rendered html.
  • Commit your work.

Set up Bower package resolution for Page Kit components

  • Install the bower glob resolver package as a devDependency:

    npm install -D bower-glob-resolver
  • Add the new resolver to the application's .bowerrc:

    { "resolvers": ["bower-glob-resolver"] }
  • Use the resolver to add the Page Kit UI components as bower dependencies in the bower.json file:

    "dependencies": {
        ...
    +   "page-kit-ui-components": "glob:node_modules/@financial-times/dotcom-ui-*/bower.json"
    }
  • Initialise the layout component in the application's client-side JS entrypoint:

    import * as pageKitLayout from '@financial-times/dotcom-ui-layout'
    
    pageKitLayout.init();
  • Include the layout component styles in the application's CSS entrypoint (this should be the first line):

    @import '@financial-times/dotcom-ui-layout/styles';
  • Delete and reinstall the bower_components directory.

  • Build the application.

    • If you see errors declaring missing bower modules, these have probably been supplied via n-ui in the past, install them directly in your application now.
    • Delete and reinstall the bower_components directory.
  • Commit your work.

Add the asset loader and provide styles and scripts to the shell

  • Install the assets middleware:

    npm install -S @financial-times/dotcom-middleware-assets
    
  • Integrate the assets middleware in the application's server file.

    • Add the middleware to the list of middlewares being used by your application and configure the hostStaticAssets and publicPath options.

    • N.B. [application-name] (used for the non-isProduction publicPath value) will likely not be prepended with ft- or next-, i.e. product rather than next-product.

      + const assetsMiddleware = require('@financial-times/dotcom-middleware-assets');
      ...
      + const isProduction = process.env.NODE_ENV === 'production';
      ...
      app.use(
      +   assetsMiddleware.init({
      +     hostStaticAssets: !isProduction,
      +     publicPath: isProduction ? '/__assets/hashed/page-kit' : '/__dev/assets/[application-name]'
      +   })
      );
  • Use the assets loader to add scripts and stylesheets properties to the existing shellProps object. (Note: The values for entrypointJS and entrypointCSS can be found in page-kit.config.js)

    const shellProps = {
    +   scripts: response.locals.assets.loader.getScriptURLsFor('scripts'),
    +   stylesheets: response.locals.assets.loader.getStylesheetURLsFor('styles')
        ...
    };
  • Build and run the application and check the output in the browser.

    • The network tab should show the expected requests for script files and stylesheets.
    • The header and footer elements should be styled.
    • The application's client-side behaviour should work. Please note you may find runtime errors with the compiled JS. The most common issues are caused by ESM and Common JS module syntax being mixed up or otherwise used incorrectly. You should fix these issues in a separate commit.
  • Commit your work.

Configure Page Kit JS code splitting

  • Install the code splitting package:

    npm install -D @financial-times/dotcom-build-code-splitting
    
  • Add the code splitting plugin to the list of plugins in page-kit.config.js:

    plugins: [
    +   require('@financial-times/dotcom-build-code-splitting').plugin()
        ...
    ]
  • Build and run the application and check the output in the browser.

    • Do the client-side assets load correctly?
    • Check the /public file, it should contain several separate assets files.
  • Commit your work.

Implement dom-loaded library to safely initialise all client-side JS code

  • Install dom-loaded:
    npm install -D dom-loaded
    
  • Implement dom-loaded in the client-side JS:
    +import domLoaded from 'dom-loaded';
    import * as pageKitLayout from '@financial-times/dotcom-ui-layout';
    
    +domLoaded.then(() => {
        pageKitLayout.init();
        ...
    +});
  • Build and run the application and check the output in the browser.
    • Client-side JS should be running
    • The drawer menu and search bar icons should show/hide those elements.
  • Commit your work.

Integrate flags data with Page Kit shell component

  • Install the flags package:
    npm install -S @financial-times/dotcom-ui-flags
    
  • Implement the flags component in the client-side JS:
    import domLoaded from 'dom-loaded';
    import * as pageKitLayout from '@financial-times/dotcom-ui-layout';
    +import * as pageKitFlags from '@financial-times/dotcom-ui-flags';
    
    domLoaded.then(() => {
    +   const flags = pageKitFlags.init();
    
        pageKitLayout.init();
        ...
    });
  • Add a flags property to shellProps:
    const shellProps = {
    +   flags: response.locals.flags,
        ...
    };
  • Find any commented out uses of flags and reinstate them.
  • Find any code relating to Easter eggs and delete it entirely now or in a separate commit.
  • Build and run the application and check the output in the browser.
    • The flags script should be populated with the relevant flags data.
  • Commit your work.

Integrate Page Kit app context data with the shell component

You may not need this step if app.locals.nUiConfig does not have tracking or ads. You will probably still need it if you have flags.

  • Install the app context packages:
    npm install \
        @financial-times/dotcom-ui-app-context \
        @financial-times/dotcom-middleware-app-context
  • Require the app context middleware and add it to the list of middlewares used in the application server file.
    +const appContextMiddleware = require('@financial-times/dotcom-middleware-app-context');
    ...
    app.use(
    +   appContextMiddleware.init()
        ...
    );
  • Implement the app context component in the client-side JS:
    import domLoaded from 'dom-loaded';
    import * as pageKitLayout from '@financial-times/dotcom-ui-layout';
    import * as pageKitFlags from '@financial-times/dotcom-ui-flags';
    +import * as pageKitAppContext from '@financial-times/dotcom-ui-app-context';
    
    domLoaded.then(() => {
        const flags = pageKitFlags.init();
    +   const appContext = pageKitAppContext.init();
    
        pageKitLayout.init();
        ...
    });
  • Add an appContext property to shellProps:
    const shellProps = {
    +   appContext: response.locals.appContext.getAll(),
        ...
    };
  • Build and run the application and check the output in the browser.
    • The app context script should be populated with the relevant context data.
  • Commit your work.

Implement n-tracking (if the app requires it)

  • Install n-tracking using npm:
    npm install -S @financial-times/n-tracking
  • Add the fallback tracking to the layout component in page-kit-wrapper.js providing that the flag is enabled:
    const { CoreTracking } = require('@financial-times/n-tracking');
    
    ...
    
    const flags = response.locals.flags;
    
    ...
    
    if (flags && flags.oTracking) {
        layoutProps.footerAfter = React.createElement(CoreTracking, {
            appContext: response.locals.appContext.getAll()
        });
    }
  • Initialise n-tracking in the client-side JS. At a minimum, include the app context as an option:
    import domLoaded from 'dom-loaded';
    import * as pageKitLayout from '@financial-times/dotcom-ui-layout';
    import * as pageKitFlags from '@financial-times/dotcom-ui-flags';
    import * as pageKitAppContext from '@financial-times/dotcom-ui-app-context';
    +import * as nTracking from '@financial-times/n-tracking';
    
    domLoaded.then(() => {
        const flags = pageKitFlags.init();
        const appContext = pageKitAppContext.init();
    
        pageKitLayout.init();
    
    +   if (flags.get('oTracking')) {
    +       nTracking.init({ appContext: appContext.getAll() });
    +   }
        ...
    });
  • Find any commented out uses of tracking.
    • Decide which uses of tracking are still being actively used or monitored. Check for any tracking documents and Chartio dashboards or speak to the data team.
    • Reinstate any tracking which has been verified as still in use.
  • Build and run the application and check the output in the browser.
    • A financial-times-o-tracking.bundle.js should have been built.
    • Tracking events to spoor-api.ft.com should be present in the network tab.
    • Compare the contents of a page view event between production and your locally running app.
  • Commit your work.

Implement n-feedback (if the app requires it)

  • Install n-feedback as a Bower dependency:
    bower i -S n-feedback
  • Add the placeholder element to the layout component providing that the flag is enabled:
    if (flags && flags.qualtrics) {
        layoutProps.footerBefore = '<div class="n-feedback__container n-feedback--hidden"></div>';
    }
  • Initialise the client-side JS:
    import domLoaded from 'dom-loaded';
    import * as pageKitLayout from '@financial-times/dotcom-ui-layout';
    import * as pageKitFlags from '@financial-times/dotcom-ui-flags';
    import * as pageKitAppContext from '@financial-times/dotcom-ui-app-context';
    import * as nTracking from '@financial-times/n-tracking';
    +import * as nFeedback from 'n-feedback';
    
    domLoaded.then(() => {
        const flags = pageKitFlags.init();
        const appContext = pageKitAppContext.init();
    
        pageKitLayout.init();
    
        if (flags.get('oTracking')) {
          nTracking.init({ appContext: appContext.getAll() });
        }
    
    +   if (flags.get('qualtrics')) {
    +       // NOTE: n-feedback mutates the configuration object passed to it but
    +       // the app context object is frozen and immutable and must be cloned.
    +       nFeedback.init({ ...appContext.getAll() });
    +   }
        ...
    });
  • Import the n-feedback UI style in the application's client-side styles entrypoint:
     @import 'n-feedback/main';
     // n-feedback depends upon styles from o-overlay, but doesn't explicitly so we have to include o-overlay
     $o-overlay-is-silent: false;
     @import 'o-overlay/main';
  • Build and run the application and check the output in the browser.
    • A teal feedback button should be present in the bottom right corner of the viewport. If you click it, an overlay should show a questionnaire.
  • Commit your work.

Generate Lighthouse reports

NOTE: This is a pre-requisite to merging your pull request to production.

  • Visit https://web.dev/measure.
  • Add the url for your application and generate a report.
  • Generate a .png of the compiled report using native browser tools or the [Full Page Screen Capture extension] (chrome://extensions/?id=fdpohaocaechififmbbbbbknoalclacl).
  • Add a comment to your pull request with the report showing page performance when the application uses n-ui.
  • Also add the report to our shared drive location with a sensible file name.
  • Once your code is merged, repeat these steps with a report showing page performance when the application uses Page Kit.