Photo by Kolleen Gladden on Unsplash
I recently created the website for my book "The Art of Micro Frontends". For this page I took a rather conservative approach - making a "true" single page (i.e., landing page) that should be as approachable and fast as possible - without sacrificing developer experience.
Surely, there are right now quite some frameworks and tools out there. But I did not want to spent countless hours learning new stuff just to be blocked by some framework restrictions. Instead, I've chosen an approach that - in my opinion - is quite convenient, super fast, and very lightweight.
The Tech Stack
I've chosen to use react
as library for writing reusable components. In a nutshell, for the page it allows me to have code like the following:
function Content() {
return (
<>
<Header />
<Grid>
<Book />
<Author />
<Buy />
<Outline />
<Reviews />
<Articles />
<Examples />
<Shops />
<Talks />
<Videos />
<Links />
</Grid>
<Footer />
</>
);
}
export default Content;
This is very easy to write, change, and align. As far as styling is concerned I've installed styled-components
. This allows me to have the CSS next to the component where it should be applied. In a nutshell, this makes writing reliable CSS very easy. Also, when I omit (or even throw out) components in the future their CSS won't be part of the output.
For instance, the Grid
component shown above is defined like:
const Grid = styled.div`
display: grid;
grid-column-gap: 1.5rem;
grid-gap: 1.5rem;
grid-row-gap: 0.5rem;
@media only screen and (max-width: 999px) {
grid-template-areas:
'book'
'buy'
'outline'
'author'
'reviews'
'articles'
'talks'
'videos'
'examples'
'shops'
'links';
}
@media only screen and (min-width: 1000px) {
grid-template-areas:
'book author'
'buy buy'
'outline outline'
'reviews reviews'
'articles videos'
'articles examples'
'articles shops'
'talks links';
grid-template-columns: 1fr 1fr;
}
`;
Theoretically, the grid layout could also be computed via JavaScript - just giving the parts that are included (which is another reason why the CSS-in-JS approach is great here). For now, I am happy with the hard-wired layout.
Personally, I always like to have an additional set of checks for my applications, which is why I use the whole thing with TypeScript. TypeScript can also handle JSX quite well, so there is no need for anything else to process the angle brackets.
Dev Setup
For the whole mechanism to work I use a custom build script. The file src/build.tsx
essentially boils down to this:
const root = resolve(__dirname, '..');
const dist = resolve(root, 'dist');
const sheet = new ServerStyleSheet();
const body = renderToStaticMarkup(sheet.collectStyles(<Page />));
const dev = process.env.NODE_ENV === 'debug' ? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1"></' + 'script>')</script>` : '';
const html = `<!DOCTYPE html>
<html lang="en">
<head>
...
${sheet.getStyleTags()}
</head>
<body>${body}${dev}</body>
</html>
`;
sheet.seal();
addAssets(resolve(__dirname, 'static'));
addAsset(Buffer.from(html, 'utf-8'), 'index.html');
writeAssets(dist);
Most importantly, the collectStyles
from styled-components
create the inline stylesheet we'd like to use for this page. The dev
variable keeps a small refresh script that will only be part of the page during local development.
For running the build.tsx
file we use ts-node
. By calling ts-node src/build.tsx
we can start the process. A few other tools that are helpful for making this a great experience are:
- LiveServer for reloading during development (i.e., the script above already uses that)
-
Nodemon for detecting changes during development (i.e., once we touch a file the
ts-node
process should restart) -
HttpServer for running a local webserver during development (i.e., we need to serve the page from somewhere -
http-server dist
is good enough for us)
All these tools can be wired together via concurrently
:
concurrently "livereload dist" "http-server dist" "nodemon"
So when a file changes we have:
-
nodemon
detecting the change and restartingts-node
- The output being placed in
dist
-
livereload
detecting a change indist
and updating the parts that changed
The whole thing is served from http-server
. The configuration for nodemon
looks as follows:
{
"watch": ["src"],
"ext": "ts,tsx,json,png,jpg",
"ignore": ["src/**/*.test.tsx?"],
"exec": "NODE_ENV=debug ts-node ./src/build.tsx"
}
One last remark on the dev setup; for getting the assets in a set of custom Node.js module handlers is used:
function installExtension(ext: string) {
require.extensions[ext] = (module, filename) => {
const content = readFileSync(filename);
const value = createHash('sha1').update(content);
const hash = value.digest('hex').substring(0, 6);
const name = basename(filename).replace(ext, `.${hash}${ext}`);
assets.push([content, name]);
module.exports.default = name;
};
}
extensions.forEach(installExtension);
Each asset will be added to a collection of assets and copied over to the dist
folder. The asset is also represented as a module with a default export in Node.js. This way, we can write code like:
import frontPng from '../assets/front-small.png';
import frontWebp from '../assets/front-small.webp';
without even thinking about it. The assets are all properly hashed and handled by Node.js. No bundler required.
CI/CD
For deploying the page I use GitHub actions. That is quite convenient as the repository is hosted anyway on GitHub.
The whole workflow is placed in the .github/workflows/node.js.yml file. There are two important steps here:
- Build / prepare everything
- Publish everything (right branch is
gh-pages
)
For the first step we use:
- name: Build Website
run: |
npm run build
echo "microfrontends.art" > dist/CNAME
cp dist/index.html dist/404.html
which automatically prepares the custom domain using the special CNAME
file. All the output is placed in the dist
folder. This will be then pushed to the gh-pages
branch.
Likewise, I decided to make a copy of index.html
with the 404.html
file. This file will be served if a user goes to a page that is not there. Such a mechanism is crucial for most SPAs - in this case we'd not really need it, but it's better than the standard GitHub 404 page.
The second step then pushes everything to the gh-pages
branch. For this you can use the gh-pages
tool.
- name: Deploy Website
run: |
git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
npx gh-pages -d "dist" -u "github-actions-bot <support+actions@github.com>"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Importantly, you need to specify the GITHUB_TOKEN
environment variable. This way, the command can actually push code.
Now that's everything for the pipeline - the page can go live and be updated with every push that I make.
Performance
So how does this little page perform? Turns out - quite well. You can go to web.dev/measure to check for yourself.
To get 100 in each column also some tricks need to be applied. For instance, instead of just using something like an img
tag you should use picture
with multiple sources. That was another reason why choosing react
was quite good:
interface ImageProps {
source: string;
fallback: string;
alt?: string;
width?: number;
height?: number;
}
function getType(file: string) {
return `image/${file.substring(file.lastIndexOf('.') + 1)}`;
}
function Image({ source, fallback, alt, width, height }: ImageProps) {
return (
<picture>
<source srcSet={source} type={getType(source)} />
<source srcSet={fallback} type={getType(fallback)} />
<img src={fallback} alt={alt} width={width} height={height} />
</picture>
);
}
export default Image;
With this little component we can write code like
<Image
source={frontWebp}
fallback={frontPng}
alt="The Art of Micro Frontends Book Cover"
width={250}
height={371}
/>
which will be applied just as mentioned. Also, quite importantly we specify the width and height of the image. In theory, we could also compute that on the fly when rendering - but as the page only has 3 images it really was not worth the effort.
Conclusion
Writing simple sites does not need to be complicated. You don't need to learn a lot of new stuff. Actually, what is there already will be sufficient most of the time.
The page I've shown easily gets the best score and performance - after all its the most minimal package delivered with - for what it does - the optimal dev experience.
The code for the page can be found on GitHub.
Top comments (7)
I understand the convenience of using react for such simple websites, because we're so used to writing JSX. Plus, it is admirable to render the react app to static HTML with no hydration in the client. Effectively, you're using JSX as a plain old templating language.
Yet, when I saw the simple website, I couldn't help to think of a proverb: "mit Kanonen auf Spatzen schießen"
Could the same website have been build with a single index.html, a style.css and assets on the side? Without computation, and all the dependencies?
Sure. But that does not scale - even for a page of this simplicity. If you just look at the HTML unminified you see it's already quite lengthy.
The picture also does show why this is not only convenient, but actually very efficient. The CSS part would not scale, too. It would be a separate sheet (not good) that just contains everything unminified (not good). The approach shown here only takes what I actually use.
If you think this is "mit Kanonen auf Spatzen schießen" then I think you've not seen any pages with Next.js, Gatsby, or others.
True, compared to Next.js and Gatsby, this is a dream :D
what happend if you add Google Analytics or another tools for production website xD?
Why would I want to do that? Not only would it be bad for perf and bad for legal issues (a consent dialog would be required), it would also add no value.
If you would really need to add such third-party scripts I'd recommend adding partytown (see partytown.builder.io/). Keeps everything minimal and at 100.
aaaa yes i forget partytown .... but i mean ...I was just suggesting a real world example. for the article, it is excellent.
Look like porridge from hatchet
Some comments have been hidden by the post's author - find out more