N
Nuxt3d ago
jakub

Hybrid rendering with static hosting

Hello 👋 I'm struggling to make the following setup work the way I envision and would appreciate it if someone with more experience could point me to the right direction. I have a statically hosted application on Amazon S3 and CloudFront. All pages are client-side rendered except for the landing page - because of SEO. This is my routeRules configuration from nuxt.config.ts.:
# I don't have the 'ssr' property explicitly configured

routeRules: {
"/": { prerender: true },
"/rules": { ssr: false },
"/profile": { ssr: false },
"/login": { ssr: false },
"/dashboard": { ssr: false },
"/table": { ssr: false },
},
# I don't have the 'ssr' property explicitly configured

routeRules: {
"/": { prerender: true },
"/rules": { ssr: false },
"/profile": { ssr: false },
"/login": { ssr: false },
"/dashboard": { ssr: false },
"/table": { ssr: false },
},
For some reason, I can't directly navigate to specific pages like /login (not client-side navigation, that works fine). When I try to, I'm automatically redirected to the / (index) page. The only way to navigate around the application is with client-side navigation. However, running npm run dev or npm run generate && npm run serve works as expected. If I try to navigate directly to the /login page, the spa-loading-template.html is displayed for a couple of milliseconds and then the pages loads. In CloudFront, I tried serving both index.html and 200.html pages but without any success. What's the expected behavior with my setup? Is there a way to pre-render the landing (index) page and serve it from the root, while leaving the rest of the application be client-side rendered? Thanks!
18 Replies
kapa.ai
kapa.ai3d ago
Hi @jakub. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Your setup and expectations are correct: you want the landing page (/) to be pre-rendered for SEO, while all other routes are client-side rendered. Your routeRules configuration is appropriate for this:
routeRules: {
"/": { prerender: true },
"/rules": { ssr: false },
"/profile": { ssr: false },
"/login": { ssr: false },
"/dashboard": { ssr: false },
"/table": { ssr: false },
}
routeRules: {
"/": { prerender: true },
"/rules": { ssr: false },
"/profile": { ssr: false },
"/login": { ssr: false },
"/dashboard": { ssr: false },
"/table": { ssr: false },
}
However, the issue you're experiencing—where direct navigation to routes like /login redirects to /—is not caused by Nuxt, but by how static hosting (like S3 + CloudFront) handles unknown routes. Why this happens:
When you deploy a statically generated Nuxt app, only the routes that are pre-rendered (i.e., have an HTML file generated at build time) exist as physical files. For client-side rendered routes (like /login), there is no corresponding login.html file. When you directly access /login, S3/CloudFront looks for /login.html (or /login/index.html) and, not finding it, falls back to your error or default document (often /index.html), which then loads your SPA and lets Vue Router handle the route. If this fallback isn't configured correctly, you get a redirect or a 404. How to fix:
You need to configure your static host (CloudFront/S3) to serve index.html (or 200.html if generated) for all unknown routes, so that client-side routing can take over. This is a common requirement for SPAs. - S3: Set the error document to index.html (or 200.html if you prefer). - CloudFront: Make sure the behavior for your distribution forwards all requests to the origin, and that the origin (S3) is set up as above. Nuxt specifics:
- When you run nuxt generate, Nuxt will output index.html, 200.html, and 404.html by default. The 200.html is a fallback for unknown routes, useful for static hosts that support it. If your host only supports index.html as a fallback, use that. - Your landing page (/) will be pre-rendered as index.html. - All other routes will be handled client-side, as long as the SPA entry point is served for unknown routes. Reference:
This behavior and the need for fallback configuration is described in the Nuxt docs:
If you prerender your app with the nuxt generate or nuxt build --prerender commands, then you will not be able to use any server endpoints as no server will be included in your output folder. If you are using purely client-side rendering, then this might be unnecessary. You might only need a single index.html file, plus 200.html and 404.html fallbacks, which you can tell your static web host to serve up for all requests.
...
This will produce three files: index.html, 200.html, 404.html. The 200.html and 404.html might be useful for the hosting provider you are using.
Universal Rendering - Skipping Client Fallback Generation Summary of steps: 1. Keep your current routeRules config. 2. Ensure your static host (S3/CloudFront) is configured to serve index.html (or 200.html) for all unknown routes. 3. Deploy the contents of .output/public to your S3 bucket. This will allow direct navigation to /login and other client-side routes to work as expected, with the landing page pre-rendered for SEO. If you still encounter issues, double-check your CloudFront behaviors and S3 error document settings. The Nuxt side of your configuration is correct for your use case. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@1388891624649654332> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
jakub
jakubOP3d ago
@kapa.ai I already have redirects for 404 and 403 error codes configured to index.html. I can change them to 200.html too if I need to. My issue is that when I navigate directly to /login page, I'm redirected back to the index (/) page. The only way to navigate around the application is through the client-side routing (e.g., clicking on the navigation bar buttons). However, when running my application locally, I can directly navigate to the /login page. Why is that happening?
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- The behavior you're experiencing—where direct navigation to /login on your static S3/CloudFront deployment redirects you back to /—is not caused by Nuxt itself, but rather by how static hosting handles routing for client-side rendered pages. Key points from the knowledge sources: - When you use static hosting (like S3/CloudFront) with a Nuxt app, only pre-rendered routes (those with generated HTML files) are directly accessible. All other routes (like /login with ssr: false) do not have their own HTML files and must rely on the SPA entry point (index.html or 200.html) to bootstrap the app and let the client-side router handle the route [Universal Rendering]. - If your static host is configured to serve index.html (or 200.html) for unknown routes, the expectation is that the SPA will load and the client-side router will display the correct page. This works locally with npm run dev or npx serve .output/public because these servers are set up to always serve the SPA entry point for any route [Deployment]. - However, if you are being redirected from /login to /, this suggests that either: - There is a redirect or rewrite rule in your S3/CloudFront configuration that is forcing all unknown routes to / (not just serving the SPA entry point, but actually redirecting the URL to /), or - There is some client-side logic (middleware, auth, etc.) in your Nuxt app that is redirecting unauthenticated users to / (though you mentioned this only happens on static hosting, not locally). What to check: - CloudFront/S3 configuration:
Make sure you are not redirecting all unknown routes to /, but instead serving index.html (or 200.html) as the response for unknown routes, while preserving the original URL in the browser. This allows the SPA router to pick up the correct route. If you redirect to /, the browser URL changes to /, and the router loads the home page. - Nuxt middleware:
If you have any authentication middleware, ensure it is not redirecting users to / on initial load. This is a common pattern, but your description suggests it only happens on static hosting, so it's less likely. Summary:
The most likely cause is that your static host is redirecting (not just serving) all unknown routes to /, which resets the URL and loads the home page. You need to configure your host to serve the SPA entry point for all unknown routes without changing the URL. This is a subtle but important distinction.
If you are using purely client-side rendering, then you might only need a single index.html file, plus 200.html and 404.html fallbacks, which you can tell your static web host to serve up for all requests.
...
This will produce three files: index.html, 200.html, 404.html. The 200.html and 404.html might be useful for the hosting provider you are using.
Universal Rendering - Skipping Client Fallback Generation If you need more details on how to configure S3/CloudFront for SPA routing, you may need to consult their documentation, as the Nuxt docs do not provide step-by-step instructions for every static host. But the Nuxt side of your setup is correct for hybrid rendering. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@1388891624649654332> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
jakub
jakubOP3d ago
@kapa.ai "When you use static hosting (like S3/CloudFront) with a Nuxt app, only pre-rendered routes (those with generated HTML files) are directly accessible." With my routeRules setup, I can actually see index.html files generated for every page. E.g., public/login/index.html file is generated for the login page. My CloudFront error pages are configured to serve /index.html for all 403 and 404 errors.
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Thank you for clarifying that you see index.html files generated for every page (e.g., public/login/index.html). This means Nuxt is pre-rendering those routes, not just the landing page. With your current routeRules, this is unexpected, because only / should be pre-rendered (prerender: true), and the others (ssr: false) should be client-side rendered and not have their own HTML files generated. What the knowledge sources say: - If you want only the landing page to be pre-rendered, you should set prerender: true only for /, and set ssr: false for the other routes. This should result in only /index.html being generated, and not login/index.html, profile/index.html, etc. [Hybrid Rendering] - If you see HTML files for all routes, it suggests that either: - The routes are being discovered and pre-rendered by the Nitro crawler (because they are linked from the landing page or other pre-rendered pages), or - There is a configuration causing all pages to be pre-rendered, not just /. From the docs:
This will build your site, stand up a nuxt instance, and, by default, prerender the root page / along with any of your site's pages it links to, any of your site's pages they link to, and so on.
...
This is important to understand since pages that are not linked to a discoverable page can't be pre-rendered automatically.
Prerendering Why are you seeing this? - If your landing page (or any pre-rendered page) contains <a href="/login">, the Nitro crawler will find and pre-render /login as well, even if you set ssr: false for /login in routeRules. This is because the crawler's default behavior is to follow all links and pre-render them unless explicitly told not to [Prerendering]. How to prevent this: - You can explicitly tell Nuxt not to pre-render certain routes using the nitro.prerender.ignore option in your nuxt.config.ts:
export default defineNuxtConfig({
nitro: {
prerender: {
ignore: ['/login', '/profile', '/dashboard', '/table', '/rules'],
},
},
});
export default defineNuxtConfig({
nitro: {
prerender: {
ignore: ['/login', '/profile', '/dashboard', '/table', '/rules'],
},
},
});
This will prevent those routes from being pre-rendered, even if they are linked from other pages. Summary:
- The reason you see index.html files for all pages is likely because the Nitro crawler is following links and pre-rendering them. - To ensure only the landing page is pre-rendered, use prerender: true for / and add the other routes to nitro.prerender.ignore. - Your CloudFront setup to serve /index.html for 403/404 is correct for SPA fallback. If you follow this approach, only the landing page will be pre-rendered, and all other routes will be handled client-side as you intend. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@1388891624649654332> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
jakub
jakubOP3d ago
@kapa.ai My problem is not that the other pages are pre-rendered. My issue is that I can't directly navigate to the /login page (or other pages except for /). My /dashboard, /table/, and /rules pages depend on user authentication (and subsequent data fetching) which can't be done while generating the site. What settings should I use for my application? Also, should I explcitly set ssr in nuxt.config.ts?
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Given your requirements: - Only the landing page (/) should be pre-rendered for SEO. - All other pages (/login, /dashboard, /table, /rules, etc.) should be client-side rendered and not pre-rendered, as they depend on authentication and dynamic data. - You want to be able to directly navigate to any route (e.g., /login) on static hosting (S3/CloudFront) and have Nuxt handle it as a client-side route. Recommended Nuxt Configuration 1. Set ssr: false in your nuxt.config.ts
This ensures your app is built as a client-side rendered SPA, which is the most reliable way to avoid pre-rendering of pages that depend on authentication or dynamic data.
export default defineNuxtConfig({
ssr: false
})

export default defineNuxtConfig({
ssr: false
})

You can enable client-side only rendering with Nuxt in your nuxt.config.ts:
export default defineNuxtConfig({
ssr: false
})
export default defineNuxtConfig({
ssr: false
})

Client-Side Rendering 2. Pre-render only the landing page (optional, for SEO):
With ssr: false, Nuxt will not pre-render any pages by default. If you want to pre-render just the landing page, you can use the prerender:generate Nitro hook to only generate index.html, 200.html, and 404.html:
export default defineNuxtConfig({
ssr: false,
nitro: {
hooks: {
'prerender:routes' ({ routes }) {
routes.clear() // Do not generate any routes except the defaults
}
}
}
})

export default defineNuxtConfig({
ssr: false,
nitro: {
hooks: {
'prerender:routes' ({ routes }) {
routes.clear() // Do not generate any routes except the defaults
}
}
}
})

This will produce only index.html, 200.html, and 404.html in your output, which is the recommended setup for SPAs on static hosting [Universal Rendering]. 3. Static Hosting Configuration:
Make sure your S3/CloudFront setup serves index.html (or 200.html) for all unknown routes, without redirecting (i.e., the URL in the browser should remain /login, /dashboard, etc.). This allows the Nuxt SPA to handle routing on the client side. Why not use routeRules for SSR control? - routeRules are powerful for hybrid rendering, but when using static hosting and you want true SPA behavior (with only the landing page pre-rendered), setting ssr: false globally is simpler and avoids accidental pre-rendering of protected or dynamic pages [Hybrid Rendering]. Summary - Set ssr: false in your config. - Optionally, use the prerender:routes hook to only generate the landing page and SPA fallbacks. - Ensure your static host serves the SPA entry point for all routes. This will give you the expected behavior: direct navigation to any route works, only the landing page is pre-rendered, and all other pages are handled client-side. If you need the landing page to be pre-rendered for SEO, but the rest to be SPA, this is the recommended and documented approach in the Nuxt docs [Universal Rendering]. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@1388891624649654332> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
jakub
jakubOP3d ago
@kapa.ai I just noticed "Note that Hybrid Rendering is not available when using nuxt generate." in the Nuxt documentation. Also, with the configuration you suggested, my landing page is not pre-rendered for SEO. Is there a way to pre-render my landing page (index.vue), while keeping the rest of the application client-side rendered? Note that I'm using nuxt generate and static hosting for deployments.
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You are correct: the Nuxt documentation explicitly states that hybrid rendering is not available when using nuxt generate. This means you cannot mix SSR/prerendered and client-only routes when using nuxt generate—all pages that are discoverable by the crawler or specified in the prerender config will be statically generated as HTML files [Hybrid Rendering].
Note that Hybrid Rendering is not available when using nuxt generate.
With nuxt generate, the crawler will follow all links from your landing page and generate static HTML for each route it finds, unless you explicitly ignore them. If you want only the landing page to be pre-rendered and all other routes to be client-side rendered, you need to prevent the crawler from generating those other pages. How to achieve this: - Use the prerender:routes hook to clear all routes except the defaults, so only index.html, 200.html, and 404.html are generated. This is documented in the Nuxt docs:
export default defineNuxtConfig({
hooks: {
'prerender:routes' ({ routes }) {
routes.clear() // Do not generate any routes (except the defaults)
}
},
})
export default defineNuxtConfig({
hooks: {
'prerender:routes' ({ routes }) {
routes.clear() // Do not generate any routes (except the defaults)
}
},
})
This will produce only index.html, 200.html, and 404.html [Universal Rendering]. - However, with this setup, only the root page (/) will be pre-rendered. All other routes will be handled by the SPA entry point (index.html or 200.html), and rendered client-side. Summary:
- You can pre-render only your landing page (/) by using the prerender:routes hook to clear all other routes during generation. - All other routes will be handled as client-side rendered SPA routes. - This is the only way, with nuxt generate and static hosting, to have a pre-rendered landing page and the rest of the app as a client-side SPA [Universal Rendering]. If you need more granular hybrid rendering (e.g., some pages SSR/prerendered, some SPA), you must use a server deployment (not pure static hosting) and configure routeRules accordingly. This is not possible with nuxt generate alone. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@1388891624649654332> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
jakub
jakubOP3d ago
@kapa.ai I found something interesting with my initial setup.
For some reason, I can't directly navigate to specific pages like /login (not client-side navigation, that works fine). When I try to, I'm automatically redirected to the / (index) page. The only way to navigate around the application is with client-side navigation.
However, if I navigate directly to /login/index.html, my app successfully loads the login page. Is this expected with my routeRules setup, S3 and CloudFront?
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Yes, this behavior is expected with your current setup, S3, and CloudFront. Why does this happen? When you use nuxt generate and your routeRules (or default settings) result in static HTML files for each route (e.g., /login/index.html), S3 and CloudFront will only serve those files if the request matches the file path exactly. So: - Navigating to /login/index.html directly loads the static file and works as expected. - Navigating to /login will only work if your S3/CloudFront configuration is set up to map /login to /login/index.html. If this mapping is not configured, S3/CloudFront may return a 404 or fall back to serving /index.html (your SPA entry), which then loads the home page. This is a common static hosting behavior:
Since I’ve subdirectories such as <http://www.example.com/about/.> The index.html needs to be added after the subdirectory i.e "https://www.example.com/about/index.html" so the page will load without 403 errors.
...
All the URLs should end with a trailing /. i.e., <https://example.co/about> should be <https://example.co/about/.> This can be done by adding trailingSlash: true inside router key of nuxt.config.js.
Stack Overflow: Nuxtjs canonical URL are getting overridden on production How to improve this: - Trailing Slash:
If you set trailingSlash: true in your Nuxt router config, Nuxt will generate routes like /login/ (which maps to /login/index.html). This matches the default S3/CloudFront behavior, where /login/ serves /login/index.html. - CloudFront/S3 Routing:
Make sure your CloudFront behaviors and S3 settings are configured to serve index.html for directory requests (e.g., /login//login/index.html). If you request /login (without the slash), S3 may not automatically redirect to /login/, unless you set up a redirect or use Lambda@Edge to handle this. Summary:
- Navigating to /login/index.html works because the file exists. - Navigating to /login may not work unless you have trailing slashes enabled and your host is configured to map /login/ to /login/index.html. - This is expected static hosting behavior and not specific to Nuxt. For best results, enable trailingSlash: true in your Nuxt config and ensure your static host is configured to serve index.html for directory requests [Stack Overflow: Nuxtjs canonical URL are getting overridden on production]. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@1388891624649654332> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
jakub
jakubOP3d ago
In case anyone else hits this problem: My Nuxt app hosted on S3 + CloudFront kept redirecting to the index page whenever I navigated directly to /login or refreshed. The reason was that pages like /login aren’t in the root directory; they live in their own folders (for example, login/index.html). So I added a CloudFront function to append /index.html automatically for each request. Before that, CloudFront would just hit a 404 and send me back to the main/root index.html page due to redirect rules configured in CloudFront. Hope this helps!

Did you find this page helpful?