- Nhan Tran Blog
- Posts
- How to deploy Storybook alongside NextJS
How to deploy Storybook alongside NextJS

Storybook, in my opinion, is an excellent tool to visually document your React components. There is one problem that we were facing: how to deploy Storybook so that our clients can use?
Here are the tech stack that we currently have:
Frontend components is built using Next 12.
Assets (fonts, images) are fetched from a private CMS (in this case, AEM), which requires username/password to authenticate.
Data is fetched as a JSON document from AEM using credentials provided in the Environment Variable.
Possible solutions:
We can either:
Deploy Storybook separately from NextJS, which includes building and running a http-server instance serving the generated static content. This is the way that both Storybook and Vercel recommended (see Publishing Storybook & Deploying Storybook with Vercel).
Build Storybook into NextJS's public folder, which can be served statically alongside NextJS.
Let's examine each solutions in detail.
Deploy Storybook separately:
Pros:
Out-of-the-box support, which means it can be up and running quickly.
Cons:
Having to create a separate Vercel project and add a sub-domain pointing to the new Storybook instance. Which means more stakeholders are getting involved and more infrastructure to manage.
Having to expose the credentials to the FE so that assets can be retrieved from the CMS.
Deploy Storybook alongside NextJS:
Assuming that you have the NextJS deployment pipeline ready, the steps to deploy Storybook are as followed:
Setup storybook's output directory to
public
folder.
// package.json
{
"scripts": {
//...
"build-storybook": "storybook build -o ./public/storybook"
},
}
Change build script to include storybook's build command.
// package.json
{
"scripts": {
//...
"build-storybook": "storybook build -o ./public/storybook",
"build": "npm run build-storybook && npm run build"
},
}
(Optional) Create a storybook page
/storybook
to redirect to/storybook/index.html
. By default, storybook will be builds tostorybook/index.html
. Having a/storybook
fits in better with other NextJS's pages.
// src/pages/storybook.tsx
import {useEffect} from "react";
/** Manually redirect to /storybook/index.html as **rewrites** doesn't work with static storybook pages */
export default function Storybook() {
useEffect(() => {
window.location.href = '/storybook/index.html';
}, [])
return null;
}
Run
npm run build
andnpm run start
. You should be able to navigate tolocalhost:3000/storybook
to view the deployed version of storybook.
Deploying the code to Vercel works...mostly. The assets cannot be fetched even though the Environment Variables are correctly set. We'll talk about this next.
Challenges
There are 2 roadblocks that I faced:
1. NextConfig.rewrites
doesn't work on static pages. This is because the static page sits outside of the NextJS app.
=> A manual redirect using pages/storybook
works wonderfully.
2. Static page cannot pick up environment variables... That's why they are static pages.
=> We'll have to route all the API requests to get assets via a NextJS API, through which we are free to manipulate the requests as we see fit.
To get started, created a catch-all API route (e.g. src/api/content/[[…slug]].ts). This API will catch all the requests started with /content, retrieve the content from our CMS and forward that back to the Frontend.
// src/api/content/[[…slug]].ts
function removeCred(str: string) {
if (!str) return str;
const urlRegex = /https?:\/\/[^ "\s]+/g;
return str.replace(urlRegex, (urlMatch) => {
try {
let url = new URL(urlMatch);
url.username = '';
url.password = '';
return url.toString();
} catch (error) {
return urlMatch;
}
});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
return res.status(405).send('Only GET method is allowed');
}
// Remove **api** from the request URL to convert it back to the original CMS URL.
const url = req.url.replace('/api', '');
const hostNameWithCredentials = process.env.host;
const hostNameWithCredentialsRemoved = removeCred(process.env.host);
const requestURL = new URL(`${hostNameWithCredentialsRemoved }${url}`);
const {username, password} = new URL(hostNameWithCredentials);
const response = await fetch(requestURL, {
headers: {
Authorization: `Basic ${btoa(`${username}:${password}`)}`,
}
});
try {
// Forward the original headers, including Content-Type and Content-Length
const headers = Object.fromEntries(response.headers.entries());
Object.entries(headers).forEach(([key, value]) => {
res.setHeader(key, value);
})
const buffer = await response.arrayBuffer();
const payload = Buffer.from(buffer);
res.status(200).send(payload);
} catch (e) {
console.error(e);
res.status(500).send({error: e});
}
}
export default GET;
Then, prepends /api to all font requests to route them through our newly created API (You may need to use FontFace for this).
// const font_name = new FontFace('Font Name', `url('/content/Fira-Sans.ttf')`);
const font_name = new FontFace('Font Name', `url('/api/content/Fira-Sans.ttf')`);
document.fonts.add(font_name);
That’s it, the 2 issues have been resolved and our storybook is now live alongside our NextJS website!