Hopp til hovedinnhold

Finn.no, the marketplace of possibilities, might be Norway's biggest sale platform.
They also happen to publish a lot of the tools they create and use as open source tools, such as the feature toggle service Unleash.
Let's try to recreate their own page with their tools! πŸ™Œ
Since Finn.no wasn't built in a day, we will restrict ourselves to the frontpage.
Also, to make the article readable in a sensible amount of time, I will not go through the entire architecture, but reading this tutorial should give you the necessary means to build it all yourself.

All the code I wrote here is pushed to my try-finn-no Github repo, feel free to check it out if you want to copy-paste or believe I made a typo here.

Finn.no's architecture: Podium

Finn.no consists of many small frontends, known as a microfrontend architecture.
To orchestrate these frontends and sew them together, the developers at Finn.no created Podium.
Podium composes the different frontends into a single page server side.

Podium defines the "maestro" in charge of composition as a Layout. Each microfrontend is called a Podlet.
So first, let's create a Podlet.
The Podium documentation has an illustration of which part of their page is a separate frontend. This illustration is however outdated, but trustable sources have verified that the header is still it's own Podlet.

Header as a React App βš›οΈ

I will recreate the header in React with TypeScript, but Podium claims to be framework agnostic as long as the result is HTML, CSS and JS, so you are free to use anything you'd like πŸ’ͺ

To create our new app, simply run:
npx create-react-app header --template typescript

Taking a quick glimpse at Finn.no, we see that the header consists of 5 elements:

  • Logo
  • Notifications ("Varsler")
  • New ad ("Ny annonse")
  • Messages ("Meldinger")
  • Login ("Logg inn")

Except for the logo, all the other elements consists of an icon and a text, which is a hyperlink.
We can then create a simple component for these links.

// header/src/LinkButton.tsx
import { PropsWithChildren } from "react";

type Props = PropsWithChildren<{
  text: string;
  url?: string;
}>;

function LinkButton({ text, url, children }: Props) {
  return (
    <a
      href={url}
      style={{
        display: "flex",
        flexDirection: "row",
        alignItems: "center",
        textDecoration: "none",
        marginLeft: "1em",
      }}
    >
      {children}
      <p style={{ marginLeft: "5px" }}>{text}</p>
    </a>
  );
}

export default LinkButton;

We now have a clickable link, with all content being clickable.
However, we still need the most important part: the icons πŸ§‘β€πŸŽ¨
Luckily for us, Finn.no's icons are also available through their design system, Fabric.
Let's install the icons in the header-project.
npm install @fabric-ds/icons

We can now use these icons in our LinkButton.
However, since Fabric's React module is still in a beta state, we will have to import the svg's directly as they are currently only exported as Vue Components.
Since I am using Create React App, I import the svg as ReactComponent.

import { ReactComponent as Bell } from "@fabric-ds/icons/dist/32/bell.svg";

export function Varslinger() {
  return (
    <LinkButton text="Varslinger">
      <Bell />
    </LinkButton>
  );
}

Now, repeat this for "Ny annonse", "Meldinger" and "Logg inn", with the icons "circle-plus", "messages" and "circle-user".
You can find an overview of all the available icons at Finn's icon overview page.

PS: You might encounter a TypeScript error related to SVG types here.
Cannot find module '@fabric-ds/icons/dist/32/bell.svg' or its corresponding type declarations
This means that you probably ran npm install @fabric-ds/icons in the wrong folder - SVGs with TypeScript should just workℒ️ in Create React App πŸ™

Now, with a little bit of styling and combining, we have our header 🎨

// header/src/App.tsx
import { Meldinger, NyAnnonse, Varslinger } from "./components/LinkButton";
import { ReactComponent as Logo } from "./logo.svg";

function App() {
  return (
    <header
      style={{
        margin: "auto 20%",
        display: "flex",
        justifyContent: "space-between",
      }}
    >
      <div style={{ display: "flex", alignItems: "center" }}>
        <Logo
          style={{ display: "inline", width: "7em", marginRight: "15px" }}
        />
        <p style={{ fontWeight: "bold" }}>Mulighetens marked</p>
      </div>
      <div style={{ display: "flex", flexDirection: "row" }}>
        <Varslinger />
        <NyAnnonse />
        <Meldinger />
        <LoggInn />
      </div>
    </header>
  );
}

export default App;

If you run Create React Apps development server with npm start, you should now see the header at localhost:3000!

Finn header recreated in React

If you want to get the logo correct as well, you can sneak over to Finn.no and replace the content of our src/logo.svg with the svg of their logo.

Layout 🎨

To serve our webpage, we will need a "maestro", a Layout server.
Podium's docs cover this step very well, but I'll repeat it here for you anyways.

First, initialize a new project. It doesn't really matter what you name it, and we can just go ahead with all the defaults ✨

mkdir layout-server
cd layout-server
npm init

Next, we add our Podiums dependencies.
npm install express @podium/layout
and lets add the types for Express as well.
npm install --save-dev @types/express

We can then go ahead by creating our index.ts and instantiate a new Express app.

// layout-server/index.ts
import express from "express";
import Layout from "@podium/layout";

const app = express()

This is still just a regular Express app, so let's initialize our layout as well.

Then we add the Podium layout middleware to the Express app, which does its magic and adds data to our responses πŸͺ„

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

app.use(layout.middleware());

All that is left is to configure our default endpoint and set the port to listen to.

app.get("/", async (req, res) => {
  const incoming = res.locals.podium;
  incoming.view.title = "My Finn.no Frontpage";

  res.podiumSend(`<div>Bonjour</div>`);
});

app.listen(7000);

All summarized, our index.ts should look something like this:

// index.ts
import express from "express";
import Layout from "@podium/layout";

const app = express();

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

app.use(layout.middleware());

app.get("/", async (req, res) => {
  const incoming = res.locals.podium;
  incoming.view.title = "My Finn.no Frontpage";

  res.podiumSend(`<div>Bonjour</div>`);
});

app.listen(7000);

To run our Layout server easily, we can use nodemon. Since we're using TypeScript, we will need ts-node as well, which nodemon will use under the hood.
npm install --save-dev nodemon ts-node.
We also have to add a start script to our package.json.

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "nodemon index.ts"
}

Start our Layout server with npm start and you should see our beautiful "Bonjour" at localhost:7000 πŸ‘‹

Webpage displaying "Bonjour"

Last I checked however, Finn.no is more than just a white page stating "Bonjour", so lets move on to serving our Header React App as a Podlet!

Header as a Podlet

To serve our Podlet, we could in theory include a static manifest.json-file with the necessary information the Layout server needs to get the content.
However, Podium also includes a module like the layout-module which helps us a lot in creating and serving our Podlet.
npm install express @podium/podlet.

We can then go ahead and create a new file in our header-project called podletServer.js.
I would have preferred to use TypeScript here as well, but I took a shortcut by using JavaScript so that I do not need to edit my tsconfig.json to properly compile the podlet server outside of the rest of the project.
We will use a Express app to serve our Podlet just like our Layout server, but with the Podlet module instead.

// header/podletServer.js
const express = require("express");
const Podlet = require("@podium/podlet");

const app = express();

const podlet = new Podlet({
  name: "headerPodlet",
  version: "0.1.0",
  pathname: "/",
  development: true,
});

This looks fairly similar to our Layout-server πŸ”Ž
We also have to tell Express to serve our build-folder as static files, to actually serve the bundled output of Create React Apps npm run build.

app.use(express.static("build"));
app.use(podlet.middleware());

The last thing we need is to add the two paths the Layout server expects: the path to our content and the path to the manifest.
We can get these paths automatically from the Podlet.
To generate the manifest, we can simply just send the podlet as a response, and it returns itself as a manifest.

app.get(podlet.manifest(), (req, res) => res.status(200).send(podlet));

The content fetch will only get the HTML-file, so we have to register the js and css output of our build as well.
These are currently dynamically named on build with a hash based on the build timestamp through Create React Apps included Webpack configuration.
We thus have to make changes to our webpack config βš™οΈ
To make it easy however, let's not eject our app entirely.
Instead, I'll use Craco to inject our changes in the Create React Apps default config.
npm install @craco/craco --save
Next, we add our Craco config πŸ”§
We could write these changes ourself, but NAV has created a small plugin set exactly for this.
I'll take a shortcut and use these tools, but feel free to take a look at the code itself. It is only 92 lines.
npm i @navikt/craco-plugins

// header/craco.config.js
const {
  ChangeJsFileName,
  ChangeCssFilename,
} = require("@navikt/craco-plugins");

module.exports = {
  plugins: [
    {
      plugin: ChangeJsFilename,
      options: {
        filename: "bundle.js",
        runtimeChunk: false,
        splitChunk: "NO_CHUNKING",
      },
    },
    {
      plugin: ChangeCssFilename,
      options: {
        filename: "bundle.css",
      },
    },
  ],
};

Finally, we replace our start & build scripts using react-scripts in our package.json with craco.

- "start": "react-scripts start",
+ "start": "craco start",
- "build": "react-scripts build",
+ "build": "craco build"

With our new static bundle names, we can easily register the build output with our Podlet.

podlet.js({ value: "/bundle.js" });
podlet.css({ value: "/bundle.css" });

Last, all we need is to tell the Express app which port it should listen to.
All together, the Header Podlet will look something like this:

const express = require("express");
const Podlet = require("@podium/podlet");

const app = express();

const podlet = new Podlet({
  name: "headerPodlet",
  version: "0.1.0",
  pathname: "/",
  development: true,
});

app.use(express.static("build"));
app.use(podlet.middleware());

podlet.js({ value: "/bundle.js" });
podlet.css({ value: "/bundle.css" });

app.get(podlet.manifest(), (req, res) => res.status(200).send(podlet));

app.listen(7100);

Let us install nodemon to run our Podlet server. Note that we don't need ts-node now, since our podletServer is written in JavaScript and not TypeScript.
npm install --save-dev nodemon

Also, let's add a script to run our Podlet server in our package.json as well.
"start:podlet": "nodemon podletServer.js"

If we start our Podlet, npm run start:podlet, we should now see our header server through Express by visiting localhost:7100.

Finn header on new port


We can also check the manifest at localhost:7100/manifest.json.
Here we can see that the manifest defined by Create React App for webworkers is prioritized before our Express-path.
Since we do not use webworkers in this article, you can go ahead and delete the manifest.json in the public-folder so that our dynamically generated manifest will be shown.
Rebuilding the app, npm run build and possibly restarting the Podlet server, npm run start:podlet, should now show us the correct manifest at localhost:7100/manifest.json, which should look more or less like this:

{
  "name": "headerPodlet",
  "version": "0.1.0",
  "content": "/",
  "fallback": "",
  "assets": { "js": "/bundle.js", "css": "/bundle.css" },
  "css": [{ "value": "/bundle.css", "type": "text/css", "rel": "stylesheet" }],
  "js": [{ "value": "/bundle.js", "type": "default" }],
  "proxy": {}
}

It all comes together 🧡

We can now register our Podlet with the Layout server.
Open index.ts in our layout-server-folder.
Then, register the Podlet through the layout.client.register-method.

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

Now we can change our output from simply "Bonjour" to our header.
First, fetch the registered Podlet and pass it some context named incoming which we get from the Layout middleware.
Add the podlets to our incoming object. This magically adds all CSS and JS to our <head>.
Last, use the content in our HTML response.
Also, React expects to be loaded after the DOM tree is finished, so let's load it to the end of our DOM by using the built-in toHTML-method.

app.get("/", async (req, res) => {
  const incoming = res.locals.podium;
  const headerResponse = await headerPodlet.fetch(incoming);

  incoming.podlets = [headerResponse];
  incoming.view.title = "My Finn.no Frontpage";

  const js = headerResponse.js[0].value;

  res.podiumSend(`<div><header>${headerResponse}</header>${js.toHTML()}</div>`);
});

All in all, our final Layout server looks like this:

// layout-server/index.ts
import express from "express";
import Layout from "@podium/layout";

const app = express();

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

app.use(layout.middleware());

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

app.get("/", async (req, res) => {
  const incoming = res.locals.podium;
  const headerResponse = await headerPodlet.fetch(incoming);

  incoming.podlets = [headerResponse];
  incoming.view.title = "My Finn.no Frontpage";

  const js = headerResponse.js[0].value;

  res.podiumSend(`<div><header>${headerResponse}</header>${js.toHTML()}</div>`);
});

Let's spin it up all together!
Start our Podlet server if it is not already running through npm run start:podlet in the header-project.
Then, run our Layout server with npm start and ...πŸ₯

Finn header in layout server

Et voila!
The Header Podlet is displayed through our Layout server!

We now have the structure for the Finn.no.
Simply add the rest of the owl and you've got our own Finn.no competitor!

how to draw an owl

Finally, I'd like to thank PΓ₯l-Edward Larsen, developer at Finn.no for answering my questions whenever I failed to understand the docs or simply turned blind from reading the docs repeatedly. πŸŽ‰

FAQ

How do you have frequently asked questions on a newly posted article?

I lied, I don't 🀷 I just predict what you might want to know after reading all this.

Are we not loading our JS bundle twice in the Layout server?

Yes, we are πŸ”Ž The keen reader might have noticed that the Podlet JS bundle will be loaded twice, one in the body and one in the head.
There are multiple ways to avoid that, either by editing the document template of the layout server or by stripping it from the response before we add it the Podlets array incoming.podlets.
I did not include it in the article to keep it short and to the point, but feel free to change it in your Finn.no frontpage!

Can I just copy and paste the header-project many times and plug them into the Layout-server to have a full webpage?

Almost πŸ™Œ In our header-project, we use the default DOM-id root. This will obviously not work if multiple projects use root.
Thus, we need to set this for each app. This can easily be done through changing the id in public/index.html as well as in src/index.tsx's line document.getElementById('root'). It would probably also be smart to use unique bundle names. You can see an example on my multiple-podlets-branch on the try-finn-no-repo.
It will probably look a bit stupid though, I would suggest you change the content a bit πŸ€”

finn with 2 headers

Did you like the post?

Feel free to share it with friends and colleagues