diff --git a/README.md b/README.md index 3c985b1..bad99a2 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,43 @@ pnpm create mbga | [`@mbga/flashnet`](./packages/flashnet) | Flashnet authentication and orchestration | | [`create-mbga`](./packages/create-mbga) | Scaffold a new MBGA project | +## Docs Deployment + +The docs site and Storybook previews are deployed as two separate Vercel projects. + +### Local Docs Workflow + +Run the docs site with embedded local Storybook assets: + +```bash +pnpm docs:dev +``` + +This builds the workspace packages, generates the `@mbga/kit` Storybook bundle, copies it into `site/public/storybook`, and starts the Vocs site with `VITE_STORYBOOK_BASE_URL=/storybook`. + +### Vercel Projects + +#### Site project + +- Root directory: `site` +- Build command: `pnpm build` +- Output directory: `dist` +- Environment variable: `VITE_STORYBOOK_BASE_URL=https://` + +#### Storybook project + +- Root directory: repository root +- Framework preset: `Other` +- Install command: `pnpm install --frozen-lockfile` +- Build command: `pnpm storybook:build:deploy` +- Output directory: `packages/kit/storybook-static` + +### Rollout Order + +1. Deploy the Storybook project and capture its stable URL. +2. Set `VITE_STORYBOOK_BASE_URL` on the site project for preview and production. +3. Redeploy the site project so embedded previews and "Open in Storybook" links point to the external Storybook deployment. + ## Community - [GitHub Discussions](https://github.com/refrakts/mbga/discussions) — ask questions and share ideas diff --git a/package.json b/package.json index 6a5142f..3ca8598 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,11 @@ "clean": "pnpm run --r --parallel clean", "storybook": "pnpm --filter @mbga/kit storybook", "storybook:build": "pnpm --filter @mbga/kit storybook:build", + "storybook:build:deploy": "pnpm build && pnpm --filter @mbga/kit storybook:build", + "storybook:sync-static": "pnpm storybook:build:deploy && rm -rf site/public/storybook && cp -r packages/kit/storybook-static site/public/storybook", "docs:typedoc": "typedoc", - "docs:dev": "pnpm storybook:build && rm -rf site/public/storybook && cp -r packages/kit/storybook-static site/public/storybook && pnpm --filter site dev", - "docs:build": "pnpm storybook:build && rm -rf site/public/storybook && cp -r packages/kit/storybook-static site/public/storybook && pnpm --filter site build", + "docs:dev": "VITE_STORYBOOK_BASE_URL=/storybook pnpm storybook:sync-static && VITE_STORYBOOK_BASE_URL=/storybook pnpm --filter site dev", + "docs:build": "VITE_STORYBOOK_BASE_URL=/storybook pnpm storybook:sync-static && VITE_STORYBOOK_BASE_URL=/storybook pnpm --filter site build", "docs:preview": "pnpm --filter site preview", "format": "biome format --write", "test": "vitest", diff --git a/site/lib/storybook.test.ts b/site/lib/storybook.test.ts new file mode 100644 index 0000000..d2622f4 --- /dev/null +++ b/site/lib/storybook.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' + +import { + createStorybookIframeUrl, + createStorybookManagerUrl, + normalizeStorybookBaseUrl, + resolveStorybookUrls, +} from './storybook' + +describe('storybook helpers', () => { + it('trims trailing slashes from the base URL', () => { + expect(normalizeStorybookBaseUrl('https://storybook.mbga.dev///')).toBe( + 'https://storybook.mbga.dev', + ) + }) + + it('builds iframe URLs for embedded story previews', () => { + expect( + createStorybookIframeUrl( + 'https://storybook.mbga.dev/', + 'components-accountmodal--default', + ), + ).toBe( + 'https://storybook.mbga.dev/iframe.html?id=components-accountmodal--default&viewMode=story', + ) + }) + + it('builds manager URLs for full Storybook pages', () => { + expect( + createStorybookManagerUrl( + 'https://storybook.mbga.dev/', + 'components-connectbutton--connected', + ), + ).toBe( + 'https://storybook.mbga.dev/?path=/story/components-connectbutton--connected', + ) + }) + + it('returns null when the Storybook base URL is missing', () => { + expect( + resolveStorybookUrls(undefined, 'components-connectmodal--default'), + ).toBeNull() + expect( + resolveStorybookUrls(' ', 'components-connectmodal--default'), + ).toBeNull() + }) +}) diff --git a/site/lib/storybook.ts b/site/lib/storybook.ts new file mode 100644 index 0000000..0e96080 --- /dev/null +++ b/site/lib/storybook.ts @@ -0,0 +1,36 @@ +export type StorybookUrls = { + iframeSrc: string + managerHref: string +} + +export function normalizeStorybookBaseUrl(baseUrl: string) { + const trimmedBaseUrl = baseUrl.trim() + + if (trimmedBaseUrl === '/') return '' + + return trimmedBaseUrl.replace(/\/+$/, '') +} + +export function createStorybookIframeUrl(baseUrl: string, id: string) { + const normalizedBaseUrl = normalizeStorybookBaseUrl(baseUrl) + + return `${normalizedBaseUrl}/iframe.html?id=${id}&viewMode=story` +} + +export function createStorybookManagerUrl(baseUrl: string, id: string) { + const normalizedBaseUrl = normalizeStorybookBaseUrl(baseUrl) + + return `${normalizedBaseUrl}/?path=/story/${id}` +} + +export function resolveStorybookUrls( + baseUrl: string | undefined, + id: string, +): StorybookUrls | null { + if (!baseUrl?.trim()) return null + + return { + iframeSrc: createStorybookIframeUrl(baseUrl, id), + managerHref: createStorybookManagerUrl(baseUrl, id), + } +} diff --git a/site/pages/components/StorybookEmbed.tsx b/site/pages/components/StorybookEmbed.tsx index 08d1aed..0b4d73d 100644 --- a/site/pages/components/StorybookEmbed.tsx +++ b/site/pages/components/StorybookEmbed.tsx @@ -1,5 +1,7 @@ 'use client' +import { resolveStorybookUrls } from '../../lib/storybook' + export function StorybookEmbed({ id, height = 200, @@ -7,18 +9,58 @@ export function StorybookEmbed({ id: string height?: number }) { + const storybookUrls = resolveStorybookUrls( + import.meta.env.VITE_STORYBOOK_BASE_URL, + id, + ) + + if (!storybookUrls) { + return ( +
+ + Storybook preview unavailable + + + Set VITE_STORYBOOK_BASE_URL to enable embedded component + previews. + +
+ ) + } + return ( -