Micro-frontend client-side composable navigation

Micro-frontend client-side composable navigation

Introduction

In this post I'm going to share my approach to developing a navigation system that can be shared across subdomains. I have an apex domain ivobos.com that hosts a portfolio of projects I am working on. Up until now, the navigation component was copy-pasted from one project to the other, and whenever I add a new project, I have to update the navigation menus on all my projects. This doesn't scale well, so I decided to re-use a single component across the subdomains.

UI design

My original navigation was a horizontal bar containing left aligned menu links, and used the Bootstrap Navbar component.

I really like the way that Apple design their navigation, a simple black band with black text links - the navigation has style and yet it doesn’t steal the show. The menu items are centered, which makes it look more symmetrical and aesthetically pleasing, and gels well with page content that is most likely to be centered too. This is easily implementable using the Bootstrap Base nav

Architecture options

I was reading the Frontend issue of increment.com magazine, and the article Micro-frontends in context provides some ideas about how to compose micro-frontends.

I wanted the navigation to be it's own micro-frontend that can be re-used, and the easiest way to achieve this is to have a horizontal stripe at the top of the screen.

page-ui-micro-frontend-split.png

I considered both server-side and client-side composition.

client-side-vs-server-side-composition.png

Server-side composition could be implemented using a npm package that is included in each subdomain project. This approach is problematic because my projects pages use different tech stacks, one is Kotlin/Gradle, and another is JS/node.

Client-side composition, where the navigation comes from its own subdomain, is a better fit. HTML on project pages loads and executes a React application from the navigation subdomain.

Implementation

The UI component tech stack is: JS, React, Bootstrap CSS, npm

For deployment I used AWS CDK with typescript. npm workspaces keep UI and deployment code separated.

The project is made up of the following workspaces:

├── deploy          // deployment code
├── navsys          // navigation UI component
└── navtst          // subdomain for testing the navigation UI component

The navigtion React code finds a html element with a well known id, and renders the navigation into it with:

ReactDOM.render(
    <div className='container-fluid bg-dark'>
        <header className='d-flex justify-content-center py-0'>
            <ul className="nav">
                {renderNavItem("Blog", BLOG_URL)}
                {renderNavItem("Typing", TYPING_URL)}
                {renderNavItem("Jumpman", JUMPMAN_URL)}
                {renderNavItem("Navtst", NAVTST_URL)}
            </ul>
        </header>
    </div>,
    document.getElementById("navsys")
);

It uses window.location.origin to determine which navigation element is the current one.

DefinePlugin allows configuring different URLs depending on the mode its running in, so during development it uses localhost, and in production it uses actual subdomain.

      new webpack.DefinePlugin({
          "BLOG_URL": JSON.stringify("https://blog.ivobos.com"),
          "TYPING_URL": JSON.stringify(argv.mode === 'development' ? "http://localhost:8081" : "https://typing.ivobos.com"),
          "JUMPMAN_URL": JSON.stringify(argv.mode === 'development' ? "http://localhost:8082" : "https://jumpman.ivobos.com"),
          "NAVTST_URL": JSON.stringify(argv.mode === 'development' ? "http://localhost:8084" : "https://navtst.ivobos.com"),
      })

To host the navigation system, a project page html has to include a div with id="navsys" and the navigation script

<script defer src="<%= htmlWebpackPlugin.options.navsys %>"></script>
...
<body>
    <div id="navsys"></div>

The HtmlWebpackPlugin is configured as

new HtmlWebpackPlugin({
    title: 'NavTst',
    template: 'index.html',
    navsys: argv.mode === 'development' ? 'http://localhost:8083/bundle.js' : 'https://navsys.ivobos.com/bundle.js'
}),

The entry point finds

<div id="navsys"></div>

and renders the navigation into it.

For local development Webpack's configuration is used to pass in different URLs depending on the environment. In development all micro-frontends run on localhost, so different ports are allocated to each. In production each micro-frontend has its own subdomain.

While building the micro-frontend navigation I registered with hashnode.com, which has the capability of hosting your blog using your own domain,

I decided to go with blog.ivobos.com, and to integrate with this hashnode.com hosted subdomain. All I had to do is configure the custom domain in hashnode, and updated my route53 configuration to redirect the apex ivobos.com DNS to blog.ivobos.com. CDK makes this easy:

        new HttpsRedirect(this, 'Redirect', {
            recordNames: ['ivobos.com'],
            targetDomain: 'blog.ivobos.com',
            zone: route53.HostedZone.fromLookup(this, 'Zone', { domainName: 'ivobos.com' })
        });

Conclusion

I am happy with the way this project turned out, the navigation can be modified and deployed independently of the other micro-frontends.