An Idiosyncratic Blog

πŸ“š Deploy Storybook to a sub-folder

Published on
β€’ 8 minutes read

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

Quick browsing on the internet, and I found some threads with potential solutions:

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.

<!DOCTYPE html>
<html lang="en">
    <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="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" />

      #storybook-root[hidden] {
        display: none !important;
    <div id="root"></div>

      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 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'

    <link href="./sb-preview/runtime.js" rel="prefetch" as="script" />

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 But with base, it would be resolved to

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!