@@ -97,6 +97,14 @@ export default defineNuxtModule<NuxtStoriesOptions>({
9797 const routeBasePath = joinURL ( '/' , options . route ?. path || '' )
9898 const frameBasePath = routeBasePath === '/' ? '/-frame' : routeBasePath + '-frame'
9999
100+ // In a two-pass static build (GitHub Actions) the frame is generated with
101+ // NUXT_APP_BASE_URL=/base/-frame/ so the iframe-facing /-frame/* path is already
102+ // encoded in the base URL. Routes must therefore live at / (not /-frame/*) so
103+ // that the generated files don't get an extra /-frame/ prefix that would make
104+ // them double-nested after the merge step.
105+ const staticFrameMode = mode === 'frame' && ! nuxt . options . dev
106+ const effectiveFrameBasePath = staticFrameMode ? '/' : frameBasePath
107+
100108 // In shell mode with a separate frame process the iframe points to the frame server URL.
101109 // When frameCwd is not set or in other modes the iframe uses same-origin routes.
102110 const frameBaseUrl = mode === 'shell' && nuxt . options . dev && options . frameCwd
@@ -233,7 +241,7 @@ export default defineNuxtModule<NuxtStoriesOptions>({
233241 name : 'stories-frame' ,
234242 file : resolver . resolve ( './runtime/components/StoryFramePage.vue' ) ,
235243 meta : { layout : false } ,
236- path : joinURL ( frameBasePath , '/:story*' ) ,
244+ path : joinURL ( effectiveFrameBasePath , '/:story*' ) ,
237245 children : frameChildren ,
238246 }
239247
@@ -286,9 +294,10 @@ export default defineNuxtModule<NuxtStoriesOptions>({
286294 // Shell mode: only registers shell routes (reads story file paths for nav but renders via iframe)
287295 // Frame mode: only registers frame routes (renders actual story components)
288296 // All mode: both (single-process, backward-compatible)
289- extendPages ( async ( pages ) => {
290- const storyPaths : string [ ] = [ ]
297+ // Shared story paths — populated in extendPages, consumed in nitro:config
298+ const storyPaths : string [ ] = [ ]
291299
300+ extendPages ( async ( pages ) => {
292301 if ( mode === 'shell' ) {
293302 pages . length = 0 // clear existing routes so only the shell route is registered at top level
294303 }
@@ -319,18 +328,6 @@ export default defineNuxtModule<NuxtStoriesOptions>({
319328
320329 if ( mode === 'shell' || mode === 'all' ) pages . push ( shellRoute )
321330 if ( mode === 'frame' || mode === 'all' ) pages . push ( frameRoute )
322-
323- // Register all story paths for static generation (nuxi generate)
324- nuxt . options . nitro . prerender ||= { }
325- nuxt . options . nitro . prerender . routes = [
326- ...( nuxt . options . nitro . prerender . routes as string [ ] ?? [ ] ) ,
327- ...( mode === 'shell' || mode === 'all'
328- ? storyPaths . map ( ( p ) => joinURL ( routeBasePath , p ) )
329- : [ ] ) ,
330- ...( mode === 'frame' || mode === 'all'
331- ? storyPaths . map ( ( p ) => joinURL ( frameBasePath , p ) )
332- : [ ] ) ,
333- ]
334331 } )
335332
336333 // WATCH (dev only)
@@ -351,6 +348,22 @@ export default defineNuxtModule<NuxtStoriesOptions>({
351348
352349 // NITRO CONFIG — serve the module's static public assets (stories.css, etc.)
353350 nuxt . hook ( 'nitro:config' , ( nitroConfig ) => {
351+ // Register story paths for static generation (nuxi generate).
352+ // Done here (not inside extendPages) to guarantee the routes are visible
353+ // to Nitro — pages:extend is awaited before nitro:config fires.
354+ if ( storyPaths . length > 0 ) {
355+ nitroConfig . prerender ||= { }
356+ nitroConfig . prerender . routes = [
357+ ...( nitroConfig . prerender . routes ?? [ ] ) ,
358+ ...( mode === 'shell' || mode === 'all'
359+ ? storyPaths . map ( ( p ) => joinURL ( routeBasePath , p ) )
360+ : [ ] ) ,
361+ ...( mode === 'frame' || mode === 'all'
362+ ? storyPaths . map ( ( p ) => joinURL ( effectiveFrameBasePath , p ) )
363+ : [ ] ) ,
364+ ]
365+ }
366+
354367 nitroConfig . publicAssets ||= [ ]
355368 nitroConfig . publicAssets . push ( {
356369 dir : resolver . resolve ( './runtime/public' ) ,
0 commit comments