π Deploy Storybook to a sub-folder
After years of following the development of Storybook, I finally got the opportunity to integrate it to one of my projects. I was working on a Next.js app, and Storybook was a perfect fit for building and showcasing the components library.
The implementation of Storybook was pretty great. All I had to was run npx sb init
, and it was ready to go. I added a few add-ons, and it was ready to be deployed.
Now the problems started when I tried to deploy it to a sub-folder. I was using AWS S3 to host the Storybook, and I wanted to deploy it to a sub-folder. I wanted the Storybook to be available at https://example.com/storybook
.
Quick browsing on the internet, and I found some threads with potential solutions:
- https://github.com/storybookjs/storybook/discussions/17433
- https://github.com/storybookjs/storybook/issues/1291#issuecomment-1331610860
Most of them recommended doing:
module.exports = {
webpackFinal: async (config, { configType }) => {
config.output.publicPath = '/storybook/'
return config
},
managerWebpack: async (config) => {
config.output.publicPath = '/storybook/'
return config
},
}
Unfortunately, none of them worked for me. I was still getting 404s for the static assets. I tried a few other things, but nothing worked.
To understand why this was an issue, we must first understand how Storybook references the static files. When you run storybook build
, it generates a static build in the storybook-static
folder. This folder contains all the static files required to run the Storybook.
When you open the index.html
file, you will see that it references the static files using relative paths.
index.html
file, you will see that it references the static files using relative paths.<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@storybook/cli - Storybook</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link
rel="prefetch"
href="./sb-common-assets/nunito-sans-regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="prefetch"
href="./sb-common-assets/nunito-sans-bold.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="stylesheet" href="./sb-common-assets/fonts.css" />
<link href="./sb-manager/runtime.js" rel="modulepreload" />
<link href="./sb-addons/links-0/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-controls-1/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-actions-2/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-backgrounds-3/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-viewport-4/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-toolbars-5/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-measure-6/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/essentials-outline-7/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/interactions-8/manager-bundle.js" rel="modulepreload" />
<link href="./sb-addons/storybook-9/manager-bundle.js" rel="modulepreload" />
<style>
#storybook-root[hidden] {
display: none !important;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
window['FEATURES'] = {
warnOnLegacyHierarchySeparator: true,
buildStoriesJson: false,
storyStoreV7: true,
argTypeTargetsV7: true,
legacyDecoratorFileOrder: false,
}
window['REFS'] = {}
window['LOGLEVEL'] = 'verbose'
window['DOCS_OPTIONS'] = {
defaultName: 'Docs',
autodocs: 'tag',
}
window['CONFIG_TYPE'] = 'PRODUCTION'
</script>
<script type="module">
import './sb-manager/runtime.js'
import './sb-addons/links-0/manager-bundle.js'
import './sb-addons/essentials-controls-1/manager-bundle.js'
import './sb-addons/essentials-actions-2/manager-bundle.js'
import './sb-addons/essentials-backgrounds-3/manager-bundle.js'
import './sb-addons/essentials-viewport-4/manager-bundle.js'
import './sb-addons/essentials-toolbars-5/manager-bundle.js'
import './sb-addons/essentials-measure-6/manager-bundle.js'
import './sb-addons/essentials-outline-7/manager-bundle.js'
import './sb-addons/interactions-8/manager-bundle.js'
import './sb-addons/storybook-9/manager-bundle.js'
</script>
<link href="./sb-preview/runtime.js" rel="prefetch" as="script" />
</body>
</html>
When you want to deploy to a sub-folder the most common way to specify the folder path is using the base
tag as the first tag in the head
of your html. This tag specifies the base URL for all relative URLs in a document.
So, without base
, ./sb-manager/runtime.js
would be resolved to https://example.com/sb-manager/runtime.js
. But with base
, it would be resolved to https://example.com/storybook/sb-manager/runtime.js
.
You'd ask why not use the managerHead
config and set the base
tag there? Valid question! Unfortunately that did now work for me because the base
tag was being appended after all the link
tags, which meant that the link
tags were still pointing to the wrong path.
According to MDN docs,
this element must come before other elements with attribute values of URLs, such as <link>
's href attribute. - The Document Base URL element
So in order to get this to work, I had to manually add the base
tag to the index.html
file in the storybook-static
folder. I added a post build
script to my package.json
to do this.
{
"scripts": {
"build-storybook": "storybook build --disable-telemetry",
"postbuild-storybook": "sed -i 's#<head>#<head>\\n<base href='/storybook/'>#' ./storybook-static/index.html"
}
}
With this, the base
tag was added after the head
, and all link
tags were referenced correctly.
I'm pretty bummed out that I had to write this "hack" to get something as simple as a sub-folder deployment to work. I'm sure there's a better way to do this, but I couldn't find it. If you know of a better way, please let me know!