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.
9 min read
ยท
By Johannes Kvamme
ยท
December 15, 2021
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 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.
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:
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
!
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.
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
๐
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!
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
.
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": {}
}
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 ...๐ฅ
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!
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. ๐
I lied, I don't ๐คท I just predict what you might want to know after reading all this.
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!
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 ๐ค